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:
- Syntax-only pass: Uses a "non-bound"
SourceFilecontaining only the AST, without a fullPrograminstance. - Type-aware pass: Executes only if required, creating a complete
Programwith 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 = trueto 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 resultssaveCache: 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
SourceFilewithout type information and have their diagnostics cached inrule2Mode. - Type-aware rules are detected at runtime when they throw errors accessing
programduring the first pass. - The
report()function inpackages/core/index.tsonly caches diagnostics when!rule2Mode.get(currentRuleId). - Cache persistence in
packages/cli/lib/cache.tsserializes only syntax-only rule results to disk. - This dual-pass approach optimizes performance while ensuring type-dependent rules receive the full
Programcontext 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →