How to Detect Phantom Imports in AI-Generated Code
Phantom Imports AI Code Quality TutorialYour AI coding assistant just wrote 200 lines of clean, well-structured code. The logic is sound. The variable names are sensible. The comments are helpful. There's just one problem: three of the imports point to modules that don't exist.
These are phantom imports — and they're one of the most common, most dangerous, and most overlooked categories of AI code hallucination. They look perfectly valid. They pass a casual code review. They even pass most linters. Then they blow up your build at 2am on a Friday.
What Are Phantom Imports?
A phantom import is an import or require statement that references a module, function, method, or package that does not actually exist. The AI model generates it because it has seen similar patterns in training data, but it gets the name wrong, confuses one library's API with another, or invents something entirely from scratch.
There are three flavours:
- Wrong export name — the package exists, but the specific function or class doesn't
- Wrong package name — the package itself doesn't exist or is named differently
- Deprecated API — the function existed once but has been removed or replaced
All three compile to the same outcome: a runtime crash that your type checker, linter, and test suite may not catch until it's too late.
Real Examples That Ship to Production
These aren't hypothetical. Every one of these has been generated by a major AI coding tool in a real project. We've seen them in Thuban scans across hundreds of codebases.
1. Wrong export name (JavaScript)
// AI-generated code
import { deepClone } from 'lodash';
const copy = deepClone(originalObject);
Looks right. Reads well. Doesn't exist. The actual lodash function is cloneDeep, not deepClone. The AI swapped the word order because "deep clone" is how developers describe the operation in natural language. Lodash named it the other way around.
// What it should be
import { cloneDeep } from 'lodash';
const copy = cloneDeep(originalObject);
2. Wrong function name (Python)
# AI-generated code
from flask import jsonify_response
@app.route('/api/users')
def get_users():
users = User.query.all()
return jsonify_response([u.to_dict() for u in users])
Flask's function is jsonify, not jsonify_response. The AI merged the concept of "JSON response" into a single function name that sounds plausible but doesn't exist in Flask's API. This will crash with an ImportError on the very first request.
# What it should be
from flask import jsonify
@app.route('/api/users')
def get_users():
users = User.query.all()
return jsonify([u.to_dict() for u in users])
3. Right package, wrong context (Next.js)
// AI-generated code in a Pages Router project
import { useRouter } from 'next/navigation';
export default function Dashboard() {
const router = useRouter();
// ...
}
next/navigation is a real module — but only in the App Router. If your project uses the Pages Router, the correct import is next/router. The AI saw both patterns in its training data and picked the wrong one for your project's architecture. This won't throw at import time in all cases, but it will produce subtle, maddening bugs where router methods behave differently than expected.
// What it should be (Pages Router)
import { useRouter } from 'next/router';
4. Wrong package name (Supabase)
// AI-generated code
import { createClient } from '@supabase/supabase';
const supabase = createClient(
process.env.SUPABASE_URL,
process.env.SUPABASE_ANON_KEY
);
The package is @supabase/supabase-js, not @supabase/supabase. The AI dropped the -js suffix because it's "obvious" that a JavaScript project would use the JavaScript client. Except npm doesn't do obvious — it does exact string matching. npm install @supabase/supabase will either fail or install something you didn't intend.
// What it should be
import { createClient } from '@supabase/supabase-js';
5. Deprecated API (Node.js)
// AI-generated code
const url = require('url');
function parseCallback(callbackUrl) {
const parsed = url.parse(callbackUrl);
return {
hostname: parsed.hostname,
pathname: parsed.pathname,
query: parsed.query
};
}
url.parse() has been deprecated since Node 11. It still works — for now — but it has known security vulnerabilities around URL parsing edge cases, and it will be removed in a future Node version. The correct replacement is the WHATWG URL constructor.
// What it should be
function parseCallback(callbackUrl) {
const parsed = new URL(callbackUrl);
return {
hostname: parsed.hostname,
pathname: parsed.pathname,
query: parsed.searchParams.toString()
};
}
The pattern is consistent: AI models generate imports based on what sounds right, not what is right. They blend similar APIs, invent plausible function names, and default to whatever version of a library was most common in their training data — which may be three major versions behind what you're running.
Why Your Linter Won't Save You
The first thing developers say when they hear about phantom imports: "ESLint would catch that." No, it wouldn't. Here's why.
ESLint checks syntax, not existence. The import/no-unresolved rule from eslint-plugin-import does check whether a module can be resolved, but it requires explicit configuration, doesn't cover dynamic imports, and is frequently disabled in projects because it conflicts with path aliases, monorepo setups, and bundler-specific resolution.
TypeScript catches some — but not all. If you're using TypeScript with strict mode, it will flag imports from packages that don't have type definitions. But it won't catch a wrong export name from a package that uses any types, it won't catch deprecated APIs unless the type definitions explicitly mark them, and it does nothing for Python, Ruby, Go, or any other language.
Test suites import the same phantoms. This is the really insidious one. If the AI generates your implementation code and your test code, the tests will import the same phantom modules. The tests fail for the same reason the code fails, but the error message says "module not found" — not "this function doesn't exist in this library." You end up debugging the test runner instead of the actual bug.
// AI-generated test file
import { deepClone } from 'lodash'; // Same phantom import
import { mergeObjects } from '../utils';
describe('mergeObjects', () => {
it('should deep merge two objects', () => {
const a = { nested: { value: 1 } };
const b = { nested: { other: 2 } };
const result = mergeObjects(a, b);
const expected = deepClone(a); // Crashes here too
// ...
});
});
Your test suite doesn't protect you from phantom imports. It amplifies them.
How Thuban Catches Phantom Imports
Thuban doesn't ask an LLM whether an import looks valid. It checks against reality. Here's what happens when you run a scan:
1. Static extraction. Thuban parses every file in your project and extracts every import, require, and from ... import statement. No regex hacks — proper AST-level parsing that handles dynamic imports, re-exports, and barrel files.
2. Package verification. For third-party imports, Thuban checks whether the package actually exists in your node_modules directory (or the equivalent for your language). If you import from @supabase/supabase but your package.json lists @supabase/supabase-js, that's a phantom.
3. Export surface verification. Even if the package exists, Thuban checks whether the specific export you're importing is real. lodash is installed? Good. Does it export deepClone? No. Flagged.
4. Deprecation detection. Thuban cross-references API calls against known deprecation lists. url.parse() still resolves, but it's been deprecated since Node 11 and has known security issues. That gets flagged as a risk, not a crash — but you still need to know about it.
5. Context-aware analysis. Importing useRouter from next/navigation is valid in an App Router project. It's a phantom in a Pages Router project. Thuban checks your project's actual configuration to determine which one you're using.
Key difference: Linters check whether your code is syntactically valid. Thuban checks whether the things your code references actually exist in the real world. Syntax vs. reality. One is necessary. The other is what actually prevents production crashes.
How to Scan Your Own Code
One command. No install. No config file. No cloud account.
npx thuban scan .
That's it. Thuban scans your current directory, checks every import against your installed packages and known API surfaces, and outputs a report. Here's what it looks like on a real project:
$ npx thuban scan .
Thuban v0.5.0 — Scanning 847 files...
PHANTOM IMPORT src/utils/data.js:3
import { deepClone } from 'lodash'
'deepClone' is not exported by 'lodash'.
Did you mean: cloneDeep?
PHANTOM IMPORT src/api/handler.py:1
from flask import jsonify_response
'jsonify_response' is not exported by 'flask'.
Did you mean: jsonify?
WRONG PACKAGE src/lib/supabase.ts:1
import { createClient } from '@supabase/supabase'
Package '@supabase/supabase' not found in node_modules.
Did you mean: @supabase/supabase-js?
DEPRECATED src/auth/callback.js:14
url.parse(callbackUrl)
url.parse() deprecated since Node 11.
Use: new URL(callbackUrl)
PHANTOM IMPORT src/pages/dashboard.tsx:2
import { useRouter } from 'next/navigation'
Project uses Pages Router (next.config.js).
Use: import { useRouter } from 'next/router'
——————————————————————————————
5 issues found · 4 phantom imports · 1 deprecated API
Scan completed in 1.8s
Every issue includes the file, line number, what's wrong, and what the fix should be. No ambiguity. No "this might be a problem." Either the import resolves or it doesn't.
The Cost of Not Catching Them
Phantom imports aren't just a nuisance. They have real, measurable costs.
Production crashes. The most obvious one. A phantom import that slips through CI will crash the moment that code path executes. If it's in an error handler or a rarely-used admin endpoint, it might not crash for weeks — until the one time you actually need that code to work.
Wasted debugging time. When deepClone throws a "not a function" error, you don't immediately think "the AI hallucinated this import." You think lodash is broken, or your bundler is misconfigured, or there's a version mismatch. You spend 45 minutes on Stack Overflow before you realise the function simply doesn't exist.
False confidence in test suites. If your tests import the same phantom modules as your source code, your test suite gives you a green checkmark right up until deployment. The tests don't fail because the import is wrong — they fail because the module isn't found. But if you're running tests in an environment where the module resolution is lenient (mocked, stubbed, or bundled differently), the tests pass and the production build doesn't.
Compounding errors. One phantom import in a utility file gets imported by twelve other files. Now you have thirteen files with the same phantom dependency. The AI keeps using the phantom in new code because it sees it in the existing codebase. The hallucination becomes self-reinforcing.
We've seen codebases with 50+ phantom imports that had been accumulating for months. Every one was generated by an AI tool. Every one passed code review. The developers assumed the imports were valid because the code "looked right" and the AI had written it with confidence.
Integrating Into Your Workflow
Running Thuban manually is fine for a one-off audit. For ongoing protection, add it to your CI pipeline:
# .github/workflows/thuban.yml
name: Thuban Scan
on: [push, pull_request]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- run: npx thuban scan . --ci
The --ci flag makes Thuban exit with a non-zero code if it finds any phantom imports, which fails the build. No phantom import gets merged into main. Ever.
You can also run it as a pre-commit hook if you want to catch phantoms before they even reach a pull request:
# package.json
{
"scripts": {
"precommit": "npx thuban scan . --ci"
}
}
The Bottom Line
AI coding tools are genuinely useful. They write code faster than you can type it. But they also invent imports with the same confidence they use to write correct code. There's no hesitation, no "I'm not sure about this one." The hallucinated import looks identical to the real one.
Your linter won't catch it. Your type checker might catch some of it. Your test suite will catch it only if you're lucky. The only reliable approach is to verify every import against the actual packages installed in your project — which is exactly what Thuban does.
Don't trust the AI's confidence. Check the imports.
Find phantom imports in your codebase
npx thuban scan .
Zero install. Zero config. Zero cloud. Checks against reality, not opinion.
thuban.dev — Free for up to 100 files, 5 scans/month
Craig is the founder of Thuban and has personally debugged more phantom imports than he'd like to admit. If your AI writes the code, something needs to check the damage.