World’s largest virtual agentic engineering & quality conference

WHENAUG 19-21
WHEREVirtual · Global
Register Now
Testing

What Is a Linter? How Linting Works and What It Misses

A linter reads code without running it. See a real ESLint run, the six errors it caught, the bug it did not, and where static analysis stops being enough.

Author

Shravan Mahajan

Author

Last Updated on: August 8, 2026

A shopping cart file was run through ESLint. It reported six errors. Every one was fixed, the linter went green, and the cart still undercharged every customer who used a coupon.

That run is reproduced in full below, because it explains what a linter is far better than a definition does. This guide covers how linting works, what it reliably catches, the exact category it cannot see, and where to put the check that catches the rest.

Overview

A linter is a static analysis tool that reads source code without running it and reports patterns that break a configured rule set. It catches likely bugs, correctness risks, and inconsistencies in seconds, and it is limited to what can be decided from the text alone, so logic that is well written and still wrong passes cleanly.

What Does a Linter Catch?

  • Likely bugs: Undefined variables, unreachable statements, duplicate object keys, and unused bindings. These are decidable from the source text, which is why a linter is certain about them rather than suggestive.
  • Correctness risks: Patterns that are legal but usually wrong, such as loose equality or an assignment inside a condition. The rule fires on the pattern; whether it is a defect is a judgement the reader makes.
  • Consistency: Naming and structural conventions across a codebase, which matters most on teams large enough that no one person reads every file.
  • Not runtime behaviour: Nothing that depends on a value the program computes while running. A function can be clean under every rule and still return the wrong number.

Where Does Linting Stop?

At the boundary between text and execution. Linting proves properties of the source; tests prove properties of the running program. Both are needed, and confusing one for the other is how a green pipeline ships a defect that a single click would have exposed.

What Is a Linter?

A linter is a program that reads your source code as text, builds a structural model of it, and reports places where that structure matches a pattern you have declared unwanted. It never executes the code it examines.

The name is inherited. The original lint was written by Stephen C. Johnson at Bell Labs in 1978 to catch problems in C programs that the compiler of the day let through, and it was named for the fibres a dryer trap collects. The metaphor has held up better than most: a linter collects small debris that individually looks harmless.

What makes it useful is speed and placement. A linter runs in seconds, inside the editor, on code that has not been committed, let alone deployed. That means the feedback arrives while the author still has the reasoning in their head, which is the cheapest moment any defect can be found.

It is also advisory rather than mandatory. A compiler must reject what it cannot translate. A linter reports on code that is entirely valid, against rules your team chose, which is why two projects in the same language can produce completely different lint output from identical code.

How Linting Works

Four steps, and understanding them explains both the strengths and the hard limit.

  • Parse. The source is read and turned into an abstract syntax tree, a structural representation where a function declaration, a condition, and a return statement are distinct nodes rather than characters.
  • Walk. The tool traverses that tree, visiting every node. Each rule registers interest in the node types it cares about, so a rule about equality only ever sees comparison nodes.
  • Match. Each rule inspects the nodes it receives and decides whether the pattern it forbids is present. This is pure structural inspection; no values exist yet, because nothing has run.
  • Report. Matches are emitted with file, line, column, rule name, and severity. Rules that can rewrite safely may also offer an automatic fix.

Step three is where the ceiling sits. A rule can see that a comparison exists and that it uses loose equality. It cannot see what the two sides will hold when the program runs, because that information does not exist until it does. Every limitation in the next section follows from that one fact.

A Real Lint Run

Here is the cart file from the opening, with six deliberate problems planted in it. Read it before the output and see how many you find.

const TAX_RATE = 0.2;
const CURRENCY = 'GBP';

export function total(items, coupon) {
  let sum = 0;
  for (const item of items) {
    sum += item.price * item.qty;
  }

  if (coupon == null) {
    sum = sum * (1 + TAX_RATE);
  }

  return sum;
  console.log('never runs');
}

export const config = { retries: 2, timeout: 5000, retries: 3 };

export function applyDiscount(sum) {
  if (sum > 100) {
    return sum - discountAmount;
  }
  return sum;
}

Run against ESLint 9.39.5 with six correctness rules enabled, the output is:

   2:7   error  'CURRENCY' is assigned a value but never used  no-unused-vars
  10:14  error  Expected '===' and instead saw '=='            eqeqeq
  15:3   error  Unreachable code                               no-unreachable
  15:3   error  'console' is not defined                       no-undef
  18:52  error  Duplicate key 'retries'                        no-dupe-keys
  22:18  error  'discountAmount' is not defined                no-undef

✖ 6 problems (6 errors, 0 warnings)

Every finding is correct and useful. Two of them are outright bugs that would throw at runtime: discountAmount does not exist, and console is undeclared in a module with no environment configured. The duplicate key silently discards the first value, so a retry setting somebody deliberately chose is thrown away without warning. This is the case for linting, made concretely.

Notice what each of these has in common. Every one is visible in the text. No value had to be computed to find it.

What It Misses

Now the same file with all six findings fixed. Loose equality tightened, unused constant removed, unreachable line deleted, duplicate key resolved, undefined reference dropped.

const TAX_RATE = 0.2;

export function total(items, coupon) {
  let sum = 0;
  for (const item of items) {
    sum += item.price * item.qty;
  }

  if (coupon === null) {
    sum = sum * (1 + TAX_RATE);
  }

  return sum;
}

export const config = { retries: 3, timeout: 5000 };

ESLint on this file reports nothing and exits zero. A pipeline gating on lint would go green. Here is what the function actually returns for a single £100 item:

no coupon   -> 120.00
with coupon -> 100.00

Tax is applied only when there is no coupon. Every discounted order loses twenty percent of its tax, silently, forever. The linter saw that condition on line nine and had an opinion about it, but the opinion was that == should be ===. The comparison operator was the only thing about that line it could evaluate.

This is not a shortcoming to be fixed by a better rule set. No lint rule can exist for it, because the code is not malformed. It is well structured, consistently written, correctly typed if you added types, and wrong. The three categories a linter is structurally unable to reach:

CategoryExampleWhy static analysis cannot see it
Wrong logic, right formThe tax branch above.The code expresses an intention. The tool has no access to the intention, only to the expression.
Wrong assumptions about dataReading a field an API stopped returning last month.The shape of the response exists only at runtime, in another system.
Wrong behaviour when assembledA button that renders and posts to the wrong endpoint.Every file is individually valid. The defect only exists once they run together in a browser.

The practical consequence is a sequencing rule. Linting is the cheapest gate and belongs first, but a green lint result is evidence about the text and nothing else. Treating it as evidence about behaviour is how a clean pipeline ships an undercharging cart.

Next-generation test execution with TestMu AI

Linter vs Formatter vs Compiler

Three tools read your code before it runs, and conflating them causes real friction, usually in the form of a linter and a formatter fighting over the same file.

QuestionLinterFormatterCompiler
AnswersDoes this code do something it should not?Does this code look the way we agreed?Can this code be translated at all?
Changes behaviourOnly if you apply a fix.Never.Not applicable; it produces output.
ConfigurableHeavily, per project.Lightly, and deliberately so.Barely. The language decides.
Can be ignoredYes, that is the design.Yes, but there is no reason to.No. It stops the build.
Typical toolESLint, Ruff, RuboCopPrettier, Black, gofmttsc, javac, rustc

The workable arrangement is to let the formatter own everything about appearance and let the linter own only correctness. Style rules inside a linter generate findings that need a human to act on something a machine could have fixed, which is the fastest way to teach a team that lint output is noise.

Linters by Language

LanguageLinterWorth knowing
JavaScript, TypeScriptESLintRules are plugins, so the ecosystem covers frameworks and accessibility as well as the core language.
PythonRuff, PylintRuff reimplements much of the older toolchain and is fast enough to run on every keystroke.
RubyRuboCopShips opinionated defaults, which is why most teams start by disabling a portion of them.
Gogolangci-lintAn aggregator that runs many analysers in one pass rather than a single rule set.
RustClippyShips with the toolchain and leans toward idiom, not just correctness.
JavaCheckstyle, SpotBugsCheckstyle covers convention, SpotBugs analyses bytecode for likely defects. They are complementary.
CSSStylelintCatches invalid properties and duplicate selectors that browsers silently ignore at runtime.

Whichever you use, start from the recommended set. A hand-built rule list assembled before the project has produced any defects tends to encode preferences rather than lessons, and preferences are what developers disable inline.

Rules Worth Turning On First

The recommended set is the right default, and a handful of rules beyond it repay their noise immediately. These are ordered by the severity of the defect they prevent rather than by how often they fire.

RuleWhat it stopsWhy it earns a place
no-floating-promisesAn async call whose result is never awaited.The most damaging silent failure in modern JavaScript. The operation appears to succeed, errors vanish, and the bug surfaces as missing data much later.
no-await-in-loopSequential awaits over a collection.Functionally correct and quietly slow. Ten records take ten round trips where one batched call would do.
eqeqeqLoose equality comparisons.Type coercion produces comparisons that are true for values a reader would not expect to match.
no-shadowAn inner binding reusing an outer name.The code reads as if it operates on the outer value. Reviews miss it because both lines look correct in isolation.
consistent-returnA function returning a value on some paths only.Callers receive undefined from a branch nobody thought about, usually the error path.
jsx-a11y/alt-textImages with no text alternative.An accessibility failure that is cheap to catch here and expensive to retrofit across a UI later.

The accessibility rules deserve a note on scope. A linter can confirm an image declares alt text; it cannot judge whether the text describes the image usefully, and it cannot see contrast, focus order, or whether a screen reader announces a component sensibly, because all three depend on the rendered page. Static rules catch the omissions, and accessibility testing covers everything they cannot reach.

That pattern repeats across every rule in the table. Each one catches an omission or a shape in the text, and each has a matching class of defect that only shows itself when the code runs.

Wiring It Into CI

Linters return a non-zero exit code when they find errors, which is all a pipeline needs.

# Fail the job on errors, allow warnings through
npx eslint . --max-warnings=0

# Only lint what this change touched, which keeps
# a legacy codebase from blocking every merge
npx eslint $(git diff --name-only origin/main --diff-filter=d | grep -E '\.[jt]sx?$')

Three decisions matter more than the command. Run it before your test suite, since it takes seconds and the tests take minutes. Separate error severity from warning severity so correctness blocks a merge and preference does not. And on an existing codebase, lint the diff rather than the repository, so the backlog is cleared as files are touched instead of in one commit nobody can review.

Once linting is green on every merge, the next gate is the one that actually runs the code. That ordering matters for the same reason the sections above did: each stage catches a class the previous one could not see. The relationship between checking the artifact and checking the behaviour is covered in verification vs validation.

Checking Runtime Behaviour

The undercharging cart needed one thing to be caught: something that added an item, applied a coupon, and read the total. No amount of static analysis substitutes for that, and this gap has widened rather than narrowed as AI coding agents write more of the code.

The reason is structural and worth stating plainly. An AI agent's own quality gates are linters, type checkers, and unit tests, and every one of those reads source code. The agent can run all three, see them pass, and report success on a checkout flow that charges the wrong amount, because nothing in its loop ever rendered the page.

Kane CLI closes that specific gap from the terminal. It takes a plain-English objective and drives real Chrome to complete it, and it is constrained to actions a real user could perform, so it cannot force a state the interface never reached in order to report a pass.

kane-cli run "add one item priced 100 to the cart, apply coupon SAVE10, \
assert the order total includes tax" --url http://localhost:3000

A pass is granted only when the expected state is confirmed through evidence such as DOM state, a URL change, or a screenshot, and each run leaves that evidence behind. In CI it returns standard POSIX exit codes, so the same command that answers the local question can gate a merge alongside the lint step rather than replacing it.

The two checks are complementary and belong in that order. Linting is faster, cheaper, and catches the six errors in the first run above, none of which need a browser. Running the flow catches the seventh, which none of the six rules could ever have found.

Note

Note: Static analysis proves the code is well formed. Driving the flow proves it charges the right amount. Kane CLI does the second from your terminal, with a pass granted only on evidence. Read the Kane CLI docs

Conclusion

Turn on the recommended rule set today and lint the diff rather than the repository. That gets you the six-error class of finding on every merge without a cleanup commit nobody can review, and it costs an afternoon.

Then hold the boundary in your head. A green linter means the text is well formed. It is not a statement about what the program does, and the cart above is what that distinction costs when it gets blurred: six errors found, one bug shipped, twenty percent of tax quietly missing from every discounted order.

Put a check that executes the code behind the one that reads it. Running those flows across real browsers is what TestMu AI's automation cloud is built for, and the automation documentation covers wiring an existing suite into a pipeline that already lints. For what belongs in those tests, see the test script guide.

Author

...

Shravan Mahajan

Blogs: 2

  • Linkedin

Shravan Mahajan is a Software Engineer at TestMu AI building Kane CLI, the command-line tool that runs browser automation from the terminal, describing flows in natural language that execute in a real Chrome browser and return pass or fail with shareable proof. He has an experience of 6 years in the Technical industry. His top skills are JavaScript, React.js, and full-stack development. At Fractal he built automated data pipelines with T-SQL, SSIS, Python, and Azure. He is also a Microsoft Certified Azure Data Engineer Associate.

Open in ChatGPT Icon

Open in ChatGPT

Open in Claude Icon

Open in Claude

Open in Perplexity Icon

Open in Perplexity

Open in Grok Icon

Open in Grok

Open in Gemini AI Icon

Open in Gemini AI

Copied to Clipboard!
...

3000+ Browsers. One Platform.

See exactly how your site performs everywhere.

Try it free
...

Write Tests in Plain English with KaneAI

Create, debug, and evolve tests using natural language.

Try for free
...
TestMu Conf 2026

World's largest virtual agentic engineering & quality conference

...

AUG 19-21, 2026

REGISTER NOW

Linter FAQs

Did you find this page helpful?

More Related Blogs

TestMu AI forEnterprise

Get access to solutions built on Enterprise
grade security, privacy, & compliance

  • Advanced access controls
  • Advanced data retention rules
  • Advanced Local Testing
  • Premium Support options
  • Early access to beta features
  • Private Slack Channel
  • Unlimited Manual Accessibility DevTools Tests