AtomCode 插件开发实战:从零构建一个完整插件
·
AtomCode 插件开发实战:从零构建一个完整插件
引言
插件系统是 AtomCode 的核心扩展机制之一,通过插件可以极大地丰富 AtomCode 的功能。本文将详细介绍如何从零开始开发一个完整的 AtomCode 插件,包括插件架构、API 使用、调试技巧,以及如何将插件发布到市场。
一、插件架构深度解析
1.1 插件系统架构
// 插件核心接口
interface IPlugin {
id: string;
name: string;
version: string;
description: string;
author: string;
// 生命周期
onInit(context: IPluginContext): Promise<void>;
onDestroy(): Promise<void>;
// 事件钩子
onFileOpen?(file: IFile): void;
onFileSave?(file: IFile): void;
onSelectionChange?(selection: ISelection): void;
onLanguageChange?(language: string): void;
// AI 增强
onAIComplete?(prompt: string): string | undefined;
onCodeGenerate?(code: string): string;
}
// 插件上下文
interface IPluginContext {
// 编辑器 API
editor: IEditorAPI;
// AI API
ai: IAIAPI;
// 项目 API
project: IProjectAPI;
// 通知 API
notifications: INotificationAPI;
// 存储 API
storage: IStorageAPI;
// 命令注册
registerCommand(command: ICommand): void;
// 设置注册
registerSetting(setting: ISetting): void;
}
1.2 插件生命周期
class PluginLifecycle {
// 插件加载流程
static async load(plugin: IPlugin, context: IPluginContext) {
// 1. 验证插件
await this.validate(plugin);
// 2. 创建沙箱环境
const sandbox = await this.createSandbox();
// 3. 初始化插件
await plugin.onInit(context);
// 4. 注册事件监听
this.registerEventListeners(plugin, context);
// 5. 激活插件
plugin.isActive = true;
}
// 插件卸载流程
static async unload(plugin: IPlugin) {
// 1. 移除事件监听
this.removeEventListeners(plugin);
// 2. 销毁插件
await plugin.onDestroy();
// 3. 清理资源
await this.cleanup(plugin);
}
}
二、开发第一个插件
2.1 插件规划
插件名称:code-quality-analyzer
插件功能:
- 实时分析代码质量
- 检测代码异味
- 提供优化建议
- 生成质量报告
技术栈:
- TypeScript
- AST 分析(@babel/parser)
- Lint 规则引擎
2.2 项目结构
code-quality-analyzer/
├── src/
│ ├── index.ts # 插件入口
│ ├── analyzer/ # 分析器
│ │ ├── complexity.ts # 复杂度分析
│ │ ├── duplication.ts # 重复代码检测
│ │ ├── naming.ts # 命名规范检查
│ │ └── style.ts # 代码风格检查
│ ├── rules/ # 规则
│ │ ├── cyclomatic.ts # 圈复杂度规则
│ │ ├── long-method.ts # 长方法规则
│ │ └── magic-number.ts # 魔法数字规则
│ ├── reporters/ # 报告生成
│ │ ├── console.ts # 控制台输出
│ │ ├── html.ts # HTML 报告
│ │ └── json.ts # JSON 报告
│ └── utils/ # 工具函数
│ ├── ast.ts # AST 工具
│ ├── config.ts # 配置管理
│ └── logger.ts # 日志工具
├── package.json
├── tsconfig.json
├── plugin.json
└── README.md
2.3 核心代码实现
插件入口
// src/index.ts
import { IPlugin, IPluginContext } from '@atomcode/core';
import { ComplexityAnalyzer } from './analyzer/complexity';
import { DuplicationDetector } from './analyzer/duplication';
import { NamingChecker } from './analyzer/naming';
import { StyleChecker } from './analyzer/style';
import { ConsoleReporter } from './reporters/console';
import { HtmlReporter } from './reporters/html';
import { JsonReporter } from './reporters/json';
import { ConfigManager } from './utils/config';
class CodeQualityAnalyzer implements IPlugin {
id = 'code-quality-analyzer';
name = 'Code Quality Analyzer';
version = '1.0.0';
description = 'Real-time code quality analysis tool';
author = 'Your Name';
private analyzers: CodeAnalyzer[] = [];
private reporters: Reporter[] = [];
private config: ConfigManager;
constructor() {
this.config = new ConfigManager();
}
async onInit(context: IPluginContext): Promise<void> {
console.log('Code Quality Analyzer initialized');
// 初始化分析器
this.analyzers = [
new ComplexityAnalyzer(),
new DuplicationDetector(),
new NamingChecker(),
new StyleChecker()
];
// 初始化报告器
this.reporters = [
new ConsoleReporter(),
new HtmlReporter(),
new JsonReporter()
];
// 注册命令
this.registerCommands(context);
// 注册设置
this.registerSettings(context);
// 加载配置
await this.config.load();
// 分析当前活动文件
const activeFile = context.editor.getActiveFile();
if (activeFile) {
await this.analyzeFile(activeFile, context);
}
}
async onDestroy(): Promise<void> {
console.log('Code Quality Analyzer destroyed');
await this.config.save();
}
async onFileSave(file: IFile): Promise<void> {
if (this.config.isAutoAnalyzeEnabled()) {
const context = this.getContext();
await this.analyzeFile(file, context);
}
}
onLanguageChange(language: string): void {
const supportedLanguages = ['typescript', 'javascript', 'python'];
this.isActive = supportedLanguages.includes(language);
}
private registerCommands(context: IPluginContext) {
context.registerCommand({
id: 'analyze-code-quality',
name: 'Analyze Code Quality',
shortcut: 'Ctrl+Shift+Q',
description: 'Analyze code quality of current file',
execute: async () => {
const file = context.editor.getActiveFile();
if (file) {
const issues = await this.analyzeFile(file, context);
this.reporters[0].report(issues);
}
}
});
context.registerCommand({
id: 'generate-quality-report',
name: 'Generate Quality Report',
shortcut: 'Ctrl+Shift+R',
description: 'Generate detailed quality report',
execute: async () => {
const files = context.project.getProjectFiles();
const allIssues: QualityIssue[] = [];
for (const file of files) {
const issues = await this.analyzeFile(file, context);
allIssues.push(...issues);
}
const report = this.reporters[1].generate(allIssues);
context.editor.openPreview(report);
}
});
}
private registerSettings(context: IPluginContext) {
context.registerSetting({
id: 'auto-analyze',
name: 'Auto Analyze',
type: 'boolean',
default: true,
description: 'Automatically analyze code quality on save'
});
context.registerSetting({
id: 'complexity-threshold',
name: 'Complexity Threshold',
type: 'number',
default: 10,
min: 1,
max: 50,
description: 'Maximum allowed cyclomatic complexity'
});
}
async analyzeFile(file: IFile, context: IPluginContext): Promise<QualityIssue[]> {
const code = file.getContent();
const language = file.getLanguage();
const issues: QualityIssue[] = [];
for (const analyzer of this.analyzers) {
if (analyzer.supports(language)) {
const foundIssues = await analyzer.analyze(code, {
filepath: file.getPath(),
config: this.config.getAll()
});
issues.push(...foundIssues);
}
}
// 显示问题
context.editor.showDiagnostics(issues.map(issue => ({
range: issue.location,
severity: issue.severity,
message: issue.message,
source: 'code-quality-analyzer'
})));
// 发送通知
if (issues.length > 0) {
context.notifications.show({
type: 'info',
title: 'Code Quality Analysis',
message: `Found ${issues.length} issues in ${file.getName()}`,
action: 'View Report',
onAction: () => this.reporters[0].report(issues)
});
}
return issues;
}
}
export default CodeQualityAnalyzer;
复杂度分析器
// src/analyzer/complexity.ts
import { CodeAnalyzer, AnalysisContext, QualityIssue } from './types';
import { parse } from '@babel/parser';
import traverse from '@babel/traverse';
import * as t from '@babel/types';
interface ComplexityConfig {
threshold: number;
includeNested: boolean;
}
export class ComplexityAnalyzer implements CodeAnalyzer {
name = 'complexity';
private config: ComplexityConfig;
constructor(config?: Partial<ComplexityConfig>) {
this.config = {
threshold: config?.threshold ?? 10,
includeNested: config?.includeNested ?? true
};
}
supports(language: string): boolean {
return ['typescript', 'javascript'].includes(language);
}
async analyze(code: string, context: AnalysisContext): Promise<QualityIssue[]> {
const issues: QualityIssue[] = [];
try {
const ast = parse(code, {
sourceType: 'module',
plugins: ['typescript', 'decorators-legacy']
});
traverse(ast, {
FunctionDeclaration(path) {
const complexity = this.calculateComplexity(path.node);
if (complexity > this.config.threshold) {
issues.push({
id: `complexity-${path.node.start}`,
type: 'complexity',
severity: complexity > this.config.threshold * 2 ? 'error' : 'warning',
message: `Function has cyclomatic complexity of ${complexity} (threshold: ${this.config.threshold})`,
location: {
startLine: path.node.loc?.start.line ?? 0,
endLine: path.node.loc?.end.line ?? 0,
startColumn: path.node.loc?.start.column ?? 0,
endColumn: path.node.loc?.end.column ?? 0
},
suggestion: 'Consider breaking this function into smaller, more manageable pieces',
code: code.substring(path.node.start ?? 0, path.node.end ?? 0)
});
}
}
});
} catch (error) {
console.error(`Error analyzing complexity in ${context.filepath}:`, error);
}
return issues;
}
private calculateComplexity(node: t.Function): number {
let complexity = 1;
const countNodes = (currentNode: t.Node) => {
if (t.isIfStatement(currentNode)) complexity++;
if (t.isForStatement(currentNode)) complexity++;
if (t.isWhileStatement(currentNode)) complexity++;
if (t.isDoWhileStatement(currentNode)) complexity++;
if (t.isSwitchCase(currentNode)) complexity++;
if (t.isLogicalExpression(currentNode)) complexity++;
if (t.isConditionalExpression(currentNode)) complexity++;
if (t.isCatchClause(currentNode)) complexity++;
for (const key in currentNode) {
if (key === 'loc' || key === 'start' || key === 'end') continue;
const child = (currentNode as any)[key];
if (child && typeof child === 'object' && child.type) {
countNodes(child);
}
}
};
countNodes(node);
return complexity;
}
}
重复代码检测
// src/analyzer/duplication.ts
import { CodeAnalyzer, AnalysisContext, QualityIssue } from './types';
interface DuplicationConfig {
minLines: number;
maxLines: number;
minTokens: number;
}
export class DuplicationDetector implements CodeAnalyzer {
name = 'duplication';
private config: DuplicationConfig;
private tokenCache: Map<string, Token[]> = new Map();
constructor(config?: Partial<DuplicationConfig>) {
this.config = {
minLines: config?.minLines ?? 5,
maxLines: config?.maxLines ?? 100,
minTokens: config?.minTokens ?? 30
};
}
supports(language: string): boolean {
return true;
}
async analyze(code: string, context: AnalysisContext): Promise<QualityIssue[]> {
const issues: QualityIssue[] = [];
const tokens = this.tokenize(code);
const normalizedTokens = this.normalizeTokens(tokens);
const patterns = this.findRepeatedPatterns(normalizedTokens);
for (const pattern of patterns) {
if (pattern.count >= 2) {
issues.push({
id: `duplication-${pattern.hash}`,
type: 'duplication',
severity: pattern.count > 3 ? 'error' : 'warning',
message: `Found ${pattern.count} occurrences of similar code pattern (${pattern.lines} lines)`,
location: {
startLine: pattern.occurrences[0].startLine,
endLine: pattern.occurrences[0].endLine,
startColumn: 0,
endColumn: 0
},
suggestion: 'Consider extracting this code into a reusable function or variable',
code: pattern.code,
relatedLocations: pattern.occurrences.slice(1).map(occ => ({
startLine: occ.startLine,
endLine: occ.endLine,
startColumn: 0,
endColumn: 0
}))
});
}
}
return issues;
}
private tokenize(code: string): Token[] {
const tokens: Token[] = [];
const regex = /\b(?:[a-zA-Z_$][\w$]*|\d+|\.\.\.|=>|[{}()[\];,.:]|==|===|!=|!==|<=|>=|&&|\|\||[+\-*/%])\b/g;
let match;
while ((match = regex.exec(code)) !== null) {
tokens.push({
type: this.classifyToken(match[0]),
value: match[0],
position: match.index
});
}
return tokens;
}
private normalizeTokens(tokens: Token[]): NormalizedToken[] {
return tokens.map(token => ({
type: token.type,
normalized: this.normalizeValue(token.value),
position: token.position
}));
}
private normalizeValue(value: string): string {
if (/^\d+$/.test(value)) return '<NUM>';
if (/^['"`].*['"`]$/.test(value)) return '<STR>';
if (/^[a-z_]+$/.test(value)) return '<IDENT>';
return value;
}
private findRepeatedPatterns(tokens: NormalizedToken[]): Pattern[] {
const patterns: Map<string, Pattern> = new Map();
for (let length = this.config.minTokens; length <= this.config.maxTokens; length++) {
for (let i = 0; i <= tokens.length - length; i++) {
const segment = tokens.slice(i, i + length);
const hash = this.hashTokens(segment);
if (!patterns.has(hash)) {
patterns.set(hash, {
hash,
count: 0,
occurrences: [],
lines: 0,
code: ''
});
}
const pattern = patterns.get(hash)!;
pattern.count++;
}
}
return Array.from(patterns.values())
.filter(p => p.count >= 2 && p.lines >= this.config.minLines);
}
private hashTokens(tokens: NormalizedToken[]): string {
return tokens.map(t => `${t.type}:${t.normalized}`).join('|');
}
}
控制台报告器
// src/reporters/console.ts
import { Reporter, QualityIssue } from '../analyzer/types';
export class ConsoleReporter implements Reporter {
name = 'console';
report(issues: QualityIssue[]): void {
if (issues.length === 0) {
console.log('✅ No code quality issues found!');
return;
}
console.log(`\n📊 Code Quality Report - Found ${issues.length} issues\n`);
const grouped = this.groupBySeverity(issues);
for (const [severity, severityIssues] of Object.entries(grouped)) {
const icon = this.getSeverityIcon(severity as QualityIssue['severity']);
const color = this.getSeverityColor(severity as QualityIssue['severity']);
console.log(`${color}${icon} ${severity.toUpperCase()}: ${severityIssues.length} issues${this.reset()}\n`);
for (const issue of severityIssues) {
console.log(` ${issue.message}`);
console.log(` Location: Line ${issue.location.startLine}-${issue.location.endLine}`);
console.log(` Type: ${issue.type}`);
console.log(` Suggestion: ${issue.suggestion}`);
console.log('');
}
}
console.log(this.generateSummary(issues));
}
generate(issues: QualityIssue[]): string {
return this.report(issues) as unknown as string;
}
private groupBySeverity(issues: QualityIssue[]): Record<string, QualityIssue[]> {
const grouped: Record<string, QualityIssue[]> = {};
for (const issue of issues) {
if (!grouped[issue.severity]) {
grouped[issue.severity] = [];
}
grouped[issue.severity].push(issue);
}
return grouped;
}
private generateSummary(issues: QualityIssue[]): string {
const total = issues.length;
const byType: Record<string, number> = {};
for (const issue of issues) {
byType[issue.type] = (byType[issue.type] || 0) + 1;
}
let summary = '📈 Summary:\n';
summary += ` Total Issues: ${total}\n`;
for (const [type, count] of Object.entries(byType)) {
summary += ` - ${type}: ${count}\n`;
}
return summary;
}
private getSeverityIcon(severity: QualityIssue['severity']): string {
const icons = { error: '🔴', warning: '🟡', info: '🔵' };
return icons[severity] || '⚪';
}
private getSeverityColor(severity: QualityIssue['severity']): string {
const colors = {
error: '\x1b[31m',
warning: '\x1b[33m',
info: '\x1b[34m'
};
return colors[severity] || '\x1b[0m';
}
private reset(): string {
return '\x1b[0m';
}
}
HTML 报告器
// src/reporters/html.ts
import { Reporter, QualityIssue } from '../analyzer/types';
export class HtmlReporter implements Reporter {
name = 'html';
report(issues: QualityIssue[]): string {
return this.generate(issues);
}
generate(issues: QualityIssue[]): string {
const summary = this.generateSummary(issues);
const groupedIssues = this.groupByFile(issues);
const charts = this.generateCharts(issues);
return `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Code Quality Report</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; background: #f5f5f5; }
.container { max-width: 1200px; margin: 0 auto; padding: 20px; }
.header { background: linear-gradient(135deg, #667eea, #764ba2); color: white; padding: 40px; border-radius: 12px; margin-bottom: 20px; }
.header h1 { font-size: 2em; margin-bottom: 10px; }
.stats { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin-bottom: 20px; }
.stat-card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
.stat-number { font-size: 2em; font-weight: bold; }
.stat-label { color: #666; }
.severity-error { color: #e74c3c; }
.severity-warning { color: #f39c12; }
.severity-info { color: #3498db; }
.file-section { background: white; padding: 20px; border-radius: 8px; margin-bottom: 20px; }
.file-section h2 { margin-bottom: 15px; }
.issue { padding: 15px; border-left: 4px solid; margin-bottom: 10px; border-radius: 4px; }
.issue.error { border-color: #e74c3c; background: #ffebee; }
.issue.warning { border-color: #f39c12; background: #fff3e0; }
.issue-info { font-size: 0.9em; color: #666; margin-top: 5px; }
.issue-suggestion { margin-top: 10px; padding: 10px; background: #f8f9fa; border-radius: 4px; }
.code-block { margin-top: 10px; padding: 10px; background: #282c34; color: #abb2bf; border-radius: 4px; overflow-x: auto; }
</style>
</head>
<body>
<div class="container">
${this.generateHeader(issues)}
<div class="stats">${this.generateStatsCards(issues)}</div>
${this.generateFileSections(groupedIssues)}
</div>
</body>
</html>
`;
}
private generateHeader(issues: QualityIssue[]): string {
const total = issues.length;
const date = new Date().toLocaleDateString();
return `
<div class="header">
<h1>📊 Code Quality Report</h1>
<p>Generated on ${date} | Total Issues: ${total}</p>
</div>
`;
}
private generateStatsCards(issues: QualityIssue[]): string {
const errors = issues.filter(i => i.severity === 'error').length;
const warnings = issues.filter(i => i.severity === 'warning').length;
const infos = issues.filter(i => i.severity === 'info').length;
return `
<div class="stat-card">
<div class="stat-number severity-error">${errors}</div>
<div class="stat-label">Errors</div>
</div>
<div class="stat-card">
<div class="stat-number severity-warning">${warnings}</div>
<div class="stat-label">Warnings</div>
</div>
<div class="stat-card">
<div class="stat-number severity-info">${infos}</div>
<div class="stat-label">Info</div>
</div>
<div class="stat-card">
<div class="stat-number">${issues.length > 0 ? (100 - Math.round((issues.filter(i => i.severity === 'error').length / issues.length) * 100)) : 100}%</div>
<div class="stat-label">Quality Score</div>
</div>
`;
}
private groupByFile(issues: QualityIssue[]): Record<string, QualityIssue[]> {
const grouped: Record<string, QualityIssue[]> = {};
for (const issue of issues) {
const filepath = (issue as any).filepath || 'unknown';
if (!grouped[filepath]) {
grouped[filepath] = [];
}
grouped[filepath].push(issue);
}
return grouped;
}
private generateFileSections(grouped: Record<string, QualityIssue[]>): string {
return Object.entries(grouped)
.map(([filepath, issues]) => `
<div class="file-section">
<h2>📁 ${filepath}</h2>
${issues.map(issue => this.generateIssueHTML(issue)).join('')}
</div>
`)
.join('');
}
private generateIssueHTML(issue: QualityIssue): string {
return `
<div class="issue ${issue.severity}">
<strong>${issue.message}</strong>
<div class="issue-info">
Type: ${issue.type} | Line: ${issue.location.startLine}-${issue.location.endLine}
</div>
<div class="issue-suggestion">
💡 <strong>Suggestion:</strong> ${issue.suggestion}
</div>
${issue.code ? `<pre class="code-block"><code>${escapeHtml(issue.code)}</code></pre>` : ''}
</div>
`;
}
private generateCharts(issues: QualityIssue[]): string {
return '';
}
private generateSummary(issues: QualityIssue[]): string {
return `Generated report for ${issues.length} issues`;
}
}
function escapeHtml(text: string): string {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
三、插件配置与调试
3.1 插件配置文件
// plugin.json
{
"id": "code-quality-analyzer",
"name": "Code Quality Analyzer",
"version": "1.0.0",
"description": "Real-time code quality analysis tool for AtomCode",
"author": "Your Name",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"keywords": [
"code-quality",
"linting",
"analysis",
"refactoring"
],
"engines": {
"atomcode": ">=1.0.0"
},
"categories": [
"Linters",
"Code Quality"
],
"activationEvents": [
"onLanguage:typescript",
"onLanguage:javascript"
],
"contributes": {
"commands": [
{
"id": "analyze-code-quality",
"name": "Analyze Code Quality",
"category": "Code Quality",
"description": "Analyze code quality of current file"
},
{
"id": "generate-quality-report",
"name": "Generate Quality Report",
"category": "Code Quality",
"description": "Generate detailed quality report for project"
}
],
"configuration": {
"title": "Code Quality Analyzer Configuration",
"properties": {
"autoAnalyze": {
"type": "boolean",
"default": true,
"description": "Automatically analyze code quality on file save"
},
"complexityThreshold": {
"type": "number",
"default": 10,
"min": 1,
"max": 50,
"description": "Maximum allowed cyclomatic complexity"
},
"minDuplicationLines": {
"type": "number",
"default": 5,
"min": 2,
"description": "Minimum lines for duplication detection"
}
}
}
}
}
3.2 插件编译
# 编译 TypeScript
npx tsc
# 或者使用 rollup
npx rollup -c rollup.config.js
# 编译后目录结构
dist/
├── index.js
├── index.d.ts
├── analyzer/
│ ├── complexity.js
│ ├── duplication.js
│ ├── naming.js
│ └── style.js
├── reporters/
│ ├── console.js
│ ├── html.js
│ └── json.js
└── utils/
├── ast.js
├── config.js
└── logger.js
3.3 插件安装与调试
# 开发模式安装
cd ~/.atomcode/plugins
ln -s /path/to/code-quality-analyzer .
# 重启 AtomCode
# 或使用命令面板 > Reload Window
# 调试模式
# 打开 Chrome DevTools
Ctrl+Shift+I
# 查看插件日志
# 输出面板 > 选择 "Plugin Logs"
调试技巧:
// 使用插件上下文的日志 API
context.notifications.show({
type: 'debug',
title: 'Debug Info',
message: `Analyzing file: ${file.getPath()}`
});
// 使用 console.log
console.log('Plugin debug:', {
file: file.getPath(),
language: file.getLanguage(),
issues: issues.length
});
// 使用断点调试
// 在 VS Code 中附加调试器
四、发布插件
4.1 打包插件
// package.json
{
"name": "code-quality-analyzer",
"version": "1.0.0",
"description": "Real-time code quality analysis tool for AtomCode",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"watch": "tsc --watch",
"package": "node scripts/package.js",
"publish": "npm publish"
},
"devDependencies": {
"@types/node": "^20.0.0",
"typescript": "^5.0.0",
"@babel/parser": "^7.24.0",
"@babel/traverse": "^7.24.0",
"@babel/types": "^7.24.0"
}
}
4.2 发布到市场
-
创建 AtomCode 账号
- 访问 https://atomgit.com/plugins
- 注册开发者账号
-
提交插件审核
- 填写插件信息
- 上传插件包
- 等待审核(通常 3-5 个工作日)
-
发布到市场
- 审核通过后自动发布
- 用户可以在插件市场搜索并安装
4.3 版本管理
# 更新版本号
npm version patch # 1.0.0 -> 1.0.1
npm version minor # 1.0.1 -> 1.1.0
npm version major # 1.1.0 -> 2.0.0
# 发布新版本
npm publish
# 更新 changelog
# CHANGELOG.md
五、进阶技巧
5.1 与 AI 模型集成
class AIEnhancedAnalyzer {
async analyzeWithAI(code: string, context: IPluginContext): Promise<QualityIssue[]> {
// 使用 AI 分析代码
const aiPrompt = `
Analyze the following code for quality issues:
\`\`\`typescript
${code}
\`\`\`
Please identify:
1. Code smells and anti-patterns
2. Potential bugs
3. Performance issues
4. Security concerns
Format the response as JSON array with fields:
{ type, severity, message, suggestion }
`;
const aiResponse = await context.ai.generate(aiPrompt);
const aiIssues = this.parseAIResponse(aiResponse);
// 结合规则引擎的结果
const ruleEngineIssues = await this.ruleEngine.analyze(code);
return [...ruleEngineIssues, ...aiIssues];
}
private parseAIResponse(response: string): QualityIssue[] {
try {
const jsonStr = response.match(/```json\n([\s\S]*?)\n```/)?.[1] || response;
const parsed = JSON.parse(jsonStr);
return parsed.map((item: any) => ({
id: `ai-${Date.now()}`,
type: item.type,
severity: item.severity,
message: item.message,
location: { startLine: 0, endLine: 0, startColumn: 0, endColumn: 0 },
suggestion: item.suggestion
}));
} catch {
console.error('Failed to parse AI response');
return [];
}
}
}
5.2 性能优化
class OptimizedAnalyzer {
private cache = new Map<string, AnalysisResult>();
private debounceTimer: NodeJS.Timeout | null = null;
async analyzeOptimized(code: string, file: IFile): Promise<QualityIssue[]> {
const cacheKey = `${file.getPath()}:${this.hash(code)}`;
// 检查缓存
const cached = this.cache.get(cacheKey);
if (cached) {
return cached.issues;
}
// 防抖处理
return new Promise((resolve) => {
if (this.debounceTimer) {
clearTimeout(this.debounceTimer);
}
this.debounceTimer = setTimeout(async () => {
const issues = await this.analyze(code, file);
this.cache.set(cacheKey, { issues, timestamp: Date.now() });
resolve(issues);
}, 300);
});
}
private hash(code: string): string {
let hash = 0;
for (let i = 0; i < code.length; i++) {
hash = ((hash << 5) - hash) + code.charCodeAt(i);
hash = hash & hash;
}
return Math.abs(hash).toString(36);
}
}
六、总结与展望
通过本篇文章,你已经学会了:
- ✅ AtomCode 插件架构和 API
- ✅ 从零开发完整的代码质量分析插件
- ✅ 插件调试和配置
- ✅ 插件发布流程
进阶建议:
- 探索更多插件 API
- 创建与你的工作流匹配的插件
- 参与 AtomCode 插件社区
- 将开源贡献作为学习的一部分
现在就开始你的 AtomCode 插件开发之旅吧!🚀
本文为原创内容,基于作者实际插件开发经验整理。如需转载,请注明出处。
更多推荐
所有评论(0)