How TSSLint’s Related Information Feature Captures Stack Traces for One‑Click Rule Source Navigation

TSSLint enriches every lint diagnostic with DiagnosticRelatedInformation objects that point directly to the source location where a rule was defined, enabling editors to render clickable "Go to rule source" links.

The johnsoncodehk/tsslint repository implements a sophisticated pipeline that transforms runtime JavaScript stack traces into editor-navigable metadata. By leveraging the TypeScript language service’s diagnostic API, TSSLint allows developers to jump from a lint warning immediately to the implementation of the rule that raised it.

The flow operates in three distinct stages: capturing the error stack, converting frames to TypeScript’s related-information format, and lazily attaching that data to diagnostics.

Stage 1: Capturing the Error Stack

When a rule throws an exception or when TSSLint needs to record the rule’s definition site, it instantiates an Error and parses its stack trace using the error-stack-parser library. This extracts structured frames containing file names, line numbers, and column numbers.

In packages/typescript-plugin/index.ts, the plugin parses the error at lines 177–179:

const stacks = ErrorStackParser.parse(err);
const relatedInfo = createRelatedInformation(ts, stacks[stackIndex]);

The stackIndex parameter allows rule authors to specify which frame in the stack represents the relevant rule source—typically skipping internal TSSLint frames to point at the user’s rule implementation.

Stage 2: Converting Stack Frames to DiagnosticRelatedInformation

The createRelatedInformation function (defined at lines 215–255 in packages/typescript-plugin/index.ts) transforms a single StackFrame into a ts.DiagnosticRelatedInformation object. This process involves several precise steps:

  1. File path normalization: Converts file:// URLs to absolute paths and standardizes separators.
  2. File caching: Checks the file’s modification time against an in-memory cache (fsFiles) to avoid redundant disk reads. If the file changed or is uncached, it reads the content and creates a TypeScript SourceFile.
  3. Position calculation: Uses SourceFile.getPositionOfLineAndCharacter to convert the 1-based line and column numbers from the stack trace into the 0-based character offset required by TypeScript diagnostics.

The resulting object contains the file, start, and length properties that editors use to render navigation links.

Stage 3: Lazy Resolution in the Reporter

To avoid the overhead of stack parsing for diagnostics that editors might never inspect, TSSLint employs lazy evaluation. In packages/core/index.ts (lines 12–14 and 44–46), the Reporter class stores the error and stack index, but only invokes getRelatedInformations when the diagnostic’s relatedInformation getter is accessed.

const error: ts.DiagnosticWithLocation = {
    // ... other properties ...
    get relatedInformation() {
        return relatedInformation ??= getRelatedInformations(location[0], location[1]);
    },
};

This pattern ensures that the expensive file I/O and source mapping only occur when a user actually opens the diagnostic details in their IDE.

Code Implementation Details

The integration between the core linter and the TypeScript plugin relies on two key functions. The createLinter function in packages/core/index.ts accepts a getRelatedInformations callback, which the Reporter invokes lazily. Meanwhile, the TypeScript plugin in packages/typescript-plugin/index.ts provides the concrete implementation that parses stacks and builds the related information objects.

When diagnostics are transported from the worker process to the editor (in packages/cli/lib/worker.ts, lines 286–304), the relatedInformation array is serialized and reconstructed, preserving the file paths and positions that enable one-click navigation.

Practical Example: Attaching Stack Information in a Custom Rule

Rule authors can opt into stack trace capture by calling the .at() method on the reporter. This example demonstrates how to link a diagnostic directly to the rule implementation:

export const noFoo: Rule = (node, ctx) => {
    if (node.getText() === 'foo') {
        ctx.report('Avoid using "foo".')
           .at(new Error('noFoo triggered'), 1)  // Capture stack, use frame index 1
           .withFix('Rename to bar', () => [{ 
               fileName: ctx.file.fileName, 
               textChanges: [{ 
                   span: { start: node.getStart(), length: node.getWidth() }, 
                   newText: 'bar' 
               }] 
           }]);
    }
};

The .at(new Error(), 1) call stores the error and specifies that the first user frame (index 1, skipping the frame inside TSSLint itself) should be used for the related information. When the diagnostic appears in VS Code, the "Related Information" section displays a clickable link that jumps to the exact line in the rule file where ctx.report was invoked.

Summary

  • TSSLint Related Information transforms JavaScript stack traces into DiagnosticRelatedInformation objects that editors render as clickable navigation links.
  • The pipeline uses error-stack-parser to extract file, line, and column data from Error stacks in packages/typescript-plugin/index.ts.
  • createRelatedInformation normalizes paths, caches source files, and converts line/column numbers to character offsets using TypeScript’s SourceFile API.
  • Lazy evaluation in packages/core/index.ts ensures stack parsing only occurs when the editor actually requests relatedInformation.
  • Rule authors trigger capture via the .at(error, stackIndex) method on the reporter, enabling one-click jumps from lint warnings to rule implementations.

Frequently Asked Questions

How does TSSLint capture the source location of a rule without hardcoding file paths?

TSSLint instantiates a native JavaScript Error object at the moment the rule reports a diagnostic. By parsing the error’s stack trace with error-stack-parser, it dynamically extracts the file name, line number, and column number from the execution context, eliminating the need for manual path configuration.

What is the purpose of the stack index parameter in the .at() method?

The stack index allows rule authors to specify which frame in the call stack represents the relevant source location. Index 0 typically points to internal TSSLint functions, while index 1 or higher points to the user’s rule implementation. This flexibility ensures the "Related Information" link points to the meaningful rule code rather than framework internals.

Lazy evaluation defers the expensive work of file system access, source file parsing, and position calculation until the editor actually requests the relatedInformation property. This optimization prevents unnecessary overhead during bulk linting operations while still providing rich navigation data when users interact with specific diagnostics in their IDE.

Any editor that implements the Language Server Protocol (LSP) or TypeScript’s native diagnostic API can render DiagnosticRelatedInformation as clickable links. VS Code provides native support, displaying related information entries beneath diagnostics with "Go to" functionality that opens the rule source file at the precise line and column captured in the stack trace.

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 →