@aiready/consistency 0.5.0 → 0.6.1
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/.turbo/turbo-build.log +24 -0
- package/.turbo/turbo-test.log +123 -0
- package/dist/chunk-HAOJLJNB.mjs +1290 -0
- package/dist/chunk-IVRBV7SE.mjs +1295 -0
- package/dist/chunk-LD3CHHU2.mjs +1297 -0
- package/dist/chunk-VODCPPET.mjs +1292 -0
- package/dist/chunk-WGH4TGZ3.mjs +1288 -0
- package/dist/cli.js +624 -189
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +6 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +1196 -182
- package/dist/index.mjs +581 -4
- package/package.json +14 -13
- package/src/analyzer.ts +4 -4
- package/src/analyzers/naming-ast.ts +378 -0
- package/src/index.ts +2 -1
- package/src/utils/ast-parser.ts +181 -0
- package/src/utils/context-detector.ts +278 -0
- package/src/utils/scope-tracker.ts +221 -0
|
@@ -0,0 +1,1292 @@
|
|
|
1
|
+
// src/analyzers/naming-ast.ts
|
|
2
|
+
import { loadConfig } from "@aiready/core";
|
|
3
|
+
import { dirname } from "path";
|
|
4
|
+
|
|
5
|
+
// src/utils/ast-parser.ts
|
|
6
|
+
import { parse } from "@typescript-eslint/typescript-estree";
|
|
7
|
+
import { readFileSync } from "fs";
|
|
8
|
+
function parseFile(filePath, content) {
|
|
9
|
+
try {
|
|
10
|
+
const code = content ?? readFileSync(filePath, "utf-8");
|
|
11
|
+
const isTypeScript = filePath.match(/\.tsx?$/);
|
|
12
|
+
return parse(code, {
|
|
13
|
+
jsx: filePath.match(/\.[jt]sx$/i) !== null,
|
|
14
|
+
loc: true,
|
|
15
|
+
range: true,
|
|
16
|
+
comment: false,
|
|
17
|
+
tokens: false,
|
|
18
|
+
// Relaxed parsing for JavaScript files
|
|
19
|
+
sourceType: "module",
|
|
20
|
+
ecmaVersion: "latest",
|
|
21
|
+
// Only use TypeScript parser features for .ts/.tsx files
|
|
22
|
+
filePath: isTypeScript ? filePath : void 0
|
|
23
|
+
});
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.warn(`Failed to parse ${filePath}:`, error instanceof Error ? error.message : error);
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function traverseAST(node, visitor, parent = null) {
|
|
30
|
+
if (!node) return;
|
|
31
|
+
visitor.enter?.(node, parent);
|
|
32
|
+
for (const key of Object.keys(node)) {
|
|
33
|
+
const value = node[key];
|
|
34
|
+
if (Array.isArray(value)) {
|
|
35
|
+
for (const child of value) {
|
|
36
|
+
if (child && typeof child === "object" && "type" in child) {
|
|
37
|
+
traverseAST(child, visitor, node);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
} else if (value && typeof value === "object" && "type" in value) {
|
|
41
|
+
traverseAST(value, visitor, node);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
visitor.leave?.(node, parent);
|
|
45
|
+
}
|
|
46
|
+
function isLoopStatement(node) {
|
|
47
|
+
return [
|
|
48
|
+
"ForStatement",
|
|
49
|
+
"ForInStatement",
|
|
50
|
+
"ForOfStatement",
|
|
51
|
+
"WhileStatement",
|
|
52
|
+
"DoWhileStatement"
|
|
53
|
+
].includes(node.type);
|
|
54
|
+
}
|
|
55
|
+
function getFunctionName(node) {
|
|
56
|
+
switch (node.type) {
|
|
57
|
+
case "FunctionDeclaration":
|
|
58
|
+
return node.id?.name ?? null;
|
|
59
|
+
case "FunctionExpression":
|
|
60
|
+
return node.id?.name ?? null;
|
|
61
|
+
case "ArrowFunctionExpression":
|
|
62
|
+
return null;
|
|
63
|
+
// Arrow functions don't have names directly
|
|
64
|
+
case "MethodDefinition":
|
|
65
|
+
if (node.key.type === "Identifier") {
|
|
66
|
+
return node.key.name;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
default:
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
function getLineNumber(node) {
|
|
74
|
+
return node.loc?.start.line ?? 0;
|
|
75
|
+
}
|
|
76
|
+
function isCoverageContext(node, ancestors) {
|
|
77
|
+
const coveragePatterns = /coverage|summary|metrics|pct|percent|statements|branches|functions|lines/i;
|
|
78
|
+
if (node.type === "Identifier" && coveragePatterns.test(node.name)) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
for (const ancestor of ancestors.slice(-3)) {
|
|
82
|
+
if (ancestor.type === "MemberExpression") {
|
|
83
|
+
const memberExpr = ancestor;
|
|
84
|
+
if (memberExpr.object.type === "Identifier" && coveragePatterns.test(memberExpr.object.name)) {
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
if (ancestor.type === "ObjectPattern" || ancestor.type === "ObjectExpression") {
|
|
89
|
+
const parent = ancestors[ancestors.indexOf(ancestor) - 1];
|
|
90
|
+
if (parent?.type === "VariableDeclarator") {
|
|
91
|
+
const varDecl = parent;
|
|
92
|
+
if (varDecl.id.type === "Identifier" && coveragePatterns.test(varDecl.id.name)) {
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// src/utils/scope-tracker.ts
|
|
102
|
+
var ScopeTracker = class {
|
|
103
|
+
constructor(rootNode) {
|
|
104
|
+
this.allScopes = [];
|
|
105
|
+
this.rootScope = {
|
|
106
|
+
type: "global",
|
|
107
|
+
node: rootNode,
|
|
108
|
+
parent: null,
|
|
109
|
+
children: [],
|
|
110
|
+
variables: /* @__PURE__ */ new Map()
|
|
111
|
+
};
|
|
112
|
+
this.currentScope = this.rootScope;
|
|
113
|
+
this.allScopes.push(this.rootScope);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Enter a new scope
|
|
117
|
+
*/
|
|
118
|
+
enterScope(type, node) {
|
|
119
|
+
const newScope = {
|
|
120
|
+
type,
|
|
121
|
+
node,
|
|
122
|
+
parent: this.currentScope,
|
|
123
|
+
children: [],
|
|
124
|
+
variables: /* @__PURE__ */ new Map()
|
|
125
|
+
};
|
|
126
|
+
this.currentScope.children.push(newScope);
|
|
127
|
+
this.currentScope = newScope;
|
|
128
|
+
this.allScopes.push(newScope);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Exit current scope and return to parent
|
|
132
|
+
*/
|
|
133
|
+
exitScope() {
|
|
134
|
+
if (this.currentScope.parent) {
|
|
135
|
+
this.currentScope = this.currentScope.parent;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Declare a variable in the current scope
|
|
140
|
+
*/
|
|
141
|
+
declareVariable(name, node, line, options = {}) {
|
|
142
|
+
const varInfo = {
|
|
143
|
+
name,
|
|
144
|
+
node,
|
|
145
|
+
declarationLine: line,
|
|
146
|
+
references: [],
|
|
147
|
+
type: options.type,
|
|
148
|
+
isParameter: options.isParameter ?? false,
|
|
149
|
+
isDestructured: options.isDestructured ?? false,
|
|
150
|
+
isLoopVariable: options.isLoopVariable ?? false
|
|
151
|
+
};
|
|
152
|
+
this.currentScope.variables.set(name, varInfo);
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Add a reference to a variable
|
|
156
|
+
*/
|
|
157
|
+
addReference(name, node) {
|
|
158
|
+
const varInfo = this.findVariable(name);
|
|
159
|
+
if (varInfo) {
|
|
160
|
+
varInfo.references.push(node);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Find a variable in current or parent scopes
|
|
165
|
+
*/
|
|
166
|
+
findVariable(name) {
|
|
167
|
+
let scope = this.currentScope;
|
|
168
|
+
while (scope) {
|
|
169
|
+
const varInfo = scope.variables.get(name);
|
|
170
|
+
if (varInfo) {
|
|
171
|
+
return varInfo;
|
|
172
|
+
}
|
|
173
|
+
scope = scope.parent;
|
|
174
|
+
}
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Get all variables in current scope (not including parent scopes)
|
|
179
|
+
*/
|
|
180
|
+
getCurrentScopeVariables() {
|
|
181
|
+
return Array.from(this.currentScope.variables.values());
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Get all variables across all scopes
|
|
185
|
+
*/
|
|
186
|
+
getAllVariables() {
|
|
187
|
+
const allVars = [];
|
|
188
|
+
for (const scope of this.allScopes) {
|
|
189
|
+
allVars.push(...Array.from(scope.variables.values()));
|
|
190
|
+
}
|
|
191
|
+
return allVars;
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Calculate actual usage count (references minus declaration)
|
|
195
|
+
*/
|
|
196
|
+
getUsageCount(varInfo) {
|
|
197
|
+
return varInfo.references.length;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Check if a variable is short-lived (used within N lines)
|
|
201
|
+
*/
|
|
202
|
+
isShortLived(varInfo, maxLines = 5) {
|
|
203
|
+
if (varInfo.references.length === 0) {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
const declarationLine = varInfo.declarationLine;
|
|
207
|
+
const maxUsageLine = Math.max(
|
|
208
|
+
...varInfo.references.map((ref) => ref.loc?.start.line ?? declarationLine)
|
|
209
|
+
);
|
|
210
|
+
return maxUsageLine - declarationLine <= maxLines;
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Check if a variable is used in a limited scope (e.g., only in one callback)
|
|
214
|
+
*/
|
|
215
|
+
isLocallyScoped(varInfo) {
|
|
216
|
+
if (varInfo.references.length === 0) return false;
|
|
217
|
+
const lines = varInfo.references.map((ref) => ref.loc?.start.line ?? 0);
|
|
218
|
+
const minLine = Math.min(...lines);
|
|
219
|
+
const maxLine = Math.max(...lines);
|
|
220
|
+
return maxLine - minLine <= 3;
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Get current scope type
|
|
224
|
+
*/
|
|
225
|
+
getCurrentScopeType() {
|
|
226
|
+
return this.currentScope.type;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* Check if currently in a loop scope
|
|
230
|
+
*/
|
|
231
|
+
isInLoop() {
|
|
232
|
+
let scope = this.currentScope;
|
|
233
|
+
while (scope) {
|
|
234
|
+
if (scope.type === "loop") {
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
scope = scope.parent;
|
|
238
|
+
}
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Check if currently in a function scope
|
|
243
|
+
*/
|
|
244
|
+
isInFunction() {
|
|
245
|
+
let scope = this.currentScope;
|
|
246
|
+
while (scope) {
|
|
247
|
+
if (scope.type === "function") {
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
scope = scope.parent;
|
|
251
|
+
}
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Get the root scope
|
|
256
|
+
*/
|
|
257
|
+
getRootScope() {
|
|
258
|
+
return this.rootScope;
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// src/utils/context-detector.ts
|
|
263
|
+
function detectFileType(filePath, ast) {
|
|
264
|
+
const path = filePath.toLowerCase();
|
|
265
|
+
if (path.match(/\.(test|spec)\.(ts|tsx|js|jsx)$/) || path.includes("__tests__")) {
|
|
266
|
+
return "test";
|
|
267
|
+
}
|
|
268
|
+
if (path.endsWith(".d.ts") || path.includes("types")) {
|
|
269
|
+
return "types";
|
|
270
|
+
}
|
|
271
|
+
if (path.match(/config|\.config\.|rc\.|setup/) || path.includes("configuration")) {
|
|
272
|
+
return "config";
|
|
273
|
+
}
|
|
274
|
+
return "production";
|
|
275
|
+
}
|
|
276
|
+
function detectCodeLayer(ast) {
|
|
277
|
+
let hasAPIIndicators = 0;
|
|
278
|
+
let hasBusinessIndicators = 0;
|
|
279
|
+
let hasDataIndicators = 0;
|
|
280
|
+
let hasUtilityIndicators = 0;
|
|
281
|
+
traverseAST(ast, {
|
|
282
|
+
enter: (node) => {
|
|
283
|
+
if (node.type === "ImportDeclaration") {
|
|
284
|
+
const source = node.source.value;
|
|
285
|
+
if (source.match(/express|fastify|koa|@nestjs|axios|fetch|http/i)) {
|
|
286
|
+
hasAPIIndicators++;
|
|
287
|
+
}
|
|
288
|
+
if (source.match(/database|prisma|typeorm|sequelize|mongoose|pg|mysql/i)) {
|
|
289
|
+
hasDataIndicators++;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (node.type === "FunctionDeclaration" && node.id) {
|
|
293
|
+
const name = node.id.name;
|
|
294
|
+
if (name.match(/^(get|post|put|delete|patch|handle|api|route|controller)/i)) {
|
|
295
|
+
hasAPIIndicators++;
|
|
296
|
+
}
|
|
297
|
+
if (name.match(/^(calculate|process|validate|transform|compute|analyze)/i)) {
|
|
298
|
+
hasBusinessIndicators++;
|
|
299
|
+
}
|
|
300
|
+
if (name.match(/^(find|create|update|delete|save|fetch|query|insert)/i)) {
|
|
301
|
+
hasDataIndicators++;
|
|
302
|
+
}
|
|
303
|
+
if (name.match(/^(format|parse|convert|normalize|sanitize|encode|decode)/i)) {
|
|
304
|
+
hasUtilityIndicators++;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (node.type === "ExportNamedDeclaration" || node.type === "ExportDefaultDeclaration") {
|
|
308
|
+
if (node.type === "ExportNamedDeclaration" && node.declaration) {
|
|
309
|
+
if (node.declaration.type === "FunctionDeclaration" && node.declaration.id) {
|
|
310
|
+
const name = node.declaration.id.name;
|
|
311
|
+
if (name.match(/handler|route|api|controller/i)) {
|
|
312
|
+
hasAPIIndicators += 2;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
const scores = {
|
|
320
|
+
api: hasAPIIndicators,
|
|
321
|
+
business: hasBusinessIndicators,
|
|
322
|
+
data: hasDataIndicators,
|
|
323
|
+
utility: hasUtilityIndicators
|
|
324
|
+
};
|
|
325
|
+
const maxScore = Math.max(...Object.values(scores));
|
|
326
|
+
if (maxScore === 0) {
|
|
327
|
+
return "unknown";
|
|
328
|
+
}
|
|
329
|
+
if (scores.api === maxScore) return "api";
|
|
330
|
+
if (scores.data === maxScore) return "data";
|
|
331
|
+
if (scores.business === maxScore) return "business";
|
|
332
|
+
if (scores.utility === maxScore) return "utility";
|
|
333
|
+
return "unknown";
|
|
334
|
+
}
|
|
335
|
+
function calculateComplexity(node) {
|
|
336
|
+
let complexity = 1;
|
|
337
|
+
traverseAST(node, {
|
|
338
|
+
enter: (childNode) => {
|
|
339
|
+
switch (childNode.type) {
|
|
340
|
+
case "IfStatement":
|
|
341
|
+
case "ConditionalExpression":
|
|
342
|
+
// ternary
|
|
343
|
+
case "SwitchCase":
|
|
344
|
+
case "ForStatement":
|
|
345
|
+
case "ForInStatement":
|
|
346
|
+
case "ForOfStatement":
|
|
347
|
+
case "WhileStatement":
|
|
348
|
+
case "DoWhileStatement":
|
|
349
|
+
case "CatchClause":
|
|
350
|
+
complexity++;
|
|
351
|
+
break;
|
|
352
|
+
case "LogicalExpression":
|
|
353
|
+
if (childNode.operator === "&&" || childNode.operator === "||") {
|
|
354
|
+
complexity++;
|
|
355
|
+
}
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
return complexity;
|
|
361
|
+
}
|
|
362
|
+
function buildCodeContext(filePath, ast) {
|
|
363
|
+
const fileType = detectFileType(filePath, ast);
|
|
364
|
+
const codeLayer = detectCodeLayer(ast);
|
|
365
|
+
let totalComplexity = 0;
|
|
366
|
+
let functionCount = 0;
|
|
367
|
+
traverseAST(ast, {
|
|
368
|
+
enter: (node) => {
|
|
369
|
+
if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") {
|
|
370
|
+
totalComplexity += calculateComplexity(node);
|
|
371
|
+
functionCount++;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
const avgComplexity = functionCount > 0 ? totalComplexity / functionCount : 1;
|
|
376
|
+
return {
|
|
377
|
+
fileType,
|
|
378
|
+
codeLayer,
|
|
379
|
+
complexity: Math.round(avgComplexity),
|
|
380
|
+
isTestFile: fileType === "test",
|
|
381
|
+
isTypeDefinition: fileType === "types"
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
function adjustSeverity(baseSeverity, context, issueType) {
|
|
385
|
+
if (context.isTestFile) {
|
|
386
|
+
if (baseSeverity === "minor") return "info";
|
|
387
|
+
if (baseSeverity === "major") return "minor";
|
|
388
|
+
}
|
|
389
|
+
if (context.isTypeDefinition) {
|
|
390
|
+
if (baseSeverity === "minor") return "info";
|
|
391
|
+
}
|
|
392
|
+
if (context.codeLayer === "api") {
|
|
393
|
+
if (baseSeverity === "info" && issueType === "unclear") return "minor";
|
|
394
|
+
if (baseSeverity === "minor" && issueType === "unclear") return "major";
|
|
395
|
+
}
|
|
396
|
+
if (context.complexity > 10) {
|
|
397
|
+
if (baseSeverity === "info") return "minor";
|
|
398
|
+
}
|
|
399
|
+
if (context.codeLayer === "utility") {
|
|
400
|
+
if (baseSeverity === "minor" && issueType === "abbreviation") return "info";
|
|
401
|
+
}
|
|
402
|
+
return baseSeverity;
|
|
403
|
+
}
|
|
404
|
+
function isAcceptableInContext(name, context, options) {
|
|
405
|
+
if (options.isLoopVariable && ["i", "j", "k", "l", "n", "m"].includes(name)) {
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
408
|
+
if (context.isTestFile) {
|
|
409
|
+
if (["a", "b", "c", "x", "y", "z"].includes(name) && options.isParameter) {
|
|
410
|
+
return true;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
if (context.codeLayer === "utility" && ["x", "y", "z"].includes(name)) {
|
|
414
|
+
return true;
|
|
415
|
+
}
|
|
416
|
+
if (options.isDestructured) {
|
|
417
|
+
if (["s", "b", "f", "l"].includes(name)) {
|
|
418
|
+
return true;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
if (options.isParameter && (options.complexity ?? context.complexity) < 3) {
|
|
422
|
+
if (name.length >= 2) {
|
|
423
|
+
return true;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// src/analyzers/naming-ast.ts
|
|
430
|
+
var COMMON_SHORT_WORDS = /* @__PURE__ */ new Set([
|
|
431
|
+
"day",
|
|
432
|
+
"key",
|
|
433
|
+
"net",
|
|
434
|
+
"to",
|
|
435
|
+
"go",
|
|
436
|
+
"for",
|
|
437
|
+
"not",
|
|
438
|
+
"new",
|
|
439
|
+
"old",
|
|
440
|
+
"top",
|
|
441
|
+
"end",
|
|
442
|
+
"run",
|
|
443
|
+
"try",
|
|
444
|
+
"use",
|
|
445
|
+
"get",
|
|
446
|
+
"set",
|
|
447
|
+
"add",
|
|
448
|
+
"put",
|
|
449
|
+
"map",
|
|
450
|
+
"log",
|
|
451
|
+
"row",
|
|
452
|
+
"col",
|
|
453
|
+
"tab",
|
|
454
|
+
"box",
|
|
455
|
+
"div",
|
|
456
|
+
"nav",
|
|
457
|
+
"tag",
|
|
458
|
+
"any",
|
|
459
|
+
"all",
|
|
460
|
+
"one",
|
|
461
|
+
"two",
|
|
462
|
+
"out",
|
|
463
|
+
"off",
|
|
464
|
+
"on",
|
|
465
|
+
"yes",
|
|
466
|
+
"no",
|
|
467
|
+
"now",
|
|
468
|
+
"max",
|
|
469
|
+
"min",
|
|
470
|
+
"sum",
|
|
471
|
+
"avg",
|
|
472
|
+
"ref",
|
|
473
|
+
"src",
|
|
474
|
+
"dst",
|
|
475
|
+
"raw",
|
|
476
|
+
"def",
|
|
477
|
+
"sub",
|
|
478
|
+
"pub",
|
|
479
|
+
"pre",
|
|
480
|
+
"mid",
|
|
481
|
+
"alt",
|
|
482
|
+
"opt",
|
|
483
|
+
"tmp",
|
|
484
|
+
"ext",
|
|
485
|
+
"sep",
|
|
486
|
+
"and",
|
|
487
|
+
"from",
|
|
488
|
+
"how",
|
|
489
|
+
"pad",
|
|
490
|
+
"bar",
|
|
491
|
+
"non",
|
|
492
|
+
"tax",
|
|
493
|
+
"cat",
|
|
494
|
+
"dog",
|
|
495
|
+
"car",
|
|
496
|
+
"bus",
|
|
497
|
+
"web",
|
|
498
|
+
"app",
|
|
499
|
+
"war",
|
|
500
|
+
"law",
|
|
501
|
+
"pay",
|
|
502
|
+
"buy",
|
|
503
|
+
"win",
|
|
504
|
+
"cut",
|
|
505
|
+
"hit",
|
|
506
|
+
"hot",
|
|
507
|
+
"pop",
|
|
508
|
+
"job",
|
|
509
|
+
"age",
|
|
510
|
+
"act",
|
|
511
|
+
"let",
|
|
512
|
+
"lot",
|
|
513
|
+
"bad",
|
|
514
|
+
"big",
|
|
515
|
+
"far",
|
|
516
|
+
"few",
|
|
517
|
+
"own",
|
|
518
|
+
"per",
|
|
519
|
+
"red",
|
|
520
|
+
"low",
|
|
521
|
+
"see",
|
|
522
|
+
"six",
|
|
523
|
+
"ten",
|
|
524
|
+
"way",
|
|
525
|
+
"who",
|
|
526
|
+
"why",
|
|
527
|
+
"yet",
|
|
528
|
+
"via",
|
|
529
|
+
"due",
|
|
530
|
+
"fee",
|
|
531
|
+
"fun",
|
|
532
|
+
"gas",
|
|
533
|
+
"gay",
|
|
534
|
+
"god",
|
|
535
|
+
"gun",
|
|
536
|
+
"guy",
|
|
537
|
+
"ice",
|
|
538
|
+
"ill",
|
|
539
|
+
"kid",
|
|
540
|
+
"mad",
|
|
541
|
+
"man",
|
|
542
|
+
"mix",
|
|
543
|
+
"mom",
|
|
544
|
+
"mrs",
|
|
545
|
+
"nor",
|
|
546
|
+
"odd",
|
|
547
|
+
"oil",
|
|
548
|
+
"pan",
|
|
549
|
+
"pet",
|
|
550
|
+
"pit",
|
|
551
|
+
"pot",
|
|
552
|
+
"pow",
|
|
553
|
+
"pro",
|
|
554
|
+
"raw",
|
|
555
|
+
"rep",
|
|
556
|
+
"rid",
|
|
557
|
+
"sad",
|
|
558
|
+
"sea",
|
|
559
|
+
"sit",
|
|
560
|
+
"sky",
|
|
561
|
+
"son",
|
|
562
|
+
"tea",
|
|
563
|
+
"tie",
|
|
564
|
+
"tip",
|
|
565
|
+
"van",
|
|
566
|
+
"war",
|
|
567
|
+
"win",
|
|
568
|
+
"won"
|
|
569
|
+
]);
|
|
570
|
+
var ACCEPTABLE_ABBREVIATIONS = /* @__PURE__ */ new Set([
|
|
571
|
+
"id",
|
|
572
|
+
"uid",
|
|
573
|
+
"gid",
|
|
574
|
+
"pid",
|
|
575
|
+
"i",
|
|
576
|
+
"j",
|
|
577
|
+
"k",
|
|
578
|
+
"n",
|
|
579
|
+
"m",
|
|
580
|
+
"url",
|
|
581
|
+
"uri",
|
|
582
|
+
"api",
|
|
583
|
+
"cdn",
|
|
584
|
+
"dns",
|
|
585
|
+
"ip",
|
|
586
|
+
"tcp",
|
|
587
|
+
"udp",
|
|
588
|
+
"http",
|
|
589
|
+
"ssl",
|
|
590
|
+
"tls",
|
|
591
|
+
"utm",
|
|
592
|
+
"seo",
|
|
593
|
+
"rss",
|
|
594
|
+
"xhr",
|
|
595
|
+
"ajax",
|
|
596
|
+
"cors",
|
|
597
|
+
"ws",
|
|
598
|
+
"wss",
|
|
599
|
+
"json",
|
|
600
|
+
"xml",
|
|
601
|
+
"yaml",
|
|
602
|
+
"csv",
|
|
603
|
+
"html",
|
|
604
|
+
"css",
|
|
605
|
+
"svg",
|
|
606
|
+
"pdf",
|
|
607
|
+
"img",
|
|
608
|
+
"txt",
|
|
609
|
+
"doc",
|
|
610
|
+
"docx",
|
|
611
|
+
"xlsx",
|
|
612
|
+
"ppt",
|
|
613
|
+
"md",
|
|
614
|
+
"rst",
|
|
615
|
+
"jpg",
|
|
616
|
+
"png",
|
|
617
|
+
"gif",
|
|
618
|
+
"db",
|
|
619
|
+
"sql",
|
|
620
|
+
"orm",
|
|
621
|
+
"dao",
|
|
622
|
+
"dto",
|
|
623
|
+
"ddb",
|
|
624
|
+
"rds",
|
|
625
|
+
"nosql",
|
|
626
|
+
"fs",
|
|
627
|
+
"dir",
|
|
628
|
+
"tmp",
|
|
629
|
+
"src",
|
|
630
|
+
"dst",
|
|
631
|
+
"bin",
|
|
632
|
+
"lib",
|
|
633
|
+
"pkg",
|
|
634
|
+
"os",
|
|
635
|
+
"env",
|
|
636
|
+
"arg",
|
|
637
|
+
"cli",
|
|
638
|
+
"cmd",
|
|
639
|
+
"exe",
|
|
640
|
+
"cwd",
|
|
641
|
+
"pwd",
|
|
642
|
+
"ui",
|
|
643
|
+
"ux",
|
|
644
|
+
"gui",
|
|
645
|
+
"dom",
|
|
646
|
+
"ref",
|
|
647
|
+
"req",
|
|
648
|
+
"res",
|
|
649
|
+
"ctx",
|
|
650
|
+
"err",
|
|
651
|
+
"msg",
|
|
652
|
+
"auth",
|
|
653
|
+
"max",
|
|
654
|
+
"min",
|
|
655
|
+
"avg",
|
|
656
|
+
"sum",
|
|
657
|
+
"abs",
|
|
658
|
+
"cos",
|
|
659
|
+
"sin",
|
|
660
|
+
"tan",
|
|
661
|
+
"log",
|
|
662
|
+
"exp",
|
|
663
|
+
"pow",
|
|
664
|
+
"sqrt",
|
|
665
|
+
"std",
|
|
666
|
+
"var",
|
|
667
|
+
"int",
|
|
668
|
+
"num",
|
|
669
|
+
"idx",
|
|
670
|
+
"now",
|
|
671
|
+
"utc",
|
|
672
|
+
"tz",
|
|
673
|
+
"ms",
|
|
674
|
+
"sec",
|
|
675
|
+
"hr",
|
|
676
|
+
"min",
|
|
677
|
+
"yr",
|
|
678
|
+
"mo",
|
|
679
|
+
"app",
|
|
680
|
+
"cfg",
|
|
681
|
+
"config",
|
|
682
|
+
"init",
|
|
683
|
+
"len",
|
|
684
|
+
"val",
|
|
685
|
+
"str",
|
|
686
|
+
"obj",
|
|
687
|
+
"arr",
|
|
688
|
+
"gen",
|
|
689
|
+
"def",
|
|
690
|
+
"raw",
|
|
691
|
+
"new",
|
|
692
|
+
"old",
|
|
693
|
+
"pre",
|
|
694
|
+
"post",
|
|
695
|
+
"sub",
|
|
696
|
+
"pub",
|
|
697
|
+
"ts",
|
|
698
|
+
"js",
|
|
699
|
+
"jsx",
|
|
700
|
+
"tsx",
|
|
701
|
+
"py",
|
|
702
|
+
"rb",
|
|
703
|
+
"vue",
|
|
704
|
+
"re",
|
|
705
|
+
"fn",
|
|
706
|
+
"fns",
|
|
707
|
+
"mod",
|
|
708
|
+
"opts",
|
|
709
|
+
"dev",
|
|
710
|
+
"s3",
|
|
711
|
+
"ec2",
|
|
712
|
+
"sqs",
|
|
713
|
+
"sns",
|
|
714
|
+
"vpc",
|
|
715
|
+
"ami",
|
|
716
|
+
"iam",
|
|
717
|
+
"acl",
|
|
718
|
+
"elb",
|
|
719
|
+
"alb",
|
|
720
|
+
"nlb",
|
|
721
|
+
"aws",
|
|
722
|
+
"ses",
|
|
723
|
+
"gst",
|
|
724
|
+
"cdk",
|
|
725
|
+
"btn",
|
|
726
|
+
"buf",
|
|
727
|
+
"agg",
|
|
728
|
+
"ocr",
|
|
729
|
+
"ai",
|
|
730
|
+
"cf",
|
|
731
|
+
"cfn",
|
|
732
|
+
"ga",
|
|
733
|
+
"fcp",
|
|
734
|
+
"lcp",
|
|
735
|
+
"cls",
|
|
736
|
+
"ttfb",
|
|
737
|
+
"tti",
|
|
738
|
+
"fid",
|
|
739
|
+
"fps",
|
|
740
|
+
"qps",
|
|
741
|
+
"rps",
|
|
742
|
+
"tps",
|
|
743
|
+
"wpm",
|
|
744
|
+
"po",
|
|
745
|
+
"e2e",
|
|
746
|
+
"a11y",
|
|
747
|
+
"i18n",
|
|
748
|
+
"l10n",
|
|
749
|
+
"spy",
|
|
750
|
+
"sk",
|
|
751
|
+
"fy",
|
|
752
|
+
"faq",
|
|
753
|
+
"og",
|
|
754
|
+
"seo",
|
|
755
|
+
"cta",
|
|
756
|
+
"roi",
|
|
757
|
+
"kpi",
|
|
758
|
+
"ttl",
|
|
759
|
+
"pct",
|
|
760
|
+
"mac",
|
|
761
|
+
"hex",
|
|
762
|
+
"esm",
|
|
763
|
+
"git",
|
|
764
|
+
"rec",
|
|
765
|
+
"loc",
|
|
766
|
+
"dup",
|
|
767
|
+
"is",
|
|
768
|
+
"has",
|
|
769
|
+
"can",
|
|
770
|
+
"did",
|
|
771
|
+
"was",
|
|
772
|
+
"are",
|
|
773
|
+
"d",
|
|
774
|
+
"t",
|
|
775
|
+
"dt",
|
|
776
|
+
"s",
|
|
777
|
+
"b",
|
|
778
|
+
"f",
|
|
779
|
+
"l",
|
|
780
|
+
// Coverage metrics
|
|
781
|
+
"vid",
|
|
782
|
+
"pic",
|
|
783
|
+
"img",
|
|
784
|
+
"doc",
|
|
785
|
+
"msg"
|
|
786
|
+
]);
|
|
787
|
+
async function analyzeNamingAST(files) {
|
|
788
|
+
const issues = [];
|
|
789
|
+
const rootDir = files.length > 0 ? dirname(files[0]) : process.cwd();
|
|
790
|
+
const config = loadConfig(rootDir);
|
|
791
|
+
const consistencyConfig = config?.tools?.["consistency"];
|
|
792
|
+
const customAbbreviations = new Set(consistencyConfig?.acceptedAbbreviations || []);
|
|
793
|
+
const customShortWords = new Set(consistencyConfig?.shortWords || []);
|
|
794
|
+
const disabledChecks = new Set(consistencyConfig?.disableChecks || []);
|
|
795
|
+
const allAbbreviations = /* @__PURE__ */ new Set([...ACCEPTABLE_ABBREVIATIONS, ...customAbbreviations]);
|
|
796
|
+
const allShortWords = /* @__PURE__ */ new Set([...COMMON_SHORT_WORDS, ...customShortWords]);
|
|
797
|
+
for (const file of files) {
|
|
798
|
+
try {
|
|
799
|
+
const ast = parseFile(file);
|
|
800
|
+
if (!ast) continue;
|
|
801
|
+
const fileIssues = analyzeFileNamingAST(
|
|
802
|
+
file,
|
|
803
|
+
ast,
|
|
804
|
+
allAbbreviations,
|
|
805
|
+
allShortWords,
|
|
806
|
+
disabledChecks
|
|
807
|
+
);
|
|
808
|
+
issues.push(...fileIssues);
|
|
809
|
+
} catch (error) {
|
|
810
|
+
console.warn(`Skipping ${file} due to parse error:`, error);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
return issues;
|
|
814
|
+
}
|
|
815
|
+
function analyzeFileNamingAST(file, ast, allAbbreviations, allShortWords, disabledChecks) {
|
|
816
|
+
const issues = [];
|
|
817
|
+
const scopeTracker = new ScopeTracker(ast);
|
|
818
|
+
const context = buildCodeContext(file, ast);
|
|
819
|
+
const ancestors = [];
|
|
820
|
+
traverseAST(ast, {
|
|
821
|
+
enter: (node, parent) => {
|
|
822
|
+
ancestors.push(node);
|
|
823
|
+
if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") {
|
|
824
|
+
scopeTracker.enterScope("function", node);
|
|
825
|
+
if ("params" in node) {
|
|
826
|
+
for (const param of node.params) {
|
|
827
|
+
if (param.type === "Identifier") {
|
|
828
|
+
scopeTracker.declareVariable(param.name, param, getLineNumber(param), {
|
|
829
|
+
isParameter: true
|
|
830
|
+
});
|
|
831
|
+
} else if (param.type === "ObjectPattern" || param.type === "ArrayPattern") {
|
|
832
|
+
extractIdentifiersFromPattern(param, scopeTracker, true);
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
} else if (node.type === "BlockStatement") {
|
|
837
|
+
scopeTracker.enterScope("block", node);
|
|
838
|
+
} else if (isLoopStatement(node)) {
|
|
839
|
+
scopeTracker.enterScope("loop", node);
|
|
840
|
+
} else if (node.type === "ClassDeclaration") {
|
|
841
|
+
scopeTracker.enterScope("class", node);
|
|
842
|
+
}
|
|
843
|
+
if (node.type === "VariableDeclarator") {
|
|
844
|
+
if (node.id.type === "Identifier") {
|
|
845
|
+
const isInCoverage = isCoverageContext(node, ancestors);
|
|
846
|
+
scopeTracker.declareVariable(
|
|
847
|
+
node.id.name,
|
|
848
|
+
node.id,
|
|
849
|
+
getLineNumber(node.id),
|
|
850
|
+
{
|
|
851
|
+
type: "typeAnnotation" in node.id ? node.id.typeAnnotation : null,
|
|
852
|
+
isDestructured: false,
|
|
853
|
+
isLoopVariable: scopeTracker.getCurrentScopeType() === "loop"
|
|
854
|
+
}
|
|
855
|
+
);
|
|
856
|
+
} else if (node.id.type === "ObjectPattern" || node.id.type === "ArrayPattern") {
|
|
857
|
+
extractIdentifiersFromPattern(node.id, scopeTracker, false, ancestors);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
if (node.type === "Identifier" && parent) {
|
|
861
|
+
if (parent.type !== "VariableDeclarator" || parent.id !== node) {
|
|
862
|
+
if (parent.type !== "FunctionDeclaration" || parent.id !== node) {
|
|
863
|
+
scopeTracker.addReference(node.name, node);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
},
|
|
868
|
+
leave: (node) => {
|
|
869
|
+
ancestors.pop();
|
|
870
|
+
if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression" || node.type === "BlockStatement" || isLoopStatement(node) || node.type === "ClassDeclaration") {
|
|
871
|
+
scopeTracker.exitScope();
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
});
|
|
875
|
+
const allVariables = scopeTracker.getAllVariables();
|
|
876
|
+
for (const varInfo of allVariables) {
|
|
877
|
+
const name = varInfo.name;
|
|
878
|
+
const line = varInfo.declarationLine;
|
|
879
|
+
if (disabledChecks.has("single-letter") && name.length === 1) continue;
|
|
880
|
+
if (disabledChecks.has("abbreviation") && name.length <= 3) continue;
|
|
881
|
+
const isInCoverage = ["s", "b", "f", "l"].includes(name) && varInfo.isDestructured;
|
|
882
|
+
if (isInCoverage) continue;
|
|
883
|
+
const functionComplexity = varInfo.node.type === "Identifier" && "parent" in varInfo.node ? calculateComplexity(varInfo.node) : context.complexity;
|
|
884
|
+
if (isAcceptableInContext(name, context, {
|
|
885
|
+
isLoopVariable: varInfo.isLoopVariable || allAbbreviations.has(name),
|
|
886
|
+
isParameter: varInfo.isParameter,
|
|
887
|
+
isDestructured: varInfo.isDestructured,
|
|
888
|
+
complexity: functionComplexity
|
|
889
|
+
})) {
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
if (name.length === 1 && !allAbbreviations.has(name) && !allShortWords.has(name)) {
|
|
893
|
+
const isShortLived = scopeTracker.isShortLived(varInfo, 5);
|
|
894
|
+
if (!isShortLived) {
|
|
895
|
+
issues.push({
|
|
896
|
+
file,
|
|
897
|
+
line,
|
|
898
|
+
type: "poor-naming",
|
|
899
|
+
identifier: name,
|
|
900
|
+
severity: adjustSeverity("minor", context, "poor-naming"),
|
|
901
|
+
suggestion: `Use descriptive variable name instead of single letter '${name}'`
|
|
902
|
+
});
|
|
903
|
+
}
|
|
904
|
+
continue;
|
|
905
|
+
}
|
|
906
|
+
if (name.length >= 2 && name.length <= 3) {
|
|
907
|
+
if (!allShortWords.has(name.toLowerCase()) && !allAbbreviations.has(name.toLowerCase())) {
|
|
908
|
+
const isShortLived = scopeTracker.isShortLived(varInfo, 5);
|
|
909
|
+
if (!isShortLived) {
|
|
910
|
+
issues.push({
|
|
911
|
+
file,
|
|
912
|
+
line,
|
|
913
|
+
type: "abbreviation",
|
|
914
|
+
identifier: name,
|
|
915
|
+
severity: adjustSeverity("info", context, "abbreviation"),
|
|
916
|
+
suggestion: `Consider using full word instead of abbreviation '${name}'`
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
continue;
|
|
921
|
+
}
|
|
922
|
+
if (!disabledChecks.has("convention-mix") && file.match(/\.(ts|tsx|js|jsx)$/)) {
|
|
923
|
+
if (name.includes("_") && !name.startsWith("_") && name.toLowerCase() === name) {
|
|
924
|
+
const camelCase = name.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
925
|
+
issues.push({
|
|
926
|
+
file,
|
|
927
|
+
line,
|
|
928
|
+
type: "convention-mix",
|
|
929
|
+
identifier: name,
|
|
930
|
+
severity: adjustSeverity("minor", context, "convention-mix"),
|
|
931
|
+
suggestion: `Use camelCase '${camelCase}' instead of snake_case in TypeScript/JavaScript`
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}
|
|
936
|
+
if (!disabledChecks.has("unclear")) {
|
|
937
|
+
traverseAST(ast, {
|
|
938
|
+
enter: (node) => {
|
|
939
|
+
if (node.type === "FunctionDeclaration" || node.type === "MethodDefinition") {
|
|
940
|
+
const name = getFunctionName(node);
|
|
941
|
+
if (!name) return;
|
|
942
|
+
const line = getLineNumber(node);
|
|
943
|
+
if (["main", "init", "setup", "bootstrap"].includes(name)) return;
|
|
944
|
+
const hasActionVerb = name.match(/^(get|set|is|has|can|should|create|update|delete|fetch|load|save|process|handle|validate|check|find|search|filter|map|reduce|make|do|run|start|stop|build|parse|format|render|calculate|compute|generate|transform|convert|normalize|sanitize|encode|decode|compress|extract|merge|split|join|sort|compare|test|verify|ensure|apply|execute|invoke|call|emit|dispatch|trigger|listen|subscribe|unsubscribe|add|remove|clear|reset|toggle|enable|disable|open|close|connect|disconnect|send|receive|read|write|import|export|register|unregister|mount|unmount|track|store|persist|upsert|derive|classify|combine|discover|activate|require|assert|expect|mask|escape|sign|put|list|complete|page|safe|mock|pick|pluralize|text)/);
|
|
945
|
+
const isFactoryPattern = name.match(/(Factory|Builder|Creator|Generator|Provider|Adapter|Mock)$/);
|
|
946
|
+
const isEventHandler = name.match(/^on[A-Z]/);
|
|
947
|
+
const isDescriptiveLong = name.length > 15;
|
|
948
|
+
const isReactHook = name.match(/^use[A-Z]/);
|
|
949
|
+
const isHelperPattern = name.match(/^(to|from|with|without|for|as|into)\w+/);
|
|
950
|
+
const isUtilityName = ["cn", "proxy", "sitemap", "robots", "gtag"].includes(name);
|
|
951
|
+
if (!hasActionVerb && !isFactoryPattern && !isEventHandler && !isDescriptiveLong && !isReactHook && !isHelperPattern && !isUtilityName) {
|
|
952
|
+
issues.push({
|
|
953
|
+
file,
|
|
954
|
+
line,
|
|
955
|
+
type: "unclear",
|
|
956
|
+
identifier: name,
|
|
957
|
+
severity: adjustSeverity("info", context, "unclear"),
|
|
958
|
+
suggestion: `Function '${name}' should start with an action verb (get, set, create, etc.)`
|
|
959
|
+
});
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
return issues;
|
|
966
|
+
}
|
|
967
|
+
function extractIdentifiersFromPattern(pattern, scopeTracker, isParameter, ancestors) {
|
|
968
|
+
if (pattern.type === "ObjectPattern") {
|
|
969
|
+
for (const prop of pattern.properties) {
|
|
970
|
+
if (prop.type === "Property" && prop.value.type === "Identifier") {
|
|
971
|
+
scopeTracker.declareVariable(
|
|
972
|
+
prop.value.name,
|
|
973
|
+
prop.value,
|
|
974
|
+
getLineNumber(prop.value),
|
|
975
|
+
{
|
|
976
|
+
isParameter,
|
|
977
|
+
isDestructured: true
|
|
978
|
+
}
|
|
979
|
+
);
|
|
980
|
+
} else if (prop.type === "RestElement" && prop.argument.type === "Identifier") {
|
|
981
|
+
scopeTracker.declareVariable(
|
|
982
|
+
prop.argument.name,
|
|
983
|
+
prop.argument,
|
|
984
|
+
getLineNumber(prop.argument),
|
|
985
|
+
{
|
|
986
|
+
isParameter,
|
|
987
|
+
isDestructured: true
|
|
988
|
+
}
|
|
989
|
+
);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
} else if (pattern.type === "ArrayPattern") {
|
|
993
|
+
for (const element of pattern.elements) {
|
|
994
|
+
if (element && element.type === "Identifier") {
|
|
995
|
+
scopeTracker.declareVariable(
|
|
996
|
+
element.name,
|
|
997
|
+
element,
|
|
998
|
+
getLineNumber(element),
|
|
999
|
+
{
|
|
1000
|
+
isParameter,
|
|
1001
|
+
isDestructured: true
|
|
1002
|
+
}
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
// src/analyzers/patterns.ts
|
|
1010
|
+
import { readFileContent } from "@aiready/core";
|
|
1011
|
+
async function analyzePatterns(files) {
|
|
1012
|
+
const issues = [];
|
|
1013
|
+
const errorHandlingIssues = await analyzeErrorHandling(files);
|
|
1014
|
+
issues.push(...errorHandlingIssues);
|
|
1015
|
+
const asyncIssues = await analyzeAsyncPatterns(files);
|
|
1016
|
+
issues.push(...asyncIssues);
|
|
1017
|
+
const importIssues = await analyzeImportStyles(files);
|
|
1018
|
+
issues.push(...importIssues);
|
|
1019
|
+
return issues;
|
|
1020
|
+
}
|
|
1021
|
+
async function analyzeErrorHandling(files) {
|
|
1022
|
+
const patterns = {
|
|
1023
|
+
tryCatch: [],
|
|
1024
|
+
throwsError: [],
|
|
1025
|
+
returnsNull: [],
|
|
1026
|
+
returnsError: []
|
|
1027
|
+
};
|
|
1028
|
+
for (const file of files) {
|
|
1029
|
+
const content = await readFileContent(file);
|
|
1030
|
+
if (content.includes("try {") || content.includes("} catch")) {
|
|
1031
|
+
patterns.tryCatch.push(file);
|
|
1032
|
+
}
|
|
1033
|
+
if (content.match(/throw new \w*Error/)) {
|
|
1034
|
+
patterns.throwsError.push(file);
|
|
1035
|
+
}
|
|
1036
|
+
if (content.match(/return null/)) {
|
|
1037
|
+
patterns.returnsNull.push(file);
|
|
1038
|
+
}
|
|
1039
|
+
if (content.match(/return \{ error:/)) {
|
|
1040
|
+
patterns.returnsError.push(file);
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
const issues = [];
|
|
1044
|
+
const strategiesUsed = Object.values(patterns).filter((p) => p.length > 0).length;
|
|
1045
|
+
if (strategiesUsed > 2) {
|
|
1046
|
+
issues.push({
|
|
1047
|
+
files: [.../* @__PURE__ */ new Set([
|
|
1048
|
+
...patterns.tryCatch,
|
|
1049
|
+
...patterns.throwsError,
|
|
1050
|
+
...patterns.returnsNull,
|
|
1051
|
+
...patterns.returnsError
|
|
1052
|
+
])],
|
|
1053
|
+
type: "error-handling",
|
|
1054
|
+
description: "Inconsistent error handling strategies across codebase",
|
|
1055
|
+
examples: [
|
|
1056
|
+
patterns.tryCatch.length > 0 ? `Try-catch used in ${patterns.tryCatch.length} files` : "",
|
|
1057
|
+
patterns.throwsError.length > 0 ? `Throws errors in ${patterns.throwsError.length} files` : "",
|
|
1058
|
+
patterns.returnsNull.length > 0 ? `Returns null in ${patterns.returnsNull.length} files` : "",
|
|
1059
|
+
patterns.returnsError.length > 0 ? `Returns error objects in ${patterns.returnsError.length} files` : ""
|
|
1060
|
+
].filter((e) => e),
|
|
1061
|
+
severity: "major"
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
return issues;
|
|
1065
|
+
}
|
|
1066
|
+
async function analyzeAsyncPatterns(files) {
|
|
1067
|
+
const patterns = {
|
|
1068
|
+
asyncAwait: [],
|
|
1069
|
+
promises: [],
|
|
1070
|
+
callbacks: []
|
|
1071
|
+
};
|
|
1072
|
+
for (const file of files) {
|
|
1073
|
+
const content = await readFileContent(file);
|
|
1074
|
+
if (content.match(/async\s+(function|\(|[a-zA-Z])/)) {
|
|
1075
|
+
patterns.asyncAwait.push(file);
|
|
1076
|
+
}
|
|
1077
|
+
if (content.match(/\.then\(/) || content.match(/\.catch\(/)) {
|
|
1078
|
+
patterns.promises.push(file);
|
|
1079
|
+
}
|
|
1080
|
+
if (content.match(/callback\s*\(/) || content.match(/\(\s*err\s*,/)) {
|
|
1081
|
+
patterns.callbacks.push(file);
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
const issues = [];
|
|
1085
|
+
if (patterns.callbacks.length > 0 && patterns.asyncAwait.length > 0) {
|
|
1086
|
+
issues.push({
|
|
1087
|
+
files: [...patterns.callbacks, ...patterns.asyncAwait],
|
|
1088
|
+
type: "async-style",
|
|
1089
|
+
description: "Mixed async patterns: callbacks and async/await",
|
|
1090
|
+
examples: [
|
|
1091
|
+
`Callbacks found in: ${patterns.callbacks.slice(0, 3).join(", ")}`,
|
|
1092
|
+
`Async/await used in: ${patterns.asyncAwait.slice(0, 3).join(", ")}`
|
|
1093
|
+
],
|
|
1094
|
+
severity: "minor"
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
if (patterns.promises.length > patterns.asyncAwait.length * 0.3 && patterns.asyncAwait.length > 0) {
|
|
1098
|
+
issues.push({
|
|
1099
|
+
files: patterns.promises,
|
|
1100
|
+
type: "async-style",
|
|
1101
|
+
description: "Consider using async/await instead of promise chains for consistency",
|
|
1102
|
+
examples: patterns.promises.slice(0, 5),
|
|
1103
|
+
severity: "info"
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
return issues;
|
|
1107
|
+
}
|
|
1108
|
+
async function analyzeImportStyles(files) {
|
|
1109
|
+
const patterns = {
|
|
1110
|
+
esModules: [],
|
|
1111
|
+
commonJS: [],
|
|
1112
|
+
mixed: []
|
|
1113
|
+
};
|
|
1114
|
+
for (const file of files) {
|
|
1115
|
+
const content = await readFileContent(file);
|
|
1116
|
+
const hasESM = content.match(/^import\s+/m);
|
|
1117
|
+
const hasCJS = content.match(/require\s*\(/);
|
|
1118
|
+
if (hasESM && hasCJS) {
|
|
1119
|
+
patterns.mixed.push(file);
|
|
1120
|
+
} else if (hasESM) {
|
|
1121
|
+
patterns.esModules.push(file);
|
|
1122
|
+
} else if (hasCJS) {
|
|
1123
|
+
patterns.commonJS.push(file);
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
const issues = [];
|
|
1127
|
+
if (patterns.mixed.length > 0) {
|
|
1128
|
+
issues.push({
|
|
1129
|
+
files: patterns.mixed,
|
|
1130
|
+
type: "import-style",
|
|
1131
|
+
description: "Mixed ES modules and CommonJS imports in same files",
|
|
1132
|
+
examples: patterns.mixed.slice(0, 5),
|
|
1133
|
+
severity: "major"
|
|
1134
|
+
});
|
|
1135
|
+
}
|
|
1136
|
+
if (patterns.esModules.length > 0 && patterns.commonJS.length > 0) {
|
|
1137
|
+
const ratio = patterns.commonJS.length / (patterns.esModules.length + patterns.commonJS.length);
|
|
1138
|
+
if (ratio > 0.2 && ratio < 0.8) {
|
|
1139
|
+
issues.push({
|
|
1140
|
+
files: [...patterns.esModules, ...patterns.commonJS],
|
|
1141
|
+
type: "import-style",
|
|
1142
|
+
description: "Inconsistent import styles across project",
|
|
1143
|
+
examples: [
|
|
1144
|
+
`ES modules: ${patterns.esModules.length} files`,
|
|
1145
|
+
`CommonJS: ${patterns.commonJS.length} files`
|
|
1146
|
+
],
|
|
1147
|
+
severity: "minor"
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
return issues;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// src/analyzer.ts
|
|
1155
|
+
import { scanFiles } from "@aiready/core";
|
|
1156
|
+
async function analyzeConsistency(options) {
|
|
1157
|
+
const {
|
|
1158
|
+
checkNaming = true,
|
|
1159
|
+
checkPatterns = true,
|
|
1160
|
+
checkArchitecture = false,
|
|
1161
|
+
// Not implemented yet
|
|
1162
|
+
minSeverity = "info",
|
|
1163
|
+
...scanOptions
|
|
1164
|
+
} = options;
|
|
1165
|
+
const filePaths = await scanFiles(scanOptions);
|
|
1166
|
+
const namingIssues = checkNaming ? await analyzeNamingAST(filePaths) : [];
|
|
1167
|
+
const patternIssues = checkPatterns ? await analyzePatterns(filePaths) : [];
|
|
1168
|
+
const results = [];
|
|
1169
|
+
const fileIssuesMap = /* @__PURE__ */ new Map();
|
|
1170
|
+
for (const issue of namingIssues) {
|
|
1171
|
+
if (!shouldIncludeSeverity(issue.severity, minSeverity)) {
|
|
1172
|
+
continue;
|
|
1173
|
+
}
|
|
1174
|
+
const consistencyIssue = {
|
|
1175
|
+
type: issue.type === "convention-mix" ? "naming-inconsistency" : "naming-quality",
|
|
1176
|
+
category: "naming",
|
|
1177
|
+
severity: issue.severity,
|
|
1178
|
+
message: `${issue.type}: ${issue.identifier}`,
|
|
1179
|
+
location: {
|
|
1180
|
+
file: issue.file,
|
|
1181
|
+
line: issue.line,
|
|
1182
|
+
column: issue.column
|
|
1183
|
+
},
|
|
1184
|
+
suggestion: issue.suggestion
|
|
1185
|
+
};
|
|
1186
|
+
if (!fileIssuesMap.has(issue.file)) {
|
|
1187
|
+
fileIssuesMap.set(issue.file, []);
|
|
1188
|
+
}
|
|
1189
|
+
fileIssuesMap.get(issue.file).push(consistencyIssue);
|
|
1190
|
+
}
|
|
1191
|
+
for (const issue of patternIssues) {
|
|
1192
|
+
if (!shouldIncludeSeverity(issue.severity, minSeverity)) {
|
|
1193
|
+
continue;
|
|
1194
|
+
}
|
|
1195
|
+
const consistencyIssue = {
|
|
1196
|
+
type: "pattern-inconsistency",
|
|
1197
|
+
category: "patterns",
|
|
1198
|
+
severity: issue.severity,
|
|
1199
|
+
message: issue.description,
|
|
1200
|
+
location: {
|
|
1201
|
+
file: issue.files[0] || "multiple files",
|
|
1202
|
+
line: 1
|
|
1203
|
+
},
|
|
1204
|
+
examples: issue.examples,
|
|
1205
|
+
suggestion: `Standardize ${issue.type} patterns across ${issue.files.length} files`
|
|
1206
|
+
};
|
|
1207
|
+
const firstFile = issue.files[0];
|
|
1208
|
+
if (firstFile && !fileIssuesMap.has(firstFile)) {
|
|
1209
|
+
fileIssuesMap.set(firstFile, []);
|
|
1210
|
+
}
|
|
1211
|
+
if (firstFile) {
|
|
1212
|
+
fileIssuesMap.get(firstFile).push(consistencyIssue);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
for (const [fileName, issues] of fileIssuesMap) {
|
|
1216
|
+
results.push({
|
|
1217
|
+
fileName,
|
|
1218
|
+
issues,
|
|
1219
|
+
metrics: {
|
|
1220
|
+
consistencyScore: calculateConsistencyScore(issues)
|
|
1221
|
+
}
|
|
1222
|
+
});
|
|
1223
|
+
}
|
|
1224
|
+
const recommendations = generateRecommendations(namingIssues, patternIssues);
|
|
1225
|
+
return {
|
|
1226
|
+
summary: {
|
|
1227
|
+
totalIssues: namingIssues.length + patternIssues.length,
|
|
1228
|
+
namingIssues: namingIssues.length,
|
|
1229
|
+
patternIssues: patternIssues.length,
|
|
1230
|
+
architectureIssues: 0,
|
|
1231
|
+
filesAnalyzed: filePaths.length
|
|
1232
|
+
},
|
|
1233
|
+
results,
|
|
1234
|
+
recommendations
|
|
1235
|
+
};
|
|
1236
|
+
}
|
|
1237
|
+
function shouldIncludeSeverity(severity, minSeverity) {
|
|
1238
|
+
const severityLevels = { info: 0, minor: 1, major: 2, critical: 3 };
|
|
1239
|
+
return severityLevels[severity] >= severityLevels[minSeverity];
|
|
1240
|
+
}
|
|
1241
|
+
function calculateConsistencyScore(issues) {
|
|
1242
|
+
const weights = { critical: 10, major: 5, minor: 2, info: 1 };
|
|
1243
|
+
const totalWeight = issues.reduce((sum, issue) => sum + weights[issue.severity], 0);
|
|
1244
|
+
return Math.max(0, 1 - totalWeight / 100);
|
|
1245
|
+
}
|
|
1246
|
+
function generateRecommendations(namingIssues, patternIssues) {
|
|
1247
|
+
const recommendations = [];
|
|
1248
|
+
if (namingIssues.length > 0) {
|
|
1249
|
+
const conventionMixCount = namingIssues.filter((i) => i.type === "convention-mix").length;
|
|
1250
|
+
if (conventionMixCount > 0) {
|
|
1251
|
+
recommendations.push(
|
|
1252
|
+
`Standardize naming conventions: Found ${conventionMixCount} snake_case variables in TypeScript/JavaScript (use camelCase)`
|
|
1253
|
+
);
|
|
1254
|
+
}
|
|
1255
|
+
const poorNamingCount = namingIssues.filter((i) => i.type === "poor-naming").length;
|
|
1256
|
+
if (poorNamingCount > 0) {
|
|
1257
|
+
recommendations.push(
|
|
1258
|
+
`Improve variable naming: Found ${poorNamingCount} single-letter or unclear variable names`
|
|
1259
|
+
);
|
|
1260
|
+
}
|
|
1261
|
+
}
|
|
1262
|
+
if (patternIssues.length > 0) {
|
|
1263
|
+
const errorHandlingIssues = patternIssues.filter((i) => i.type === "error-handling");
|
|
1264
|
+
if (errorHandlingIssues.length > 0) {
|
|
1265
|
+
recommendations.push(
|
|
1266
|
+
"Standardize error handling strategy across the codebase (prefer try-catch with typed errors)"
|
|
1267
|
+
);
|
|
1268
|
+
}
|
|
1269
|
+
const asyncIssues = patternIssues.filter((i) => i.type === "async-style");
|
|
1270
|
+
if (asyncIssues.length > 0) {
|
|
1271
|
+
recommendations.push(
|
|
1272
|
+
"Use async/await consistently instead of mixing with promise chains or callbacks"
|
|
1273
|
+
);
|
|
1274
|
+
}
|
|
1275
|
+
const importIssues = patternIssues.filter((i) => i.type === "import-style");
|
|
1276
|
+
if (importIssues.length > 0) {
|
|
1277
|
+
recommendations.push(
|
|
1278
|
+
"Use ES modules consistently across the project (avoid mixing with CommonJS)"
|
|
1279
|
+
);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
if (recommendations.length === 0) {
|
|
1283
|
+
recommendations.push("No major consistency issues found! Your codebase follows good practices.");
|
|
1284
|
+
}
|
|
1285
|
+
return recommendations;
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
export {
|
|
1289
|
+
analyzeNamingAST,
|
|
1290
|
+
analyzePatterns,
|
|
1291
|
+
analyzeConsistency
|
|
1292
|
+
};
|