诊断 API
Trae IDE 的诊断 API 允许扩展报告和管理代码中的错误、警告和信息提示。
概述
诊断功能包括:
- 错误和警告报告
- 代码问题标记
- 诊断信息管理
- 快速修复建议
- 诊断集合管理
主要接口
Diagnostic
typescript
interface Diagnostic {
// 位置信息
range: Range
// 诊断内容
message: string
severity?: DiagnosticSeverity
// 分类信息
source?: string
code?: string | number | {
value: string | number
target: Uri
}
// 关联信息
relatedInformation?: DiagnosticRelatedInformation[]
tags?: DiagnosticTag[]
}DiagnosticSeverity
typescript
enum DiagnosticSeverity {
Error = 0,
Warning = 1,
Information = 2,
Hint = 3
}DiagnosticCollection
typescript
interface DiagnosticCollection {
readonly name: string
// 设置诊断
set(uri: Uri, diagnostics: Diagnostic[]): void
set(entries: [Uri, Diagnostic[]][]): void
// 删除诊断
delete(uri: Uri): void
clear(): void
// 查询诊断
get(uri: Uri): Diagnostic[] | undefined
has(uri: Uri): boolean
// 遍历
forEach(callback: (uri: Uri, diagnostics: Diagnostic[], collection: DiagnosticCollection) => any, thisArg?: any): void
// 销毁
dispose(): void
}创建诊断集合
基本创建
typescript
import { languages, Diagnostic, DiagnosticSeverity, Range, Position } from 'trae-api'
// 创建诊断集合
const diagnosticCollection = languages.createDiagnosticCollection('myExtension')
// 创建诊断项
const diagnostic = new Diagnostic(
new Range(new Position(0, 0), new Position(0, 10)), // 位置范围
'这里有一个错误', // 错误消息
DiagnosticSeverity.Error // 严重程度
)
// 设置诊断源
diagnostic.source = 'MyLinter'
diagnostic.code = 'E001'
// 添加到文件
diagnosticCollection.set(document.uri, [diagnostic])批量设置诊断
typescript
// 为多个文件设置诊断
const diagnostics: [Uri, Diagnostic[]][] = [
[Uri.file('/path/to/file1.js'), [
new Diagnostic(
new Range(0, 0, 0, 10),
'未使用的变量',
DiagnosticSeverity.Warning
)
]],
[Uri.file('/path/to/file2.js'), [
new Diagnostic(
new Range(5, 0, 5, 20),
'语法错误',
DiagnosticSeverity.Error
)
]]
]
diagnosticCollection.set(diagnostics)诊断类型和严重程度
错误诊断
typescript
const errorDiagnostic = new Diagnostic(
range,
'语法错误:缺少分号',
DiagnosticSeverity.Error
)
errorDiagnostic.source = 'TypeScript'
errorDiagnostic.code = 'TS1005'警告诊断
typescript
const warningDiagnostic = new Diagnostic(
range,
'未使用的变量 "unusedVar"',
DiagnosticSeverity.Warning
)
warningDiagnostic.source = 'ESLint'
warningDiagnostic.code = 'no-unused-vars'信息诊断
typescript
const infoDiagnostic = new Diagnostic(
range,
'建议使用 const 而不是 let',
DiagnosticSeverity.Information
)
infoDiagnostic.source = 'CodeStyle'提示诊断
typescript
const hintDiagnostic = new Diagnostic(
range,
'可以简化为箭头函数',
DiagnosticSeverity.Hint
)
hintDiagnostic.source = 'Refactoring'诊断标签
不必要的代码
typescript
import { DiagnosticTag } from 'trae-api'
const diagnostic = new Diagnostic(
range,
'未使用的导入',
DiagnosticSeverity.Hint
)
diagnostic.tags = [DiagnosticTag.Unnecessary]已弃用的代码
typescript
const deprecatedDiagnostic = new Diagnostic(
range,
'此方法已弃用,请使用 newMethod()',
DiagnosticSeverity.Warning
)
deprecatedDiagnostic.tags = [DiagnosticTag.Deprecated]关联信息
添加相关信息
typescript
import { DiagnosticRelatedInformation, Location } from 'trae-api'
const diagnostic = new Diagnostic(
range,
'变量 "x" 已在此处声明',
DiagnosticSeverity.Error
)
// 添加相关位置信息
diagnostic.relatedInformation = [
new DiagnosticRelatedInformation(
new Location(
Uri.file('/path/to/other-file.js'),
new Range(10, 0, 10, 10)
),
'首次声明位置'
),
new DiagnosticRelatedInformation(
new Location(
document.uri,
new Range(5, 0, 5, 10)
),
'之前的使用位置'
)
]实用示例
语法检查器
typescript
class SyntaxChecker {
private diagnosticCollection: DiagnosticCollection
constructor() {
this.diagnosticCollection = languages.createDiagnosticCollection('syntaxChecker')
}
async checkSyntax(document: TextDocument) {
const diagnostics: Diagnostic[] = []
const text = document.getText()
const lines = text.split('\n')
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
// 检查缺少分号
if (this.needsSemicolon(line)) {
const diagnostic = new Diagnostic(
new Range(i, line.length, i, line.length),
'缺少分号',
DiagnosticSeverity.Error
)
diagnostic.source = 'SyntaxChecker'
diagnostic.code = 'missing-semicolon'
diagnostics.push(diagnostic)
}
// 检查未使用的变量
const unusedVars = this.findUnusedVariables(line, i)
diagnostics.push(...unusedVars)
}
this.diagnosticCollection.set(document.uri, diagnostics)
}
private needsSemicolon(line: string): boolean {
const trimmed = line.trim()
return trimmed.length > 0 &&
!trimmed.endsWith(';') &&
!trimmed.endsWith('{') &&
!trimmed.endsWith('}') &&
!trimmed.startsWith('//')
}
private findUnusedVariables(line: string, lineNumber: number): Diagnostic[] {
const diagnostics: Diagnostic[] = []
const varRegex = /\b(let|const|var)\s+(\w+)/g
let match
while ((match = varRegex.exec(line)) !== null) {
const varName = match[2]
const startPos = match.index + match[1].length + 1
const endPos = startPos + varName.length
// 简单检查:如果变量名以下划线开头,认为是未使用的
if (varName.startsWith('_')) {
const diagnostic = new Diagnostic(
new Range(lineNumber, startPos, lineNumber, endPos),
`变量 "${varName}" 未使用`,
DiagnosticSeverity.Warning
)
diagnostic.source = 'SyntaxChecker'
diagnostic.code = 'unused-variable'
diagnostic.tags = [DiagnosticTag.Unnecessary]
diagnostics.push(diagnostic)
}
}
return diagnostics
}
dispose() {
this.diagnosticCollection.dispose()
}
}代码质量检查器
typescript
class CodeQualityChecker {
private diagnosticCollection: DiagnosticCollection
constructor() {
this.diagnosticCollection = languages.createDiagnosticCollection('codeQuality')
}
async analyzeCode(document: TextDocument) {
const diagnostics: Diagnostic[] = []
const text = document.getText()
// 检查函数复杂度
const complexityIssues = this.checkComplexity(text)
diagnostics.push(...complexityIssues)
// 检查代码重复
const duplicationIssues = this.checkDuplication(text)
diagnostics.push(...duplicationIssues)
// 检查命名规范
const namingIssues = this.checkNaming(text)
diagnostics.push(...namingIssues)
this.diagnosticCollection.set(document.uri, diagnostics)
}
private checkComplexity(text: string): Diagnostic[] {
const diagnostics: Diagnostic[] = []
const lines = text.split('\n')
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
// 简单的复杂度检查:嵌套层级
const indentLevel = (line.match(/^\s*/)?.[0].length || 0) / 2
if (indentLevel > 4) {
const diagnostic = new Diagnostic(
new Range(i, 0, i, line.length),
'代码嵌套层级过深,建议重构',
DiagnosticSeverity.Information
)
diagnostic.source = 'CodeQuality'
diagnostic.code = 'high-complexity'
diagnostics.push(diagnostic)
}
}
return diagnostics
}
private checkDuplication(text: string): Diagnostic[] {
const diagnostics: Diagnostic[] = []
const lines = text.split('\n')
const lineMap = new Map<string, number[]>()
// 记录每行内容出现的位置
lines.forEach((line, index) => {
const trimmed = line.trim()
if (trimmed.length > 10) { // 只检查较长的行
if (!lineMap.has(trimmed)) {
lineMap.set(trimmed, [])
}
lineMap.get(trimmed)!.push(index)
}
})
// 找出重复的行
lineMap.forEach((positions, content) => {
if (positions.length > 1) {
positions.forEach(pos => {
const diagnostic = new Diagnostic(
new Range(pos, 0, pos, lines[pos].length),
'发现重复代码,建议提取为函数',
DiagnosticSeverity.Information
)
diagnostic.source = 'CodeQuality'
diagnostic.code = 'code-duplication'
// 添加其他重复位置的信息
diagnostic.relatedInformation = positions
.filter(p => p !== pos)
.map(p => new DiagnosticRelatedInformation(
new Location(Uri.file(''), new Range(p, 0, p, lines[p].length)),
'其他重复位置'
))
diagnostics.push(diagnostic)
})
}
})
return diagnostics
}
private checkNaming(text: string): Diagnostic[] {
const diagnostics: Diagnostic[] = []
const lines = text.split('\n')
for (let i = 0; i < lines.length; i++) {
const line = lines[i]
// 检查变量命名(简单的驼峰命名检查)
const varRegex = /\b(let|const|var)\s+([a-z][a-zA-Z0-9_]*)/g
let match
while ((match = varRegex.exec(line)) !== null) {
const varName = match[2]
// 检查是否使用了下划线(不符合驼峰命名)
if (varName.includes('_') && !varName.startsWith('_')) {
const startPos = match.index + match[1].length + 1
const endPos = startPos + varName.length
const diagnostic = new Diagnostic(
new Range(i, startPos, i, endPos),
`变量名 "${varName}" 建议使用驼峰命名`,
DiagnosticSeverity.Information
)
diagnostic.source = 'CodeQuality'
diagnostic.code = 'naming-convention'
diagnostics.push(diagnostic)
}
}
}
return diagnostics
}
}实时诊断更新
typescript
class RealTimeDiagnostics {
private diagnosticCollection: DiagnosticCollection
private disposables: Disposable[] = []
constructor() {
this.diagnosticCollection = languages.createDiagnosticCollection('realTime')
this.setupEventListeners()
}
private setupEventListeners() {
// 监听文档变化
this.disposables.push(
workspace.onDidChangeTextDocument(event => {
this.updateDiagnostics(event.document)
})
)
// 监听文档打开
this.disposables.push(
workspace.onDidOpenTextDocument(document => {
this.updateDiagnostics(document)
})
)
// 监听文档关闭
this.disposables.push(
workspace.onDidCloseTextDocument(document => {
this.diagnosticCollection.delete(document.uri)
})
)
}
private async updateDiagnostics(document: TextDocument) {
// 防抖处理,避免频繁更新
clearTimeout(this.updateTimer)
this.updateTimer = setTimeout(() => {
this.performDiagnostics(document)
}, 500)
}
private updateTimer: NodeJS.Timeout | undefined
private async performDiagnostics(document: TextDocument) {
const diagnostics: Diagnostic[] = []
// 执行各种检查
const syntaxErrors = await this.checkSyntax(document)
const styleIssues = await this.checkStyle(document)
const logicWarnings = await this.checkLogic(document)
diagnostics.push(...syntaxErrors, ...styleIssues, ...logicWarnings)
this.diagnosticCollection.set(document.uri, diagnostics)
}
private async checkSyntax(document: TextDocument): Promise<Diagnostic[]> {
// 语法检查逻辑
return []
}
private async checkStyle(document: TextDocument): Promise<Diagnostic[]> {
// 代码风格检查逻辑
return []
}
private async checkLogic(document: TextDocument): Promise<Diagnostic[]> {
// 逻辑检查逻辑
return []
}
dispose() {
this.disposables.forEach(d => d.dispose())
this.diagnosticCollection.dispose()
if (this.updateTimer) {
clearTimeout(this.updateTimer)
}
}
}诊断管理
清理诊断
typescript
// 清理特定文件的诊断
diagnosticCollection.delete(document.uri)
// 清理所有诊断
diagnosticCollection.clear()
// 有条件地清理诊断
diagnosticCollection.forEach((uri, diagnostics) => {
// 只保留错误级别的诊断
const errors = diagnostics.filter(d => d.severity === DiagnosticSeverity.Error)
if (errors.length !== diagnostics.length) {
diagnosticCollection.set(uri, errors)
}
})诊断过滤
typescript
class DiagnosticFilter {
static filterBySeverity(
diagnostics: Diagnostic[],
severity: DiagnosticSeverity
): Diagnostic[] {
return diagnostics.filter(d => d.severity === severity)
}
static filterBySource(
diagnostics: Diagnostic[],
source: string
): Diagnostic[] {
return diagnostics.filter(d => d.source === source)
}
static filterByRange(
diagnostics: Diagnostic[],
range: Range
): Diagnostic[] {
return diagnostics.filter(d => range.contains(d.range))
}
}最佳实践
- 性能优化: 使用防抖机制避免频繁更新诊断
- 用户体验: 提供清晰的错误消息和有用的相关信息
- 资源管理: 及时清理不需要的诊断信息
- 分类管理: 使用不同的诊断集合管理不同类型的问题
- 渐进式检查: 优先显示严重错误,然后是警告和提示