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

> Discover how TSSLint differentiates syntax vs type-aware rules with smart caching. Learn about its efficient two-pass linting process to improve performance.

- Repository: [Johnson Chu/tsslint](https://github.com/johnsoncodehk/tsslint)
- Tags: internals
- Published: 2026-03-04

---

**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`](https://github.com/johnsoncodehk/tsslint/blob/main/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`](https://github.com/johnsoncodehk/tsslint/blob/master/packages/core/index.ts#L53), 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`](https://github.com/johnsoncodehk/tsslint/blob/main/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`](https://github.com/johnsoncodehk/tsslint/blob/master/packages/core/index.ts#L65-L66), 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`](https://github.com/johnsoncodehk/tsslint/blob/master/packages/core/index.ts#L31-L36) 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`](https://github.com/johnsoncodehk/tsslint/blob/main/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`](https://github.com/johnsoncodehk/tsslint/blob/master/packages/cli/lib/cache.ts#L6-L27), 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)

```typescript
// 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)

```typescript
// 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`](https://github.com/johnsoncodehk/tsslint/blob/master/packages/core/index.ts#L31), 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`](https://github.com/johnsoncodehk/tsslint/blob/main/packages/core/index.ts) only caches diagnostics when `!rule2Mode.get(currentRuleId)`.
- Cache persistence in [`packages/cli/lib/cache.ts`](https://github.com/johnsoncodehk/tsslint/blob/main/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`](https://github.com/johnsoncodehk/tsslint/blob/master/packages/core/index.ts#L31) 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`](https://github.com/johnsoncodehk/tsslint/blob/master/packages/core/index.ts#L22) 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`](https://github.com/johnsoncodehk/tsslint/blob/main/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.