What AI Code Scanners Catch That Linters Can’t
Code Quality Linters AI DetectionLinters were built for human mistakes. Missed semicolons. Unused variables. Inconsistent formatting. The kinds of errors that happen when a developer is tired, distracted, or just moving fast.
AI makes different mistakes.
AI-generated code doesn’t forget semicolons. It doesn’t misspell variable names. It produces syntactically flawless code that imports modules which don’t exist, calls API methods that were removed two major versions ago, and references packages that were never published to any registry. The code compiles. The tests might even pass — if the tests were also written by the same AI and share the same blind spots.
Your linter sees nothing wrong. Your CI pipeline goes green. And somewhere in production, a function call is about to hit a method that isn’t there.
This is the gap. And it’s growing every day as more code gets generated rather than written.
I am not here to tell you that ESLint is bad or that SonarQube is obsolete. I use both. But I am going to walk through exactly what each tool catches, what each tool misses, and why the gap between them is the most dangerous blind spot in modern software development.
What ESLint Actually Catches
ESLint is a good tool. I use it. You should use it. But let’s be honest about what it does and what it doesn’t.
ESLint catches:
- Syntax errors — missing brackets, invalid tokens, malformed expressions
- Formatting violations — inconsistent indentation, spacing, quote styles
- Basic code patterns — unused variables, unreachable code, accidental globals
- Style enforcement — naming conventions, import ordering, line length
- Simple anti-patterns —
==instead of===,eval()usage,console.login production
These are real problems. They matter. But they are all problems that exist within a single file, at the syntax level. ESLint reads your code as text and checks it against a ruleset. It does not understand what your code does.
What ESLint Misses Completely
Here is a line of code that ESLint will never flag:
import { validateSchema } from '@utils/schema-validator';
Syntactically perfect. ESLint sees a named import from a local module path. No rule violation. Green checkmark.
But @utils/schema-validator doesn’t exist in your project. The AI invented it. It looked at your directory structure, saw a @utils alias, and hallucinated a module that seemed like it should exist. The import resolves to nothing. The build might even succeed if your bundler is configured to silently skip missing modules. The failure happens at runtime, in production, when a user triggers the code path that calls validateSchema().
This is a phantom import. The module compiles, but the method doesn’t exist. ESLint cannot catch it because ESLint doesn’t resolve imports against your actual file system. It checks syntax, not truth.
Here is another one:
import { useRouter } from 'next/navigation';
const router = useRouter();
router.prefetch('/dashboard', { priority: 'high' });
The import is real. The method is real. But the priority option does not exist on router.prefetch() in Next.js. The AI invented a plausible-sounding parameter. No syntax error. No runtime crash — the option is silently ignored. Your prefetching strategy just does not work the way you think it does, and you will never know unless you read the Next.js source code line by line.
ESLint has no opinion on this. It cannot have an opinion on this. It does not know what router.prefetch accepts. That is not what linters do.
The pattern repeats across categories:
Deprecated APIs that still parse correctly. The AI learned from training data that included code from 2021. It calls crypto.createCipher() instead of crypto.createCipheriv(). ESLint sees a valid function call. The function was deprecated in Node 10 and removed in Node 22.
Hallucinated packages. The AI generates npm install fast-json-cache in your setup instructions. The package doesn’t exist on npm. Or worse — it exists but was published by someone who anticipated exactly this kind of hallucination and uploaded a malicious package with that name.
Architecture-level issues. Your service layer imports directly from your database layer, bypassing the repository pattern your team established. Every individual import is valid. The architecture is broken. ESLint has no concept of architectural boundaries.
What SonarQube Catches
SonarQube goes deeper than ESLint. It’s a proper static analysis platform with a server, a database, and years of rule engineering behind it. Credit where it’s due.
SonarQube catches:
- Code smells — long methods, deep nesting, high parameter counts
- Cyclomatic complexity — functions with too many branches
- Duplicate code blocks — copy-paste detection across files
- Security vulnerabilities — SQL injection patterns, XSS vectors, hardcoded credentials
- Deprecated API usage — for well-known frameworks with maintained rule packs
- Test coverage gaps — lines and branches not exercised by tests
This is meaningful analysis. SonarQube has saved countless teams from shipping vulnerable code. But it was designed for a world where humans write code and the primary risks are complexity, duplication, and known vulnerability patterns.
SonarQube also requires infrastructure. You need a server. You need a database. You need to configure projects, set quality gates, manage rule profiles. For enterprise teams with dedicated DevOps, this is fine. For a two-person startup that just scaffolded their entire backend with Claude Code in an afternoon, it is a barrier that means the tool never gets set up at all.
And even when it is set up, SonarQube is looking for the wrong kind of problem.
What SonarQube Misses
SonarQube doesn’t know that your code was generated by an AI. It doesn’t know that the AI hallucinated three of your twelve service files. It doesn’t know that the code looks production-ready but is actually scaffold — structurally complete, functionally empty.
Specifically, SonarQube misses:
AI-specific hallucinations. SonarQube checks code against known patterns. It doesn’t check whether the code references things that actually exist in your project. A phantom import to a non-existent utility module passes every SonarQube rule because the rules assume that if you wrote an import, the module exists.
Mother Code DNA coverage. When you use AI to scaffold a project, the generated code often drifts from your original architecture — the patterns, conventions, and structural decisions that define how your system is supposed to work. SonarQube can tell you that a function is too complex. It cannot tell you that the function violates the architectural contract your team established in the original codebase.
Codebase-level health scoring. SonarQube gives you metrics per file and per project. It does not give you a single, actionable health score that tells you whether your codebase is getting better or worse over time in a way that accounts for AI-generated drift, phantom dependencies, and architectural erosion.
The “is this code real or scaffold” question. AI-generated code often includes complete file structures with proper naming, proper exports, proper TypeScript types — and empty implementations. The function signatures are there. The logic is not. SonarQube sees well-structured code. A human reviewer sees a shell.
Here is what AI scaffold looks like in practice:
export class PaymentService {
async processPayment(amount: number, currency: string): Promise<PaymentResult> {
// TODO: implement payment processing
return { success: true, transactionId: '' };
}
async refundPayment(transactionId: string): Promise<RefundResult> {
// TODO: implement refund logic
return { success: true };
}
}
Types are correct. Exports are correct. The class follows the naming convention. SonarQube gives it a clean bill of health. But this code does not process payments. It returns hardcoded success responses. If this ships, every payment “succeeds” without actually charging anyone. Your revenue dashboard looks great. Your bank account does not.
The core issue: SonarQube was built to answer “is this code well-written?” The question we need to answer now is “is this code real?”
The New Category: AI Code Verification
What we need is not a better linter. We need a different kind of tool entirely.
Not linting — verification
Linting asks: “Does this code follow the rules?”
Verification asks: “Does this code actually work?”
A linter checks syntax. A verifier checks truth. When the AI writes import { sendGrid } from '@services/email', a linter checks whether the import statement is well-formed. A verifier checks whether @services/email exists on disk and whether it exports something called sendGrid.
Not security scanning — health scanning
Security scanners ask: “Can an attacker exploit this?”
Health scanners ask: “Is this codebase maintainable?”
Security is binary — vulnerable or not. Health is a gradient. A codebase can be secure and still be rotting from the inside: phantom dependencies accumulating, architectural patterns eroding, AI-generated scaffold masquerading as implementation. Health scanning tracks the trajectory, not just the snapshot.
A health score tells you something no security scanner can: is this codebase getting better or worse with each commit? If your team merged 40 AI-generated files last week and the health score dropped from B+ to C-, that is a signal. Not a vulnerability. Not a syntax error. A trajectory. And trajectories are what kill projects.
Not code review — code truth-checking
Code review asks: “Would I approve this PR?”
Truth-checking asks: “Did the AI make this up?”
A code reviewer evaluates style, logic, and approach. A truth-checker verifies that every import resolves, every API call targets a real method, every dependency exists in the registry, and every architectural pattern matches the established codebase DNA. It is not a matter of opinion. It is a matter of fact.
Here is what the difference looks like in practice:
# What a linter sees
import { sendEmail } from '@services/mailer'; // valid syntax
import { hashPassword } from 'bcrypt-utils'; // valid syntax
import { createClient } from '@supabase/ssr'; // valid syntax
# What a verifier sees
import { sendEmail } from '@services/mailer'; // PHANTOM: file does not exist
import { hashPassword } from 'bcrypt-utils'; // PHANTOM: package not on npm
import { createClient } from '@supabase/ssr'; // VALID: package exists, export exists
Same three lines. The linter sees three valid import statements. The verifier sees two hallucinations and one real dependency. That is the difference between checking syntax and checking truth.
Side-by-Side: What Each Tool Covers
Here is the honest comparison. No tool does everything. But the gaps matter.
| Feature | ESLint | SonarQube | Thuban |
|---|---|---|---|
| Phantom import detection | No | No | Yes |
| AI hallucination detection | No | No | Yes |
| Health score & grade | No | Partial | Yes |
| Zero config | Yes | No (server required) | Yes |
| Deprecated API detection | Partial | Yes | Yes |
| Architecture drift | No | No | Yes |
| Auto-fix | Formatting only | No | Yes (issues) |
| Compare two codebases | No | No | Yes |
| Mother Code DNA | No | No | Yes |
The left two columns are not failures. ESLint and SonarQube do what they were designed to do, and they do it well. The gap is not a flaw in those tools — it is a new category of problem that did not exist at scale before AI started writing production code.
Look at the bottom three rows. Compare two codebases. Mother Code DNA. Architecture drift. These are not features that ESLint or SonarQube chose not to build. These are concepts that did not need to exist when humans wrote all the code. When a human writes a service file, they know whether the utility module they are importing exists — because they wrote it, or they saw a colleague write it, or they searched for it in the codebase before using it.
AI does none of that. It predicts the next token. If the next token is an import path that sounds right, it writes it. Whether the file exists is not part of the prediction. That is why verification — not linting — is the tool this moment requires.
Use ESLint AND Thuban
This is not an either-or decision. You should run ESLint. You should probably run SonarQube if your team has the infrastructure for it. And you should run Thuban.
They cover different layers:
- ESLint catches syntax-level issues. It keeps your code consistent and catches the obvious mistakes. Run it on every save.
- SonarQube catches complexity and security patterns. It keeps your code maintainable and flags known vulnerability classes. Run it in CI.
- Thuban catches AI-generated hallucinations, phantom imports, architectural drift, and codebase health degradation. It tells you whether the code is real. Run it before every merge.
Think of it as layers of defense. ESLint is the spell checker. SonarQube is the grammar checker. Thuban is the fact checker. You need all three because AI-generated code can be perfectly spelled, grammatically correct, and completely made up.
A real example from our own codebase: We ran Thuban against a project that had been partially scaffolded by Cursor. ESLint reported zero errors. SonarQube gave it an A rating. Thuban found 275 phantom imports — modules that compiled but referenced methods that did not exist. Every single one would have failed at runtime.
275 issues. Zero of them visible to the two most popular code quality tools on the market. That is the gap.
If you are using AI to write code — and statistically, you are — your quality pipeline has a blind spot the size of a continent. ESLint and SonarQube are watching the front door. The AI hallucinations are walking in through the walls.
A practical workflow
Here is how we run it on our own projects:
- On save: ESLint catches formatting and syntax issues instantly. Fast feedback loop. No friction.
- On commit: Pre-commit hooks run ESLint with
--fixto auto-correct what it can. Consistent code hits the repo. - On PR:
npx thuban scan .runs in CI. Phantom imports, deprecated APIs, hallucinated packages, and health score are checked. If the health grade drops, the PR gets flagged. - Weekly: SonarQube runs a full analysis for security vulnerabilities, complexity trends, and coverage tracking.
Each tool runs where it adds the most value. No overlap. No gaps. The entire pipeline takes less than five minutes to set up and costs nothing for small teams.
See the Gap for Yourself
Run Thuban against a codebase that already passes ESLint. See what it finds. The results tend to be uncomfortable — not because Thuban is harsh, but because the problems were always there. You just didn’t have a tool that could see them.
Pick your messiest project. The one that was half-written by Copilot, half-written by Cursor, and held together by optimism. The one where you are not entirely sure which imports are real and which ones the AI invented. Run the scan. Look at the health score. Then decide whether syntax checking alone is enough.
One command. Zero config. Zero cloud.
npx thuban scan .
Free for up to 100 files, 5 scans per month. No account required.
Works on JavaScript, TypeScript, Python, PHP, Ruby, and more. Results in under 3 seconds.
Your linter checks your syntax. Your scanner checks your patterns. Neither checks whether the AI made it up. That is the job Thuban was built for.
Craig is the founder of Thuban and has spent the last year watching AI-generated code pass every existing quality gate and fail in production. Thuban exists because the tools we had were not built for the code we now write.