# getNonBoundSourceFile vs Full TypeChecker Mode in TSSLint: Rule Execution and Performance

> Understand TSSLint's getNonBoundSourceFile vs full TypeChecker mode for rule execution and performance. Learn how TSSLint optimizes rule checks for faster development.

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

---

**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`](https://github.com/johnsoncodehk/tsslint/blob/main/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`](https://github.com/johnsoncodehk/tsslint/blob/main/packages/core/index.ts) (lines 64-70) implements this logic:

```typescript
const typeAwareMode = !getNonBoundSourceFile
    || shouldEnableTypeAware && !Object.keys(rules).some(ruleId => !rule2Mode.has(ruleId));

```

The execution flow works as follows:

1. **Initial Pass**: TSSLint starts with `typeAwareMode` set to `false` (if `getNonBoundSourceFile` is available), using the fast non-bound source file.
2. **Error Detection**: If a rule attempts to access `ctx.program` and throws (or otherwise fails), the catch block records the rule as type-aware using `rule2Mode.set(currentRuleId, true)`.
3. **Retry Mechanism**: The system sets `shouldRetry = true`. After the current pass completes, TSSLint retries the file with `shouldEnableTypeAware = 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:

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

```typescript
// 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 `getNonBoundSourceFile` to provide fast, syntax-only analysis without type-checker overhead, but hides `RuleContext.program` to prevent type access.
- **Full TypeChecker Mode** provides a bound `SourceFile` from `languageService.getProgram()`, exposing a live `ts.Program` for 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 in [`packages/core/index.ts`](https://github.com/johnsoncodehk/tsslint/blob/main/packages/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`](https://github.com/johnsoncodehk/tsslint/blob/main/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.