autosnippet 2.12.0 → 2.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -34,6 +34,8 @@ import { SnippetInstaller } from '../service/snippet/SnippetInstaller.js';
34
34
  import { ExclusionManager } from '../service/guard/ExclusionManager.js';
35
35
  import { RuleLearner } from '../service/guard/RuleLearner.js';
36
36
  import { ViolationsStore } from '../service/guard/ViolationsStore.js';
37
+ import { ComplianceReporter } from '../service/guard/ComplianceReporter.js';
38
+ import { GuardFeedbackLoop } from '../service/guard/GuardFeedbackLoop.js';
37
39
 
38
40
  // ─── P1: Token Usage Tracking ─────────────────────────
39
41
  import { TokenUsageStore } from '../repository/token/TokenUsageStore.js';
@@ -459,6 +461,33 @@ export class ServiceContainer {
459
461
  return this.singletons.violationsStore;
460
462
  });
461
463
 
464
+ // Guard: ComplianceReporter
465
+ this.register('complianceReporter', () => {
466
+ if (!this.singletons.complianceReporter) {
467
+ const config = this.singletons._config || {};
468
+ this.singletons.complianceReporter = new ComplianceReporter(
469
+ this.get('guardCheckEngine'),
470
+ this.get('violationsStore'),
471
+ this.get('ruleLearner'),
472
+ this.get('exclusionManager'),
473
+ config.qualityGate || {},
474
+ );
475
+ }
476
+ return this.singletons.complianceReporter;
477
+ });
478
+
479
+ // Guard: GuardFeedbackLoop
480
+ this.register('guardFeedbackLoop', () => {
481
+ if (!this.singletons.guardFeedbackLoop) {
482
+ this.singletons.guardFeedbackLoop = new GuardFeedbackLoop(
483
+ this.get('violationsStore'),
484
+ this.get('feedbackCollector'),
485
+ { guardCheckEngine: this.get('guardCheckEngine') },
486
+ );
487
+ }
488
+ return this.singletons.guardFeedbackLoop;
489
+ });
490
+
462
491
  // Token Usage: 持久化 AI token 消耗
463
492
  this.register('tokenUsageStore', () => {
464
493
  if (!this.singletons.tokenUsageStore) {
@@ -211,9 +211,20 @@ function _auditSingleFile(engine, fullPath, code, detectLanguage, scope = 'file'
211
211
  console.log(` 🛡️ ${errors.length} errors, ${warnings.length} warnings`);
212
212
  for (const v of errors) {
213
213
  console.log(` ❌ L${v.line} [${v.ruleId}] ${v.message}`);
214
+ if (v.fixSuggestion) console.log(` 🔧 修复建议: ${v.fixSuggestion}`);
214
215
  }
215
216
  for (const v of warnings.slice(0, 5)) {
216
217
  console.log(` ⚠️ L${v.line} [${v.ruleId}] ${v.message}`);
218
+ if (v.fixSuggestion) console.log(` 🔧 修复建议: ${v.fixSuggestion}`);
217
219
  }
218
220
  }
221
+
222
+ // Guard ↔ Recipe 闭环:检测修复并自动确认使用(fire-and-forget)
223
+ import('../../../injection/ServiceContainer.js').then(({ ServiceContainer }) => {
224
+ try {
225
+ const container = ServiceContainer.getInstance();
226
+ const feedbackLoop = container.get('guardFeedbackLoop');
227
+ feedbackLoop.processFixDetection({ violations }, fullPath);
228
+ } catch { /* guardFeedbackLoop not available */ }
229
+ }).catch(() => { /* ignored */ });
219
230
  }
@@ -0,0 +1,335 @@
1
+ /**
2
+ * ComplianceReporter — 全项目 Guard 合规报告生成
3
+ *
4
+ * 依赖:
5
+ * - GuardCheckEngine.auditFiles() — 原始 violations 数据
6
+ * - ViolationsStore — 历史统计 & 趋势
7
+ * - RuleLearner — 规则 P/R/F1
8
+ * - ExclusionManager — 排除项(不计入合规分)
9
+ * - config.qualityGate — 阈值配置
10
+ *
11
+ * 输出:
12
+ * ComplianceReport { qualityGate, summary, topViolations, fileHotspots, ruleHealth, trend }
13
+ */
14
+
15
+ import Logger from '../../infrastructure/logging/Logger.js';
16
+ import { collectSourceFilesWithContent } from './SourceFileCollector.js';
17
+
18
+ /**
19
+ * Quality Gate 评分算法
20
+ */
21
+ function computeScore(summary, ruleHealth = []) {
22
+ let score = 100;
23
+
24
+ // 扣分:每个 error -5,每个 warning -1,info -0.2
25
+ score -= summary.errors * 5;
26
+ score -= summary.warnings * 1;
27
+ score -= (summary.infos || 0) * 0.2;
28
+
29
+ // 加分:规则平均 F1 > 0.8 加 5 分
30
+ if (ruleHealth.length > 0) {
31
+ const avgF1 = ruleHealth.reduce((s, r) => s + (r.f1 || 0), 0) / ruleHealth.length;
32
+ if (avgF1 > 0.8) score += 5;
33
+ }
34
+
35
+ // 扣分:高误报规则每条 -3
36
+ const problematic = ruleHealth.filter(r => (r.precision || 1) < 0.5);
37
+ score -= problematic.length * 3;
38
+
39
+ return Math.max(0, Math.min(100, Math.round(score)));
40
+ }
41
+
42
+ /**
43
+ * 判定 Quality Gate 状态
44
+ */
45
+ function evaluateGate(summary, score, thresholds) {
46
+ const { maxErrors = 0, maxWarnings = 20, minScore = 70 } = thresholds;
47
+
48
+ if (summary.errors > maxErrors) return 'FAIL';
49
+ if (score < minScore) return 'FAIL';
50
+ if (summary.warnings > maxWarnings) return 'WARN';
51
+ return 'PASS';
52
+ }
53
+
54
+ export class ComplianceReporter {
55
+ /**
56
+ * @param {import('./GuardCheckEngine.js').GuardCheckEngine} guardCheckEngine
57
+ * @param {import('./ViolationsStore.js').ViolationsStore} violationsStore
58
+ * @param {import('./RuleLearner.js').RuleLearner} ruleLearner
59
+ * @param {import('./ExclusionManager.js').ExclusionManager} exclusionManager
60
+ * @param {object} qualityGateConfig - { maxErrors, maxWarnings, minScore }
61
+ */
62
+ constructor(guardCheckEngine, violationsStore, ruleLearner, exclusionManager, qualityGateConfig = {}) {
63
+ this.engine = guardCheckEngine;
64
+ this.violationsStore = violationsStore;
65
+ this.ruleLearner = ruleLearner;
66
+ this.exclusionManager = exclusionManager;
67
+ this.qualityGateConfig = {
68
+ maxErrors: 0,
69
+ maxWarnings: 20,
70
+ minScore: 70,
71
+ ...qualityGateConfig,
72
+ };
73
+ this.logger = Logger.getInstance();
74
+ }
75
+
76
+ /**
77
+ * 生成全项目合规报告
78
+ * @param {string} projectRoot - 项目根目录
79
+ * @param {object} options
80
+ * @param {object} [options.qualityGate] - 覆盖默认的 Quality Gate 阈值
81
+ * @param {number} [options.maxFiles] - 最大扫描文件数
82
+ * @returns {Promise<ComplianceReport>}
83
+ */
84
+ async generate(projectRoot, options = {}) {
85
+ const thresholds = { ...this.qualityGateConfig, ...(options.qualityGate || {}) };
86
+ const maxFiles = options.maxFiles || 500;
87
+
88
+ // 1. 收集源文件
89
+ const files = await collectSourceFilesWithContent(projectRoot, { maxFiles });
90
+ this.logger.info(`[ComplianceReporter] Collected ${files.length} source files`);
91
+
92
+ // 2. 批量审计
93
+ const auditResult = this.engine.auditFiles(files, { scope: 'project' });
94
+
95
+ // 3. 通过 ExclusionManager 过滤被排除的项
96
+ const filteredFiles = [];
97
+ for (const fileResult of auditResult.files || []) {
98
+ if (this.exclusionManager?.isPathExcluded?.(fileResult.filePath)) continue;
99
+
100
+ const filteredViolations = fileResult.violations.filter(v => {
101
+ // isRuleExcluded 内部已检查全局排除
102
+ if (this.exclusionManager?.isRuleExcluded?.(v.ruleId, fileResult.filePath)) return false;
103
+ return true;
104
+ });
105
+
106
+ filteredFiles.push({
107
+ ...fileResult,
108
+ violations: filteredViolations,
109
+ summary: {
110
+ total: filteredViolations.length,
111
+ errors: filteredViolations.filter(v => v.severity === 'error').length,
112
+ warnings: filteredViolations.filter(v => v.severity === 'warning').length,
113
+ infos: filteredViolations.filter(v => v.severity === 'info').length,
114
+ },
115
+ });
116
+ }
117
+
118
+ // 4. 汇总
119
+ const summary = {
120
+ filesScanned: files.length,
121
+ totalViolations: filteredFiles.reduce((s, f) => s + f.summary.total, 0),
122
+ errors: filteredFiles.reduce((s, f) => s + f.summary.errors, 0),
123
+ warnings: filteredFiles.reduce((s, f) => s + f.summary.warnings, 0),
124
+ infos: filteredFiles.reduce((s, f) => s + f.summary.infos, 0),
125
+ };
126
+
127
+ // 5. 按规则 ID 聚合 top violations
128
+ const ruleAgg = new Map();
129
+ for (const f of filteredFiles) {
130
+ for (const v of f.violations) {
131
+ const key = v.ruleId;
132
+ if (!ruleAgg.has(key)) {
133
+ ruleAgg.set(key, {
134
+ ruleId: key,
135
+ message: v.message,
136
+ severity: v.severity,
137
+ fileCount: new Set(),
138
+ occurrences: 0,
139
+ fixRecipeId: null,
140
+ fixRecipeTitle: null,
141
+ });
142
+ }
143
+ const agg = ruleAgg.get(key);
144
+ agg.fileCount.add(f.filePath);
145
+ agg.occurrences++;
146
+ if (v.fixSuggestion && !agg.fixRecipeId) {
147
+ agg.fixRecipeId = v.fixSuggestion.replace(/^recipe:/, '');
148
+ }
149
+ }
150
+ }
151
+
152
+ const topViolations = [...ruleAgg.values()]
153
+ .map(v => ({ ...v, fileCount: v.fileCount.size }))
154
+ .sort((a, b) => b.occurrences - a.occurrences)
155
+ .slice(0, 20);
156
+
157
+ // 6. 文件热点
158
+ const fileHotspots = filteredFiles
159
+ .filter(f => f.summary.total > 0)
160
+ .map(f => ({
161
+ filePath: f.filePath,
162
+ violationCount: f.summary.total,
163
+ errorCount: f.summary.errors,
164
+ }))
165
+ .sort((a, b) => b.violationCount - a.violationCount)
166
+ .slice(0, 20);
167
+
168
+ // 7. 规则健康度(来自 RuleLearner)
169
+ let ruleHealth = [];
170
+ try {
171
+ if (this.ruleLearner?.getAllStats) {
172
+ const allStats = this.ruleLearner.getAllStats();
173
+ ruleHealth = Object.entries(allStats).map(([ruleId, stat]) => ({
174
+ ruleId,
175
+ precision: stat.metrics?.precision ?? 1,
176
+ recall: stat.metrics?.recall ?? 1,
177
+ f1: stat.metrics?.f1 ?? 1,
178
+ triggers: stat.triggers || 0,
179
+ warning: (stat.metrics?.precision ?? 1) < 0.5 ? '高误报' : null,
180
+ }));
181
+ }
182
+ } catch {
183
+ // RuleLearner not available
184
+ }
185
+
186
+ // 8. 趋势
187
+ let trend = { errorsChange: 0, warningsChange: 0, hasHistory: false };
188
+ try {
189
+ if (this.violationsStore?.getTrend) {
190
+ trend = this.violationsStore.getTrend();
191
+ }
192
+ } catch {
193
+ // ViolationsStore not available
194
+ }
195
+
196
+ // 9. 评分 + Gate
197
+ const score = computeScore(summary, ruleHealth);
198
+ const gateStatus = evaluateGate(summary, score, thresholds);
199
+
200
+ // 10. 写入 ViolationsStore(记录本次运行)
201
+ try {
202
+ if (this.violationsStore?.appendRun) {
203
+ const allViolations = filteredFiles.flatMap(f =>
204
+ f.violations.map(v => ({ ...v, filePath: f.filePath }))
205
+ );
206
+ this.violationsStore.appendRun({
207
+ filePath: projectRoot,
208
+ violations: allViolations,
209
+ summary: `Compliance scan: score=${score} ${gateStatus} | ${summary.errors}E ${summary.warnings}W`,
210
+ });
211
+ }
212
+ } catch {
213
+ // Persist failure — non-critical
214
+ }
215
+
216
+ return {
217
+ timestamp: new Date().toISOString(),
218
+ projectRoot,
219
+ qualityGate: {
220
+ status: gateStatus,
221
+ score,
222
+ thresholds,
223
+ },
224
+ summary,
225
+ topViolations,
226
+ fileHotspots,
227
+ ruleHealth,
228
+ trend,
229
+ };
230
+ }
231
+
232
+ /**
233
+ * 终端格式化输出报告
234
+ * @param {object} report - generate() 产出的报告
235
+ * @param {object} options - { format: 'text' | 'markdown' | 'json' }
236
+ */
237
+ printReport(report, options = {}) {
238
+ const { format = 'text' } = options;
239
+
240
+ if (format === 'json') {
241
+ console.log(JSON.stringify(report, null, 2));
242
+ return;
243
+ }
244
+
245
+ if (format === 'markdown') {
246
+ this._printMarkdown(report);
247
+ return;
248
+ }
249
+
250
+ // text format
251
+ this._printText(report);
252
+ }
253
+
254
+ _printText(report) {
255
+ const { qualityGate, summary, topViolations, fileHotspots, trend } = report;
256
+
257
+ const gateIcon = qualityGate.status === 'PASS' ? '✅' : qualityGate.status === 'WARN' ? '⚠️' : '❌';
258
+ console.log(`\n${'═'.repeat(60)}`);
259
+ console.log(` 🛡️ Guard Compliance Report`);
260
+ console.log(`${'═'.repeat(60)}`);
261
+ console.log(` ${gateIcon} Quality Gate: ${qualityGate.status} (Score: ${qualityGate.score}/100)`);
262
+ console.log(` 📁 Files Scanned: ${summary.filesScanned}`);
263
+ console.log(` 📊 Violations: ${summary.errors} errors, ${summary.warnings} warnings, ${summary.infos || 0} infos`);
264
+
265
+ if (trend.hasHistory) {
266
+ const errTrend = trend.errorsChange > 0 ? `+${trend.errorsChange}` : `${trend.errorsChange}`;
267
+ const warnTrend = trend.warningsChange > 0 ? `+${trend.warningsChange}` : `${trend.warningsChange}`;
268
+ console.log(` 📈 Trend: errors ${errTrend}, warnings ${warnTrend}`);
269
+ }
270
+
271
+ if (topViolations.length > 0) {
272
+ console.log(`\n Top Violations:`);
273
+ for (const v of topViolations.slice(0, 10)) {
274
+ const fix = v.fixRecipeId ? ` → 🔧 recipe:${v.fixRecipeId}` : '';
275
+ console.log(` [${v.severity}] ${v.ruleId} — ${v.occurrences} hits in ${v.fileCount} files${fix}`);
276
+ }
277
+ }
278
+
279
+ if (fileHotspots.length > 0) {
280
+ console.log(`\n File Hotspots:`);
281
+ for (const f of fileHotspots.slice(0, 10)) {
282
+ console.log(` 📄 ${f.filePath} — ${f.violationCount} violations (${f.errorCount} errors)`);
283
+ }
284
+ }
285
+
286
+ console.log(`${'═'.repeat(60)}\n`);
287
+ }
288
+
289
+ _printMarkdown(report) {
290
+ const { qualityGate, summary, topViolations, fileHotspots, trend } = report;
291
+ const lines = [];
292
+
293
+ lines.push('# Guard Compliance Report');
294
+ lines.push('');
295
+ lines.push(`| Metric | Value |`);
296
+ lines.push(`|--------|-------|`);
297
+ lines.push(`| Quality Gate | ${qualityGate.status} (Score: ${qualityGate.score}/100) |`);
298
+ lines.push(`| Files Scanned | ${summary.filesScanned} |`);
299
+ lines.push(`| Errors | ${summary.errors} |`);
300
+ lines.push(`| Warnings | ${summary.warnings} |`);
301
+ lines.push(`| Infos | ${summary.infos || 0} |`);
302
+
303
+ if (trend.hasHistory) {
304
+ lines.push(`| Errors Trend | ${trend.errorsChange > 0 ? '+' : ''}${trend.errorsChange} |`);
305
+ lines.push(`| Warnings Trend | ${trend.warningsChange > 0 ? '+' : ''}${trend.warningsChange} |`);
306
+ }
307
+
308
+ if (topViolations.length > 0) {
309
+ lines.push('');
310
+ lines.push('## Top Violations');
311
+ lines.push('');
312
+ lines.push('| Rule | Severity | Files | Hits | Fix |');
313
+ lines.push('|------|----------|-------|------|-----|');
314
+ for (const v of topViolations.slice(0, 20)) {
315
+ const fix = v.fixRecipeId ? `recipe:${v.fixRecipeId}` : '-';
316
+ lines.push(`| ${v.ruleId} | ${v.severity} | ${v.fileCount} | ${v.occurrences} | ${fix} |`);
317
+ }
318
+ }
319
+
320
+ if (fileHotspots.length > 0) {
321
+ lines.push('');
322
+ lines.push('## File Hotspots');
323
+ lines.push('');
324
+ lines.push('| File | Violations | Errors |');
325
+ lines.push('|------|-----------|--------|');
326
+ for (const f of fileHotspots.slice(0, 20)) {
327
+ lines.push(`| ${f.filePath} | ${f.violationCount} | ${f.errorCount} |`);
328
+ }
329
+ }
330
+
331
+ console.log(lines.join('\n'));
332
+ }
333
+ }
334
+
335
+ export default ComplianceReporter;
@@ -2,10 +2,11 @@
2
2
  * GuardCheckEngine - Guard 规则检查引擎
3
3
  *
4
4
  * 从 V1 guard/ios 迁移,适配 V2 架构
5
- * 支持: 正则模式匹配 + code-level 检查 + 多维度审计
5
+ * 支持: 正则模式匹配 + AST 语义规则 + code-level 检查 + 多维度审计
6
6
  */
7
7
 
8
8
  import Logger from '../../infrastructure/logging/Logger.js';
9
+ import * as AstAnalyzerModule from '../../core/AstAnalyzer.js';
9
10
 
10
11
  /**
11
12
  * 内置默认规则集(iOS: ObjC + Swift)
@@ -227,6 +228,7 @@ export class GuardCheckEngine {
227
228
  this.logger = Logger.getInstance();
228
229
  this._builtInRules = BUILT_IN_RULES;
229
230
  this._customRulesCache = null;
231
+ this._astRulesCache = null;
230
232
  this._cacheTime = 0;
231
233
  this._cacheTTL = options.cacheTTL || 60_000; // 1min
232
234
  }
@@ -251,24 +253,40 @@ export class GuardCheckEngine {
251
253
  AND lifecycle = 'active'`
252
254
  ).all();
253
255
  } catch { /* table may not exist */ }
254
- this._customRulesCache = rows.map(r => {
256
+
257
+ const regexRules = [];
258
+ const astRules = [];
259
+
260
+ for (const r of rows) {
255
261
  let guards = [];
256
262
  try {
257
263
  const constraints = JSON.parse(r.constraints || '{}');
258
264
  guards = constraints.guards || [];
259
265
  } catch { /* ignore */ }
260
- // Each guard entry becomes a rule
261
- return guards.map(g => ({
262
- id: g.id || r.id,
263
- name: g.name || r.title,
264
- message: g.message || r.description || r.title,
265
- pattern: g.pattern || '',
266
- languages: r.language ? [r.language] : [],
267
- severity: g.severity || 'warning',
268
- dimension: r.scope || 'file',
269
- source: 'database',
270
- }));
271
- }).flat().filter(r => r.pattern);
266
+
267
+ for (const g of guards) {
268
+ const ruleType = g.type || 'regex';
269
+ const base = {
270
+ id: g.id || r.id,
271
+ name: g.name || r.title,
272
+ message: g.message || r.description || r.title,
273
+ languages: r.language ? [r.language] : [],
274
+ severity: g.severity || 'warning',
275
+ dimension: r.scope || 'file',
276
+ source: 'database',
277
+ fixSuggestion: g.fixSuggestion || null,
278
+ };
279
+
280
+ if (ruleType === 'ast' && g.astQuery) {
281
+ astRules.push({ ...base, type: 'ast', astQuery: g.astQuery });
282
+ } else if (g.pattern) {
283
+ regexRules.push({ ...base, type: 'regex', pattern: g.pattern });
284
+ }
285
+ }
286
+ }
287
+
288
+ this._customRulesCache = regexRules;
289
+ this._astRulesCache = astRules;
272
290
  this._cacheTime = now;
273
291
  }
274
292
  rules.push(...this._customRulesCache);
@@ -289,6 +307,8 @@ export class GuardCheckEngine {
289
307
  severity: rule.severity,
290
308
  dimension: rule.dimension || 'file',
291
309
  source: 'built-in',
310
+ type: 'regex',
311
+ fixSuggestion: rule.fixSuggestion || null,
292
312
  });
293
313
  }
294
314
  }
@@ -298,6 +318,15 @@ export class GuardCheckEngine {
298
318
  rules = rules.filter(r => !r.languages?.length || r.languages.includes(language));
299
319
  }
300
320
 
321
+ // 合并 AST 规则(供外部调用者使用,如 GuardFeedbackLoop.查找 fixSuggestion)
322
+ if (this._astRulesCache?.length) {
323
+ let astRules = this._astRulesCache;
324
+ if (language) {
325
+ astRules = astRules.filter(r => !r.languages?.length || r.languages.includes(language));
326
+ }
327
+ rules.push(...astRules);
328
+ }
329
+
301
330
  return rules;
302
331
  }
303
332
 
@@ -306,7 +335,7 @@ export class GuardCheckEngine {
306
335
  * @param {string} code - 源代码
307
336
  * @param {string} language - 'objc'|'swift'|'javascript' 等
308
337
  * @param {object} options - {scope, filePath}
309
- * @returns {Array<{ruleId, message, severity, line, snippet, dimension?}>}
338
+ * @returns {Array<{ruleId, message, severity, line, snippet, dimension?, fixSuggestion?}>}
310
339
  */
311
340
  checkCode(code, language, options = {}) {
312
341
  const { scope = null } = options;
@@ -346,6 +375,7 @@ export class GuardCheckEngine {
346
375
  line: i + 1,
347
376
  snippet: lines[i].trim().slice(0, 120),
348
377
  ...(rule.dimension ? { dimension: rule.dimension } : {}),
378
+ ...(rule.fixSuggestion ? { fixSuggestion: rule.fixSuggestion } : {}),
349
379
  });
350
380
  }
351
381
  }
@@ -354,6 +384,9 @@ export class GuardCheckEngine {
354
384
  // Code-level 检查(不依赖正则)
355
385
  violations.push(...this._runCodeLevelChecks(code, language, lines));
356
386
 
387
+ // AST 语义规则检查
388
+ violations.push(...this._runAstRuleChecks(code, language));
389
+
357
390
  // 跟踪 Guard 命中次数(回写 Recipe 统计)
358
391
  this.trackGuardHits(violations);
359
392
 
@@ -363,11 +396,133 @@ export class GuardCheckEngine {
363
396
  reasoning: {
364
397
  whatViolated: v.ruleId,
365
398
  whyItMatters: v.message,
366
- suggestedFix: v.suggestedFix || null,
399
+ suggestedFix: v.fixSuggestion || v.suggestedFix || null,
367
400
  },
368
401
  }));
369
402
  }
370
403
 
404
+ /**
405
+ * AST 语义规则检查
406
+ * 支持 3 种查询类型: mustCallThrough, mustNotUseInContext, mustConformToProtocol
407
+ * 仅在 Tree-sitter 可用且语言为 ObjC/Swift 时执行
408
+ * @param {string} code - 源代码
409
+ * @param {string} language - 语言标识
410
+ * @returns {Array} violations
411
+ */
412
+ _runAstRuleChecks(code, language) {
413
+ // AST 规则仅支持 ObjC + Swift
414
+ const astLangMap = { objc: 'objectivec', swift: 'swift' };
415
+ const astLang = astLangMap[language];
416
+ if (!astLang) return [];
417
+
418
+ // 获取缓存中的 AST 规则
419
+ const astRules = (this._astRulesCache || []).filter(r =>
420
+ !r.languages?.length || r.languages.includes(language)
421
+ );
422
+ if (astRules.length === 0) return [];
423
+
424
+ // 延迟加载 AstAnalyzer
425
+ let AstAnalyzer;
426
+ try {
427
+ // 使用 dynamic import 会是 async,这里用 require 风格同步加载
428
+ // AstAnalyzer 作为 ESM 模块,在 constructor 时已被引入
429
+ AstAnalyzer = this._getAstAnalyzer();
430
+ if (!AstAnalyzer || !AstAnalyzer.isAvailable()) return [];
431
+ } catch {
432
+ this.logger.debug('AstAnalyzer not available, skipping AST rules');
433
+ return [];
434
+ }
435
+
436
+ const violations = [];
437
+
438
+ for (const rule of astRules) {
439
+ const { astQuery } = rule;
440
+ if (!astQuery?.queryType) continue;
441
+
442
+ try {
443
+ switch (astQuery.queryType) {
444
+ case 'mustCallThrough': {
445
+ // 检查某 API 是否只在指定 wrapper 类中调用
446
+ const { targetAPI, wrapperClass } = astQuery.params || {};
447
+ if (!targetAPI || !wrapperClass) break;
448
+
449
+ const calls = AstAnalyzer.findCallExpressions(code, astLang, targetAPI);
450
+ for (const call of calls) {
451
+ if (call.enclosingClass !== wrapperClass) {
452
+ violations.push({
453
+ ruleId: rule.id,
454
+ message: rule.message,
455
+ severity: rule.severity,
456
+ line: call.line,
457
+ snippet: call.snippet,
458
+ dimension: rule.dimension || 'file',
459
+ ...(rule.fixSuggestion ? { fixSuggestion: rule.fixSuggestion } : {}),
460
+ });
461
+ }
462
+ }
463
+ break;
464
+ }
465
+
466
+ case 'mustNotUseInContext': {
467
+ // 在特定上下文中禁止使用某模式
468
+ const { pattern: textPattern, forbiddenContext } = astQuery.params || {};
469
+ if (!textPattern || !forbiddenContext) break;
470
+
471
+ const matches = AstAnalyzer.findPatternInContext(code, astLang, textPattern, {
472
+ forbiddenContext,
473
+ });
474
+ for (const match of matches) {
475
+ violations.push({
476
+ ruleId: rule.id,
477
+ message: rule.message,
478
+ severity: rule.severity,
479
+ line: match.line,
480
+ snippet: match.snippet,
481
+ dimension: rule.dimension || 'file',
482
+ ...(rule.fixSuggestion ? { fixSuggestion: rule.fixSuggestion } : {}),
483
+ });
484
+ }
485
+ break;
486
+ }
487
+
488
+ case 'mustConformToProtocol': {
489
+ // 检查类是否实现了指定协议
490
+ const { className, protocolName } = astQuery.params || {};
491
+ if (!className || !protocolName) break;
492
+
493
+ const result = AstAnalyzer.checkProtocolConformance(code, astLang, className, protocolName);
494
+ if (result.classFound && !result.conforms) {
495
+ violations.push({
496
+ ruleId: rule.id,
497
+ message: rule.message,
498
+ severity: rule.severity,
499
+ line: result.classDeclLine || 1,
500
+ snippet: `class ${className} — missing ${protocolName} conformance`,
501
+ dimension: rule.dimension || 'file',
502
+ ...(rule.fixSuggestion ? { fixSuggestion: rule.fixSuggestion } : {}),
503
+ });
504
+ }
505
+ break;
506
+ }
507
+
508
+ default:
509
+ this.logger.debug(`Unknown AST query type: ${astQuery.queryType}`);
510
+ }
511
+ } catch (err) {
512
+ this.logger.debug(`AST rule ${rule.id} check failed: ${err.message}`);
513
+ }
514
+ }
515
+
516
+ return violations;
517
+ }
518
+
519
+ /**
520
+ * 获取 AstAnalyzer 模块(静态 import,带可用性检测)
521
+ */
522
+ _getAstAnalyzer() {
523
+ return AstAnalyzerModule;
524
+ }
525
+
371
526
  /**
372
527
  * 将 Guard 命中计数回写到对应 Recipe 的 guard_hit_count
373
528
  * @param {Array<{ruleId: string}>} violations