@chemx/create-chemx 26.9.28-276 → 26.10.4-235
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/cli/audit/csharp-analyzer.js +360 -7
- package/cli/audit/csharp-analyzer.spec.js +216 -0
- package/cli/audit/metrics.js +5 -3
- package/cli/audit/reporter-markdown.js +1 -1
- package/cli/audit/reporter-sections.js +1 -1
- package/cli/audit/rules-registry.js +10 -0
- package/cli/audit/rules.js +1 -1
- package/cli/audit.js +16 -8
- package/cli/commands/cmd-audit.js +6 -4
- package/cli/installer-templates.js +37 -6
- package/cli/languages.js +8 -0
- package/cli/languages.spec.js +21 -0
- package/cli/path-scope.js +50 -0
- package/cli/polyglot.spec.js +111 -0
- package/cli/search-ast.js +54 -0
- package/cli/search.d.ts +2 -1
- package/cli/search.js +2 -21
- package/cli/search.spec.js +27 -0
- package/package.json +1 -1
- package/scripts/pre-commit.sh +28 -6
|
@@ -1,18 +1,334 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Chemical X C# / .NET Structural Analyzer
|
|
3
|
-
* Evaluates .NET Clean Architecture, MediatR handlers, EF Core usage,
|
|
3
|
+
* Evaluates .NET Clean Architecture, MediatR handlers, EF Core usage,
|
|
4
|
+
* cyclomatic complexity, control flow nesting depth, and AI slop.
|
|
4
5
|
*/
|
|
5
6
|
import { RULE_REGISTRY } from './rules-registry.js';
|
|
6
7
|
|
|
7
|
-
const SHALLOW_CATCH_PATTERN = /catch\s*(?:\([^)]*\))?\s*\{\s*(?:\/\/[^\n]*)?\s*\}/;
|
|
8
8
|
const IN_MEMORY_DB_PATTERN = /\bUseInMemoryDatabase\s*\(/;
|
|
9
|
+
const SQLITE_PROVIDER_PATTERN = /\b(?:UseSqlite|SqliteConnection|DataSource\s*=\s*:memory:)\b/;
|
|
9
10
|
const SIMULATED_DELAY_PATTERN = /\bTask\.Delay\s*\(\s*\d+\s*\)/;
|
|
10
11
|
const HARDCODED_LIST_MOCK_PATTERN = /new\s+List<[A-Za-z0-9_]+>\s*\{\s*new\s+[A-Za-z0-9_]+\s*\{/;
|
|
12
|
+
const SIMULATION_SWITCH_PATTERN = /(?:public|private|protected|internal)?\s*(?:static\s+)?(?:readonly\s+)?(?:const\s+)?bool\s+(?:Simulate|UseSimulation|Mock|Fake|Stub)[A-Za-z0-9_]*\s*(?:\{\s*get;\s*(?:set|init)?;\s*\}\s*)?=\s*true\b/i;
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
const isVerbatimStringPrefix = (code, i) => {
|
|
15
|
+
const ch = code[i];
|
|
16
|
+
const next = code[i + 1];
|
|
17
|
+
const isVerbatim = ch === '@' && next === '"';
|
|
18
|
+
const isInterpVerbatim = (ch === '$' && next === '@') || (ch === '@' && next === '$');
|
|
19
|
+
const hasQuote = code[i + 2] === '"';
|
|
20
|
+
return isVerbatim || (isInterpVerbatim && hasQuote);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export const maskCommentsAndStrings = (code) => {
|
|
24
|
+
let out = '';
|
|
25
|
+
let i = 0;
|
|
26
|
+
const len = code.length;
|
|
27
|
+
while (i < len) {
|
|
28
|
+
const ch = code[i];
|
|
29
|
+
const next = code[i + 1];
|
|
30
|
+
|
|
31
|
+
if (ch === '/' && next === '/') {
|
|
32
|
+
out += ' ';
|
|
33
|
+
i += 2;
|
|
34
|
+
while (i < len && code[i] !== '\n') {
|
|
35
|
+
out += ' ';
|
|
36
|
+
i++;
|
|
37
|
+
}
|
|
38
|
+
} else if (ch === '/' && next === '*') {
|
|
39
|
+
out += ' ';
|
|
40
|
+
i += 2;
|
|
41
|
+
while (i < len && !(code[i] === '*' && code[i + 1] === '/')) {
|
|
42
|
+
out += code[i] === '\n' ? '\n' : ' ';
|
|
43
|
+
i++;
|
|
44
|
+
}
|
|
45
|
+
if (i < len) {
|
|
46
|
+
out += ' ';
|
|
47
|
+
i += 2;
|
|
48
|
+
}
|
|
49
|
+
} else if (isVerbatimStringPrefix(code, i)) {
|
|
50
|
+
const isVerbatim = ch === '@' && next === '"';
|
|
51
|
+
const skip = isVerbatim ? 2 : 3;
|
|
52
|
+
out += ' '.repeat(skip);
|
|
53
|
+
i += skip;
|
|
54
|
+
while (i < len) {
|
|
55
|
+
if (code[i] === '"' && code[i + 1] === '"') {
|
|
56
|
+
out += ' ';
|
|
57
|
+
i += 2;
|
|
58
|
+
} else if (code[i] === '"') {
|
|
59
|
+
out += ' ';
|
|
60
|
+
i++;
|
|
61
|
+
break;
|
|
62
|
+
} else {
|
|
63
|
+
out += code[i] === '\n' ? '\n' : ' ';
|
|
64
|
+
i++;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
} else if (ch === '"') {
|
|
68
|
+
out += ' ';
|
|
69
|
+
i++;
|
|
70
|
+
while (i < len && code[i] !== '"' && code[i] !== '\n') {
|
|
71
|
+
if (code[i] === '\\') {
|
|
72
|
+
out += ' ';
|
|
73
|
+
i += 2;
|
|
74
|
+
} else {
|
|
75
|
+
out += ' ';
|
|
76
|
+
i++;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (i < len && code[i] === '"') {
|
|
80
|
+
out += ' ';
|
|
81
|
+
i++;
|
|
82
|
+
}
|
|
83
|
+
} else if (ch === "'") {
|
|
84
|
+
out += ' ';
|
|
85
|
+
i++;
|
|
86
|
+
while (i < len && code[i] !== "'" && code[i] !== '\n') {
|
|
87
|
+
if (code[i] === '\\') {
|
|
88
|
+
out += ' ';
|
|
89
|
+
i += 2;
|
|
90
|
+
} else {
|
|
91
|
+
out += ' ';
|
|
92
|
+
i++;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
if (i < len && code[i] === "'") {
|
|
96
|
+
out += ' ';
|
|
97
|
+
i++;
|
|
98
|
+
}
|
|
99
|
+
} else {
|
|
100
|
+
out += ch;
|
|
101
|
+
i++;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const extractMethods = (maskedContent) => {
|
|
108
|
+
const methods = [];
|
|
109
|
+
const methodRegex = /(?:public|internal|protected|private|static|async|override|virtual|sealed|partial)\s+[\w<>\[\],\s?]+?\s+([A-Za-z0-9_]+)\s*\(([\s\S]*?)\)\s*(?:where[^{]+)?\s*\{/g;
|
|
110
|
+
let match;
|
|
111
|
+
|
|
112
|
+
while ((match = methodRegex.exec(maskedContent)) !== null) {
|
|
113
|
+
const methodName = match[1];
|
|
114
|
+
const signature = match[0];
|
|
115
|
+
const startIdx = match.index + signature.length - 1;
|
|
116
|
+
let depth = 0;
|
|
117
|
+
let endIdx = -1;
|
|
118
|
+
|
|
119
|
+
for (let i = startIdx; i < maskedContent.length; i++) {
|
|
120
|
+
if (maskedContent[i] === '{') depth++;
|
|
121
|
+
else if (maskedContent[i] === '}') {
|
|
122
|
+
depth--;
|
|
123
|
+
if (depth === 0) {
|
|
124
|
+
endIdx = i;
|
|
125
|
+
break;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (endIdx !== -1) {
|
|
131
|
+
const startLine = maskedContent.slice(0, match.index).split('\n').length;
|
|
132
|
+
methods.push({
|
|
133
|
+
name: methodName,
|
|
134
|
+
startLine,
|
|
135
|
+
body: maskedContent.slice(startIdx, endIdx + 1),
|
|
136
|
+
startIdx,
|
|
137
|
+
endIdx
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return methods;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
const evaluateMethodComplexityAndNesting = (method, relativePath, violations, config = {}) => {
|
|
146
|
+
const maxComplexity = config.maxCyclomaticComplexity || 12;
|
|
147
|
+
const maxNestingLimit = 4;
|
|
148
|
+
const body = method.body;
|
|
149
|
+
|
|
150
|
+
let cyclomaticComplexity = 1;
|
|
151
|
+
const ifMatches = body.match(/\bif\s*\(/g);
|
|
152
|
+
if (ifMatches) cyclomaticComplexity += ifMatches.length;
|
|
153
|
+
const caseMatches = body.match(/\bcase\b[^:]+:/g);
|
|
154
|
+
if (caseMatches) cyclomaticComplexity += caseMatches.length;
|
|
155
|
+
const whileMatches = body.match(/\bwhile\s*\(/g);
|
|
156
|
+
if (whileMatches) cyclomaticComplexity += whileMatches.length;
|
|
157
|
+
const forMatches = body.match(/\bfor\s*\(/g);
|
|
158
|
+
if (forMatches) cyclomaticComplexity += forMatches.length;
|
|
159
|
+
const foreachMatches = body.match(/\bforeach\s*\(/g);
|
|
160
|
+
if (foreachMatches) cyclomaticComplexity += foreachMatches.length;
|
|
161
|
+
const catchMatches = body.match(/\bcatch\b/g);
|
|
162
|
+
if (catchMatches) cyclomaticComplexity += catchMatches.length;
|
|
163
|
+
const andMatches = body.match(/&&/g);
|
|
164
|
+
if (andMatches) cyclomaticComplexity += andMatches.length;
|
|
165
|
+
const orMatches = body.match(/\|\|/g);
|
|
166
|
+
if (orMatches) cyclomaticComplexity += orMatches.length;
|
|
167
|
+
const nullCoalMatches = body.match(/\?\?/g);
|
|
168
|
+
if (nullCoalMatches) cyclomaticComplexity += nullCoalMatches.length;
|
|
169
|
+
|
|
170
|
+
if (cyclomaticComplexity > maxComplexity) {
|
|
171
|
+
const meta = RULE_REGISTRY.COMPLEXITY_CYCLOMATIC_HIGH;
|
|
172
|
+
violations.push({
|
|
173
|
+
filePath: relativePath,
|
|
174
|
+
line: method.startLine,
|
|
175
|
+
column: 1,
|
|
176
|
+
hazard: `High cyclomatic complexity in method ${method.name} (${cyclomaticComplexity} > ${maxComplexity} threshold)`,
|
|
177
|
+
rule: 'COMPLEXITY_CYCLOMATIC_HIGH',
|
|
178
|
+
severity: meta.severity,
|
|
179
|
+
pillar: meta.pillar,
|
|
180
|
+
directive: meta.directive
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let braceLevel = 0;
|
|
185
|
+
const controlStack = [];
|
|
186
|
+
let maxControlDepth = 0;
|
|
187
|
+
let maxDepthLine = method.startLine;
|
|
188
|
+
const bodyTokens = body.split(/(\bif\s*\(|\belse\s*if\s*\(|\belse\b|\bwhile\s*\(|\bfor\s*\(|\bforeach\s*\(|\bswitch\s*\(|\bcatch\b|\{|\})/g);
|
|
189
|
+
let curLine = method.startLine;
|
|
190
|
+
let pendingControl = null;
|
|
191
|
+
|
|
192
|
+
for (const token of bodyTokens) {
|
|
193
|
+
curLine += (token.match(/\n/g) || []).length;
|
|
194
|
+
if (/\b(?:if|else|while|for|foreach|switch|catch)\b/.test(token)) {
|
|
195
|
+
pendingControl = { line: curLine };
|
|
196
|
+
} else if (token === '{') {
|
|
197
|
+
braceLevel++;
|
|
198
|
+
if (pendingControl) {
|
|
199
|
+
controlStack.push({ braceLevel, line: pendingControl.line });
|
|
200
|
+
pendingControl = null;
|
|
201
|
+
if (controlStack.length > maxControlDepth) {
|
|
202
|
+
maxControlDepth = controlStack.length;
|
|
203
|
+
maxDepthLine = curLine;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
} else if (token === '}') {
|
|
207
|
+
if (controlStack.length > 0 && controlStack[controlStack.length - 1].braceLevel === braceLevel) {
|
|
208
|
+
controlStack.pop();
|
|
209
|
+
}
|
|
210
|
+
braceLevel--;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (maxControlDepth >= maxNestingLimit) {
|
|
215
|
+
const meta = RULE_REGISTRY.STRUCTURAL_WEIGHT_EXCEEDED;
|
|
216
|
+
violations.push({
|
|
217
|
+
filePath: relativePath,
|
|
218
|
+
line: maxDepthLine,
|
|
219
|
+
column: 1,
|
|
220
|
+
hazard: `Excessive control flow nesting depth (${maxControlDepth} levels >= ${maxNestingLimit}) in method ${method.name}`,
|
|
221
|
+
rule: 'STRUCTURAL_WEIGHT_EXCEEDED',
|
|
222
|
+
severity: meta.severity,
|
|
223
|
+
pillar: meta.pillar,
|
|
224
|
+
directive: 'Flatten control flow using early-return guard clauses or decomposed methods'
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const condRegex = /\b(?:if|while)\s*\(([\s\S]*?)\)/g;
|
|
229
|
+
let condMatch;
|
|
230
|
+
while ((condMatch = condRegex.exec(body)) !== null) {
|
|
231
|
+
const expr = condMatch[1];
|
|
232
|
+
const logicOps = (expr.match(/&&|\|\|/g) || []).length;
|
|
233
|
+
if (logicOps >= 3) {
|
|
234
|
+
const lineNum = method.startLine + body.slice(0, condMatch.index).split('\n').length - 1;
|
|
235
|
+
const meta = RULE_REGISTRY.CONTROL_FLOW_INLINE_BOOLEAN;
|
|
236
|
+
violations.push({
|
|
237
|
+
filePath: relativePath,
|
|
238
|
+
line: lineNum,
|
|
239
|
+
column: 1,
|
|
240
|
+
hazard: `Complex inline boolean condition detected in method ${method.name} (${logicOps} logical operators)`,
|
|
241
|
+
rule: 'CONTROL_FLOW_INLINE_BOOLEAN',
|
|
242
|
+
severity: meta.severity,
|
|
243
|
+
pillar: meta.pillar,
|
|
244
|
+
directive: meta.directive
|
|
245
|
+
});
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const checkCleanArchitectureAndCoupling = (content, relativePath, violations) => {
|
|
252
|
+
const isController =
|
|
253
|
+
relativePath.includes('/Controllers/') ||
|
|
254
|
+
relativePath.endsWith('Controller.cs') ||
|
|
255
|
+
/:\s*(?:ControllerBase|Controller)\b/.test(content) ||
|
|
256
|
+
/\[ApiController\]/.test(content);
|
|
257
|
+
|
|
258
|
+
if (!isController) return;
|
|
259
|
+
|
|
260
|
+
const ctorRegex = /(?:public|internal)\s+(?:class\s+)?([A-Za-z0-9_]+)\s*\(([\s\S]*?)\)\s*(?::\s*(?:base|this)\s*\([^)]*\)\s*)?\{/g;
|
|
261
|
+
let ctorMatch;
|
|
262
|
+
|
|
263
|
+
while ((ctorMatch = ctorRegex.exec(content)) !== null) {
|
|
264
|
+
const paramsText = ctorMatch[2].trim();
|
|
265
|
+
if (!paramsText) continue;
|
|
266
|
+
|
|
267
|
+
const lineNum = content.slice(0, ctorMatch.index).split('\n').length;
|
|
268
|
+
const rawParams = paramsText.split(',').map((p) => p.trim()).filter(Boolean);
|
|
269
|
+
|
|
270
|
+
if (rawParams.length > 5) {
|
|
271
|
+
const meta = RULE_REGISTRY.COUPLING_EXCESSIVE_INJECTION;
|
|
272
|
+
violations.push({
|
|
273
|
+
filePath: relativePath,
|
|
274
|
+
line: lineNum,
|
|
275
|
+
column: 1,
|
|
276
|
+
hazard: `Excessive constructor dependency injection (${rawParams.length} dependencies > 5 threshold) in controller`,
|
|
277
|
+
rule: 'COUPLING_EXCESSIVE_INJECTION',
|
|
278
|
+
severity: meta.severity,
|
|
279
|
+
pillar: meta.pillar,
|
|
280
|
+
directive: meta.directive
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const hasDbContext = rawParams.some((p) => {
|
|
285
|
+
const isDb = /\b(?:[A-Za-z0-9_]*DbContext|DbContext|[A-Za-z0-9_]*Context)\b/.test(p);
|
|
286
|
+
const isHttp = /\b(?:HttpContext|ActionContext|ControllerContext)\b/.test(p);
|
|
287
|
+
return isDb && !isHttp;
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
const hasService = rawParams.some((p) => /\b(?:[A-Za-z0-9_]*Service|[A-Za-z0-9_]*Repository)\b/.test(p));
|
|
291
|
+
|
|
292
|
+
if (hasDbContext) {
|
|
293
|
+
const meta = RULE_REGISTRY.LAYER_VIOLATION_CONTROLLER;
|
|
294
|
+
const hazardDesc = hasService
|
|
295
|
+
? 'Clean Architecture layer violation: Controller directly injects DbContext and service dependencies, bypassing MediatR CQRS'
|
|
296
|
+
: 'Clean Architecture layer violation: Controller directly injects DbContext, bypassing application/MediatR layer';
|
|
297
|
+
|
|
298
|
+
violations.push({
|
|
299
|
+
filePath: relativePath,
|
|
300
|
+
line: lineNum,
|
|
301
|
+
column: 1,
|
|
302
|
+
hazard: hazardDesc,
|
|
303
|
+
rule: 'LAYER_VIOLATION_CONTROLLER',
|
|
304
|
+
severity: meta.severity,
|
|
305
|
+
pillar: meta.pillar,
|
|
306
|
+
directive: meta.directive
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const fromServicesDbMatches = content.matchAll(/\[FromServices\]\s*(?:[A-Za-z0-9_]*DbContext|DbContext|[A-Za-z0-9_]*Context)\b/g);
|
|
312
|
+
for (const m of fromServicesDbMatches) {
|
|
313
|
+
const lineNum = content.slice(0, m.index).split('\n').length;
|
|
314
|
+
const meta = RULE_REGISTRY.LAYER_VIOLATION_CONTROLLER;
|
|
315
|
+
violations.push({
|
|
316
|
+
filePath: relativePath,
|
|
317
|
+
line: lineNum,
|
|
318
|
+
column: 1,
|
|
319
|
+
hazard: 'Clean Architecture layer violation: Action method directly injects DbContext via [FromServices]',
|
|
320
|
+
rule: 'LAYER_VIOLATION_CONTROLLER',
|
|
321
|
+
severity: meta.severity,
|
|
322
|
+
pillar: meta.pillar,
|
|
323
|
+
directive: meta.directive
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
export const analyzeCSharpCode = (content, relativePath, violations, config = {}) => {
|
|
13
329
|
const lines = content.split('\n');
|
|
330
|
+
const maskedContent = maskCommentsAndStrings(content);
|
|
14
331
|
|
|
15
|
-
// Multi-line and single-line shallow catch detection
|
|
16
332
|
const catchMatches = content.matchAll(/catch\s*(?:\([^)]*\))?\s*\{(?:\s*|\s*\/\/[^\n]*\s*)\}/g);
|
|
17
333
|
for (const m of catchMatches) {
|
|
18
334
|
const lineNum = content.slice(0, m.index).split('\n').length;
|
|
@@ -25,14 +341,29 @@ export const analyzeCSharpCode = (content, relativePath, violations) => {
|
|
|
25
341
|
rule: 'AI_SLOP_SHALLOW_CATCH',
|
|
26
342
|
severity: meta.severity,
|
|
27
343
|
pillar: meta.pillar,
|
|
28
|
-
directive: meta.directive
|
|
344
|
+
directive: meta.directive,
|
|
345
|
+
isAiSlop: true
|
|
29
346
|
});
|
|
30
347
|
}
|
|
31
348
|
|
|
32
349
|
lines.forEach((lineText, idx) => {
|
|
33
350
|
const lineNum = idx + 1;
|
|
34
351
|
|
|
35
|
-
|
|
352
|
+
if (SIMULATION_SWITCH_PATTERN.test(lineText)) {
|
|
353
|
+
const meta = RULE_REGISTRY.AI_SLOP_LAZY_PLACEHOLDER;
|
|
354
|
+
violations.push({
|
|
355
|
+
filePath: relativePath,
|
|
356
|
+
line: lineNum,
|
|
357
|
+
column: 1,
|
|
358
|
+
hazard: `Global simulation switch detected: "${lineText.trim()}" (unverified synthetic component mode)`,
|
|
359
|
+
rule: 'AI_SLOP_LAZY_PLACEHOLDER',
|
|
360
|
+
severity: 'CRITICAL',
|
|
361
|
+
pillar: meta.pillar,
|
|
362
|
+
directive: meta.directive,
|
|
363
|
+
isAiSlop: true
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
|
|
36
367
|
if (IN_MEMORY_DB_PATTERN.test(lineText)) {
|
|
37
368
|
const meta = RULE_REGISTRY.SYNTHETIC_MOCK_DATA;
|
|
38
369
|
violations.push({
|
|
@@ -47,6 +378,20 @@ export const analyzeCSharpCode = (content, relativePath, violations) => {
|
|
|
47
378
|
});
|
|
48
379
|
}
|
|
49
380
|
|
|
381
|
+
if (SQLITE_PROVIDER_PATTERN.test(lineText)) {
|
|
382
|
+
const meta = RULE_REGISTRY.SYNTHETIC_MOCK_DATA;
|
|
383
|
+
violations.push({
|
|
384
|
+
filePath: relativePath,
|
|
385
|
+
line: lineNum,
|
|
386
|
+
column: 1,
|
|
387
|
+
hazard: 'SQLite provider detected in configuration or test setup (potential production database parity mismatch)',
|
|
388
|
+
rule: 'SYNTHETIC_MOCK_DATA',
|
|
389
|
+
severity: 'HIGH',
|
|
390
|
+
pillar: meta.pillar,
|
|
391
|
+
directive: 'Use Testcontainers or production-identical database engine for integration tests'
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
50
395
|
if (SIMULATED_DELAY_PATTERN.test(lineText)) {
|
|
51
396
|
const meta = RULE_REGISTRY.AI_SLOP_LAZY_PLACEHOLDER;
|
|
52
397
|
violations.push({
|
|
@@ -57,7 +402,8 @@ export const analyzeCSharpCode = (content, relativePath, violations) => {
|
|
|
57
402
|
rule: 'AI_SLOP_LAZY_PLACEHOLDER',
|
|
58
403
|
severity: meta.severity,
|
|
59
404
|
pillar: meta.pillar,
|
|
60
|
-
directive: meta.directive
|
|
405
|
+
directive: meta.directive,
|
|
406
|
+
isAiSlop: true
|
|
61
407
|
});
|
|
62
408
|
}
|
|
63
409
|
|
|
@@ -76,5 +422,12 @@ export const analyzeCSharpCode = (content, relativePath, violations) => {
|
|
|
76
422
|
}
|
|
77
423
|
});
|
|
78
424
|
|
|
425
|
+
const methods = extractMethods(maskedContent);
|
|
426
|
+
for (const method of methods) {
|
|
427
|
+
evaluateMethodComplexityAndNesting(method, relativePath, violations, config);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
checkCleanArchitectureAndCoupling(content, relativePath, violations);
|
|
431
|
+
|
|
79
432
|
return violations;
|
|
80
433
|
};
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import test from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { analyzeCSharpCode, maskCommentsAndStrings } from "./csharp-analyzer.js";
|
|
4
|
+
import { calculateAiSlopScore, calculateMolecularHealthScore } from "./metrics.js";
|
|
5
|
+
|
|
6
|
+
test("csharp-analyzer: maskCommentsAndStrings preserves layout and line count", () => {
|
|
7
|
+
const code = "// comment\n/* block */\nstring s = \"hello\";\n";
|
|
8
|
+
const masked = maskCommentsAndStrings(code);
|
|
9
|
+
assert.equal(masked.split("\n").length, code.split("\n").length);
|
|
10
|
+
assert.equal(masked.includes("hello"), false);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("csharp-analyzer: flags global simulation switches with isAiSlop: true", () => {
|
|
14
|
+
const code = `
|
|
15
|
+
namespace Nadena.Domain;
|
|
16
|
+
|
|
17
|
+
public class MarketSimulator
|
|
18
|
+
{
|
|
19
|
+
public static bool SimulateBuyers = true;
|
|
20
|
+
public static bool SimulateOrders { get; set; } = true;
|
|
21
|
+
private static bool UseSimulation = true;
|
|
22
|
+
}
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
const violations = [];
|
|
26
|
+
analyzeCSharpCode(code, "Domain/MarketSimulator.cs", violations);
|
|
27
|
+
|
|
28
|
+
const slopMatches = violations.filter((v) => v.rule === "AI_SLOP_LAZY_PLACEHOLDER" && v.isAiSlop);
|
|
29
|
+
assert.ok(slopMatches.length >= 3, "Must flag all 3 simulation switches");
|
|
30
|
+
assert.equal(slopMatches[0].severity, "CRITICAL");
|
|
31
|
+
assert.ok(slopMatches[0].hazard.includes("SimulateBuyers"));
|
|
32
|
+
|
|
33
|
+
// Verify AI Slop scoring calculation is degraded
|
|
34
|
+
const slopScore = calculateAiSlopScore(violations, 1);
|
|
35
|
+
assert.ok(slopScore.score < 90, "AI Slop score must drop from 100");
|
|
36
|
+
assert.notEqual(slopScore.grade, "A+");
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("csharp-analyzer: detects deep control flow nesting (>= 4 levels) in methods", () => {
|
|
40
|
+
const code = `
|
|
41
|
+
namespace Nadena.Services;
|
|
42
|
+
|
|
43
|
+
public class OrderProcessor
|
|
44
|
+
{
|
|
45
|
+
public async Task ProcessAsync(Order order)
|
|
46
|
+
{
|
|
47
|
+
if (order != null)
|
|
48
|
+
{
|
|
49
|
+
if (order.IsValid)
|
|
50
|
+
{
|
|
51
|
+
if (order.HasItems)
|
|
52
|
+
{
|
|
53
|
+
if (order.Total > 0)
|
|
54
|
+
{
|
|
55
|
+
if (order.IsPaid)
|
|
56
|
+
{
|
|
57
|
+
await SaveAsync(order);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
`;
|
|
66
|
+
|
|
67
|
+
const violations = [];
|
|
68
|
+
analyzeCSharpCode(code, "Services/OrderProcessor.cs", violations);
|
|
69
|
+
|
|
70
|
+
const nestingViolation = violations.find((v) => v.rule === "STRUCTURAL_WEIGHT_EXCEEDED");
|
|
71
|
+
assert.ok(nestingViolation, "Must flag excessive control flow nesting depth");
|
|
72
|
+
assert.ok(nestingViolation.hazard.includes("nesting depth"));
|
|
73
|
+
assert.equal(nestingViolation.severity, "HIGH");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("csharp-analyzer: detects high cyclomatic complexity (> 12) in methods", () => {
|
|
77
|
+
const code = `
|
|
78
|
+
namespace Nadena.Services;
|
|
79
|
+
|
|
80
|
+
public class ComplexEvaluator
|
|
81
|
+
{
|
|
82
|
+
public int EvaluateScore(int a, int b, int c, int d, int e, int f)
|
|
83
|
+
{
|
|
84
|
+
int score = 0;
|
|
85
|
+
if (a > 1) score++;
|
|
86
|
+
if (b > 2) score++;
|
|
87
|
+
if (c > 3) score++;
|
|
88
|
+
if (d > 4) score++;
|
|
89
|
+
if (e > 5) score++;
|
|
90
|
+
if (f > 6) score++;
|
|
91
|
+
if (a == b) score++;
|
|
92
|
+
if (b == c) score++;
|
|
93
|
+
if (c == d) score++;
|
|
94
|
+
if (d == e) score++;
|
|
95
|
+
if (e == f) score++;
|
|
96
|
+
if (a + b > 10) score++;
|
|
97
|
+
if (c + d > 10) score++;
|
|
98
|
+
return score;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
`;
|
|
102
|
+
|
|
103
|
+
const violations = [];
|
|
104
|
+
analyzeCSharpCode(code, "Services/ComplexEvaluator.cs", violations);
|
|
105
|
+
|
|
106
|
+
const complexityViolation = violations.find((v) => v.rule === "COMPLEXITY_CYCLOMATIC_HIGH");
|
|
107
|
+
assert.ok(complexityViolation, "Must flag cyclomatic complexity > 12");
|
|
108
|
+
assert.ok(complexityViolation.hazard.includes("EvaluateScore"));
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("csharp-analyzer: detects complex inline boolean conditions in control flow", () => {
|
|
112
|
+
const code = `
|
|
113
|
+
namespace Nadena.Services;
|
|
114
|
+
|
|
115
|
+
public class Validator
|
|
116
|
+
{
|
|
117
|
+
public bool Check(Item item)
|
|
118
|
+
{
|
|
119
|
+
if (item.A && item.B && item.C || item.D)
|
|
120
|
+
{
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
`;
|
|
127
|
+
|
|
128
|
+
const violations = [];
|
|
129
|
+
analyzeCSharpCode(code, "Services/Validator.cs", violations);
|
|
130
|
+
|
|
131
|
+
const inlineBool = violations.find((v) => v.rule === "CONTROL_FLOW_INLINE_BOOLEAN");
|
|
132
|
+
assert.ok(inlineBool, "Must flag complex inline boolean condition with >= 3 operators");
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
test("csharp-analyzer: flags Clean Architecture layer violations in controllers", () => {
|
|
136
|
+
const controllerCode = `
|
|
137
|
+
namespace Nadena.Controllers;
|
|
138
|
+
|
|
139
|
+
[ApiController]
|
|
140
|
+
[Route("api/[controller]")]
|
|
141
|
+
public class OrdersController : ControllerBase
|
|
142
|
+
{
|
|
143
|
+
private readonly AppDbContext _context;
|
|
144
|
+
private readonly IOrderService _orderService;
|
|
145
|
+
|
|
146
|
+
public OrdersController(AppDbContext context, IOrderService orderService)
|
|
147
|
+
{
|
|
148
|
+
_context = context;
|
|
149
|
+
_orderService = orderService;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
[HttpGet]
|
|
153
|
+
public IActionResult GetDirect([FromServices] ApplicationDbContext inlineDb)
|
|
154
|
+
{
|
|
155
|
+
return Ok();
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
`;
|
|
159
|
+
|
|
160
|
+
const violations = [];
|
|
161
|
+
analyzeCSharpCode(controllerCode, "Controllers/OrdersController.cs", violations);
|
|
162
|
+
|
|
163
|
+
const layerViolations = violations.filter((v) => v.rule === "LAYER_VIOLATION_CONTROLLER");
|
|
164
|
+
assert.ok(layerViolations.length >= 2, "Must flag constructor DbContext and [FromServices] DbContext");
|
|
165
|
+
assert.equal(layerViolations[0].severity, "CRITICAL");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("csharp-analyzer: flags excessive controller constructor injection (> 5 dependencies)", () => {
|
|
169
|
+
const controllerCode = `
|
|
170
|
+
namespace Nadena.Controllers;
|
|
171
|
+
|
|
172
|
+
public class HeavyController : ControllerBase
|
|
173
|
+
{
|
|
174
|
+
public HeavyController(
|
|
175
|
+
IAuthService auth,
|
|
176
|
+
IUserService user,
|
|
177
|
+
IOrderService order,
|
|
178
|
+
IEmailService email,
|
|
179
|
+
IPaymentService payment,
|
|
180
|
+
ILogger<HeavyController> logger)
|
|
181
|
+
{
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
`;
|
|
185
|
+
|
|
186
|
+
const violations = [];
|
|
187
|
+
analyzeCSharpCode(controllerCode, "Controllers/HeavyController.cs", violations);
|
|
188
|
+
|
|
189
|
+
const coupling = violations.find((v) => v.rule === "COUPLING_EXCESSIVE_INJECTION");
|
|
190
|
+
assert.ok(coupling, "Must flag constructor with 6 dependencies");
|
|
191
|
+
assert.equal(coupling.severity, "HIGH");
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test("csharp-analyzer: flags SQLite test provider in tests/configuration", () => {
|
|
195
|
+
const testCode = `
|
|
196
|
+
using Microsoft.EntityFrameworkCore;
|
|
197
|
+
using Xunit;
|
|
198
|
+
|
|
199
|
+
public class OrderTests
|
|
200
|
+
{
|
|
201
|
+
[Fact]
|
|
202
|
+
public void TestDatabase()
|
|
203
|
+
{
|
|
204
|
+
var options = new DbContextOptionsBuilder<AppDbContext>()
|
|
205
|
+
.UseSqlite("DataSource=:memory:")
|
|
206
|
+
.Options;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
`;
|
|
210
|
+
|
|
211
|
+
const violations = [];
|
|
212
|
+
analyzeCSharpCode(testCode, "Tests/OrderTests.cs", violations);
|
|
213
|
+
|
|
214
|
+
const sqliteViolation = violations.find((v) => v.rule === "SYNTHETIC_MOCK_DATA" && v.hazard.includes("SQLite"));
|
|
215
|
+
assert.ok(sqliteViolation, "Must flag UseSqlite/DataSource=:memory: provider");
|
|
216
|
+
});
|
package/cli/audit/metrics.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { PILLARS } from './rules.js';
|
|
2
2
|
|
|
3
|
+
export const isSlopViolation = (v) => Boolean(v.isAiSlop || (v.rule && v.rule.startsWith('AI_SLOP_')));
|
|
4
|
+
|
|
3
5
|
export const calculateMolecularHealthScore = (violations, totalFiles) => {
|
|
4
6
|
if (totalFiles === 0) {
|
|
5
7
|
return { score: 100, grade: 'A+', label: 'Crystalline Molecular' };
|
|
@@ -7,7 +9,7 @@ export const calculateMolecularHealthScore = (violations, totalFiles) => {
|
|
|
7
9
|
|
|
8
10
|
let penalty = 0;
|
|
9
11
|
for (const v of violations) {
|
|
10
|
-
if (v
|
|
12
|
+
if (isSlopViolation(v)) continue;
|
|
11
13
|
if (v.severity === 'CRITICAL') penalty += 8;
|
|
12
14
|
else if (v.severity === 'HIGH') penalty += 4;
|
|
13
15
|
else if (v.severity === 'MEDIUM') penalty += 2;
|
|
@@ -57,7 +59,7 @@ export const calculatePillarBreakdown = (violations) => {
|
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
for (const v of violations) {
|
|
60
|
-
if (v
|
|
62
|
+
if (isSlopViolation(v)) continue;
|
|
61
63
|
const pillarName = v.pillar || PILLARS.PILLAR_1;
|
|
62
64
|
if (!breakdown[pillarName]) {
|
|
63
65
|
breakdown[pillarName] = {
|
|
@@ -181,7 +183,7 @@ export const calculateHotspots = (violations, fileStats, limit = 5) => {
|
|
|
181
183
|
export const calculateQuantumHealthScore = calculateMolecularHealthScore;
|
|
182
184
|
|
|
183
185
|
export const calculateAiSlopScore = (violations, totalFiles) => {
|
|
184
|
-
const slopViolations = violations.filter(
|
|
186
|
+
const slopViolations = violations.filter(isSlopViolation);
|
|
185
187
|
if (totalFiles === 0) {
|
|
186
188
|
return {
|
|
187
189
|
score: 100,
|
|
@@ -128,7 +128,7 @@ export const generateMarkdownReport = (report) => {
|
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
-
const slopViolations = violations.filter((v) => v.isAiSlop);
|
|
131
|
+
const slopViolations = violations.filter((v) => v.isAiSlop || (v.rule && v.rule.startsWith('AI_SLOP_')));
|
|
132
132
|
lines.push('---');
|
|
133
133
|
lines.push('');
|
|
134
134
|
lines.push('## 5. AI Slop & Code Authenticity Audit');
|
|
@@ -27,7 +27,7 @@ import {
|
|
|
27
27
|
} from './prompts.js';
|
|
28
28
|
import { resolveFirstDefined } from './rules-predicates.js';
|
|
29
29
|
|
|
30
|
-
const isSlopViolation = (v) => Boolean(v.isAiSlop);
|
|
30
|
+
const isSlopViolation = (v) => Boolean(v.isAiSlop || (v.rule && v.rule.startsWith('AI_SLOP_')));
|
|
31
31
|
|
|
32
32
|
const formatHotspotItem = (h, idx) => {
|
|
33
33
|
const monolithBadge = resolveHotspotBadge(h.lineCount);
|
|
@@ -242,5 +242,15 @@ export const RULE_REGISTRY = {
|
|
|
242
242
|
pillar: PILLARS.PILLAR_4,
|
|
243
243
|
severity: 'MEDIUM',
|
|
244
244
|
directive: 'Encapsulate flat primitive props into a cohesive domain type or composable'
|
|
245
|
+
},
|
|
246
|
+
LAYER_VIOLATION_CONTROLLER: {
|
|
247
|
+
pillar: PILLARS.PILLAR_4,
|
|
248
|
+
severity: 'CRITICAL',
|
|
249
|
+
directive: 'Do not inject DbContext or raw persistence into presentation controllers; route through MediatR/CQRS handlers'
|
|
250
|
+
},
|
|
251
|
+
COUPLING_EXCESSIVE_INJECTION: {
|
|
252
|
+
pillar: PILLARS.PILLAR_1,
|
|
253
|
+
severity: 'HIGH',
|
|
254
|
+
directive: 'Decompose high-coupling controllers; reduce injected services or route via MediatR'
|
|
245
255
|
}
|
|
246
256
|
};
|