@chemx/create-chemx 26.9.26-253 → 26.9.28-1234

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.
@@ -23,12 +23,18 @@ export const createAstVisitors = ({ relativePath, violations, hookRegistry, conf
23
23
  const isCustomHook = isCustomHookFunction(astPath);
24
24
 
25
25
  // Pillar 3: Hook Saturation
26
+ const CONTROLLER_WRAPPER_EXCLUSIONS = new Set([
27
+ 'useSharedStatusController',
28
+ 'useSharedController',
29
+ 'useController'
30
+ ]);
26
31
  let hookCount = 0;
27
32
  astPath.traverse({
28
33
  CallExpression(callPath) {
29
34
  const callee = callPath.node.callee;
30
35
  if (t.isIdentifier(callee) && /^use[A-Z0-9]/.test(callee.name)) {
31
- if (callPath.getFunctionParent() === astPath) {
36
+ const isExcluded = CONTROLLER_WRAPPER_EXCLUSIONS.has(callee.name);
37
+ if (!isExcluded && callPath.getFunctionParent() === astPath) {
32
38
  hookCount += 1;
33
39
  }
34
40
  }
@@ -1,18 +1,334 @@
1
1
  /**
2
2
  * Chemical X C# / .NET Structural Analyzer
3
- * Evaluates .NET Clean Architecture, MediatR handlers, EF Core usage, and AI slop.
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
- export const analyzeCSharpCode = (content, relativePath, violations) => {
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
- // Simulated Mock Data / Stubs
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
+ });
@@ -32,7 +32,10 @@ const JAVASCRIPT_URL_TEMPLATE_PATTERN = /\b(?:href|src|action|formaction)\s*=\s*
32
32
  /**
33
33
  * Pillar 10: Check for co-located test files for molecule capsules.
34
34
  */
35
- export const checkMoleculeCoLocatedTest = (filePath, relativePath, violations) => {
35
+ export const checkMoleculeCoLocatedTest = (filePath, relativePath, violations, config = {}) => {
36
+ const enforceColocated = config.enforceColocatedTests ?? (config.profile === 'atomic-strict');
37
+ if (!enforceColocated) return;
38
+
36
39
  const baseName = path.basename(filePath);
37
40
  const ext = path.extname(filePath);
38
41
 
@@ -194,8 +197,8 @@ export const checkControllerViewContract = (filePath, relativePath, content, vio
194
197
  }
195
198
  };
196
199
 
197
- export const checkExtendedTextPatterns = (content, lines, relativePath, filePath, violations) => {
198
- checkMoleculeCoLocatedTest(filePath, relativePath, violations);
200
+ export const checkExtendedTextPatterns = (content, lines, relativePath, filePath, violations, config = {}) => {
201
+ checkMoleculeCoLocatedTest(filePath, relativePath, violations, config);
199
202
  checkControllerViewContract(filePath, relativePath, content, violations);
200
203
 
201
204
  const isTestFile = /\.(test|spec)\.[jt]sx?$/.test(filePath)