How to Implement Code Fixes with .withFix() and Refactorings with .withRefactor() in TSSLint Rules

Use the Reporter object returned by report() to chain .withFix() for quick fixes or .withRefactor() for refactoring actions, providing a title and a callback that returns FileTextChanges[] to modify source code.

TSSLint is a TypeScript linter that operates as a language service plugin, enabling real-time feedback in your editor. When building custom rules in the johnsoncodehk/tsslint repository, you can enhance diagnostics with automated solutions by implementing code fixes with .withFix() and refactorings with .withRefactor() in TSSLint rules.

Understanding the TSSLint Reporter API

When a rule detects an issue, it calls report(message, start, end), which returns a Reporter object. This object acts as a mutable builder that lets you attach code actions before the rule finishes execution.

Where the API Lives

The type definitions and implementation reside in two key locations:

  • packages/types/index.ts (lines 50-60): Defines the Reporter interface with withFix and withRefactor methods
  • packages/core/index.ts (lines 69-80): Contains the concrete reporter implementation that collects fixes and refactors into internal arrays

When you chain .withFix() or .withRefactor(), the core stores your supplied getEdits callback along with a title. Later, during the code-action phase, TSSLint invokes these callbacks to obtain FileTextChanges[] and exposes them through getCodeFixes and getRefactors.

Implementing Code Fixes with .withFix()

Code fixes appear as "quick fixes" in your editor (the lightbulb icon). Use .withFix() when you want to provide an immediate solution to a diagnostic.

The Fix Signature

.withFix(title: string, getEdits: () => FileTextChanges[])

The getEdits callback must return an array of objects matching TypeScript's FileTextChanges structure:

{
  fileName: string;               // absolute path of the file
  textChanges: ts.TextChange[];   // { newText: string, span: { start: number, length: number } }
}

Real-World Example: Remove Console Calls

The fixtures/noConsoleRule.ts file (lines 10-26) demonstrates a complete implementation:

import { defineRule } from '@tsslint/config';

export default defineRule(({ typescript: ts, file, report }) => {
  ts.forEachChild(file, function walk(node) {
    if (
      ts.isPropertyAccessExpression(node) &&
      ts.isIdentifier(node.expression) &&
      node.expression.text === 'console'
    ) {
      // Report the diagnostic
      report(
        `Calls to 'console.${node.name.text}' are not allowed.`,
        node.parent.getStart(file),
        node.parent.getEnd(),
      )
        // Attach a fix that replaces the entire call with a comment
        .withFix(
          `Remove console.${node.name.text}`,
          () => [
            {
              fileName: file.fileName,
              textChanges: [
                {
                  newText: '/* deleted */',
                  span: {
                    start: node.parent.getStart(file),
                    length: node.parent.getWidth(file),
                  },
                },
              ],
            },
          ],
        );
    }
    ts.forEachChild(node, walk);
  });
});

This rule detects console calls and offers a quick fix that replaces the entire call expression with /* deleted */.

Implementing Refactorings with .withRefactor()

Refactorings appear in the "Refactor..." menu in supported editors. Use .withRefactor() for optional transformations that improve code quality but aren't necessarily fixing errors.

The Refactor Signature

.withRefactor(title: string, getEdits: () => FileTextChanges[])

The API is identical to .withFix(), but TSSLint categorizes these actions separately, exposing them through getRefactors instead of getCodeFixes.

Example: Convert var to let

import { defineRule } from '@tsslint/config';

export default defineRule(({ typescript: ts, file, report }) => {
  ts.forEachChild(file, function walk(node) {
    if (ts.isVariableDeclarationList(node) && (node.flags & ts.NodeFlags.Var)) {
      const start = node.getStart(file);
      const length = node.getWidth(file);

      report(
        'Prefer let/const over var.',
        start,
        start + length,
      )
        .withRefactor(
          'Convert var to let',
          () => [
            {
              fileName: file.fileName,
              textChanges: [
                {
                  newText: 'let',
                  span: { start, length: 3 }, // replace the keyword "var"
                },
              ],
            },
          ],
        );
    }
    ts.forEachChild(node, walk);
  });
});

When users trigger the refactor command in their editor, they will see "Convert var to let" as an available action.

How Fixes and Refactorings Flow to the Editor

Understanding the lifecycle helps debug why an action might not appear:

  1. Rule Execution: When report() is called, TSSLint creates a reporter instance (implemented in packages/core/index.ts lines 69-80)
  2. Action Registration: Chaining .withFix() or .withRefactor() pushes { title, getEdits } into internal fixes or refactors arrays
  3. Result Mapping: After all rules run, the core builds a lintResults map linking files to their diagnostics and associated actions
  4. Editor Integration: When the editor requests code actions, getCodeFixes (for fixes) or getRefactors (for refactorings) in packages/core/index.ts iterates stored callbacks, invokes getEdits() to obtain concrete FileTextChanges, and returns ts.CodeFixAction or ts.RefactorAction objects

ESLint Compatibility Layer

If you're migrating ESLint rules to TSSLint, the compatibility layer in packages/compat-eslint/index.ts (lines 120-135) demonstrates how to map ESLint's suggest entries to TSSLint's refactor API:

// Inside packages/compat-eslint/index.ts
reporter.withRefactor(
  suggest.message,               // title shown to the user
  () => [
    {
      fileName: file.fileName,
      textChanges: getTextChanges(file, suggest.fix as ESLint.Rule.ReportFixer),
    },
  ],
);

This pattern allows you to expose ESLint suggestions as native TSSLint refactoring actions.

Summary

  • Reporter Object: Calling report() returns a mutable builder that accepts .withFix() and .withRefactor() chains
  • FileTextChanges: Both methods require a callback returning FileTextChanges[], specifying fileName and textChanges with newText and span
  • Categorization: .withFix() creates quick fixes (lightbulb), while .withRefactor() creates refactoring actions (refactor menu)
  • Implementation: The core logic resides in packages/core/index.ts, while type definitions are in packages/types/index.ts
  • ESLint Migration: Use packages/compat-eslint/index.ts as a reference for mapping ESLint suggestions to TSSLint refactorings

Frequently Asked Questions

What's the difference between a code fix and a refactoring in TSSLint?

Code fixes are intended to correct errors or violations reported by your rule, appearing as quick fixes (lightbulb icon) in the editor. Refactorings are optional transformations that improve code structure or style, appearing in the "Refactor..." menu. While both use the same FileTextChanges[] return type, TSSLint routes fixes through getCodeFixes and refactorings through getRefactors, allowing editors to present them in appropriate contexts.

What must the callback passed to .withFix() return?

The callback must return an array of FileTextChanges objects, where each object contains a fileName (absolute path) and a textChanges array. Each textChange must specify newText (the replacement string) and span (an object with start and length numbers indicating the position in the file). This matches TypeScript's native FileTextChanges interface used by the language service.

How does TSSLint expose these actions to VS Code?

TSSLint operates as a TypeScript language service plugin. When you register fixes or refactorings in a rule, the core implementation in packages/core/index.ts stores these actions. When VS Code requests code fixes or refactorings via the TypeScript language service protocol, TSSLint's getCodeFixes and getRefactors methods invoke the stored callbacks, convert the returned FileTextChanges into standard ts.CodeFixAction or ts.RefactorAction objects, and return them to the editor.

Can I use these APIs in ESLint-compatible rules?

Yes. The packages/compat-eslint/index.ts compatibility layer demonstrates how to map ESLint's suggest API to TSSLint's .withRefactor() method. When migrating ESLint rules, you can extract the fix logic from ESLint's suggest entries and wrap them in TSSLint's refactor callbacks, returning the appropriate FileTextChanges array to provide the same functionality within TSSLint's architecture.

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 →