Integration
- TypeScript course
- Expert
- Lesson 31/35
- 5 min
Build the working plugin by combining all parts.
Integrated plugin
A working lint plugin combines four pieces: skeleton (PluginCreateInfo → LanguageService), config reading (info.config), diagnostic production (getSemanticDiagnostics) and code fix (getCodeFixesAtPosition). Both walk the AST with ts.forEachChild.
function createPlugin(info: ts.server.PluginCreateInfo): ts.LanguageService {
const ls = info.languageService;
const prefix = info.config?.prefix ?? "[lint]";
return {
...ls,
getSemanticDiagnostics: function(fileName) { /* ... */ },
getCodeFixesAtPosition: function(fileName) { /* ... */ },
};
}Working plugin test
To test the plugin you create a mock language service: getSemanticDiagnostics and getCodeFixesAtPosition return empty arrays, getProgram().getSourceFile returns the test source. You call the plugin and verify diagnostic and fix counts.
const fakeLs = {
getSemanticDiagnostics: function() { return []; },
getCodeFixesAtPosition: function() { return []; },
getProgram: function() { return { getSourceFile: ... }; },
} as unknown as ts.LanguageService;Practice Integrated lint plugin
Write an integrated plugin: takes PluginCreateInfo, returns LanguageService. Read prefix from info.config?.prefix (default "[lint]"). Wrap getSemanticDiagnostics: produce a Warning diagnostic for each VariableDeclaration without a type annotation, message starting with prefix. Wrap getCodeFixesAtPosition: produce a fix with fixName: "add-any" for the same nodes, appending : any. Walk with ts.forEachChild in both. Test: source let x = 1;, prefix "[L]"; print diagnostic count and first fix name as count fixName.
Starter code
import * as ts from "typescript";
// Tümleşik bir plugin yaz:
// - PluginCreateInfo alır, LanguageService döndürür
// - info'dan prefix ayarını oku (varsayılan "[lint]")
// - Diagnostic metodunu sar: tür eklemesi olmayan VariableDeclaration için diagnostic üret
// - Code fix metodunu sar: aynı düğümler için ": any" ekleme fix'i üret
// - Her ikisinde de forEachChild ile AST'yı gez
// Test: kaynak "let x = 1;", prefix "[L]"; diagnostic sayısı ve fix adını yazdır.
// kodunu buraya yazSample solution
import ts from "typescript";
function createPlugin(info: ts.server.PluginCreateInfo): ts.LanguageService {
const ls = info.languageService;
const prefix: string = info.config?.prefix ?? "[lint]";
return {
...ls,
getSemanticDiagnostics: function(fileName: string) {
const prior = ls.getSemanticDiagnostics(fileName);
const sf = ls.getProgram().getSourceFile(fileName);
if (!sf) return prior;
const extras: ts.Diagnostic[] = [];
function visit(node: ts.Node) {
if (ts.isVariableDeclaration(node) && !node.type) {
extras.push({
file: sf,
start: node.getStart(sf),
length: node.getWidth(sf),
category: ts.DiagnosticCategory.Warning,
code: 9001,
messageText: prefix + " Missing type annotation",
});
}
ts.forEachChild(node, visit);
}
visit(sf);
return [...prior, ...extras];
},
getCodeFixesAtPosition: function(fileName: string, start: number, end: number, errorCodes: readonly number[], formatOptions: ts.FormatCodeSettings, preferences: ts.UserPreferences) {
const prior = ls.getCodeFixesAtPosition(fileName, start, end, errorCodes, formatOptions, preferences);
const sf = ls.getProgram().getSourceFile(fileName);
if (!sf) return prior;
const fixes: ts.CodeFixAction[] = [];
function visit(node: ts.Node) {
if (ts.isVariableDeclaration(node) && !node.type) {
fixes.push({
fixName: "add-any",
description: "Add type annotation",
changes: [{
fileName,
textChanges: [{ span: { start: node.name.getEnd(), length: 0 }, newText: ": any" }],
}],
});
}
ts.forEachChild(node, visit);
}
visit(sf);
return [...prior, ...fixes];
},
};
}
const sf = ts.createSourceFile("app.ts", "let x = 1;", ts.ScriptTarget.Latest, true);
const fakeLs = {
getSemanticDiagnostics: function() { return [] as ts.Diagnostic[]; },
getCodeFixesAtPosition: function() { return [] as ts.CodeFixAction[]; },
getProgram: function() { return { getSourceFile: function(n: string) { return n === "app.ts" ? sf : undefined; } } as unknown as ts.Program; },
} as unknown as ts.LanguageService;
const info = { languageService: fakeLs, config: { prefix: "[L]" } } as unknown as ts.server.PluginCreateInfo;
const plugin = createPlugin(info);
const diags = plugin.getSemanticDiagnostics("app.ts");
const fixes = plugin.getCodeFixesAtPosition("app.ts", 0, 1, [], {} as ts.FormatCodeSettings, {} as ts.UserPreferences);
console.log(diags.length + " " + fixes[0].fixName);Expected output
1 add-any
Questions and answers
Review the lesson questions together with their answers. To test yourself, solve the lesson interactively.
1 Which four pieces does a working lint plugin combine?
- PluginCreateInfo + getSemanticDiagnostics + getCodeFixesAtPosition + info.config Correct answer
- getProgram + getSourceFile + getText + writeFile
- getTypeChecker + writeFile + readFile + getText
- createSourceFile + JSON.stringify + writeFile + console.log
Explanation: PluginCreateInfo is the plugin input, getSemanticDiagnostics produces diagnostics, getCodeFixesAtPosition provides code fixes, and info.config carries configuration. These four form a working plugin.
2 Return the diagnostic list
return [...prior, ...____];
return [...prior, ...extras];
Explanation: The prior diagnostics are merged with the extras; diags and errors are not variable names here, and fixes is used for code fixes.
3 Integrated plugin parts
| Term | Meaning |
|---|---|
info.config |
Configuration is read |
getSemanticDiagnostics |
Diagnostics are produced |
getCodeFixesAtPosition |
Fixes are produced |
ts.forEachChild |
AST is walked |