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 theReporterinterface withwithFixandwithRefactormethodspackages/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:
- Rule Execution: When
report()is called, TSSLint creates a reporter instance (implemented inpackages/core/index.tslines 69-80) - Action Registration: Chaining
.withFix()or.withRefactor()pushes{ title, getEdits }into internalfixesorrefactorsarrays - Result Mapping: After all rules run, the core builds a
lintResultsmap linking files to their diagnostics and associated actions - Editor Integration: When the editor requests code actions,
getCodeFixes(for fixes) orgetRefactors(for refactorings) inpackages/core/index.tsiterates stored callbacks, invokesgetEdits()to obtain concreteFileTextChanges, and returnsts.CodeFixActionorts.RefactorActionobjects
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[], specifyingfileNameandtextChangeswithnewTextandspan - 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 inpackages/types/index.ts - ESLint Migration: Use
packages/compat-eslint/index.tsas 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →