Static analysis

When a linter is wrong, check the tree before the rule

A pulled-open library card catalog drawer of identical index cards, with a single card filed behind the wrong divider, askew and lit, while the rest recede into shadow.

A linter flags code that is correct. You read the message, you find the rule that produced it, and you start editing the rule. That is the wrong first move often enough that it is worth a habit to interrupt it. The false positive you are looking at might not live in the rule at all. It might live in the parse tree the rule was handed, and from the command line the two are indistinguishable.

I worked two of these in the same week, both in sqlfluff, both in the same family of rules: the ones that decide whether a column reference is qualified and consistent. From the CLI they looked like the same kind of bug. The linter shouted at SQL that was perfectly valid. Underneath, they were not the same bug at all, and the fix for one would have done nothing for the other.

The one that was the rule's fault

The first was a BROADCAST hint. In some SQL dialects you can write an optimizer hint that names a table by its alias, and sqlfluff's RF02 flagged that alias as an inconsistent reference. The complaint was technically about a real reference. The alias inside the hint is a reference to the table; the parser labels it correctly as one. The tree was telling the truth.

The rule was the part that was wrong. RF02 checks that references stay consistent across a single-table statement, and to do that it collects every reference it can see. It was collecting the one inside the hint and counting it as if it were a column reference in the body of the query. A hint is metadata about execution, not a column projection, so a reference living inside it should never enter that consistency tally. The fix was a small exclusion in the rule, mirroring an exclusion the rule already made elsewhere. Nothing about the grammar changed, because nothing about the grammar was wrong.

The one that was the tree's fault

The second looked nearly identical from the outside. A JSON_TABLE call on MySQL, and RF03 flagging the column names inside its COLUMNS clause as unqualified references. Same rule family, same shape of message, same valid SQL being scolded.

But this time the rule was doing exactly what it should. The names it flagged really were sitting in the tree as column references. The problem was that they should never have been column references in the first place. The COLUMNS clause of JSON_TABLE does not use columns, it defines them; each entry names a new column in the table the function produces. MySQL's dialect had no dedicated grammar for JSON_TABLE, so the parser fell through to its generic handling and labeled those definitions as plain column references. The rule then collected them, correctly by its own logic, and flagged names that were never references to anything.

You cannot fix that in the rule without teaching the rule to special-case a construct it has no business knowing about. The honest fix is in the grammar: give the dialect a real JSON_TABLE definition so the column names parse as identifiers, the way an established dialect like Oracle already does it. Once the tree stops lying, the rule needs no change. It was right the whole time; it had just been handed a mislabeled node.

The diagnostic is one command

The thing that separates these two is not cleverness, it is a single step taken before any editing: dump the parse tree and look at the node the rule is complaining about. Every serious linter has this. In sqlfluff it is sqlfluff parse. The question to ask of the output is narrow. Is this node labeled correctly?

If the node is labeled correctly and the rule still flags it, the bug is in the rule. The collector is gathering something it should skip, or a condition is too broad. You edit rule logic. That was the hint.

If the node is labeled wrong, a definition showing up as a reference, an identifier showing up as a function, then the rule is reasoning correctly about a false premise, and no amount of rule editing makes that clean. You fix the grammar so the node carries its true type, and you leave the rule alone. That was JSON_TABLE.

Why the habit pays

The reason this is worth a fixed habit rather than case-by-case judgment is that the rule is the tempting place to look. It is where the error message comes from, it is the file with your rule's name on it, and a guard added to a rule feels like a fix. Sometimes it is. But a guard bolted onto a rule to dodge a mislabeled node is a patch on top of a lie, and it tends to grow more special cases as more mislabeled inputs arrive. The grammar fix removes the lie once, and every rule that reads that part of the tree benefits, not just the one that happened to complain.

So before editing the rule, I look one layer down. The message points at the rule, but the rule is only ever as right as the tree beneath it. When a linter is wrong about correct code, the real question is not what the rule did. It is whether the rule was told the truth.