Regex Linters Lie. Build A Tree-sitter Linter That Does Not
ESLint just blocked my merge because it “found” eval in a file that never calls eval.
Regex Linters Lie. Build A Tree-sitter Linter That Does Not
ESLint just blocked my merge because it “found” eval in a file that never calls eval.
The “match” was inside a string literal that contained an example, not executable code. The rule did what it was programmed to do. My brain did what it always does in that moment: I stopped trusting the tool.
That is the real cost of a lying linter. It does not waste minutes. It breaks the relationship.
Once your team learns that lint warnings are negotiable, the warnings that matter start to slip through with them.
The Lie Regex Linters Tell With Confidence
A regex linter does not understand code. It understands text.
That sounds obvious, but the consequences are brutal. Code is full of places where words appear without meaning: comments, strings, template literals, doc blocks, examples, test snapshots, generated files, embedded SQL, embedded JSON, and on and on.
Regex sees eval and panics. It cannot know whether the runtime can reach it.
Here is the smallest version of the lie. This is the kind of rule that looks “good enough” until it ruins your afternoon.
const re = /\beval\s*\(/;
const src = `
// safe doc
const guide = "Never use eval() here";
const ok = true;
`;
console.log(re.test(src)); // true
That output is true, and it feels authoritative. It is also useless, because it is not answering the real question.
The real question is: is there a call expression in the program that invokes eval?
Regex cannot answer that. Not reliably. Not across languages. Not across syntax forms. Not across modern codebases that embed other languages inside strings.
So the linter starts accumulating exceptions. Then the exceptions need exceptions. Then every PR includes a mini debate about whether the rule is wrong or the code is wrong.
The linter becomes noise, and noise trains people to ignore signal.
Why This Turns Into A Team Problem
When a linter lies, it creates a specific kind of fatigue. It is the same shape as alert fatigue in production monitoring.
If half your alerts are false, you stop reacting fast. You start waiting for someone else to confirm. You start muting.
A noisy linter does the same thing to code review. It teaches engineers to optimize for making the tool quiet, not for making the system safer.
That is how you end up with a codebase where style rules are enforced more strictly than correctness rules, because style rules are easy for regex and correctness rules are hard.
You do not fix this by writing better regex. You fix this by using a tool that can see structure.
What Tree-sitter Sees That Regex Cannot
Tree-sitter parses your source code into a concrete syntax tree. Not a guess. Not heuristics. A real tree of nodes that represent the language grammar.
Once you have that tree, you stop scanning characters and start matching meaning.
Here is the mental model that makes Tree-sitter click.
Source Code
|
v
Tree-sitter Parser
|
v
Syntax Tree (Nodes And Structure)
|
v
Query (Match Specific Nodes)
|
v
Findings (Row, Column, Message)
You can point at a node and say: this is a call expression. This is a string. This is a comment. This is an identifier. This is an argument list.
Your linter stops guessing. It starts knowing.
Build A Rule That Only Flags Real eval() Calls
Below is a minimal Tree-sitter linter for JavaScript that flags direct calls to eval.
It does not match eval inside strings. It does not match comments. It does not match variables named evalCount. It matches only actual call expressions where the callee identifier is exactly eval.
import Parser from "tree-sitter";
import JavaScript from "tree-sitter-javascript";
const p = new Parser();
p.setLanguage(JavaScript);
const qText = `
(call_expression
function: (identifier) @fn
(#eq? @fn "eval")) @hit
`;
export function lint(src) {
const tree = p.parse(src);
const q = new Parser.Query(JavaScript, qText);
const out = [];
for (const c of q.captures(tree.rootNode)) {
if (c.name !== "hit") continue;
const n = c.node;
out.push({
row: n.startPosition.row + 1,
col: n.startPosition.column + 1,
msg: "Avoid eval. Use a parser or a safe sandbox."
});
}
return out;
}
Now make it feel real with a tiny input.
const src = `
const guide = "Never use eval() here";
// eval("not real")
function run(x) { return eval(x); }
`;
console.log(lint(src));
Expected output is one finding, pointing to the real call inside run.
If your regex linter was yelling at the string and the comment, Tree-sitter stays quiet. It only speaks when the program actually does the thing you care about.
The Quiet Relief When A Linter Stops Guessing
The emotional shift is immediate.
With regex, every warning carries doubt. You read the message and your first thought is: is this another hallucination?
With Tree-sitter, warnings start to feel grounded. You can click to a node location, see the structure, and agree with the tool even when you do not like the rule.
That is when a linter becomes useful again. Not because it enforces style, but because it earns trust.
Trust is what makes engineers fix issues early, before reviewers ask, before production asks, before customers ask.
The Hidden Performance Win
There is another advantage that shows up once your rules grow.
Regex rules multiply into multiple passes across the same file, and edge cases cause backtracking. You can get unpredictable slowdowns in the exact files you most want to lint: big, messy, real-world files.
Tree-sitter parsing is designed to be predictable, and it supports incremental updates. Even when you run it in CI, the behavior stays stable, and stability matters more than chasing a theoretical best-case speed number.
The best linter is the one your team does not disable out of frustration.
The Line I Wish Someone Told Me Earlier
Regex linters lie because they lint text.
Tree-sitter linters work because they lint code.
If you have been adding exclusions to silence false positives, you are not improving the rule. You are paying a tax for using the wrong level of abstraction.
메타데이터
- post_id
- 271db65acf1a
- slug
- regex-linters-lie-build-a-tree-sitter-linter-that-does-not-271db65acf1a
- url
- https://medium.com/@maahisoft20/regex-linters-lie-build-a-tree-sitter-linter-that-does-not-271db65acf1a
- canonical_url
- https://medium.com/@maahisoft20/regex-linters-lie-build-a-tree-sitter-linter-that-does-not-271db65acf1a
- author_url
- https://medium.com/@maahisoft20
- status
- ok
- fetched_at
- 2026-06-09 14:34:10