How TSSLint Rule Caching Differentiates Between Syntax-Aware and Type-Aware Rules

TSSLint runs every rule in a fast syntax-only mode first, caches results only for rules that succeed without type information, and automatically retries with a full TypeScript Program only when a rule throws an error accessing type data.

TSSLint is an open-source TypeScript linter that optimizes performance through intelligent TSSLint rule caching. The system distinguishes between rules that analyze pure syntax (AST-only) and those requiring type information, ensuring that expensive type-checking operations are avoided when unnecessary while maintaining correctness for rules that depend on the TypeScript type system.

The Two-Pass Execution Model

TSSLint implements a dual-pass strategy to minimize performance overhead. Each file is linted at most twice per session:

  1. Syntax-only pass: Uses a "non-bound" SourceFile containing only the AST, without a full Program instance.
  2. Type-aware pass: Executes only if required, creating a complete Program with access to the type checker.

This approach ensures that lightweight, AST-based rules run quickly and cache their results, while heavier type-aware rules trigger a retry only when explicitly needed.

Core Caching Mechanics

The caching system tracks each rule's requirements using a runtime state machine defined in packages/core/index.ts.

Rule Mode Tracking

A Map<string, boolean> named rule2Mode stores the execution requirement for each rule identifier. According to the source at packages/core/index.ts:53, a value of false indicates syntax-only capability, while true marks the rule as type-aware.

Conditional Caching in report()

Diagnostics are cached only when a rule operates in syntax-only mode. The report() function applies a critical guard at lines 22-23 of packages/core/index.ts, storing diagnostics only when !rule2Mode.get(currentRuleId) evaluates to true. Rules marked as type-aware bypass the cache write entirely.

Mode Selection Logic

At packages/core/index.ts:65-66, TSSLint determines whether to enable type-aware mode for a file based on two conditions:

  • A previous rule already triggered the type-aware requirement (shouldEnableTypeAware)
  • The specific rule is already known to require type information from prior executions

Error-Driven Type Detection

TSSLint uses a try-catch mechanism to detect type dependencies dynamically. When typeAwareMode is false and a rule throws an exception—typically because it attempted to access program.getTypeChecker() on an undefined program—the engine interprets this as a signal that the rule requires type information.

The error handling logic at packages/core/index.ts:31-36 performs three actions:

  • Catches the runtime error
  • Sets rule2Mode.set(currentRuleId, true) to mark the rule as type-aware
  • Sets shouldRetry = true to schedule a full re-lint with type information

Diagnostics from the failed syntax-only pass are discarded and not cached.

Cache Persistence

The CLI layer handles serialization of valid cache entries to disk. Located in packages/cli/lib/cache.ts, the system uses two functions:

  • loadCache: Deserializes previous results
  • saveCache: Writes cache entries for syntax-only rules only

The cache structure follows Record<string, core.FileLintCache>, where each file maps to rule entries containing [hasFix, diagnostics] tuples. As implemented in packages/cli/lib/cache.ts:6-27, only rules with rule2Mode === false persist across sessions.

Practical Implementation Examples

The following examples demonstrate how TSSLint distinguishes between cacheable syntax rules and non-cacheable type-aware rules.

Syntax-Only Rule (Cached)

// myRule.ts
export const myRule = (ctx: RuleContext) => {
  const { file, report } = ctx;
  const text = file.getFullText();
  
  if (text.includes('eval(')) {
    report('Avoid eval()', 0, 4);
  }
};

This rule runs successfully in the first pass. Because it does not access program, TSSLint marks it as syntax-only (rule2Mode.set('myRule', false)) and caches its diagnostics.

Type-Aware Rule (Not Cached in First Pass)

// typeAwareRule.ts
export const typeAwareRule = (ctx: RuleContext) => {
  const { program, file, report } = ctx;
  
  // Throws in syntax-only mode because program is undefined
  const checker = program.getTypeChecker();
  const type = checker.getTypeAtLocation(file);
  
  if (type.flags & TypeFlags.Any) {
    report('Unexpected any type', 0, file.getFullText().length);
  }
};

When executed during the first (syntax-only) lint, this rule throws because program is unavailable. TSSLint catches the error at packages/core/index.ts:31, marks rule2Mode.set('typeAwareRule', true), and schedules a retry with typeAwareMode = true. No cache entry is created for this rule during the initial failed pass.

Summary

  • Syntax-only rules run against a lightweight SourceFile without type information and have their diagnostics cached in rule2Mode.
  • Type-aware rules are detected at runtime when they throw errors accessing program during the first pass.
  • The report() function in packages/core/index.ts only caches diagnostics when !rule2Mode.get(currentRuleId).
  • Cache persistence in packages/cli/lib/cache.ts serializes only syntax-only rule results to disk.
  • This dual-pass approach optimizes performance while ensuring type-dependent rules receive the full Program context they require.

Frequently Asked Questions

How does TSSLint know if a rule needs type information?

TSSLint initially assumes all rules are syntax-only. If a rule throws an exception during the first pass—typically by attempting to access program.getTypeChecker() when program is undefined—the engine catches this error at packages/core/index.ts:31 and marks the rule as type-aware in the rule2Mode map. The file is then re-linted with a full TypeScript Program.

Why are type-aware rule results not cached during the first pass?

The first pass executes without a complete Program or type checker. If a rule fails because it requires type information, the diagnostics generated during this incomplete run would be incorrect or partial. TSSLint deliberately skips caching via the guard at packages/core/index.ts:22 for any rule where rule2Mode.get(currentRuleId) returns true, ensuring only valid, complete results persist.

Where does TSSLint store the cache data?

The CLI module in packages/cli/lib/cache.ts manages disk persistence using loadCache and saveCache functions. The cache stores a record mapping file paths to FileLintCache objects, where each entry contains a tuple of [hasFix, diagnostics] per rule. This structure allows incremental builds to skip re-linting unchanged files for syntax-only rules.

Can a rule switch from type-aware back to syntax-only?

No, once a rule is marked as type-aware in the rule2Mode map, it remains type-aware for the duration of the session. This immutable classification ensures that subsequent file processing immediately allocates the necessary Program resources rather than attempting the syntax-only pass that is guaranteed to fail.

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 →