pumuki-ast-hooks 5.5.47 → 5.5.49
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/docs/CODE_STANDARDS.md +5 -0
- package/docs/VIOLATIONS_RESOLUTION_PLAN.md +5 -140
- package/package.json +1 -1
- package/scripts/hooks-system/.audit_tmp/hook-metrics.jsonl +96 -0
- package/scripts/hooks-system/application/services/installation/VSCodeTaskConfigurator.js +3 -1
- package/scripts/hooks-system/bin/gitflow-cycle.js +0 -0
- package/scripts/hooks-system/config/project.config.json +1 -1
- package/scripts/hooks-system/infrastructure/ast/android/analyzers/AndroidSOLIDAnalyzer.js +11 -255
- package/scripts/hooks-system/infrastructure/ast/android/detectors/android-solid-detectors.js +227 -0
- package/scripts/hooks-system/infrastructure/ast/ast-core.js +12 -3
- package/scripts/hooks-system/infrastructure/ast/ast-intelligence.js +36 -13
- package/scripts/hooks-system/infrastructure/ast/backend/ast-backend.js +10 -83
- package/scripts/hooks-system/infrastructure/ast/backend/detectors/god-class-detector.js +83 -0
- package/scripts/hooks-system/infrastructure/ast/common/ast-common.js +17 -2
- package/scripts/hooks-system/infrastructure/ast/frontend/analyzers/FrontendArchitectureDetector.js +12 -142
- package/scripts/hooks-system/infrastructure/ast/frontend/detectors/frontend-architecture-strategies.js +126 -0
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSASTIntelligentAnalyzer.js +30 -783
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSArchitectureDetector.js +21 -224
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSArchitectureRules.js +18 -605
- package/scripts/hooks-system/infrastructure/ast/ios/analyzers/iOSModernPracticesRules.js +4 -1
- package/scripts/hooks-system/infrastructure/ast/ios/ast-ios.js +4 -1
- package/scripts/hooks-system/infrastructure/ast/ios/detectors/ios-architecture-rules-strategies.js +595 -0
- package/scripts/hooks-system/infrastructure/ast/ios/detectors/ios-architecture-strategies.js +192 -0
- package/scripts/hooks-system/infrastructure/ast/ios/detectors/ios-ast-intelligent-strategies.js +789 -0
- package/scripts/hooks-system/infrastructure/ast/ios/detectors/ios-god-class-detector.js +79 -0
- package/scripts/hooks-system/infrastructure/ast/ios/native-bridge.js +4 -1
- package/scripts/hooks-system/infrastructure/orchestration/intelligent-audit.js +24 -13
- package/skills/android-guidelines/SKILL.md +1 -0
- package/skills/backend-guidelines/SKILL.md +1 -0
- package/skills/frontend-guidelines/SKILL.md +1 -0
- package/skills/ios-guidelines/SKILL.md +1 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { SyntaxKind } = require(path.join(__dirname, '../../ast-core'));
|
|
3
|
+
|
|
4
|
+
function analyzeOCP(sf, findings, pushFinding) {
|
|
5
|
+
const filePath = sf.getFilePath();
|
|
6
|
+
const fileName = filePath.split('/').pop() || 'unknown';
|
|
7
|
+
|
|
8
|
+
const functions = sf.getFunctions();
|
|
9
|
+
const arrowFunctions = sf.getVariableDeclarations().filter(vd => {
|
|
10
|
+
const init = vd.getInitializer();
|
|
11
|
+
return init && init.getKind() === SyntaxKind.ArrowFunction;
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const classes = sf.getClasses();
|
|
15
|
+
|
|
16
|
+
const allNodes = [
|
|
17
|
+
...functions.map(f => ({ type: 'function', node: f, name: f.getName() || 'anonymous' })),
|
|
18
|
+
...arrowFunctions.map(af => ({ type: 'arrow', node: af, name: af.getName() || 'anonymous' })),
|
|
19
|
+
...classes.map(c => ({ type: 'class', node: c, name: c.getName() || 'AnonymousClass' })),
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
allNodes.forEach(({ node, name }) => {
|
|
23
|
+
analyzeNodeForOCP(node, name, fileName, sf, pushFinding);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function analyzeNodeForOCP(node, nodeName, fileName, sf, pushFinding) {
|
|
28
|
+
let body;
|
|
29
|
+
|
|
30
|
+
if (node.getKind() === SyntaxKind.FunctionDeclaration ||
|
|
31
|
+
node.getKind() === SyntaxKind.FunctionExpression) {
|
|
32
|
+
body = node.getBody();
|
|
33
|
+
} else if (node.getKind() === SyntaxKind.VariableDeclaration) {
|
|
34
|
+
const init = node.getInitializer();
|
|
35
|
+
if (init && init.getKind() === SyntaxKind.ArrowFunction) {
|
|
36
|
+
body = init.getBody();
|
|
37
|
+
}
|
|
38
|
+
} else if (node.getKind() === SyntaxKind.ClassDeclaration) {
|
|
39
|
+
const methods = node.getMethods();
|
|
40
|
+
methods.forEach(method => {
|
|
41
|
+
const methodBody = method.getBody();
|
|
42
|
+
if (methodBody) {
|
|
43
|
+
analyzeBodyForOCP(methodBody, `${nodeName}.${method.getName()}`, fileName, sf, pushFinding);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
return;
|
|
47
|
+
} else {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (body) {
|
|
52
|
+
analyzeBodyForOCP(body, nodeName, fileName, sf, pushFinding);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function analyzeBodyForOCP(body, name, fileName, sf, pushFinding) {
|
|
57
|
+
const switchStatements = body.getDescendantsOfKind(SyntaxKind.SwitchStatement);
|
|
58
|
+
const stringLiterals = body.getDescendantsOfKind(SyntaxKind.StringLiteral);
|
|
59
|
+
|
|
60
|
+
let hasTypeCodes = false;
|
|
61
|
+
const suspectStrings = ['type', 'kind', 'state', 'status', 'mode', 'screen', 'variant'];
|
|
62
|
+
for (const s of stringLiterals) {
|
|
63
|
+
const value = (s.getLiteralText && s.getLiteralText()) || s.getText() || '';
|
|
64
|
+
if (suspectStrings.some(x => value.toLowerCase().includes(x))) {
|
|
65
|
+
hasTypeCodes = true;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (switchStatements.length >= 3 && hasTypeCodes) {
|
|
71
|
+
const message = `OCP VIOLATION in ${fileName}::${name}: ${switchStatements.length} switch statements with type codes - consider polymorphism or sealed classes`;
|
|
72
|
+
pushFinding('solid.ocp.switch_typecodes', 'critical', sf, body, message, []);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const ifStatements = body.getDescendantsOfKind(SyntaxKind.IfStatement);
|
|
77
|
+
const hasManyIfs = ifStatements.length >= 6;
|
|
78
|
+
const hasElseIfChains = ifStatements.some(ifStmt => {
|
|
79
|
+
const elseStmt = ifStmt.getElseStatement();
|
|
80
|
+
return elseStmt && elseStmt.getKind() === SyntaxKind.IfStatement;
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
if (hasManyIfs && hasElseIfChains) {
|
|
84
|
+
const message = `OCP WARNING in ${fileName}::${name}: multiple if/else-if chains - consider strategy/visitor`;
|
|
85
|
+
pushFinding('solid.ocp.if_chains', 'high', sf, body, message, []);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function analyzeDIP(sf, findings, pushFinding) {
|
|
90
|
+
const filePath = sf.getFilePath();
|
|
91
|
+
const fileName = filePath.split('/').pop() || 'unknown';
|
|
92
|
+
|
|
93
|
+
const isDomain = /\/domain\//i.test(filePath);
|
|
94
|
+
|
|
95
|
+
if (isDomain) {
|
|
96
|
+
const imports = sf.getImportDeclarations();
|
|
97
|
+
|
|
98
|
+
imports.forEach(imp => {
|
|
99
|
+
const importPath = imp.getModuleSpecifierValue();
|
|
100
|
+
|
|
101
|
+
if (/\/infrastructure\//i.test(importPath) ||
|
|
102
|
+
/androidx|kotlinx|retrofit|room|hilt/i.test(importPath)) {
|
|
103
|
+
const message = `DIP VIOLATION in ${fileName}: Domain layer importing from Infrastructure/Framework: ${importPath} - Domain should depend only on abstractions`;
|
|
104
|
+
pushFinding('solid.dip.domain_depends_infrastructure', 'critical', sf, imp, message, findings);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const isPresentation = /\/presentation\//i.test(filePath);
|
|
110
|
+
|
|
111
|
+
if (isPresentation) {
|
|
112
|
+
const imports = sf.getImportDeclarations();
|
|
113
|
+
|
|
114
|
+
imports.forEach(imp => {
|
|
115
|
+
const importPath = imp.getModuleSpecifierValue();
|
|
116
|
+
|
|
117
|
+
if (/\/infrastructure\//i.test(importPath) &&
|
|
118
|
+
!/\/infrastructure\/repositories\/|\/infrastructure\/config\//i.test(importPath)) {
|
|
119
|
+
const message = `DIP VIOLATION in ${fileName}: Presentation layer importing from Infrastructure: ${importPath} - use repository interfaces or abstractions`;
|
|
120
|
+
pushFinding('solid.dip.presentation_infrastructure', 'critical', sf, imp, message, findings);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const classes = sf.getClasses();
|
|
126
|
+
classes.forEach(cls => {
|
|
127
|
+
const className = cls.getName() || 'AnonymousClass';
|
|
128
|
+
|
|
129
|
+
if (/ViewModel|UseCase/i.test(className)) {
|
|
130
|
+
const imports = sf.getImportDeclarations();
|
|
131
|
+
|
|
132
|
+
imports.forEach(imp => {
|
|
133
|
+
const importPath = imp.getModuleSpecifierValue();
|
|
134
|
+
|
|
135
|
+
if (/Repository|Service|Client/i.test(importPath) &&
|
|
136
|
+
!/interface|protocol|Repository.*Protocol/i.test(importPath)) {
|
|
137
|
+
const message = `DIP VIOLATION in ${fileName}::${className}: depends on concrete implementation '${importPath}' - inject interface/abstraction`;
|
|
138
|
+
pushFinding('solid.dip.concrete_dependency', 'critical', sf, imp, message, findings);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function analyzeSRP(sf, findings, pushFinding) {
|
|
146
|
+
const filePath = sf.getFilePath();
|
|
147
|
+
const fileName = filePath.split('/').pop() || 'unknown';
|
|
148
|
+
|
|
149
|
+
const functions = sf.getFunctions();
|
|
150
|
+
const arrowFunctions = sf.getVariableDeclarations().filter(vd => {
|
|
151
|
+
const init = vd.getInitializer();
|
|
152
|
+
return init && init.getKind() === SyntaxKind.ArrowFunction;
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
[...functions, ...arrowFunctions].forEach(func => {
|
|
156
|
+
const funcName = func.getName?.() || 'anonymous';
|
|
157
|
+
const body = func.getBody?.() || func.getInitializer()?.getBody();
|
|
158
|
+
|
|
159
|
+
if (!body) return;
|
|
160
|
+
|
|
161
|
+
const statements = body.getStatements();
|
|
162
|
+
const ifStatements = body.getDescendantsOfKind(SyntaxKind.IfStatement);
|
|
163
|
+
const switchStatements = body.getDescendantsOfKind(SyntaxKind.SwitchStatement);
|
|
164
|
+
const loops = body.getDescendantsOfKind(SyntaxKind.ForStatement)
|
|
165
|
+
.concat(body.getDescendantsOfKind(SyntaxKind.ForInStatement))
|
|
166
|
+
.concat(body.getDescendantsOfKind(SyntaxKind.WhileStatement));
|
|
167
|
+
|
|
168
|
+
if (statements.length > 30 || ifStatements.length > 10 || switchStatements.length > 2 || loops.length > 5) {
|
|
169
|
+
const message = `SRP VIOLATION in ${fileName}::${funcName}: high complexity (${statements.length} statements, ${ifStatements.length} ifs, ${switchStatements.length} switches) - extract responsibilities`;
|
|
170
|
+
pushFinding('solid.srp.high_complexity', 'critical', sf, func, message, findings);
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const classes = sf.getClasses();
|
|
175
|
+
classes.forEach(cls => {
|
|
176
|
+
const className = cls.getName() || 'AnonymousClass';
|
|
177
|
+
const methods = cls.getMethods();
|
|
178
|
+
|
|
179
|
+
if (methods.length > 20) {
|
|
180
|
+
const message = `SRP VIOLATION in ${fileName}::${className}: God class with ${methods.length} methods - split into focused classes`;
|
|
181
|
+
pushFinding('solid.srp.god_class', 'critical', sf, cls, message, findings);
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function detectMethodConcern(name) {
|
|
187
|
+
if (!name) return 'unknown';
|
|
188
|
+
const lower = name.toLowerCase();
|
|
189
|
+
if (/fetch|get|load|retrieve|query/.test(lower)) return 'data';
|
|
190
|
+
if (/update|set|save|persist|write/.test(lower)) return 'mutation';
|
|
191
|
+
if (/validate|check|verify|ensure/.test(lower)) return 'validation';
|
|
192
|
+
if (/render|draw|display|view/.test(lower)) return 'ui';
|
|
193
|
+
if (/handle|process|execute|run|perform/.test(lower)) return 'logic';
|
|
194
|
+
return 'unknown';
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function analyzeISP(sf, findings, pushFinding) {
|
|
198
|
+
const interfaces = sf.getInterfaces();
|
|
199
|
+
|
|
200
|
+
interfaces.forEach(iface => {
|
|
201
|
+
const interfaceName = iface.getName();
|
|
202
|
+
const properties = iface.getProperties();
|
|
203
|
+
const methods = iface.getMethods();
|
|
204
|
+
|
|
205
|
+
if (properties.length > 10) {
|
|
206
|
+
const message = `ISP VIOLATION: ${interfaceName} has ${properties.length} properties - split into focused interfaces`;
|
|
207
|
+
pushFinding('solid.isp.fat_interface', 'critical', sf, iface, message, findings);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (methods.length > 0) {
|
|
211
|
+
const methodConcerns = methods.map(m => detectMethodConcern(m.getName()));
|
|
212
|
+
const uniqueConcerns = new Set(methodConcerns.filter(c => c !== 'unknown'));
|
|
213
|
+
|
|
214
|
+
if (uniqueConcerns.size >= 3) {
|
|
215
|
+
const message = `ISP VIOLATION: ${interfaceName} mixes ${uniqueConcerns.size} concerns (${Array.from(uniqueConcerns).join(', ')}) - segregate into focused interfaces`;
|
|
216
|
+
pushFinding('solid.isp.multiple_concerns', 'critical', sf, iface, message, findings);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
module.exports = {
|
|
223
|
+
analyzeOCP,
|
|
224
|
+
analyzeDIP,
|
|
225
|
+
analyzeSRP,
|
|
226
|
+
analyzeISP,
|
|
227
|
+
};
|
|
@@ -30,7 +30,10 @@ function getRepoRoot() {
|
|
|
30
30
|
try {
|
|
31
31
|
const { execSync } = require('child_process');
|
|
32
32
|
return execSync('git rev-parse --show-toplevel', { encoding: 'utf-8' }).trim();
|
|
33
|
-
} catch {
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (process.env.DEBUG) {
|
|
35
|
+
console.debug(`[ast-core] Failed to detect git repo root, using cwd: ${error.message}`);
|
|
36
|
+
}
|
|
34
37
|
return process.cwd();
|
|
35
38
|
}
|
|
36
39
|
}
|
|
@@ -191,7 +194,10 @@ function pushFinding(ruleId, severity, sf, node, message, findings, metrics = {}
|
|
|
191
194
|
let strictRegex;
|
|
192
195
|
try {
|
|
193
196
|
strictRegex = new RegExp(strictRegexSource, 'i');
|
|
194
|
-
} catch {
|
|
197
|
+
} catch (error) {
|
|
198
|
+
if (process.env.DEBUG) {
|
|
199
|
+
console.debug(`[ast-core] Invalid STRICT_CRITICAL_RULES_REGEX, using default: ${error.message}`);
|
|
200
|
+
}
|
|
195
201
|
strictRegex = new RegExp(env.getBool('AUDIT_LIBRARY', false) ? defaultStrictCriticalRegexLibrary : defaultStrictCriticalRegex, 'i');
|
|
196
202
|
}
|
|
197
203
|
isStrictCriticalRule = strictRegex.test(ruleId);
|
|
@@ -261,7 +267,10 @@ function pushFileFinding(ruleId, severity, filePath, line, column, message, find
|
|
|
261
267
|
let strictRegex;
|
|
262
268
|
try {
|
|
263
269
|
strictRegex = new RegExp(strictRegexSource, 'i');
|
|
264
|
-
} catch {
|
|
270
|
+
} catch (error) {
|
|
271
|
+
if (process.env.DEBUG) {
|
|
272
|
+
console.debug(`[ast-core] Invalid STRICT_CRITICAL_RULES_REGEX, using default: ${error.message}`);
|
|
273
|
+
}
|
|
265
274
|
strictRegex = new RegExp(env.getBool('AUDIT_LIBRARY', false) ? defaultStrictCriticalRegexLibrary : defaultStrictCriticalRegex, 'i');
|
|
266
275
|
}
|
|
267
276
|
isStrictCriticalRule = strictRegex.test(ruleId);
|
|
@@ -115,7 +115,10 @@ function getStagedFilesRel(root) {
|
|
|
115
115
|
.split('\n')
|
|
116
116
|
.map(s => s.trim())
|
|
117
117
|
.filter(Boolean);
|
|
118
|
-
} catch {
|
|
118
|
+
} catch (error) {
|
|
119
|
+
if (process.env.DEBUG) {
|
|
120
|
+
console.debug(`[ast-intelligence] Failed to read staged files: ${error.message}`);
|
|
121
|
+
}
|
|
119
122
|
return [];
|
|
120
123
|
}
|
|
121
124
|
}
|
|
@@ -172,7 +175,10 @@ function runProjectHardcodedThresholdAudit(root, allFiles, findings) {
|
|
|
172
175
|
let content;
|
|
173
176
|
try {
|
|
174
177
|
content = fs.readFileSync(filePath, 'utf8');
|
|
175
|
-
} catch {
|
|
178
|
+
} catch (error) {
|
|
179
|
+
if (process.env.DEBUG) {
|
|
180
|
+
console.debug(`[ast-intelligence] Failed to read file ${filePath}: ${error.message}`);
|
|
181
|
+
}
|
|
176
182
|
continue;
|
|
177
183
|
}
|
|
178
184
|
|
|
@@ -214,7 +220,10 @@ function runHardcodedThresholdAudit(root, findings) {
|
|
|
214
220
|
].filter((d) => {
|
|
215
221
|
try {
|
|
216
222
|
return fs.existsSync(d) && fs.statSync(d).isDirectory();
|
|
217
|
-
} catch {
|
|
223
|
+
} catch (error) {
|
|
224
|
+
if (process.env.DEBUG) {
|
|
225
|
+
console.debug(`[ast-intelligence] Failed to stat dir ${d}: ${error.message}`);
|
|
226
|
+
}
|
|
218
227
|
return false;
|
|
219
228
|
}
|
|
220
229
|
});
|
|
@@ -238,7 +247,10 @@ function runHardcodedThresholdAudit(root, findings) {
|
|
|
238
247
|
let entries;
|
|
239
248
|
try {
|
|
240
249
|
entries = fs.readdirSync(current, { withFileTypes: true });
|
|
241
|
-
} catch {
|
|
250
|
+
} catch (error) {
|
|
251
|
+
if (process.env.DEBUG) {
|
|
252
|
+
console.debug(`[ast-intelligence] Failed to read dir ${current}: ${error.message}`);
|
|
253
|
+
}
|
|
242
254
|
continue;
|
|
243
255
|
}
|
|
244
256
|
|
|
@@ -290,7 +302,10 @@ function runHardcodedThresholdAudit(root, findings) {
|
|
|
290
302
|
let content;
|
|
291
303
|
try {
|
|
292
304
|
content = fs.readFileSync(filePath, 'utf8');
|
|
293
|
-
} catch {
|
|
305
|
+
} catch (error) {
|
|
306
|
+
if (process.env.DEBUG) {
|
|
307
|
+
console.debug(`[ast-intelligence] Failed to read file ${filePath}: ${error.message}`);
|
|
308
|
+
}
|
|
294
309
|
continue;
|
|
295
310
|
}
|
|
296
311
|
|
|
@@ -394,15 +409,23 @@ function generateOutput(findings, context, project, root) {
|
|
|
394
409
|
.filter(Boolean);
|
|
395
410
|
|
|
396
411
|
if (stagedRel.length > 0) {
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
})
|
|
412
|
+
try {
|
|
413
|
+
findings = findings.filter((f) => {
|
|
414
|
+
if (!f || !f.filePath) return false;
|
|
415
|
+
const fp = String(f.filePath).replace(/\\/g, '/');
|
|
416
|
+
return stagedRel.some(rel => fp.endsWith(rel) || fp.includes(`/${rel}`));
|
|
417
|
+
});
|
|
418
|
+
} catch (error) {
|
|
419
|
+
if (process.env.DEBUG) {
|
|
420
|
+
console.debug(`[ast-intelligence] Failed to filter findings by staged files: ${error.message}`);
|
|
421
|
+
}
|
|
422
|
+
findings = [];
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
} catch (error) {
|
|
426
|
+
if (process.env.DEBUG) {
|
|
427
|
+
console.debug(`[ast-intelligence] Failed to get staged files: ${error.message}`);
|
|
404
428
|
}
|
|
405
|
-
} catch {
|
|
406
429
|
findings = [];
|
|
407
430
|
}
|
|
408
431
|
}
|
|
@@ -21,6 +21,7 @@ const {
|
|
|
21
21
|
getRepoRoot,
|
|
22
22
|
} = require(path.join(__dirname, '../ast-core'));
|
|
23
23
|
const { BackendArchitectureDetector } = require(path.join(__dirname, 'analyzers/BackendArchitectureDetector'));
|
|
24
|
+
const { analyzeGodClasses } = require(path.join(__dirname, 'detectors/god-class-detector'));
|
|
24
25
|
|
|
25
26
|
/**
|
|
26
27
|
* Run Backend-specific AST intelligence analysis
|
|
@@ -267,10 +268,13 @@ function runBackendIntelligence(project, findings, platform) {
|
|
|
267
268
|
}
|
|
268
269
|
}
|
|
269
270
|
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
271
|
+
const fullTextUpper = sf.getFullText();
|
|
272
|
+
const usesEnvHelper = /config\/env/.test(fullTextUpper) || /env\.get(Bool|Number)?\(/.test(fullTextUpper);
|
|
273
|
+
const hasEnvSpecific = fullTextUpper.includes("process.env.NODE_ENV") ||
|
|
274
|
+
fullTextUpper.includes("app.get('env')") ||
|
|
275
|
+
fullTextUpper.includes("ConfigService") ||
|
|
276
|
+
usesEnvHelper;
|
|
277
|
+
const hasConfigUsage = /process\.env\b|ConfigService|env\.get(Bool|Number)?\(|\bconfig\s*[\.\[]/i.test(sf.getFullText());
|
|
274
278
|
if (!hasEnvSpecific && hasConfigUsage && !isTestFile(filePath)) {
|
|
275
279
|
pushFinding("backend.config.missing_env_separation", "info", sf, sf, "Missing environment-specific configuration - consider NODE_ENV or ConfigService", findings);
|
|
276
280
|
}
|
|
@@ -283,83 +287,7 @@ function runBackendIntelligence(project, findings, platform) {
|
|
|
283
287
|
}
|
|
284
288
|
|
|
285
289
|
if (godClassBaseline) {
|
|
286
|
-
sf
|
|
287
|
-
const className = cls.getName() || '';
|
|
288
|
-
const isValueObject = /Metrics|ValueObject|VO$|Dto$|Entity$/.test(className);
|
|
289
|
-
const isTestClass = /Spec$|Test$|Mock/.test(className);
|
|
290
|
-
if (isValueObject || isTestClass) return;
|
|
291
|
-
|
|
292
|
-
const methodsCount = cls.getMethods().length;
|
|
293
|
-
const propertiesCount = cls.getProperties().length;
|
|
294
|
-
const startLine = cls.getStartLineNumber();
|
|
295
|
-
const endLine = cls.getEndLineNumber();
|
|
296
|
-
const lineCount = Math.max(0, endLine - startLine);
|
|
297
|
-
|
|
298
|
-
const decisionKinds = [
|
|
299
|
-
SyntaxKind.IfStatement,
|
|
300
|
-
SyntaxKind.ForStatement,
|
|
301
|
-
SyntaxKind.ForInStatement,
|
|
302
|
-
SyntaxKind.ForOfStatement,
|
|
303
|
-
SyntaxKind.WhileStatement,
|
|
304
|
-
SyntaxKind.DoStatement,
|
|
305
|
-
SyntaxKind.SwitchStatement,
|
|
306
|
-
SyntaxKind.ConditionalExpression,
|
|
307
|
-
SyntaxKind.TryStatement,
|
|
308
|
-
SyntaxKind.CatchClause
|
|
309
|
-
];
|
|
310
|
-
const complexity = decisionKinds.reduce((acc, kind) => acc + cls.getDescendantsOfKind(kind).length, 0);
|
|
311
|
-
|
|
312
|
-
const clsText = cls.getFullText();
|
|
313
|
-
const concerns = new Set();
|
|
314
|
-
if (/\bfs\.|\bfs\.promises\b|readFileSync|writeFileSync|mkdirSync|unlinkSync|readdirSync/.test(clsText)) concerns.add('io');
|
|
315
|
-
if (/\bpath\.|join\(|resolve\(|dirname\(|basename\(/.test(clsText)) concerns.add('path');
|
|
316
|
-
if (/execSync\(|spawnSync\(|spawn\(|child_process/.test(clsText)) concerns.add('process');
|
|
317
|
-
if (/\bfetch\b|axios\b|http\.|https\.|request\(/.test(clsText)) concerns.add('network');
|
|
318
|
-
if (/\bcrypto\b|encrypt|decrypt|hash|jwt|bearer|token/i.test(clsText)) concerns.add('security');
|
|
319
|
-
if (/setTimeout\(|setInterval\(|clearInterval\(|cron|schedule/i.test(clsText)) concerns.add('scheduling');
|
|
320
|
-
if (/\brepo\b|repository|prisma|typeorm|mongoose|sequelize|knex|\bdb\b|database|sql/i.test(clsText)) concerns.add('persistence');
|
|
321
|
-
if (/notification|notifier|terminal-notifier|osascript/i.test(clsText)) concerns.add('notifications');
|
|
322
|
-
if (/\bgit\b|rev-parse|git diff|git status|git log/i.test(clsText)) concerns.add('git');
|
|
323
|
-
const concernCount = concerns.size;
|
|
324
|
-
|
|
325
|
-
const methodsZ = godClassBaseline.robustZ(methodsCount, godClassBaseline.med.methodsCount, godClassBaseline.mad.methodsCount);
|
|
326
|
-
const propsZ = godClassBaseline.robustZ(propertiesCount, godClassBaseline.med.propertiesCount, godClassBaseline.mad.propertiesCount);
|
|
327
|
-
const linesZ = godClassBaseline.robustZ(lineCount, godClassBaseline.med.lineCount, godClassBaseline.mad.lineCount);
|
|
328
|
-
const complexityZ = godClassBaseline.robustZ(complexity, godClassBaseline.med.complexity, godClassBaseline.mad.complexity);
|
|
329
|
-
|
|
330
|
-
const sizeOutlier =
|
|
331
|
-
methodsZ >= godClassBaseline.thresholds.outlier.methodsCountZ ||
|
|
332
|
-
propsZ >= godClassBaseline.thresholds.outlier.propertiesCountZ ||
|
|
333
|
-
linesZ >= godClassBaseline.thresholds.outlier.lineCountZ;
|
|
334
|
-
const complexityOutlier = complexityZ >= godClassBaseline.thresholds.outlier.complexityZ;
|
|
335
|
-
const concernOutlier = concernCount >= godClassBaseline.thresholds.outlier.concerns;
|
|
336
|
-
|
|
337
|
-
// Detección híbrida: estadística + umbrales absolutos
|
|
338
|
-
// Archivo masivo: cualquier archivo con >500 líneas es sospechoso
|
|
339
|
-
const isMassiveFile = lineCount > 500;
|
|
340
|
-
// God class absoluta: archivo muy grande O (grande + complejo) O (grande + muchos métodos)
|
|
341
|
-
const isAbsoluteGod = lineCount > 1000 ||
|
|
342
|
-
(lineCount > 500 && complexity > 50) ||
|
|
343
|
-
(lineCount > 500 && methodsCount > 20) ||
|
|
344
|
-
(lineCount > 600 && methodsCount > 30 && complexity > 80);
|
|
345
|
-
const isUnderThreshold = lineCount < 300 && methodsCount < 15 && complexity < 30;
|
|
346
|
-
|
|
347
|
-
let signalCount = 0;
|
|
348
|
-
if (sizeOutlier) signalCount++;
|
|
349
|
-
if (complexityOutlier) signalCount++;
|
|
350
|
-
if (concernOutlier) signalCount++;
|
|
351
|
-
if (isMassiveFile) signalCount++; // Añadir señal extra por tamaño masivo
|
|
352
|
-
|
|
353
|
-
const isInternalAstToolingFile = /infrastructure\/ast\//i.test(filePath);
|
|
354
|
-
const isInfrastructureService = /application\/services.*\/(RealtimeGuardService|EvidenceManager|HookInstaller|InstallService|EvidenceMonitor)/i.test(filePath);
|
|
355
|
-
if (!isUnderThreshold && !isInternalAstToolingFile && !isInfrastructureService && (signalCount >= 2 || isAbsoluteGod)) {
|
|
356
|
-
console.error(`[GOD CLASS DEBUG] ${className}: methods=${methodsCount}, props=${propertiesCount}, lines=${lineCount}, complexity=${complexity}, concerns=${concernCount}, isAbsoluteGod=${isAbsoluteGod}, signalCount=${signalCount}`);
|
|
357
|
-
pushFinding("backend.antipattern.god_classes", "critical", sf, cls,
|
|
358
|
-
`God class detected: ${methodsCount} methods, ${propertiesCount} properties, ${lineCount} lines, complexity ${complexity}, concerns ${concernCount} - VIOLATES SRP`,
|
|
359
|
-
findings
|
|
360
|
-
);
|
|
361
|
-
}
|
|
362
|
-
});
|
|
290
|
+
analyzeGodClasses(sf, findings, { SyntaxKind, pushFinding, godClassBaseline });
|
|
363
291
|
}
|
|
364
292
|
|
|
365
293
|
sf.getDescendantsOfKind(SyntaxKind.ClassDeclaration).forEach((cls) => {
|
|
@@ -760,8 +688,7 @@ function runBackendIntelligence(project, findings, platform) {
|
|
|
760
688
|
|
|
761
689
|
sf.getDescendantsOfKind(SyntaxKind.StringLiteral).forEach((str) => {
|
|
762
690
|
const text = str.getLiteralValue();
|
|
763
|
-
const
|
|
764
|
-
const sqlPattern = new RegExp(sqlKeywords.join('|'), 'i');
|
|
691
|
+
const sqlPattern = /\b(select|insert|update|delete|drop|alter|truncate)\b\s+(from|into|table|database|schema|where)/i;
|
|
765
692
|
if (sqlPattern.test(text)) {
|
|
766
693
|
pushFinding("backend.database.raw_sql", "medium", sf, str, "Raw SQL detected - prefer ORM queries for type safety and security", findings);
|
|
767
694
|
if (/[\"'`].*\+.*[\"'`]/.test(text) || /\$\{.*\}/.test(text)) {
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* God Class detector extracted from ast-backend.js to keep responsibilities separated.
|
|
3
|
+
*/
|
|
4
|
+
function analyzeGodClasses(sourceFile, findings, { SyntaxKind, pushFinding, godClassBaseline }) {
|
|
5
|
+
if (!godClassBaseline) return;
|
|
6
|
+
|
|
7
|
+
sourceFile.getDescendantsOfKind(SyntaxKind.ClassDeclaration).forEach((cls) => {
|
|
8
|
+
const className = cls.getName() || '';
|
|
9
|
+
const isValueObject = /Metrics|ValueObject|VO$|Dto$|Entity$/.test(className);
|
|
10
|
+
const isTestClass = /Spec$|Test$|Mock/.test(className);
|
|
11
|
+
if (isValueObject || isTestClass) return;
|
|
12
|
+
|
|
13
|
+
const methodsCount = cls.getMethods().length;
|
|
14
|
+
const propertiesCount = cls.getProperties().length;
|
|
15
|
+
const startLine = cls.getStartLineNumber();
|
|
16
|
+
const endLine = cls.getEndLineNumber();
|
|
17
|
+
const lineCount = Math.max(0, endLine - startLine);
|
|
18
|
+
|
|
19
|
+
const decisionKinds = [
|
|
20
|
+
SyntaxKind.IfStatement,
|
|
21
|
+
SyntaxKind.ForStatement,
|
|
22
|
+
SyntaxKind.ForInStatement,
|
|
23
|
+
SyntaxKind.ForOfStatement,
|
|
24
|
+
SyntaxKind.WhileStatement,
|
|
25
|
+
SyntaxKind.DoStatement,
|
|
26
|
+
SyntaxKind.SwitchStatement,
|
|
27
|
+
SyntaxKind.ConditionalExpression,
|
|
28
|
+
SyntaxKind.TryStatement,
|
|
29
|
+
SyntaxKind.CatchClause
|
|
30
|
+
];
|
|
31
|
+
const complexity = decisionKinds.reduce((acc, kind) => acc + cls.getDescendantsOfKind(kind).length, 0);
|
|
32
|
+
|
|
33
|
+
const clsText = cls.getFullText();
|
|
34
|
+
const concerns = new Set();
|
|
35
|
+
if (/\bfs\.|\bfs\.promises\b|readFileSync|writeFileSync|mkdirSync|unlinkSync|readdirSync/.test(clsText)) concerns.add('io');
|
|
36
|
+
if (/\bpath\.|join\(|resolve\(|dirname\(|basename\(/.test(clsText)) concerns.add('path');
|
|
37
|
+
if (/execSync\(|spawnSync\(|spawn\(|child_process/.test(clsText)) concerns.add('process');
|
|
38
|
+
if (/\bfetch\b|axios\b|http\.|https\.|request\(/.test(clsText)) concerns.add('network');
|
|
39
|
+
if (/\bcrypto\b|encrypt|decrypt|hash|jwt|bearer|token/i.test(clsText)) concerns.add('security');
|
|
40
|
+
if (/setTimeout\(|setInterval\(|clearInterval\(|cron|schedule/i.test(clsText)) concerns.add('scheduling');
|
|
41
|
+
if (/\brepo\b|repository|prisma|typeorm|mongoose|sequelize|knex|\bdb\b|database|sql/i.test(clsText)) concerns.add('persistence');
|
|
42
|
+
if (/notification|notifier|terminal-notifier|osascript/i.test(clsText)) concerns.add('notifications');
|
|
43
|
+
if (/\bgit\b|rev-parse|git diff|git status|git log/i.test(clsText)) concerns.add('git');
|
|
44
|
+
const concernCount = concerns.size;
|
|
45
|
+
|
|
46
|
+
const methodsZ = godClassBaseline.robustZ(methodsCount, godClassBaseline.med.methodsCount, godClassBaseline.mad.methodsCount);
|
|
47
|
+
const propsZ = godClassBaseline.robustZ(propertiesCount, godClassBaseline.med.propertiesCount, godClassBaseline.mad.propertiesCount);
|
|
48
|
+
const linesZ = godClassBaseline.robustZ(lineCount, godClassBaseline.med.lineCount, godClassBaseline.mad.lineCount);
|
|
49
|
+
const complexityZ = godClassBaseline.robustZ(complexity, godClassBaseline.med.complexity, godClassBaseline.mad.complexity);
|
|
50
|
+
|
|
51
|
+
const sizeOutlier =
|
|
52
|
+
methodsZ >= godClassBaseline.thresholds.outlier.methodsCountZ ||
|
|
53
|
+
propsZ >= godClassBaseline.thresholds.outlier.propertiesCountZ ||
|
|
54
|
+
linesZ >= godClassBaseline.thresholds.outlier.lineCountZ;
|
|
55
|
+
const complexityOutlier = complexityZ >= godClassBaseline.thresholds.outlier.complexityZ;
|
|
56
|
+
const concernOutlier = concernCount >= godClassBaseline.thresholds.outlier.concerns;
|
|
57
|
+
|
|
58
|
+
const isMassiveFile = lineCount > 500;
|
|
59
|
+
const isAbsoluteGod = lineCount > 1000 ||
|
|
60
|
+
(lineCount > 500 && complexity > 50) ||
|
|
61
|
+
(lineCount > 500 && methodsCount > 20) ||
|
|
62
|
+
(lineCount > 600 && methodsCount > 30 && complexity > 80);
|
|
63
|
+
const isUnderThreshold = lineCount < 300 && methodsCount < 15 && complexity < 30;
|
|
64
|
+
|
|
65
|
+
let signalCount = 0;
|
|
66
|
+
if (sizeOutlier) signalCount++;
|
|
67
|
+
if (complexityOutlier) signalCount++;
|
|
68
|
+
if (concernOutlier) signalCount++;
|
|
69
|
+
if (isMassiveFile) signalCount++;
|
|
70
|
+
|
|
71
|
+
if (!isUnderThreshold && (signalCount >= 2 || isAbsoluteGod)) {
|
|
72
|
+
console.error(`[GOD CLASS DEBUG] ${className}: methods=${methodsCount}, props=${propertiesCount}, lines=${lineCount}, complexity=${complexity}, concerns=${concernCount}, isAbsoluteGod=${isAbsoluteGod}, signalCount=${signalCount}`);
|
|
73
|
+
pushFinding("backend.antipattern.god_classes", "critical", sourceFile, cls,
|
|
74
|
+
`God class detected: ${methodsCount} methods, ${propertiesCount} properties, ${lineCount} lines, complexity ${complexity}, concerns ${concernCount} - VIOLATES SRP`,
|
|
75
|
+
findings
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = {
|
|
82
|
+
analyzeGodClasses,
|
|
83
|
+
};
|
|
@@ -145,6 +145,21 @@ function runCommonIntelligence(project, findings) {
|
|
|
145
145
|
}
|
|
146
146
|
});
|
|
147
147
|
|
|
148
|
+
sf.getDescendantsOfKind(SyntaxKind.CatchClause).forEach((clause) => {
|
|
149
|
+
const block = typeof clause.getBlock === 'function' ? clause.getBlock() : null;
|
|
150
|
+
const statements = block && typeof block.getStatements === 'function' ? block.getStatements() : [];
|
|
151
|
+
if ((statements || []).length === 0) {
|
|
152
|
+
pushFinding(
|
|
153
|
+
'common.error.empty_catch',
|
|
154
|
+
'critical',
|
|
155
|
+
sf,
|
|
156
|
+
clause,
|
|
157
|
+
'Empty catch block detected - always handle errors (log, rethrow, wrap, or return Result)',
|
|
158
|
+
findings
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
|
|
148
163
|
sf.getDescendantsOfKind(SyntaxKind.AnyKeyword).forEach((node) => {
|
|
149
164
|
|
|
150
165
|
const isUtilityScript = /\/(scripts?|migrations?|seeders?|fixtures?)\//i.test(filePath);
|
|
@@ -318,7 +333,7 @@ function runCommonIntelligence(project, findings) {
|
|
|
318
333
|
withoutStrings = withoutStrings.replace(/`[^`]*`/g, '');
|
|
319
334
|
withoutStrings = withoutStrings.replace(/"[^"]*"/g, '');
|
|
320
335
|
withoutStrings = withoutStrings.replace(/'[^']*'/g, '');
|
|
321
|
-
|
|
336
|
+
|
|
322
337
|
if (!/\/\/|\/\*/.test(withoutStrings)) return;
|
|
323
338
|
|
|
324
339
|
const withoutUrls = withoutStrings.replace(/https?:\/\//g, '');
|
|
@@ -327,7 +342,7 @@ function runCommonIntelligence(project, findings) {
|
|
|
327
342
|
const withoutJSDoc = withoutUrls.replace(/\/\*\*[\s\S]*?\*\//g, '');
|
|
328
343
|
const hasOnlyJSDoc = !/\/\/|\/\*/.test(withoutJSDoc);
|
|
329
344
|
if (hasOnlyJSDoc) return;
|
|
330
|
-
|
|
345
|
+
|
|
331
346
|
const withoutAllComments = withoutJSDoc.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '');
|
|
332
347
|
if (!/\/\/|\/\*/.test(withoutAllComments)) return;
|
|
333
348
|
|