getNonBoundSourceFile vs Full TypeChecker Mode in TSSLint: Rule Execution and Performance
TSSLint executes rules in either a fast syntax-only mode using getNonBoundSourceFile or a slower type-aware mode with full TypeChecker access, automatically retrying with the latter when rules attempt to access program information.
TSSLint is an open-source TypeScript linting framework that optimizes performance by distinguishing between lightweight syntactic analysis and comprehensive type-checking. Understanding the difference between getNonBoundSourceFile and full TypeChecker mode is essential for writing efficient rules and diagnosing performance characteristics in the johnsoncodehk/tsslint repository.
How getNonBoundSourceFile Works in TSSLint
In Non-Bound Source File Mode, TSSLint obtains the target file through LanguageService.getNonBoundSourceFile. This method returns a syntactic SourceFile that is not bound to a Program, meaning it contains no type-checker information, symbol resolution, or type relationships.
According to the implementation in packages/core/index.ts, when operating in this mode, the RuleContext is constructed with a program getter that deliberately throws an error with the message "Not supported". This design signals to rule authors that they are operating in syntax-only mode and cannot access type information.
This mode is used by default because the TypeScript language service can parse files without invoking the full type checker, significantly reducing CPU and memory overhead for large projects.
Full TypeChecker Mode and Program Access
Full TypeChecker Mode activates when rules require type information. In this mode, TSSLint retrieves the SourceFile from the real Program via languageService.getProgram().getSourceFile(). This file is fully bound and type-checked, enabling complete symbol and type queries.
When operating in this mode, RuleContext.program provides a live ts.Program instance. Rule authors can call program.getTypeChecker() to obtain the TypeChecker, then use methods like getTypeAtLocation(), getSymbolAtLocation(), or getTypeFromTypeNode() to analyze type relationships.
The performance cost is significant: invoking the full TypeScript compiler engine requires binding symbols, checking types, and resolving module dependencies, which can be expensive for many files or complex type hierarchies.
Automatic Mode Detection and Retry Logic
TSSLint automatically detects when a rule requires type information and switches modes accordingly. The core lint loop in packages/core/index.ts (lines 64-70) implements this logic:
const typeAwareMode = !getNonBoundSourceFile
|| shouldEnableTypeAware && !Object.keys(rules).some(ruleId => !rule2Mode.has(ruleId));
The execution flow works as follows:
- Initial Pass: TSSLint starts with
typeAwareModeset tofalse(ifgetNonBoundSourceFileis available), using the fast non-bound source file. - Error Detection: If a rule attempts to access
ctx.programand throws (or otherwise fails), the catch block records the rule as type-aware usingrule2Mode.set(currentRuleId, true). - Retry Mechanism: The system sets
shouldRetry = true. After the current pass completes, TSSLint retries the file withshouldEnableTypeAware = true, switching the entire linting run to full TypeChecker mode for that file.
This transparent retry mechanism allows rule authors to write type-aware rules without manually configuring execution modes, while maintaining optimal performance for syntax-only rules.
Writing Rules for Each Mode
Syntax-Only Rules (Non-Bound Mode)
Rules that operate on AST structure alone should avoid accessing ctx.program. These rules work efficiently in non-bound mode:
// fixtures/noConsoleRule.ts
export default {
'no-console': (ctx) => {
const { file, report } = ctx;
// Simple AST walk – no type information needed
const walk = (node: ts.Node) => {
if (ts.isCallExpression(node) && node.expression.getText() === 'console.log') {
const { line, character } = file.getLineAndCharacterOfPosition(node.getStart());
report(`Avoid console.log`, node.getStart(), node.getEnd())
.withFix('Remove console.log', () => [
{
fileName: file.fileName,
textChanges: [{ span: { start: node.getStart(), length: node.getWidth() }, newText: '' }],
},
]);
}
ts.forEachChild(node, walk);
};
walk(file);
},
};
This rule inspects the syntax tree for console.log calls without requiring type resolution, allowing it to run in the fast non-bound mode.
Type-Aware Rules (Full TypeChecker Mode)
Rules requiring type information must access ctx.program. When they do, TSSLint automatically promotes execution to full type-checker mode:
// fixtures/typeAwareRule.ts
export default {
'no-any': (ctx) => {
const { program, file, report } = ctx; // `program` is only defined in type-aware mode
const checker = program.getTypeChecker();
const walk = (node: ts.Node) => {
if (ts.isVariableDeclaration(node) && node.type) {
const type = checker.getTypeFromTypeNode(node.type);
if (type.flags & ts.TypeFlags.Any) {
report('Avoid using "any" type', node.type.getStart(), node.type.getEnd())
.asError();
}
}
ts.forEachChild(node, walk);
};
walk(file);
},
};
When this rule first executes, it attempts to access program. In non-bound mode, this throws an error, triggering TSSLint's retry mechanism. On the second pass, program contains a valid ts.Program instance with getTypeChecker() available, allowing the rule to analyze type annotations.
Summary
- Non-Bound Source File Mode uses
getNonBoundSourceFileto provide fast, syntax-only analysis without type-checker overhead, but hidesRuleContext.programto prevent type access. - Full TypeChecker Mode provides a bound
SourceFilefromlanguageService.getProgram(), exposing a livets.Programfor symbol and type queries at the cost of increased CPU and memory usage. - Automatic Detection: TSSLint defaults to fast mode but automatically retries with full type-checking when rules throw errors accessing
ctx.program, transparently handling mode switching inpackages/core/index.ts. - Performance Impact: Non-bound mode is significantly faster for large projects, while full mode is necessary for rules that validate type relationships or symbol semantics.
Frequently Asked Questions
What happens if a rule tries to access program in non-bound mode?
If a rule attempts to access ctx.program while running in non-bound source file mode, the getter throws an error with the message "Not supported". TSSLint catches this error in the core lint loop, marks the rule as requiring type-aware mode using rule2Mode.set(currentRuleId, true), and automatically retries the linting pass with full TypeChecker access enabled.
How does TSSLint decide which mode to use initially?
TSSLint checks for the availability of getNonBoundSourceFile on the language service. If the method exists and no rules have been previously flagged as type-aware during the current session, TSSLint starts in non-bound mode for optimal performance. The decision logic resides in packages/core/index.ts, where the boolean typeAwareMode is computed based on the presence of getNonBoundSourceFile and the shouldEnableTypeAware flag.
Can I force a rule to always run in full TypeChecker mode?
Currently, TSSLint does not provide a static configuration option to force type-aware mode. Instead, it uses runtime detection: when a rule throws due to accessing program in non-bound mode, TSSLint records this requirement in the rule2Mode map and ensures subsequent executions use the full TypeChecker. This design eliminates manual configuration while maintaining performance for syntax-only rules.
What is the performance difference between the two modes?
Non-bound source file mode is significantly faster because it performs only syntactic parsing without binding symbols or checking types. Full TypeChecker mode invokes the complete TypeScript compiler engine, including symbol resolution, type inference, and relationship checking, which incurs substantial CPU and memory overhead—especially noticeable in large projects with complex type hierarchies. TSSLint defaults to the faster mode to maximize throughput for syntax-only linting tasks.
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 →