AI Keeps Using Deprecated Syntax Despite Lint Errors

AI generates code with deprecated APIs, fixes one lint error, then re-introduces the same pattern in the next file. Pin the rule and ground the prompt.

You banned componentWillMount two years ago. ESLint flags it. The codebase has zero usages. Cursor generates a new component and quietly uses componentWillMount. You point out the lint error, the AI says “good catch, fixing now” and rewrites that one file. Three prompts later, in an unrelated component, componentWillMount is back. The AI sees the lint rule, agrees, then forgets across turns. Same story with var, require() in an ESM project, useEffect cleanup patterns from React 16, pg.Pool.connect callbacks instead of promises. The pattern is universal: the AI’s training data still contains the deprecated form, lint feedback is short-lived, and your guardrails are not reaching it where it matters.

Common causes

Ordered by how often each appears in real sessions.

1. The AI trained heavily on pre-deprecation code

Most public code on GitHub still uses the old form. The model saw a million componentWillMount examples and a thousand componentDidMount ones. Probability does the rest.

How to spot it: The deprecated pattern keeps coming back across unrelated files, not just where you corrected it once.

2. Lint feedback only reaches a single turn

Cursor or Claude Code reads the lint output, fixes the one flagged line, and moves on. The next prompt starts a new conversation slice — the rule is not in long-term memory.

How to spot it: Same lint rule fires repeatedly across sessions; the “fix” only sticks within the same conversation.

3. No project-level instruction telling the AI what is banned

.cursorrules, CLAUDE.md, or system prompt does not mention the deprecation. The AI has no signal that this codebase has a stricter policy than the wider web.

How to spot it: Check your rules file — if the deprecated pattern is not listed, the AI has no reason not to use it.

4. ESLint config disables the relevant rule or sets it to “warn”

The rule exists but emits a warning, not an error. The AI’s “lint passed” check passes; the warning gets lost in noise.

How to spot it: eslint --max-warnings 0 fails on the file even though eslint alone exits 0.

5. The deprecation is project-specific (custom rule) and the AI cannot see it

You banned console.log in production code via a custom rule. The AI does not know the rule exists until it runs lint, and even then it may not understand the reason.

How to spot it: Lint output mentions a rule name the AI never references in its explanations.

6. Library upgrade silently deprecated the old form mid-project

You upgraded pg from v7 to v8 and callbacks became deprecated. The AI’s training data is split — half v7, half v8 — and it picks whichever feels right.

How to spot it: npm ls <package> shows a recent major version; the deprecated form maps to a previous major.

Before you start

  • List every deprecated pattern you currently care about. Write them down explicitly — “we never use X, always use Y”.
  • Run eslint --max-warnings 0 to confirm your rules are actually enforced as errors, not warnings.
  • Check .cursorrules or CLAUDE.md content — what is currently there.
  • Capture one example of the AI reintroducing the deprecated form, with the file and surrounding prompt.

Information to collect

  • The deprecated pattern (e.g. componentWillMount, var, require(), callback API).
  • The replacement (the right way).
  • The lint rule name that should fire (react/no-deprecated, no-var, etc.).
  • Whether the rule is enabled and at what severity in .eslintrc / eslint.config.js.
  • The contents of .cursorrules / CLAUDE.md / AGENTS.md.
  • Library version if the deprecation is library-tied.

Step-by-step fix

Ordered from quickest wins to durable policy.

Step 1: Make the lint rule a hard error, not a warning

In eslint.config.js or .eslintrc:

{
  "rules": {
    "react/no-deprecated": "error",
    "no-var": "error",
    "import/no-commonjs": "error"
  }
}

Then enforce zero warnings in CI:

eslint . --max-warnings 0

The AI’s “I ran lint, it passed” check now actually fails on the deprecated form.

Step 2: Add explicit bans to your rules file

In .cursorrules or CLAUDE.md at repo root:

## Banned patterns (do NOT generate)

- componentWillMount, componentWillReceiveProps, componentWillUpdate
  → use componentDidMount + useEffect equivalents
- var → use const, let
- require(), module.exports → use ESM import / export
- pg client callback API → use the promise / async-await form
- console.log in src/ outside of src/lib/logger.ts

If you generate any of the above, the lint check WILL fail.
Verify your code does not contain these patterns before responding.

This puts the prohibition into the AI’s working context on every turn.

Step 3: Show the AI the right pattern by example

Add a docs/conventions.md (or similar) with side-by-side examples and reference it from the rules file:

# Banned vs preferred

## React lifecycle

WRONG:
  componentWillMount() { this.fetch(); }

RIGHT:
  useEffect(() => { fetch(); }, []);

## Module system

WRONG:
  const x = require("foo");

RIGHT:
  import x from "foo";

Concrete pairs beat abstract bans. The AI patterns its output on the “RIGHT” examples.

Step 4: Run lint in a pre-commit hook

In package.json:

{
  "scripts": {
    "lint": "eslint . --max-warnings 0"
  },
  "lint-staged": {
    "*.{ts,tsx,js,jsx}": "eslint --max-warnings 0"
  }
}

And a pre-commit hook (via husky / lefthook). The AI cannot bypass commit-time enforcement.

Step 5: Add a --fix autofix where possible

Many deprecation rules have autofixes (e.g. no-var, prefer-const, prefer-arrow-callback):

eslint . --fix

Run this after every AI-generated batch. It removes the cognitive load of catching trivial regressions manually.

Step 6: Use codemods for project-wide cleanup

For larger migrations (callback → promise, class → hooks, CommonJS → ESM), use jscodeshift or ts-morph codemods:

npx jscodeshift -t ./codemods/cb-to-promise.js src/

Once codemods run, the codebase has zero examples of the deprecated form — which means the AI, doing RAG over your codebase, sees only the new pattern and starts copying that style.

Verify

  • eslint . --max-warnings 0 exits 0 with no warnings anywhere.
  • Generate three new files via AI on different prompts — none reintroduce the deprecated pattern.
  • grep -r "<deprecated-pattern>" src/ returns no hits.
  • Pre-commit hook blocks a deliberately deprecated change with a clear error message.

Long-term prevention

  • Add a “deprecations” section to your CLAUDE.md / .cursorrules and revisit it whenever you upgrade a major dependency.
  • Keep ESLint, TSC, and any custom AST linters all set to error-level for deprecated APIs — warnings are training noise that the AI ignores.
  • For each banned pattern, include the replacement in the rules file, not just the prohibition. AI is much better at “do this” than “do not do that”.
  • Periodically run grep -r "<deprecated>" src/ and clean up any that slipped through; AI tools mirror what already exists in the repo.
  • When upgrading a major library version, dedicate one PR to codemod-driven cleanup of the deprecated API across the whole codebase. Mixed states confuse both humans and AI.
  • Document the why of each deprecation briefly. AI grounds better on “componentWillMount is unsafe in concurrent mode” than on a flat ban.

Common pitfalls

  • Leaving the rule at “warn” severity. The AI sees a passing lint check and considers itself done.
  • Adding the ban to your personal notes but never to the rules file the AI reads.
  • Fixing the deprecated pattern in one file and not running grep for the rest — usually there are 5-10 more lurking.
  • Assuming the AI “learns” from the correction within a session. It does not persist across sessions, only across turns of the same conversation.
  • Mixing deprecated and modern patterns in the same file. The AI picks whichever style appears nearby and propagates it.

For related issues see AI suggests a stale dependency, AI-introduced TypeScript errors, and Claude Code missing project context.

FAQ

Q: I keep correcting it and it keeps doing the same thing. Am I doing something wrong?

The correction only persists within one conversation. Move the rule into .cursorrules / CLAUDE.md so every new session inherits it. In-chat reminders are short-term memory only.

Q: The lint rule is enabled but the AI still generates the deprecated form. Why?

The AI writes the code, then runs lint, then “fixes” only that file in that turn. If you accept the file as-is before lint runs, or run lint manually later, the deprecated form ships. Make lint a hard pre-commit gate.

Q: Should I just delete the deprecated API from the library?

If you control the library, yes — that is the cleanest fix. If it is a third-party library, you cannot. Use the rules file and lint enforcement instead.

Q: How do I get the AI to suggest the modern pattern proactively, not just avoid the old one?

Pair every ban with a concrete RIGHT example in your rules / docs. AI mimics positive patterns far better than it respects abstract prohibitions.

Tags: #Troubleshooting #AI coding #deprecation #eslint #linting