How to Handle False Positives in Type-Aware TSSLint Rules Using Cache Invalidation

False positives in type-aware TSSLint rules occur when cached diagnostics persist after auto-fixes modify the TypeScript program state, which TSSLint resolves by invalidating the per-file cache whenever a rule reports a fixable diagnostic.

Type-aware linting in TSSLint 2 runs rules against the TypeScript type-checker, making it powerful but computationally expensive. To optimize performance, TSSLint maintains an aggressive caching layer that stores lint results per file. However, when a rule's auto-fix changes the source code, the underlying program's type information shifts, potentially rendering cached diagnostics stale and causing persistent false positives. Understanding the cache invalidation mechanism in the johnsoncodehk/tsslint repository is essential for maintaining accurate lint results across fix iterations.

Why False Positives Occur in Type-Aware Rules

Type-aware rules depend on the TypeScript program's type-checker state, including symbol tables and type resolutions. When a rule applies an auto-fix, it modifies the source file's AST, which changes the type information for subsequent analysis. If the linter continues to serve diagnostics from the cache entry created before the fix, it reports errors based on the stale program state. This mismatch between the current code and the cached type-checker results manifests as false positives that persist across lint runs.

The Cache Architecture in TSSLint

TSSLint implements a multi-layered caching strategy across its core and CLI packages to balance performance with accuracy.

Core Linter Cache Structure

In packages/core/index.ts, the core linter maintains per-file cache entries using a tuple named FileLintCache with the structure [mtime, lintResult, minimatchResult]. The lintResult object (accessed via cache[1]) stores diagnostic results keyed by ruleId, while minimatchResult (cache[2]) tracks file matching results.

When a rule reports a diagnostic, the linter records it in cache[1][ruleId]. Crucially, if the diagnostic includes a fix, the system sets a hasFix flag at cache[1][ruleId][0]. The linter monitors this flag to determine when cache contents have become potentially invalid due to program mutations.

CLI Worker Cache Synchronization

The CLI worker in packages/cli/lib/worker.ts acts as a thin wrapper around the core linter. After a fix is applied, the worker performs its own cache cleanup by setting cache[fileName] = undefined. This ensures that the next iteration of the linting process starts from a clean state, preventing stale diagnostics from propagating through the worker's lifecycle.

Persistent Cache Layer

For cross-session performance, packages/cli/lib/cache.ts implements loadCache and saveCache functions. The loadCache function reads a JSON cache file named with a hash of the project configuration, while saveCache persists the updated cache after linting completes. The CLI entry point in packages/cli/index.ts orchestrates this lifecycle, tracking cached hits and ensuring only clean state reaches the disk.

Cache Invalidation Strategy

TSSLint employs a deterministic invalidation protocol triggered by fix detection rather than file modification time alone.

Detecting Fixes and Triggering Invalidation

Each rule's report function may return a fixer function. When present, TSSLint internally sets hasFix = true for that rule's cache entry. The core linter checks these flags after each lint pass. As implemented in packages/core/index.ts at lines 160-164 and 182-186, if any rule reports a fix, the linter immediately executes:

cache[1] = {}; // Clear lintResult
cache[2] = {}; // Clear minimatchResult

This aggressive reset forces a fresh type-checker run for the next lint pass, ensuring diagnostics reflect the updated program state.

Per-File Cache Reset Mechanism

The invalidation occurs at the file level. When the core linter resets cache[1] and cache[2], it effectively wipes all stored diagnostics and matching results for that specific file. The CLI worker mirrors this by nullifying the entire file entry in the shared cache object. This dual-layer approach ensures that neither the in-memory session cache nor the worker's reference retains stale data.

Implementing Rules with Proper Cache Handling

When authoring type-aware rules, explicitly marking diagnostics as fixable ensures TSSLint's cache invalidation kicks in automatically:

import { Rule } from '@tsslint/core';

export const myRule: Rule = (ctx) => ({
  name: 'my-rule',
  meta: { 
    type: 'problem', 
    fixable: true, 
    typeAware: true 
  },
  create(context) {
    return {
      CallExpression(node) {
        // Type-aware analysis using context.program.getTypeChecker()
        const type = context.program.getTypeChecker().getTypeAtLocation(node);
        
        if (needsFix(type)) {
          context.report({
            node,
            messageId: 'avoidBadCall',
            // Providing a fix trigger cache invalidation
            fix: (fixer) => fixer.replaceText(node, 'goodCall()'),
          });
        }
      },
    };
  },
});

The presence of the fix property in the report object signals the core linter to set the hasFix flag, which initiates the cache clearing sequence described above.

Practical Workflow Example

To eliminate false positives during development:


# Initial lint run populates the cache

tsslint

# Apply fixes - this triggers cache invalidation for modified files

tsslint --fix

# Subsequent run uses fresh type-checker state, false positives eliminated

tsslint

When tsslint --fix executes, the CLI worker clears the cache entry for any file receiving a fix. The next invocation of tsslint forces a fresh type-aware analysis with updated program information.

Summary

  • False positives in type-aware rules stem from cached diagnostics referencing stale type-checker state after auto-fixes modify source code.
  • TSSLint stores per-file results in a FileLintCache tuple ([mtime, lintResult, minimatchResult]) within packages/core/index.ts.
  • Cache invalidation triggers when any rule sets the hasFix flag, causing the linter to reset cache[1] and cache[2] for that file.
  • The CLI worker in packages/cli/lib/worker.ts synchronizes this by nullifying the file's cache entry after fixes.
  • Persistent caching in packages/cli/lib/cache.ts preserves only valid state between CLI invocations.

Frequently Asked Questions

What causes false positives in type-aware linting?

False positives occur when a rule's auto-fix changes the source code, altering the TypeScript program's type information, but the linter continues to report diagnostics from the cache entry generated before the fix. Since the cached diagnostics reference the old program state while the code has evolved, the linter reports errors that no longer apply to the current AST.

How does TSSLint detect when to invalidate the cache?

TSSLint monitors the hasFix flag stored in the cache tuple. When a rule's report method includes a fix property, the core linter sets this flag for that rule's entry. After processing, if any rule has hasFix set to true, TSSLint invalidates the entire per-file cache by resetting the lintResult and minimatchResult objects, forcing fresh analysis on the next pass.

Can I manually clear the cache if false positives persist?

Yes. While TSSLint automatically handles cache invalidation during fix cycles, you can force a full refresh by deleting the persistent cache file. The cache file is located in your project directory with a name derived from a hash of your configuration, managed by packages/cli/lib/cache.ts. Alternatively, modifying the file's content to change its mtime invalidates the entry, though letting the automatic hasFix mechanism handle this is the recommended approach.

Does cache invalidation affect performance?

Cache invalidation occurs only for files that receive fixes, minimizing performance impact. Since type-aware linting is expensive due to type-checker initialization, TSSLint preserves cache entries for unchanged files. The invalidation resets only the specific file's results (cache[1] and cache[2]), not the entire project cache, ensuring that subsequent lint runs remain fast for unaffected files while guaranteeing accuracy for modified code.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →