实战:配置驱动代码检查插件
本文实现一个完整的代码检查插件 "RuleLint":规则全部由配置驱动,用户在设置中开关规则、调整级别,检查结果实时反映到问题面板,并支持一键修复。
设计目标
| 能力 | 实现方式 |
|---|---|
| 可配置规则 | contributes.configuration 定义规则开关与级别 |
| 实时检查 | 文档保存/变更触发诊断 |
| 配置实时生效 | onDidChangeConfiguration 监听刷新 |
| 自动修复 | CodeActionProvider 提供快速修复 |
第一步:package.json
json
{
"name": "rulelint",
"displayName": "RuleLint",
"description": "配置驱动的代码检查插件",
"version": "1.0.0",
"publisher": "mypublisher",
"engines": { "vscode": "^1.80.0" },
"categories": ["Linters"],
"activationEvents": ["onStartupFinished"],
"main": "./out/extension.js",
"contributes": {
"commands": [
{ "command": "rulelint.run", "title": "RuleLint: 检查当前文件" },
{ "command": "rulelint.fixAll", "title": "RuleLint: 修复全部问题" }
],
"keybindings": [
{
"command": "rulelint.run",
"key": "ctrl+shift+alt+r",
"mac": "cmd+shift+alt+r",
"when": "editorTextFocus"
}
],
"menus": {
"commandPalette": [
{ "command": "rulelint.run", "when": "rulelint.enabled" },
{ "command": "rulelint.fixAll", "when": "rulelint.enabled" }
],
"editor/context": [
{
"command": "rulelint.run",
"when": "editorTextFocus",
"group": "navigation"
}
]
},
"configuration": {
"title": "RuleLint",
"properties": {
"rulelint.enable": {
"type": "boolean",
"default": true,
"description": "启用 RuleLint 代码检查"
},
"rulelint.runOnSave": {
"type": "boolean",
"default": true,
"description": "保存时自动检查"
},
"rulelint.rules": {
"type": "object",
"markdownDescription": "规则配置,键为规则名,值为级别:\n\n- `\"error\"` — 错误\n- `\"warning\"` — 警告\n- `\"off\"` — 关闭\n\n支持的规则:`no-console`、`no-var`、`no-magic-number`",
"default": {
"no-console": "warning",
"no-var": "error",
"no-magic-number": "off"
}
},
"rulelint.maxWarnings": {
"type": "number",
"default": 100,
"minimum": 1,
"description": "警告数量上限"
}
}
}
},
"scripts": {
"compile": "tsc -p ./"
},
"devDependencies": {
"@types/vscode": "^1.80.0",
"typescript": "^5.0.0"
}
}第二步:规则引擎
规则按配置动态加载,每一条规则是一个独立检查器:
typescript
// rules.ts
import * as vscode from 'vscode';
export type RuleLevel = 'error' | 'warning' | 'off';
export interface RuleIssue {
range: vscode.Range;
message: string;
rule: string;
severity: vscode.DiagnosticSeverity;
fix?: vscode.WorkspaceEdit;
}
export interface Rule {
name: string;
check(document: vscode.TextDocument): RuleIssue[];
}
// 规则一:禁止 console
const noConsole: Rule = {
name: 'no-console',
check(document) {
const issues: RuleIssue[] = [];
const text = document.getText();
// 逐行查找 console.xxx
for (let i = 0; i < document.lineCount; i++) {
const line = document.lineAt(i);
const matches = line.text.matchAll(/console\.(log|warn|error|info)\(/g);
for (const m of matches) {
const start = new vscode.Position(i, m.index);
const end = new vscode.Position(i, m.index + m[0].length - 1);
issues.push({
range: new vscode.Range(start, end),
message: '禁止使用 console 调用,请使用日志工具',
rule: 'no-console',
severity: vscode.DiagnosticSeverity.Warning
});
}
}
return issues;
}
};
// 规则二:禁止 var
const noVar: Rule = {
name: 'no-var',
check(document) {
const issues: RuleIssue[] = [];
for (let i = 0; i < document.lineCount; i++) {
const line = document.lineAt(i);
const matches = line.text.matchAll(/\bvar\s+([a-zA-Z_$][\w$]*)/g);
for (const m of matches) {
issues.push({
range: new vscode.Range(
new vscode.Position(i, m.index),
new vscode.Position(i, m.index + 3)
),
message: `变量 ${m[1]} 使用了 var,请改用 let 或 const`,
rule: 'no-var',
severity: vscode.DiagnosticSeverity.Error,
fix: createFix(document, i, m.index, m[1])
});
}
}
return issues;
}
};
// 规则三:禁止魔法数字(裸数字字面量)
const noMagicNumber: Rule = {
name: 'no-magic-number',
check(document) {
const issues: RuleIssue[] = [];
const text = document.getText();
// 简单检测:等号右侧的裸数字
const regex = /=\s*(\d+)(?![\w.])/g;
let match: RegExpExecArray | null;
while ((match = regex.exec(text)) !== null) {
const offset = match.index + match[0].indexOf(match[1]);
const pos = document.positionAt(offset);
issues.push({
range: new vscode.Range(pos, pos.translate(0, match[1].length)),
message: `魔法数字 ${match[1]} 应定义为常量`,
rule: 'no-magic-number',
severity: vscode.DiagnosticSeverity.Warning
});
}
return issues;
}
};
export const ALL_RULES: Rule[] = [noConsole, noVar, noMagicNumber];
// 自动修复:var → const
function createFix(
document: vscode.TextDocument,
line: number,
col: number,
name: string
): vscode.WorkspaceEdit {
const edit = new vscode.WorkspaceEdit();
edit.replace(
document.uri,
new vscode.Range(new vscode.Position(line, col), new vscode.Position(line, col + 3)),
'const'
);
return edit;
}第三步:诊断控制器
控制器负责读取配置、运行规则、填充诊断集合:
typescript
// linter.ts
import * as vscode from 'vscode';
import { ALL_RULES, RuleIssue, RuleLevel } from './rules';
export class Linter {
private diagnostics = vscode.languages.createDiagnosticCollection('rulelint');
private enabled = true;
private rules: Record<string, RuleLevel> = {};
private maxWarnings = 100;
constructor(private context: vscode.ExtensionContext) {
// 监听配置变更(实时生效)
context.subscriptions.push(
vscode.workspace.onDidChangeConfiguration((e) => {
if (e.affectsConfiguration('rulelint')) {
this.applyConfig();
this.recheckAll(); // 配置变化立即重新检查
}
})
);
// 保存时检查
context.subscriptions.push(
vscode.workspace.onDidSaveTextDocument((doc) => {
if (this.shouldCheck(doc)) {
this.checkDocument(doc);
}
})
);
// 打开文档时检查
context.subscriptions.push(
vscode.workspace.onDidOpenTextDocument((doc) => {
if (this.shouldCheck(doc)) {
this.checkDocument(doc);
}
})
);
this.applyConfig();
}
// 应用配置
private applyConfig() {
const config = vscode.workspace.getConfiguration('rulelint');
this.enabled = config.get<boolean>('enable', true);
this.rules = config.get<Record<string, RuleLevel>>('rules', {});
this.maxWarnings = config.get<number>('maxWarnings', 100);
}
// 判断是否需要检查
private shouldCheck(document: vscode.TextDocument): boolean {
return this.enabled &&
document.languageId === 'javascript' &&
!document.isUntitled;
}
// 检查文档
checkDocument(document: vscode.TextDocument) {
if (!this.shouldCheck(document)) return;
const issues: RuleIssue[] = [];
// 运行所有启用的规则
for (const rule of ALL_RULES) {
const level = this.rules[rule.name] ?? 'off';
if (level === 'off') continue;
for (const issue of rule.check(document)) {
issue.severity = level === 'error'
? vscode.DiagnosticSeverity.Error
: vscode.DiagnosticSeverity.Warning;
issues.push(issue);
}
}
// 警告数上限控制
const warnings = issues.filter(
(i) => i.severity === vscode.DiagnosticSeverity.Warning
);
if (warnings.length > this.maxWarnings) {
vscode.window.showWarningMessage(
`警告数量超过上限 ${this.maxWarnings},已截断显示`
);
}
// 填充诊断集合
this.diagnostics.set(document.uri, issues.map((issue) => {
const diag = new vscode.Diagnostic(
issue.range,
issue.message,
issue.severity
);
diag.source = 'RuleLint';
diag.code = issue.rule;
// 保存修复信息(后续 CodeAction 使用)
(diag as any).ruleFix = issue.fix;
return diag;
}));
}
// 重新检查所有打开的文档
recheckAll() {
this.diagnostics.clear();
if (!this.enabled) return;
vscode.workspace.textDocuments.forEach((doc) => {
if (this.shouldCheck(doc)) {
this.checkDocument(doc);
}
});
}
// 清除指定文件的诊断
clear(document: vscode.TextDocument) {
this.diagnostics.delete(document.uri);
}
}第四步:CodeActionProvider 自动修复
typescript
// fixProvider.ts
import * as vscode from 'vscode';
export class RuleLintFixProvider implements vscode.CodeActionProvider {
static readonly providedCodeActionKinds = [
vscode.CodeActionKind.QuickFix
];
provideCodeActions(
document: vscode.TextDocument,
range: vscode.Range,
context: vscode.CodeActionContext,
token: vscode.CancellationToken
): vscode.CodeAction[] {
const actions: vscode.CodeAction[] = [];
// 遍历命中位置的诊断
for (const diagnostic of context.diagnostics) {
if (diagnostic.source !== 'RuleLint') continue;
const fix = (diagnostic as any).ruleFix;
if (fix) {
const action = new vscode.CodeAction(
`修复:${diagnostic.code}`,
vscode.CodeActionKind.QuickFix
);
action.edit = fix;
action.diagnostics = [diagnostic];
action.isPreferred = true;
actions.push(action);
}
// 添加"禁用此规则"操作
const disableAction = new vscode.CodeAction(
`禁用规则 ${diagnostic.code}`,
vscode.CodeActionKind.QuickFix
);
disableAction.command = {
command: 'rulelint.disableRule',
title: '禁用规则',
arguments: [diagnostic.code]
};
disableAction.diagnostics = [diagnostic];
actions.push(disableAction);
}
return actions;
}
}第五步:激活与命令
typescript
// extension.ts
import * as vscode from 'vscode';
import { Linter } from './linter';
import { RuleLintFixProvider } from './fixProvider';
export function activate(context: vscode.ExtensionContext) {
// 初始化检查器
const linter = new Linter(context);
// 注册自动修复
context.subscriptions.push(
vscode.languages.registerCodeActionsProvider(
'javascript',
new RuleLintFixProvider(),
{ providedCodeActionKinds: RuleLintFixProvider.providedCodeActionKinds }
)
);
// 命令:检查当前文件
context.subscriptions.push(
vscode.commands.registerCommand('rulelint.run', () => {
const editor = vscode.window.activeTextEditor;
if (editor) {
linter.checkDocument(editor.document);
vscode.window.showInformationMessage('检查完成');
}
})
);
// 命令:修复全部
context.subscriptions.push(
vscode.commands.registerCommand('rulelint.fixAll', async () => {
const editor = vscode.window.activeTextEditor;
if (!editor) return;
await vscode.window.withProgress(
{ location: vscode.ProgressLocation.Notification, title: '修复中...' },
async () => {
// 收集该文件所有带 fix 的诊断
const diagnostics = vscode.languages.getDiagnostics(editor.document.uri)
.filter((d) => d.source === 'RuleLint' && (d as any).ruleFix);
const edit = new vscode.WorkspaceEdit();
for (const diag of diagnostics) {
const fix = (diag as any).ruleFix;
if (fix && fix.entries) {
for (const entry of fix.entries) {
edit.replace(entry[0], entry[1][0].range, entry[1][0].newText);
}
}
}
await vscode.workspace.applyEdit(edit);
linter.recheckAll();
}
);
})
);
// 命令:禁用规则(更新配置)
context.subscriptions.push(
vscode.commands.registerCommand('rulelint.disableRule', async (rule: string) => {
const config = vscode.workspace.getConfiguration('rulelint');
const rules = config.get<Record<string, string>>('rules', {});
rules[rule] = 'off';
await config.update('rules', rules, vscode.ConfigurationTarget.Workspace);
vscode.window.showInformationMessage(`规则 ${rule} 已禁用`);
})
);
// 初始检查
linter.recheckAll();
}
export function deactivate() {}第六步:配置驱动的联动流程
text
用户修改设置(rulelint.rules)
↓
onDidChangeConfiguration 触发
↓
applyConfig 读取新规则
↓
recheckAll 重新检查全部文档
↓
诊断集合更新 → 问题面板实时刷新
用户悬停问题 → 快速修复
↓
CodeActionProvider 返回修复动作
↓
选择"修复" → WorkspaceEdit 应用
↓
linter.recheckAll 重新检查使用验证
text
1. 打开 JS 文件,写入 console.log 与 var 声明
2. 保存 → 问题面板出现诊断(警告/错误按配置级别)
3. 修改设置 rulelint.rules 调整级别 → 立即生效
4. 悬停错误 → 出现"修复:no-var" → 点击自动替换为 const
5. Ctrl+Shift+Alt+R 手动检查
6. 快速修复里选择"禁用规则 no-console" → 配置自动写入扩展方向
| 方向 | 实现 |
|---|---|
| 更多语言 | 注册多个语言的 provider,按语言加载规则集 |
| 规则参数化 | 配置中传参(如 no-magic-number 的允许列表) |
| 忽略注释 | 支持 // rulelint-disable-next-line 注释 |
| 项目级配置 | 读取 .rulelint.json 项目规则文件 |
| 增量检查 | 监听 onDidChangeTextDocument 做局部重检 |
配置驱动模式让插件的每个行为都可以被用户调节:规则开关、级别、上限、保存时机全部由设置控制,配合诊断与快速修复形成了完整闭环。