autosnippet 3.0.11 → 3.1.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.
- package/bin/api-server.js +2 -0
- package/bin/cli.js +84 -16
- package/config/default.json +10 -1
- package/dashboard/dist/assets/{index-I2ySoCmF.js → index-Bnm26ulL.js} +47 -47
- package/dashboard/dist/index.html +1 -1
- package/lib/bootstrap.js +4 -4
- package/lib/cli/SetupService.js +116 -29
- package/lib/cli/UpgradeService.js +16 -6
- package/lib/core/AstAnalyzer.js +1 -1
- package/lib/core/ast/ensure-grammars.js +1 -1
- package/lib/core/ast/index.js +62 -11
- package/lib/core/ast/lang-dart.js +27 -21
- package/lib/core/ast/lang-go.js +6 -20
- package/lib/core/ast/lang-rust.js +53 -28
- package/lib/core/ast/parser-init.js +9 -5
- package/lib/core/discovery/DartDiscoverer.js +4 -10
- package/lib/core/discovery/GenericDiscoverer.js +4 -28
- package/lib/core/discovery/GoDiscoverer.js +45 -25
- package/lib/core/discovery/NodeDiscoverer.js +1 -3
- package/lib/core/discovery/PythonDiscoverer.js +7 -1
- package/lib/core/discovery/RustDiscoverer.js +111 -38
- package/lib/core/discovery/index.js +2 -2
- package/lib/core/enhancement/django-enhancement.js +10 -4
- package/lib/core/enhancement/fastapi-enhancement.js +16 -9
- package/lib/core/enhancement/go-grpc-enhancement.js +2 -1
- package/lib/core/enhancement/go-web-enhancement.js +3 -6
- package/lib/core/enhancement/ml-enhancement.js +6 -3
- package/lib/core/enhancement/nextjs-enhancement.js +17 -7
- package/lib/core/enhancement/node-server-enhancement.js +4 -2
- package/lib/core/enhancement/react-enhancement.js +6 -3
- package/lib/core/enhancement/rust-tokio-enhancement.js +6 -2
- package/lib/core/enhancement/rust-web-enhancement.js +13 -7
- package/lib/core/enhancement/vue-enhancement.js +10 -5
- package/lib/external/ai/AiFactory.js +3 -1
- package/lib/external/ai/AiProvider.js +3 -1
- package/lib/external/mcp/McpServer.js +2 -0
- package/lib/external/mcp/handlers/bootstrap/base-dimensions.js +245 -0
- package/lib/external/mcp/handlers/bootstrap/pipeline/checkpoint.js +86 -0
- package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-configs.js +275 -0
- package/lib/external/mcp/handlers/bootstrap/pipeline/noAiFallback.js +629 -0
- package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +131 -348
- package/lib/external/mcp/handlers/bootstrap/refine.js +364 -0
- package/lib/external/mcp/handlers/bootstrap.js +7 -597
- package/lib/external/mcp/handlers/browse.js +123 -9
- package/lib/external/mcp/handlers/guard.js +29 -6
- package/lib/external/mcp/handlers/search.js +56 -24
- package/lib/external/mcp/handlers/skill.js +6 -2
- package/lib/http/HttpServer.js +1 -1
- package/lib/http/routes/candidates.js +3 -1
- package/lib/http/routes/extract.js +4 -5
- package/lib/http/routes/guardRules.js +9 -17
- package/lib/http/routes/modules.js +9 -3
- package/lib/http/routes/skills.js +54 -6
- package/lib/http/routes/violations.js +4 -3
- package/lib/infrastructure/external/ClipboardManager.js +24 -7
- package/lib/infrastructure/external/NativeUi.js +3 -1
- package/lib/infrastructure/external/OpenBrowser.js +1 -0
- package/lib/infrastructure/external/XcodeAutomation.js +5 -5
- package/lib/infrastructure/vector/IndexingPipeline.js +14 -5
- package/lib/injection/ServiceContainer.js +45 -13
- package/lib/platform/ios/index.js +20 -25
- package/lib/platform/ios/routes/spm.js +6 -3
- package/lib/platform/ios/snippet/PlaceholderConverter.js +6 -2
- package/lib/platform/ios/snippet/XcodeCodec.js +4 -2
- package/lib/platform/ios/spm/SpmDiscoverer.js +1 -1
- package/lib/platform/ios/spm/SpmService.js +3 -1
- package/lib/platform/ios/xcode/XcodeImportResolver.js +434 -0
- package/lib/platform/ios/xcode/XcodeIntegration.js +43 -664
- package/lib/platform/ios/xcode/XcodeWriteUtils.js +225 -0
- package/lib/service/automation/FileWatcher.js +1 -3
- package/lib/service/automation/handlers/CreateHandler.js +3 -5
- package/lib/service/automation/handlers/GuardHandler.js +11 -32
- package/lib/service/automation/handlers/SearchHandler.js +9 -9
- package/lib/service/chat/CandidateGuardrail.js +11 -6
- package/lib/service/chat/ChatAgent.js +51 -421
- package/lib/service/chat/ChatAgentPrompts.js +149 -0
- package/lib/service/chat/ChatAgentTasks.js +297 -0
- package/lib/service/chat/HandoffProtocol.js +5 -2
- package/lib/service/chat/tools/_shared.js +61 -0
- package/lib/service/chat/tools/ai-analysis.js +284 -0
- package/lib/service/chat/tools/ast-graph.js +681 -0
- package/lib/service/chat/tools/composite.js +497 -0
- package/lib/service/chat/tools/guard.js +265 -0
- package/lib/service/chat/tools/index.js +239 -0
- package/lib/service/chat/tools/infrastructure.js +227 -0
- package/lib/service/chat/tools/knowledge-graph.js +234 -0
- package/lib/service/chat/tools/lifecycle.js +486 -0
- package/lib/service/chat/tools/project-access.js +919 -0
- package/lib/service/chat/tools/query.js +264 -0
- package/lib/service/chat/tools.js +13 -3994
- package/lib/service/cursor/AgentInstructionsGenerator.js +413 -0
- package/lib/service/cursor/CursorDeliveryPipeline.js +71 -11
- package/lib/service/cursor/FileProtection.js +116 -0
- package/lib/service/cursor/KnowledgeCompressor.js +70 -11
- package/lib/service/cursor/SkillsSyncer.js +5 -3
- package/lib/service/cursor/TopicClassifier.js +19 -3
- package/lib/service/guard/ComplianceReporter.js +5 -2
- package/lib/service/guard/ExclusionManager.js +26 -2
- package/lib/service/guard/GuardCheckEngine.js +83 -388
- package/lib/service/guard/GuardCodeChecks.js +391 -0
- package/lib/service/guard/GuardCrossFileChecks.js +326 -0
- package/lib/service/guard/GuardPatternUtils.js +187 -0
- package/lib/service/guard/GuardService.js +80 -38
- package/lib/service/module/ModuleService.js +181 -56
- package/lib/service/recipe/RecipeCandidateValidator.js +11 -8
- package/lib/service/search/SearchEngine.js +10 -2
- package/lib/service/snippet/SnippetFactory.js +3 -3
- package/lib/service/snippet/SnippetInstaller.js +35 -11
- package/lib/service/snippet/codecs/VSCodeCodec.js +2 -2
- package/lib/service/wiki/WikiGenerator.js +247 -1535
- package/lib/service/wiki/WikiRenderers.js +1903 -0
- package/lib/service/wiki/WikiUtils.js +1044 -0
- package/lib/shared/LanguageService.js +359 -2
- package/lib/shared/PathGuard.js +0 -8
- package/package.json +3 -9
- package/scripts/bench-real-projects.mjs +29 -29
- package/scripts/generate-recipe-drafts.js +17 -27
- package/scripts/init-snippets.js +43 -24
- package/scripts/install-vscode-copilot.js +3 -19
- package/scripts/setup-mcp-config.js +0 -4
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GuardCrossFileChecks - Guard 跨文件检查
|
|
3
|
+
*
|
|
4
|
+
* 从 GuardCheckEngine._runCrossFileChecks 拆分
|
|
5
|
+
* 包含: 跨文件规则检查 + 路径归一化工具
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 解析相对 import 路径为归一化路径(去掉扩展名)
|
|
10
|
+
* @param {string} fromDir - 当前文件目录
|
|
11
|
+
* @param {string} importPath - 相对路径如 './foo' 或 '../bar/baz'
|
|
12
|
+
* @returns {string|null}
|
|
13
|
+
*/
|
|
14
|
+
export function resolveImportPath(fromDir, importPath) {
|
|
15
|
+
try {
|
|
16
|
+
const parts = `${fromDir}/${importPath}`.split('/');
|
|
17
|
+
const resolved = [];
|
|
18
|
+
for (const p of parts) {
|
|
19
|
+
if (p === '.' || p === '') {
|
|
20
|
+
continue;
|
|
21
|
+
}
|
|
22
|
+
if (p === '..') {
|
|
23
|
+
resolved.pop();
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
resolved.push(p);
|
|
27
|
+
}
|
|
28
|
+
// 去掉扩展名归一化
|
|
29
|
+
let result = resolved.join('/');
|
|
30
|
+
result = result.replace(/\.(js|ts|jsx|tsx|mjs|mts)$/, '');
|
|
31
|
+
// 移除 /index 后缀(index barrel 导入)
|
|
32
|
+
result = result.replace(/\/index$/, '');
|
|
33
|
+
return result;
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* 归一化文件路径(去扩展名,用于 import 比较)
|
|
41
|
+
* @param {string} filePath
|
|
42
|
+
* @returns {string}
|
|
43
|
+
*/
|
|
44
|
+
export function normalizeFilePath(filePath) {
|
|
45
|
+
return filePath.replace(/\.(js|ts|jsx|tsx|mjs|mts)$/, '').replace(/\/index$/, '');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* 跨文件检查 — 需要多文件上下文才能发现的问题
|
|
50
|
+
* @param {Array<{path: string, content: string}>} files
|
|
51
|
+
* @param {object} options
|
|
52
|
+
* @param {string[]} [options.disabledRules] - 禁用的规则 ID 列表
|
|
53
|
+
* @returns {Array<{ruleId, message, severity, locations}>}
|
|
54
|
+
*/
|
|
55
|
+
export function runCrossFileChecks(files, options = {}) {
|
|
56
|
+
const violations = [];
|
|
57
|
+
const disabledSet = new Set(options.disabledRules || []);
|
|
58
|
+
const isDisabled = (ruleId) => disabledSet.has(ruleId);
|
|
59
|
+
|
|
60
|
+
// ── ObjC Category 跨文件重名检查 ──
|
|
61
|
+
if (!isDisabled('objc-cross-file-duplicate-category')) {
|
|
62
|
+
const categoryMap = new Map();
|
|
63
|
+
const categoryRegex = /@interface\s+(\w+)\s*\(\s*(\w+)\s*\)/g;
|
|
64
|
+
|
|
65
|
+
for (const { path: filePath, content } of files) {
|
|
66
|
+
const ext = filePath.split('.').pop()?.toLowerCase();
|
|
67
|
+
if (ext !== 'm' && ext !== 'mm' && ext !== 'h') {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const lines = content.split(/\r?\n/);
|
|
72
|
+
for (let i = 0; i < lines.length; i++) {
|
|
73
|
+
categoryRegex.lastIndex = 0;
|
|
74
|
+
let m;
|
|
75
|
+
while ((m = categoryRegex.exec(lines[i])) !== null) {
|
|
76
|
+
const key = `${m[1]}(${m[2]})`;
|
|
77
|
+
if (!categoryMap.has(key)) {
|
|
78
|
+
categoryMap.set(key, []);
|
|
79
|
+
}
|
|
80
|
+
categoryMap.get(key).push({
|
|
81
|
+
filePath,
|
|
82
|
+
line: i + 1,
|
|
83
|
+
snippet: lines[i].trim().slice(0, 120),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
for (const [key, locations] of categoryMap) {
|
|
90
|
+
if (locations.length <= 1) {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const hFiles = locations.filter((l) => l.filePath.endsWith('.h'));
|
|
95
|
+
const mFiles = locations.filter((l) => !l.filePath.endsWith('.h'));
|
|
96
|
+
const hasDuplicateH = hFiles.length > 1;
|
|
97
|
+
const hasDuplicateM = mFiles.length > 1;
|
|
98
|
+
const tooMany = locations.length > 2;
|
|
99
|
+
|
|
100
|
+
if (hasDuplicateH || hasDuplicateM || tooMany) {
|
|
101
|
+
const conflictLocations = tooMany
|
|
102
|
+
? locations
|
|
103
|
+
: hasDuplicateH && hasDuplicateM
|
|
104
|
+
? locations
|
|
105
|
+
: hasDuplicateH
|
|
106
|
+
? hFiles
|
|
107
|
+
: mFiles;
|
|
108
|
+
|
|
109
|
+
violations.push({
|
|
110
|
+
ruleId: 'objc-cross-file-duplicate-category',
|
|
111
|
+
message: `Category ${key} 在 ${conflictLocations.length} 个文件中重复声明,可能导致方法覆盖或未定义行为`,
|
|
112
|
+
severity: 'warning',
|
|
113
|
+
locations: conflictLocations,
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
} // end isDisabled('objc-cross-file-duplicate-category')
|
|
118
|
+
|
|
119
|
+
// ── JS/TS 循环依赖检查 ──
|
|
120
|
+
// 检测 A imports B 且 B imports A 的直接循环
|
|
121
|
+
if (!isDisabled('js-circular-import')) {
|
|
122
|
+
const jsImportMap = new Map(); // filePath → Set<importedPath>
|
|
123
|
+
const jsExts = new Set(['js', 'ts', 'jsx', 'tsx', 'mjs', 'mts']);
|
|
124
|
+
const importRegex =
|
|
125
|
+
/(?:import\s+.+?\s+from\s+['"](.+?)['"]|require\s*\(\s*['"](.+?)['"]\s*\))/g;
|
|
126
|
+
|
|
127
|
+
for (const { path: filePath, content } of files) {
|
|
128
|
+
const ext = filePath.split('.').pop()?.toLowerCase();
|
|
129
|
+
if (!jsExts.has(ext)) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const imports = new Set();
|
|
134
|
+
const lines = content.split(/\r?\n/);
|
|
135
|
+
for (const line of lines) {
|
|
136
|
+
importRegex.lastIndex = 0;
|
|
137
|
+
let m;
|
|
138
|
+
while ((m = importRegex.exec(line)) !== null) {
|
|
139
|
+
const importPath = m[1] || m[2];
|
|
140
|
+
if (importPath.startsWith('.')) {
|
|
141
|
+
// 解析相对路径为归一化 key
|
|
142
|
+
const dir = filePath.substring(0, filePath.lastIndexOf('/'));
|
|
143
|
+
const resolved = resolveImportPath(dir, importPath);
|
|
144
|
+
if (resolved) {
|
|
145
|
+
imports.add(resolved);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (imports.size > 0) {
|
|
151
|
+
jsImportMap.set(normalizeFilePath(filePath), imports);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// 检测直接双向循环: A→B 且 B→A
|
|
156
|
+
const reportedCycles = new Set();
|
|
157
|
+
for (const [fileA, importsA] of jsImportMap) {
|
|
158
|
+
for (const depB of importsA) {
|
|
159
|
+
const importsB = jsImportMap.get(depB);
|
|
160
|
+
if (importsB?.has(fileA)) {
|
|
161
|
+
const cycleKey = [fileA, depB].sort().join(' <-> ');
|
|
162
|
+
if (!reportedCycles.has(cycleKey)) {
|
|
163
|
+
reportedCycles.add(cycleKey);
|
|
164
|
+
violations.push({
|
|
165
|
+
ruleId: 'js-circular-import',
|
|
166
|
+
message: `检测到循环依赖,两个模块互相导入可能导致运行时 undefined`,
|
|
167
|
+
severity: 'warning',
|
|
168
|
+
locations: [
|
|
169
|
+
{ filePath: fileA, line: 1, snippet: `imports ${depB.split('/').pop()}` },
|
|
170
|
+
{ filePath: depB, line: 1, snippet: `imports ${fileA.split('/').pop()}` },
|
|
171
|
+
],
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
} // end isDisabled('js-circular-import')
|
|
178
|
+
|
|
179
|
+
// ── Java/Kotlin 同名类跨文件检查 ──
|
|
180
|
+
if (!isDisabled('java-duplicate-class-name')) {
|
|
181
|
+
const classMap = new Map(); // className → [{filePath, line, snippet}]
|
|
182
|
+
const javaClassRegex = /(?:public\s+)?(?:abstract\s+)?(?:final\s+)?class\s+(\w+)/;
|
|
183
|
+
const jkExts = new Set(['java', 'kt']);
|
|
184
|
+
|
|
185
|
+
for (const { path: filePath, content } of files) {
|
|
186
|
+
const ext = filePath.split('.').pop()?.toLowerCase();
|
|
187
|
+
if (!jkExts.has(ext)) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const lines = content.split(/\r?\n/);
|
|
192
|
+
for (let i = 0; i < lines.length; i++) {
|
|
193
|
+
const m = javaClassRegex.exec(lines[i]);
|
|
194
|
+
if (m) {
|
|
195
|
+
const className = m[1];
|
|
196
|
+
if (!classMap.has(className)) {
|
|
197
|
+
classMap.set(className, []);
|
|
198
|
+
}
|
|
199
|
+
classMap.get(className).push({
|
|
200
|
+
filePath,
|
|
201
|
+
line: i + 1,
|
|
202
|
+
snippet: lines[i].trim().slice(0, 120),
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
for (const [className, locations] of classMap) {
|
|
209
|
+
if (locations.length > 1) {
|
|
210
|
+
violations.push({
|
|
211
|
+
ruleId: 'java-duplicate-class-name',
|
|
212
|
+
message: `类名 "${className}" 在 ${locations.length} 个文件中定义,可能导致导入歧义`,
|
|
213
|
+
severity: 'info',
|
|
214
|
+
locations,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
} // end isDisabled('java-duplicate-class-name')
|
|
219
|
+
|
|
220
|
+
// ── Go 多文件 init() 函数检查 ──
|
|
221
|
+
// 同一 package 下多个文件都有 init(),执行顺序依赖文件名排序,容易出错
|
|
222
|
+
if (!isDisabled('go-multiple-init')) {
|
|
223
|
+
const goInitMap = new Map(); // dirPath → [{filePath, line}]
|
|
224
|
+
|
|
225
|
+
for (const { path: filePath, content } of files) {
|
|
226
|
+
if (!filePath.endsWith('.go')) {
|
|
227
|
+
continue;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const lines = content.split(/\r?\n/);
|
|
231
|
+
for (let i = 0; i < lines.length; i++) {
|
|
232
|
+
if (/^func\s+init\s*\(\s*\)/.test(lines[i].trim())) {
|
|
233
|
+
const dir = filePath.substring(0, filePath.lastIndexOf('/'));
|
|
234
|
+
if (!goInitMap.has(dir)) {
|
|
235
|
+
goInitMap.set(dir, []);
|
|
236
|
+
}
|
|
237
|
+
goInitMap.get(dir).push({
|
|
238
|
+
filePath,
|
|
239
|
+
line: i + 1,
|
|
240
|
+
snippet: lines[i].trim().slice(0, 120),
|
|
241
|
+
});
|
|
242
|
+
break; // 每个文件只记录一次
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
for (const [dir, locations] of goInitMap) {
|
|
248
|
+
if (locations.length > 2) {
|
|
249
|
+
violations.push({
|
|
250
|
+
ruleId: 'go-multiple-init',
|
|
251
|
+
message: `同一 package (${dir.split('/').pop()}) 中 ${locations.length} 个文件都定义了 init(),执行顺序依赖文件名排序`,
|
|
252
|
+
severity: 'info',
|
|
253
|
+
locations,
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
} // end isDisabled('go-multiple-init')
|
|
258
|
+
|
|
259
|
+
// ── Swift Extension 方法跨文件冲突检查 ──
|
|
260
|
+
if (!isDisabled('swift-cross-file-extension-conflict')) {
|
|
261
|
+
const swiftExtMethodMap = new Map(); // "TypeName.methodName" → [{filePath, line}]
|
|
262
|
+
const swiftExtRegex = /extension\s+(\w+)/;
|
|
263
|
+
const swiftFuncRegex = /func\s+(\w+)\s*\(/;
|
|
264
|
+
|
|
265
|
+
for (const { path: filePath, content } of files) {
|
|
266
|
+
if (!filePath.endsWith('.swift')) {
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const lines = content.split(/\r?\n/);
|
|
271
|
+
let currentExt = null;
|
|
272
|
+
let braceDepth = 0;
|
|
273
|
+
|
|
274
|
+
for (let i = 0; i < lines.length; i++) {
|
|
275
|
+
const extMatch = swiftExtRegex.exec(lines[i]);
|
|
276
|
+
if (extMatch && !currentExt) {
|
|
277
|
+
currentExt = extMatch[1];
|
|
278
|
+
braceDepth = 0;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (currentExt) {
|
|
282
|
+
for (const ch of lines[i]) {
|
|
283
|
+
if (ch === '{') {
|
|
284
|
+
braceDepth++;
|
|
285
|
+
} else if (ch === '}') {
|
|
286
|
+
braceDepth--;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const funcMatch = swiftFuncRegex.exec(lines[i]);
|
|
291
|
+
if (funcMatch && braceDepth >= 1) {
|
|
292
|
+
const key = `${currentExt}.${funcMatch[1]}`;
|
|
293
|
+
if (!swiftExtMethodMap.has(key)) {
|
|
294
|
+
swiftExtMethodMap.set(key, []);
|
|
295
|
+
}
|
|
296
|
+
swiftExtMethodMap.get(key).push({
|
|
297
|
+
filePath,
|
|
298
|
+
line: i + 1,
|
|
299
|
+
snippet: lines[i].trim().slice(0, 120),
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (braceDepth <= 0) {
|
|
304
|
+
currentExt = null;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
for (const [key, locations] of swiftExtMethodMap) {
|
|
311
|
+
if (locations.length > 1) {
|
|
312
|
+
const uniqueFiles = new Set(locations.map((l) => l.filePath));
|
|
313
|
+
if (uniqueFiles.size > 1) {
|
|
314
|
+
violations.push({
|
|
315
|
+
ruleId: 'swift-cross-file-extension-conflict',
|
|
316
|
+
message: `Extension 方法 ${key} 在 ${uniqueFiles.size} 个文件中定义,可能导致方法冲突`,
|
|
317
|
+
severity: 'warning',
|
|
318
|
+
locations,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
} // end isDisabled('swift-cross-file-extension-conflict')
|
|
324
|
+
|
|
325
|
+
return violations;
|
|
326
|
+
}
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GuardPatternUtils - Guard 模式匹配与掩码工具函数
|
|
3
|
+
*
|
|
4
|
+
* 从 GuardCheckEngine 拆分,包含:
|
|
5
|
+
* - compilePattern: 正则编译(带缓存)
|
|
6
|
+
* - clearPatternCache: 清除正则缓存
|
|
7
|
+
* - buildTestBlockMask: 测试块掩码(Rust #[cfg(test)])
|
|
8
|
+
* - buildCommentMask: 注释行掩码
|
|
9
|
+
* - detectLanguage: 文件扩展名推断语言
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { LanguageService } from '../../shared/LanguageService.js';
|
|
13
|
+
|
|
14
|
+
/** @type {Map<string, RegExp>} 已编译的正则缓存 (pattern string → RegExp) */
|
|
15
|
+
const _regexCache = new Map();
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* 编译正则模式(支持 RegExp 对象和 string,带缓存)
|
|
19
|
+
* @param {RegExp|string} pattern
|
|
20
|
+
* @returns {RegExp}
|
|
21
|
+
*/
|
|
22
|
+
export function compilePattern(pattern) {
|
|
23
|
+
if (pattern instanceof RegExp) {
|
|
24
|
+
return pattern;
|
|
25
|
+
}
|
|
26
|
+
const key = String(pattern);
|
|
27
|
+
let cached = _regexCache.get(key);
|
|
28
|
+
if (!cached) {
|
|
29
|
+
cached = new RegExp(key);
|
|
30
|
+
_regexCache.set(key, cached);
|
|
31
|
+
}
|
|
32
|
+
return cached;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 清除正则缓存
|
|
37
|
+
*/
|
|
38
|
+
export function clearPatternCache() {
|
|
39
|
+
_regexCache.clear();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* 构建内联测试块掩码
|
|
44
|
+
* 目前支持 Rust #[cfg(test)] mod xxx { ... } 块
|
|
45
|
+
* @param {string[]} lines
|
|
46
|
+
* @param {string} language
|
|
47
|
+
* @returns {boolean[]} 每行是否在测试块内
|
|
48
|
+
*/
|
|
49
|
+
export function buildTestBlockMask(lines, language) {
|
|
50
|
+
const mask = new Array(lines.length).fill(false);
|
|
51
|
+
|
|
52
|
+
// 目前仅 Rust 需要 — #[cfg(test)] 内联测试模块
|
|
53
|
+
if (language !== 'rust') {
|
|
54
|
+
return mask;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
let inTestBlock = false;
|
|
58
|
+
let braceDepth = 0;
|
|
59
|
+
|
|
60
|
+
for (let i = 0; i < lines.length; i++) {
|
|
61
|
+
const trimmed = lines[i].trimStart();
|
|
62
|
+
|
|
63
|
+
if (!inTestBlock) {
|
|
64
|
+
// 检测 #[cfg(test)] 属性行
|
|
65
|
+
if (/^#\[cfg\(test\)\]/.test(trimmed)) {
|
|
66
|
+
// 向后找 mod xxx { — 标记为测试块起始
|
|
67
|
+
// 可能在同一行: #[cfg(test)] mod tests {
|
|
68
|
+
// 也可能在下一行: mod tests {
|
|
69
|
+
const restOfLine = trimmed.slice('#[cfg(test)]'.length).trim();
|
|
70
|
+
if (/^mod\s+\w+/.test(restOfLine)) {
|
|
71
|
+
// 同一行有 mod 声明
|
|
72
|
+
inTestBlock = true;
|
|
73
|
+
braceDepth = 0;
|
|
74
|
+
// 计算本行的花括号
|
|
75
|
+
for (const ch of lines[i]) {
|
|
76
|
+
if (ch === '{') {
|
|
77
|
+
braceDepth++;
|
|
78
|
+
} else if (ch === '}') {
|
|
79
|
+
braceDepth--;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
mask[i] = true;
|
|
83
|
+
if (braceDepth <= 0) {
|
|
84
|
+
inTestBlock = false; // 单行 mod 声明 (mod tests;)
|
|
85
|
+
}
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
// 检查下一行是否是 mod xxx {
|
|
89
|
+
if (i + 1 < lines.length && /^\s*mod\s+\w+/.test(lines[i + 1])) {
|
|
90
|
+
mask[i] = true; // #[cfg(test)] 行本身也标记
|
|
91
|
+
inTestBlock = true;
|
|
92
|
+
braceDepth = 0;
|
|
93
|
+
}
|
|
94
|
+
// 单行 #[cfg(test)] 但后面不是 mod — 不处理
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
// 正在测试块内 — 追踪花括号深度
|
|
98
|
+
mask[i] = true;
|
|
99
|
+
for (const ch of lines[i]) {
|
|
100
|
+
if (ch === '{') {
|
|
101
|
+
braceDepth++;
|
|
102
|
+
} else if (ch === '}') {
|
|
103
|
+
braceDepth--;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (braceDepth <= 0) {
|
|
107
|
+
inTestBlock = false; // 测试块结束
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return mask;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* 构建注释行掩码 — 识别行注释和块注释内部行
|
|
117
|
+
*
|
|
118
|
+
* 支持的注释形式:
|
|
119
|
+
* // 行注释, /// 文档注释, //! 内部文档注释 (C/Java/JS/TS/Go/Rust/Swift/Kotlin/Dart)
|
|
120
|
+
* # 行注释 (Python)
|
|
121
|
+
* /* ... * / 块注释 (C/Java/JS/TS/Go/Rust/Swift/Kotlin)
|
|
122
|
+
* \"\"\" ... \"\"\" (Python doc-string — 简化: 整行以 \"\"\" 开头的行)
|
|
123
|
+
*
|
|
124
|
+
* @param {string[]} lines
|
|
125
|
+
* @param {string} language
|
|
126
|
+
* @returns {boolean[]} 每行是否为注释行
|
|
127
|
+
*/
|
|
128
|
+
export function buildCommentMask(lines, language) {
|
|
129
|
+
const mask = new Array(lines.length).fill(false);
|
|
130
|
+
let inBlock = false; // 是否在 /* ... */ 块内
|
|
131
|
+
|
|
132
|
+
const usesHash = language === 'python'; // Python 用 # 注释
|
|
133
|
+
const usesSlash = !usesHash; // 其他语言用 //
|
|
134
|
+
|
|
135
|
+
for (let i = 0; i < lines.length; i++) {
|
|
136
|
+
const trimmed = lines[i].trimStart();
|
|
137
|
+
|
|
138
|
+
// 块注释延续
|
|
139
|
+
if (inBlock) {
|
|
140
|
+
mask[i] = true;
|
|
141
|
+
if (trimmed.includes('*/')) {
|
|
142
|
+
inBlock = false;
|
|
143
|
+
}
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 块注释开始(同行不闭合)
|
|
148
|
+
if (usesSlash && /^\s*\/\*/.test(lines[i])) {
|
|
149
|
+
mask[i] = true;
|
|
150
|
+
if (!trimmed.includes('*/')) {
|
|
151
|
+
inBlock = true;
|
|
152
|
+
}
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 行注释: // 或 /// 或 //!
|
|
157
|
+
if (usesSlash && /^\s*\/\//.test(lines[i])) {
|
|
158
|
+
mask[i] = true;
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Python 行注释: #
|
|
163
|
+
if (usesHash && /^\s*#/.test(lines[i])) {
|
|
164
|
+
mask[i] = true;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Python docstring 行 (简化: 整行以 """ 或 ''' 开头)
|
|
169
|
+
if (usesHash && /^\s*("""|''')/.test(lines[i])) {
|
|
170
|
+
mask[i] = true;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return mask;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* 从文件扩展名推断语言
|
|
179
|
+
*/
|
|
180
|
+
export function detectLanguage(filePath) {
|
|
181
|
+
if (!filePath) {
|
|
182
|
+
return 'unknown';
|
|
183
|
+
}
|
|
184
|
+
const lang = LanguageService.inferLang(filePath);
|
|
185
|
+
// 向后兼容: Guard 内置规则使用 'objc' 而非 'objectivec'
|
|
186
|
+
return LanguageService.toGuardLangId(lang);
|
|
187
|
+
}
|
|
@@ -12,12 +12,18 @@ import { unixNow } from '../../shared/utils/common.js';
|
|
|
12
12
|
export class GuardService {
|
|
13
13
|
/**
|
|
14
14
|
* @param {import('../../domain/knowledge/KnowledgeRepository.js').KnowledgeRepository} knowledgeRepository
|
|
15
|
+
* @param {object} auditLogger
|
|
16
|
+
* @param {object} gateway
|
|
17
|
+
* @param {object} [deps] - 可选依赖注入
|
|
18
|
+
* @param {import('./GuardCheckEngine.js').GuardCheckEngine} [deps.guardCheckEngine] - 核心引擎实例
|
|
15
19
|
*/
|
|
16
|
-
constructor(knowledgeRepository, auditLogger, gateway) {
|
|
20
|
+
constructor(knowledgeRepository, auditLogger, gateway, deps = {}) {
|
|
17
21
|
this.knowledgeRepository = knowledgeRepository;
|
|
18
22
|
this.auditLogger = auditLogger;
|
|
19
23
|
this.gateway = gateway;
|
|
20
24
|
this.logger = Logger.getInstance();
|
|
25
|
+
/** @type {import('./GuardCheckEngine.js').GuardCheckEngine|null} */
|
|
26
|
+
this._engine = deps.guardCheckEngine || null;
|
|
21
27
|
}
|
|
22
28
|
|
|
23
29
|
/**
|
|
@@ -152,7 +158,8 @@ export class GuardService {
|
|
|
152
158
|
|
|
153
159
|
/**
|
|
154
160
|
* 检查代码是否匹配 Guard 规则
|
|
155
|
-
*
|
|
161
|
+
* 优先代理到 GuardCheckEngine(完整管线: 内置 + DB + EP + Code-Level + AST),
|
|
162
|
+
* 若引擎不可用则降级为仅 DB 规则的简化检查
|
|
156
163
|
*/
|
|
157
164
|
async checkCode(code, options = {}) {
|
|
158
165
|
try {
|
|
@@ -162,51 +169,86 @@ export class GuardService {
|
|
|
162
169
|
|
|
163
170
|
const { language = null } = options;
|
|
164
171
|
|
|
165
|
-
//
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
matches: codeMatches.map((m) => ({
|
|
187
|
-
match: m[0],
|
|
188
|
-
index: m.index,
|
|
189
|
-
line: code.substring(0, m.index).split('\\n').length,
|
|
190
|
-
})),
|
|
191
|
-
matchCount: codeMatches.length,
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
} catch (e) {
|
|
195
|
-
this.logger.warn('Error matching guard pattern', {
|
|
196
|
-
entryId: entry.id,
|
|
197
|
-
error: e.message,
|
|
198
|
-
});
|
|
199
|
-
}
|
|
172
|
+
// ── 优先路径: 代理到 GuardCheckEngine(完整管线)──
|
|
173
|
+
if (this._engine) {
|
|
174
|
+
try {
|
|
175
|
+
const violations = this._engine.checkCode(code, language || 'unknown', {
|
|
176
|
+
scope: 'file',
|
|
177
|
+
});
|
|
178
|
+
return violations.map((v) => ({
|
|
179
|
+
ruleId: v.ruleId,
|
|
180
|
+
ruleName: v.ruleId,
|
|
181
|
+
severity: v.severity || 'warning',
|
|
182
|
+
message: v.message || '',
|
|
183
|
+
line: v.line,
|
|
184
|
+
snippet: v.snippet,
|
|
185
|
+
matchCount: 1,
|
|
186
|
+
...(v.fixSuggestion ? { fixSuggestion: v.fixSuggestion } : {}),
|
|
187
|
+
...(v.reasoning ? { reasoning: v.reasoning } : {}),
|
|
188
|
+
}));
|
|
189
|
+
} catch (engineErr) {
|
|
190
|
+
this.logger.debug('GuardCheckEngine.checkCode failed, falling back to DB-only check', {
|
|
191
|
+
error: engineErr.message,
|
|
192
|
+
});
|
|
200
193
|
}
|
|
201
194
|
}
|
|
202
195
|
|
|
203
|
-
|
|
196
|
+
// ── 降级路径: 仅 DB 规则简化检查 ──
|
|
197
|
+
return this._checkCodeDbOnly(code, { language });
|
|
204
198
|
} catch (error) {
|
|
205
199
|
this.logger.error('Error checking code against rules', { error: error.message });
|
|
206
200
|
throw error;
|
|
207
201
|
}
|
|
208
202
|
}
|
|
209
203
|
|
|
204
|
+
/**
|
|
205
|
+
* 仅 DB 规则的简化检查(降级路径)
|
|
206
|
+
* @private
|
|
207
|
+
*/
|
|
208
|
+
async _checkCodeDbOnly(code, options = {}) {
|
|
209
|
+
const { language = null } = options;
|
|
210
|
+
|
|
211
|
+
// V3: 使用 findActiveRules() 查询 kind='rule' + lifecycle='active'
|
|
212
|
+
let guardEntries = await this.knowledgeRepository.findActiveRules();
|
|
213
|
+
|
|
214
|
+
// 按语言过滤
|
|
215
|
+
if (language) {
|
|
216
|
+
guardEntries = guardEntries.filter((e) => !e.language || e.language === language);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const matches = [];
|
|
220
|
+
for (const entry of guardEntries) {
|
|
221
|
+
const guards = entry.constraints?.guards || [];
|
|
222
|
+
for (const guard of guards) {
|
|
223
|
+
try {
|
|
224
|
+
const regex = new RegExp(guard.pattern, 'gm');
|
|
225
|
+
const codeMatches = [...code.matchAll(regex)];
|
|
226
|
+
if (codeMatches.length > 0) {
|
|
227
|
+
matches.push({
|
|
228
|
+
ruleId: entry.id,
|
|
229
|
+
ruleName: entry.title,
|
|
230
|
+
severity: guard.severity || 'warning',
|
|
231
|
+
message: guard.message || '',
|
|
232
|
+
matches: codeMatches.map((m) => ({
|
|
233
|
+
match: m[0],
|
|
234
|
+
index: m.index,
|
|
235
|
+
line: code.substring(0, m.index).split('\\n').length,
|
|
236
|
+
})),
|
|
237
|
+
matchCount: codeMatches.length,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
} catch (e) {
|
|
241
|
+
this.logger.warn('Error matching guard pattern', {
|
|
242
|
+
entryId: entry.id,
|
|
243
|
+
error: e.message,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
return matches;
|
|
250
|
+
}
|
|
251
|
+
|
|
210
252
|
/**
|
|
211
253
|
* 查询规则列表 (kind='rule' + knowledgeType='boundary-constraint')
|
|
212
254
|
*/
|