@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
package/dist/index.js
CHANGED
|
@@ -22,6 +22,7 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
analyzeConsistency: () => analyzeConsistency,
|
|
24
24
|
analyzeNaming: () => analyzeNaming,
|
|
25
|
+
analyzeNamingAST: () => analyzeNamingAST,
|
|
25
26
|
analyzePatterns: () => analyzePatterns,
|
|
26
27
|
detectNamingConventions: () => detectNamingConventions
|
|
27
28
|
});
|
|
@@ -30,11 +31,436 @@ module.exports = __toCommonJS(index_exports);
|
|
|
30
31
|
// src/analyzer.ts
|
|
31
32
|
var import_core3 = require("@aiready/core");
|
|
32
33
|
|
|
33
|
-
// src/analyzers/naming.ts
|
|
34
|
+
// src/analyzers/naming-ast.ts
|
|
34
35
|
var import_core = require("@aiready/core");
|
|
35
36
|
var import_path = require("path");
|
|
37
|
+
|
|
38
|
+
// src/utils/ast-parser.ts
|
|
39
|
+
var import_typescript_estree = require("@typescript-eslint/typescript-estree");
|
|
40
|
+
var import_fs = require("fs");
|
|
41
|
+
function parseFile(filePath, content) {
|
|
42
|
+
try {
|
|
43
|
+
const code = content ?? (0, import_fs.readFileSync)(filePath, "utf-8");
|
|
44
|
+
const isTypeScript = filePath.match(/\.tsx?$/);
|
|
45
|
+
return (0, import_typescript_estree.parse)(code, {
|
|
46
|
+
jsx: filePath.match(/\.[jt]sx$/i) !== null,
|
|
47
|
+
loc: true,
|
|
48
|
+
range: true,
|
|
49
|
+
comment: false,
|
|
50
|
+
tokens: false,
|
|
51
|
+
// Relaxed parsing for JavaScript files
|
|
52
|
+
sourceType: "module",
|
|
53
|
+
ecmaVersion: "latest",
|
|
54
|
+
// Only use TypeScript parser features for .ts/.tsx files
|
|
55
|
+
filePath: isTypeScript ? filePath : void 0
|
|
56
|
+
});
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.warn(`Failed to parse ${filePath}:`, error instanceof Error ? error.message : error);
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function traverseAST(node, visitor, parent = null) {
|
|
63
|
+
if (!node) return;
|
|
64
|
+
visitor.enter?.(node, parent);
|
|
65
|
+
for (const key of Object.keys(node)) {
|
|
66
|
+
const value = node[key];
|
|
67
|
+
if (Array.isArray(value)) {
|
|
68
|
+
for (const child of value) {
|
|
69
|
+
if (child && typeof child === "object" && "type" in child) {
|
|
70
|
+
traverseAST(child, visitor, node);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
} else if (value && typeof value === "object" && "type" in value) {
|
|
74
|
+
traverseAST(value, visitor, node);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
visitor.leave?.(node, parent);
|
|
78
|
+
}
|
|
79
|
+
function isLoopStatement(node) {
|
|
80
|
+
return [
|
|
81
|
+
"ForStatement",
|
|
82
|
+
"ForInStatement",
|
|
83
|
+
"ForOfStatement",
|
|
84
|
+
"WhileStatement",
|
|
85
|
+
"DoWhileStatement"
|
|
86
|
+
].includes(node.type);
|
|
87
|
+
}
|
|
88
|
+
function getFunctionName(node) {
|
|
89
|
+
switch (node.type) {
|
|
90
|
+
case "FunctionDeclaration":
|
|
91
|
+
return node.id?.name ?? null;
|
|
92
|
+
case "FunctionExpression":
|
|
93
|
+
return node.id?.name ?? null;
|
|
94
|
+
case "ArrowFunctionExpression":
|
|
95
|
+
return null;
|
|
96
|
+
// Arrow functions don't have names directly
|
|
97
|
+
case "MethodDefinition":
|
|
98
|
+
if (node.key.type === "Identifier") {
|
|
99
|
+
return node.key.name;
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
default:
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function getLineNumber(node) {
|
|
107
|
+
return node.loc?.start.line ?? 0;
|
|
108
|
+
}
|
|
109
|
+
function isCoverageContext(node, ancestors) {
|
|
110
|
+
const coveragePatterns = /coverage|summary|metrics|pct|percent|statements|branches|functions|lines/i;
|
|
111
|
+
if (node.type === "Identifier" && coveragePatterns.test(node.name)) {
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
for (const ancestor of ancestors.slice(-3)) {
|
|
115
|
+
if (ancestor.type === "MemberExpression") {
|
|
116
|
+
const memberExpr = ancestor;
|
|
117
|
+
if (memberExpr.object.type === "Identifier" && coveragePatterns.test(memberExpr.object.name)) {
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (ancestor.type === "ObjectPattern" || ancestor.type === "ObjectExpression") {
|
|
122
|
+
const parent = ancestors[ancestors.indexOf(ancestor) - 1];
|
|
123
|
+
if (parent?.type === "VariableDeclarator") {
|
|
124
|
+
const varDecl = parent;
|
|
125
|
+
if (varDecl.id.type === "Identifier" && coveragePatterns.test(varDecl.id.name)) {
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// src/utils/scope-tracker.ts
|
|
135
|
+
var ScopeTracker = class {
|
|
136
|
+
constructor(rootNode) {
|
|
137
|
+
this.allScopes = [];
|
|
138
|
+
this.rootScope = {
|
|
139
|
+
type: "global",
|
|
140
|
+
node: rootNode,
|
|
141
|
+
parent: null,
|
|
142
|
+
children: [],
|
|
143
|
+
variables: /* @__PURE__ */ new Map()
|
|
144
|
+
};
|
|
145
|
+
this.currentScope = this.rootScope;
|
|
146
|
+
this.allScopes.push(this.rootScope);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Enter a new scope
|
|
150
|
+
*/
|
|
151
|
+
enterScope(type, node) {
|
|
152
|
+
const newScope = {
|
|
153
|
+
type,
|
|
154
|
+
node,
|
|
155
|
+
parent: this.currentScope,
|
|
156
|
+
children: [],
|
|
157
|
+
variables: /* @__PURE__ */ new Map()
|
|
158
|
+
};
|
|
159
|
+
this.currentScope.children.push(newScope);
|
|
160
|
+
this.currentScope = newScope;
|
|
161
|
+
this.allScopes.push(newScope);
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Exit current scope and return to parent
|
|
165
|
+
*/
|
|
166
|
+
exitScope() {
|
|
167
|
+
if (this.currentScope.parent) {
|
|
168
|
+
this.currentScope = this.currentScope.parent;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Declare a variable in the current scope
|
|
173
|
+
*/
|
|
174
|
+
declareVariable(name, node, line, options = {}) {
|
|
175
|
+
const varInfo = {
|
|
176
|
+
name,
|
|
177
|
+
node,
|
|
178
|
+
declarationLine: line,
|
|
179
|
+
references: [],
|
|
180
|
+
type: options.type,
|
|
181
|
+
isParameter: options.isParameter ?? false,
|
|
182
|
+
isDestructured: options.isDestructured ?? false,
|
|
183
|
+
isLoopVariable: options.isLoopVariable ?? false
|
|
184
|
+
};
|
|
185
|
+
this.currentScope.variables.set(name, varInfo);
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Add a reference to a variable
|
|
189
|
+
*/
|
|
190
|
+
addReference(name, node) {
|
|
191
|
+
const varInfo = this.findVariable(name);
|
|
192
|
+
if (varInfo) {
|
|
193
|
+
varInfo.references.push(node);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Find a variable in current or parent scopes
|
|
198
|
+
*/
|
|
199
|
+
findVariable(name) {
|
|
200
|
+
let scope = this.currentScope;
|
|
201
|
+
while (scope) {
|
|
202
|
+
const varInfo = scope.variables.get(name);
|
|
203
|
+
if (varInfo) {
|
|
204
|
+
return varInfo;
|
|
205
|
+
}
|
|
206
|
+
scope = scope.parent;
|
|
207
|
+
}
|
|
208
|
+
return null;
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Get all variables in current scope (not including parent scopes)
|
|
212
|
+
*/
|
|
213
|
+
getCurrentScopeVariables() {
|
|
214
|
+
return Array.from(this.currentScope.variables.values());
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Get all variables across all scopes
|
|
218
|
+
*/
|
|
219
|
+
getAllVariables() {
|
|
220
|
+
const allVars = [];
|
|
221
|
+
for (const scope of this.allScopes) {
|
|
222
|
+
allVars.push(...Array.from(scope.variables.values()));
|
|
223
|
+
}
|
|
224
|
+
return allVars;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Calculate actual usage count (references minus declaration)
|
|
228
|
+
*/
|
|
229
|
+
getUsageCount(varInfo) {
|
|
230
|
+
return varInfo.references.length;
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Check if a variable is short-lived (used within N lines)
|
|
234
|
+
*/
|
|
235
|
+
isShortLived(varInfo, maxLines = 5) {
|
|
236
|
+
if (varInfo.references.length === 0) {
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
const declarationLine = varInfo.declarationLine;
|
|
240
|
+
const maxUsageLine = Math.max(
|
|
241
|
+
...varInfo.references.map((ref) => ref.loc?.start.line ?? declarationLine)
|
|
242
|
+
);
|
|
243
|
+
return maxUsageLine - declarationLine <= maxLines;
|
|
244
|
+
}
|
|
245
|
+
/**
|
|
246
|
+
* Check if a variable is used in a limited scope (e.g., only in one callback)
|
|
247
|
+
*/
|
|
248
|
+
isLocallyScoped(varInfo) {
|
|
249
|
+
if (varInfo.references.length === 0) return false;
|
|
250
|
+
const lines = varInfo.references.map((ref) => ref.loc?.start.line ?? 0);
|
|
251
|
+
const minLine = Math.min(...lines);
|
|
252
|
+
const maxLine = Math.max(...lines);
|
|
253
|
+
return maxLine - minLine <= 3;
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Get current scope type
|
|
257
|
+
*/
|
|
258
|
+
getCurrentScopeType() {
|
|
259
|
+
return this.currentScope.type;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* Check if currently in a loop scope
|
|
263
|
+
*/
|
|
264
|
+
isInLoop() {
|
|
265
|
+
let scope = this.currentScope;
|
|
266
|
+
while (scope) {
|
|
267
|
+
if (scope.type === "loop") {
|
|
268
|
+
return true;
|
|
269
|
+
}
|
|
270
|
+
scope = scope.parent;
|
|
271
|
+
}
|
|
272
|
+
return false;
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Check if currently in a function scope
|
|
276
|
+
*/
|
|
277
|
+
isInFunction() {
|
|
278
|
+
let scope = this.currentScope;
|
|
279
|
+
while (scope) {
|
|
280
|
+
if (scope.type === "function") {
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
scope = scope.parent;
|
|
284
|
+
}
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Get the root scope
|
|
289
|
+
*/
|
|
290
|
+
getRootScope() {
|
|
291
|
+
return this.rootScope;
|
|
292
|
+
}
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
// src/utils/context-detector.ts
|
|
296
|
+
function detectFileType(filePath, ast) {
|
|
297
|
+
const path = filePath.toLowerCase();
|
|
298
|
+
if (path.match(/\.(test|spec)\.(ts|tsx|js|jsx)$/) || path.includes("__tests__")) {
|
|
299
|
+
return "test";
|
|
300
|
+
}
|
|
301
|
+
if (path.endsWith(".d.ts") || path.includes("types")) {
|
|
302
|
+
return "types";
|
|
303
|
+
}
|
|
304
|
+
if (path.match(/config|\.config\.|rc\.|setup/) || path.includes("configuration")) {
|
|
305
|
+
return "config";
|
|
306
|
+
}
|
|
307
|
+
return "production";
|
|
308
|
+
}
|
|
309
|
+
function detectCodeLayer(ast) {
|
|
310
|
+
let hasAPIIndicators = 0;
|
|
311
|
+
let hasBusinessIndicators = 0;
|
|
312
|
+
let hasDataIndicators = 0;
|
|
313
|
+
let hasUtilityIndicators = 0;
|
|
314
|
+
traverseAST(ast, {
|
|
315
|
+
enter: (node) => {
|
|
316
|
+
if (node.type === "ImportDeclaration") {
|
|
317
|
+
const source = node.source.value;
|
|
318
|
+
if (source.match(/express|fastify|koa|@nestjs|axios|fetch|http/i)) {
|
|
319
|
+
hasAPIIndicators++;
|
|
320
|
+
}
|
|
321
|
+
if (source.match(/database|prisma|typeorm|sequelize|mongoose|pg|mysql/i)) {
|
|
322
|
+
hasDataIndicators++;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (node.type === "FunctionDeclaration" && node.id) {
|
|
326
|
+
const name = node.id.name;
|
|
327
|
+
if (name.match(/^(get|post|put|delete|patch|handle|api|route|controller)/i)) {
|
|
328
|
+
hasAPIIndicators++;
|
|
329
|
+
}
|
|
330
|
+
if (name.match(/^(calculate|process|validate|transform|compute|analyze)/i)) {
|
|
331
|
+
hasBusinessIndicators++;
|
|
332
|
+
}
|
|
333
|
+
if (name.match(/^(find|create|update|delete|save|fetch|query|insert)/i)) {
|
|
334
|
+
hasDataIndicators++;
|
|
335
|
+
}
|
|
336
|
+
if (name.match(/^(format|parse|convert|normalize|sanitize|encode|decode)/i)) {
|
|
337
|
+
hasUtilityIndicators++;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (node.type === "ExportNamedDeclaration" || node.type === "ExportDefaultDeclaration") {
|
|
341
|
+
if (node.type === "ExportNamedDeclaration" && node.declaration) {
|
|
342
|
+
if (node.declaration.type === "FunctionDeclaration" && node.declaration.id) {
|
|
343
|
+
const name = node.declaration.id.name;
|
|
344
|
+
if (name.match(/handler|route|api|controller/i)) {
|
|
345
|
+
hasAPIIndicators += 2;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
});
|
|
352
|
+
const scores = {
|
|
353
|
+
api: hasAPIIndicators,
|
|
354
|
+
business: hasBusinessIndicators,
|
|
355
|
+
data: hasDataIndicators,
|
|
356
|
+
utility: hasUtilityIndicators
|
|
357
|
+
};
|
|
358
|
+
const maxScore = Math.max(...Object.values(scores));
|
|
359
|
+
if (maxScore === 0) {
|
|
360
|
+
return "unknown";
|
|
361
|
+
}
|
|
362
|
+
if (scores.api === maxScore) return "api";
|
|
363
|
+
if (scores.data === maxScore) return "data";
|
|
364
|
+
if (scores.business === maxScore) return "business";
|
|
365
|
+
if (scores.utility === maxScore) return "utility";
|
|
366
|
+
return "unknown";
|
|
367
|
+
}
|
|
368
|
+
function calculateComplexity(node) {
|
|
369
|
+
let complexity = 1;
|
|
370
|
+
traverseAST(node, {
|
|
371
|
+
enter: (childNode) => {
|
|
372
|
+
switch (childNode.type) {
|
|
373
|
+
case "IfStatement":
|
|
374
|
+
case "ConditionalExpression":
|
|
375
|
+
// ternary
|
|
376
|
+
case "SwitchCase":
|
|
377
|
+
case "ForStatement":
|
|
378
|
+
case "ForInStatement":
|
|
379
|
+
case "ForOfStatement":
|
|
380
|
+
case "WhileStatement":
|
|
381
|
+
case "DoWhileStatement":
|
|
382
|
+
case "CatchClause":
|
|
383
|
+
complexity++;
|
|
384
|
+
break;
|
|
385
|
+
case "LogicalExpression":
|
|
386
|
+
if (childNode.operator === "&&" || childNode.operator === "||") {
|
|
387
|
+
complexity++;
|
|
388
|
+
}
|
|
389
|
+
break;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
return complexity;
|
|
394
|
+
}
|
|
395
|
+
function buildCodeContext(filePath, ast) {
|
|
396
|
+
const fileType = detectFileType(filePath, ast);
|
|
397
|
+
const codeLayer = detectCodeLayer(ast);
|
|
398
|
+
let totalComplexity = 0;
|
|
399
|
+
let functionCount = 0;
|
|
400
|
+
traverseAST(ast, {
|
|
401
|
+
enter: (node) => {
|
|
402
|
+
if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") {
|
|
403
|
+
totalComplexity += calculateComplexity(node);
|
|
404
|
+
functionCount++;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
const avgComplexity = functionCount > 0 ? totalComplexity / functionCount : 1;
|
|
409
|
+
return {
|
|
410
|
+
fileType,
|
|
411
|
+
codeLayer,
|
|
412
|
+
complexity: Math.round(avgComplexity),
|
|
413
|
+
isTestFile: fileType === "test",
|
|
414
|
+
isTypeDefinition: fileType === "types"
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
function adjustSeverity(baseSeverity, context, issueType) {
|
|
418
|
+
if (context.isTestFile) {
|
|
419
|
+
if (baseSeverity === "minor") return "info";
|
|
420
|
+
if (baseSeverity === "major") return "minor";
|
|
421
|
+
}
|
|
422
|
+
if (context.isTypeDefinition) {
|
|
423
|
+
if (baseSeverity === "minor") return "info";
|
|
424
|
+
}
|
|
425
|
+
if (context.codeLayer === "api") {
|
|
426
|
+
if (baseSeverity === "info" && issueType === "unclear") return "minor";
|
|
427
|
+
if (baseSeverity === "minor" && issueType === "unclear") return "major";
|
|
428
|
+
}
|
|
429
|
+
if (context.complexity > 10) {
|
|
430
|
+
if (baseSeverity === "info") return "minor";
|
|
431
|
+
}
|
|
432
|
+
if (context.codeLayer === "utility") {
|
|
433
|
+
if (baseSeverity === "minor" && issueType === "abbreviation") return "info";
|
|
434
|
+
}
|
|
435
|
+
return baseSeverity;
|
|
436
|
+
}
|
|
437
|
+
function isAcceptableInContext(name, context, options) {
|
|
438
|
+
if (options.isLoopVariable && ["i", "j", "k", "l", "n", "m"].includes(name)) {
|
|
439
|
+
return true;
|
|
440
|
+
}
|
|
441
|
+
if (context.isTestFile) {
|
|
442
|
+
if (["a", "b", "c", "x", "y", "z"].includes(name) && options.isParameter) {
|
|
443
|
+
return true;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (context.codeLayer === "utility" && ["x", "y", "z"].includes(name)) {
|
|
447
|
+
return true;
|
|
448
|
+
}
|
|
449
|
+
if (options.isDestructured) {
|
|
450
|
+
if (["s", "b", "f", "l"].includes(name)) {
|
|
451
|
+
return true;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (options.isParameter && (options.complexity ?? context.complexity) < 3) {
|
|
455
|
+
if (name.length >= 2) {
|
|
456
|
+
return true;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// src/analyzers/naming-ast.ts
|
|
36
463
|
var COMMON_SHORT_WORDS = /* @__PURE__ */ new Set([
|
|
37
|
-
// Full English words (1-3 letters)
|
|
38
464
|
"day",
|
|
39
465
|
"key",
|
|
40
466
|
"net",
|
|
@@ -90,14 +516,12 @@ var COMMON_SHORT_WORDS = /* @__PURE__ */ new Set([
|
|
|
90
516
|
"tmp",
|
|
91
517
|
"ext",
|
|
92
518
|
"sep",
|
|
93
|
-
// Prepositions and conjunctions
|
|
94
519
|
"and",
|
|
95
520
|
"from",
|
|
96
521
|
"how",
|
|
97
522
|
"pad",
|
|
98
523
|
"bar",
|
|
99
524
|
"non",
|
|
100
|
-
// Additional full words commonly flagged
|
|
101
525
|
"tax",
|
|
102
526
|
"cat",
|
|
103
527
|
"dog",
|
|
@@ -177,18 +601,15 @@ var COMMON_SHORT_WORDS = /* @__PURE__ */ new Set([
|
|
|
177
601
|
"won"
|
|
178
602
|
]);
|
|
179
603
|
var ACCEPTABLE_ABBREVIATIONS = /* @__PURE__ */ new Set([
|
|
180
|
-
// Standard identifiers
|
|
181
604
|
"id",
|
|
182
605
|
"uid",
|
|
183
606
|
"gid",
|
|
184
607
|
"pid",
|
|
185
|
-
// Loop counters and iterators
|
|
186
608
|
"i",
|
|
187
609
|
"j",
|
|
188
610
|
"k",
|
|
189
611
|
"n",
|
|
190
612
|
"m",
|
|
191
|
-
// Web/Network
|
|
192
613
|
"url",
|
|
193
614
|
"uri",
|
|
194
615
|
"api",
|
|
@@ -208,7 +629,6 @@ var ACCEPTABLE_ABBREVIATIONS = /* @__PURE__ */ new Set([
|
|
|
208
629
|
"cors",
|
|
209
630
|
"ws",
|
|
210
631
|
"wss",
|
|
211
|
-
// Data formats
|
|
212
632
|
"json",
|
|
213
633
|
"xml",
|
|
214
634
|
"yaml",
|
|
@@ -217,7 +637,6 @@ var ACCEPTABLE_ABBREVIATIONS = /* @__PURE__ */ new Set([
|
|
|
217
637
|
"css",
|
|
218
638
|
"svg",
|
|
219
639
|
"pdf",
|
|
220
|
-
// File types & extensions
|
|
221
640
|
"img",
|
|
222
641
|
"txt",
|
|
223
642
|
"doc",
|
|
@@ -229,7 +648,6 @@ var ACCEPTABLE_ABBREVIATIONS = /* @__PURE__ */ new Set([
|
|
|
229
648
|
"jpg",
|
|
230
649
|
"png",
|
|
231
650
|
"gif",
|
|
232
|
-
// Databases
|
|
233
651
|
"db",
|
|
234
652
|
"sql",
|
|
235
653
|
"orm",
|
|
@@ -238,7 +656,6 @@ var ACCEPTABLE_ABBREVIATIONS = /* @__PURE__ */ new Set([
|
|
|
238
656
|
"ddb",
|
|
239
657
|
"rds",
|
|
240
658
|
"nosql",
|
|
241
|
-
// File system
|
|
242
659
|
"fs",
|
|
243
660
|
"dir",
|
|
244
661
|
"tmp",
|
|
@@ -247,7 +664,6 @@ var ACCEPTABLE_ABBREVIATIONS = /* @__PURE__ */ new Set([
|
|
|
247
664
|
"bin",
|
|
248
665
|
"lib",
|
|
249
666
|
"pkg",
|
|
250
|
-
// Operating system
|
|
251
667
|
"os",
|
|
252
668
|
"env",
|
|
253
669
|
"arg",
|
|
@@ -256,20 +672,17 @@ var ACCEPTABLE_ABBREVIATIONS = /* @__PURE__ */ new Set([
|
|
|
256
672
|
"exe",
|
|
257
673
|
"cwd",
|
|
258
674
|
"pwd",
|
|
259
|
-
// UI/UX
|
|
260
675
|
"ui",
|
|
261
676
|
"ux",
|
|
262
677
|
"gui",
|
|
263
678
|
"dom",
|
|
264
679
|
"ref",
|
|
265
|
-
// Request/Response
|
|
266
680
|
"req",
|
|
267
681
|
"res",
|
|
268
682
|
"ctx",
|
|
269
683
|
"err",
|
|
270
684
|
"msg",
|
|
271
685
|
"auth",
|
|
272
|
-
// Mathematics/Computing
|
|
273
686
|
"max",
|
|
274
687
|
"min",
|
|
275
688
|
"avg",
|
|
@@ -287,7 +700,6 @@ var ACCEPTABLE_ABBREVIATIONS = /* @__PURE__ */ new Set([
|
|
|
287
700
|
"int",
|
|
288
701
|
"num",
|
|
289
702
|
"idx",
|
|
290
|
-
// Time
|
|
291
703
|
"now",
|
|
292
704
|
"utc",
|
|
293
705
|
"tz",
|
|
@@ -297,7 +709,6 @@ var ACCEPTABLE_ABBREVIATIONS = /* @__PURE__ */ new Set([
|
|
|
297
709
|
"min",
|
|
298
710
|
"yr",
|
|
299
711
|
"mo",
|
|
300
|
-
// Common patterns
|
|
301
712
|
"app",
|
|
302
713
|
"cfg",
|
|
303
714
|
"config",
|
|
@@ -316,7 +727,6 @@ var ACCEPTABLE_ABBREVIATIONS = /* @__PURE__ */ new Set([
|
|
|
316
727
|
"post",
|
|
317
728
|
"sub",
|
|
318
729
|
"pub",
|
|
319
|
-
// Programming/Framework specific
|
|
320
730
|
"ts",
|
|
321
731
|
"js",
|
|
322
732
|
"jsx",
|
|
@@ -330,7 +740,6 @@ var ACCEPTABLE_ABBREVIATIONS = /* @__PURE__ */ new Set([
|
|
|
330
740
|
"mod",
|
|
331
741
|
"opts",
|
|
332
742
|
"dev",
|
|
333
|
-
// Cloud/Infrastructure
|
|
334
743
|
"s3",
|
|
335
744
|
"ec2",
|
|
336
745
|
"sqs",
|
|
@@ -354,7 +763,6 @@ var ACCEPTABLE_ABBREVIATIONS = /* @__PURE__ */ new Set([
|
|
|
354
763
|
"cf",
|
|
355
764
|
"cfn",
|
|
356
765
|
"ga",
|
|
357
|
-
// Metrics/Performance
|
|
358
766
|
"fcp",
|
|
359
767
|
"lcp",
|
|
360
768
|
"cls",
|
|
@@ -366,14 +774,12 @@ var ACCEPTABLE_ABBREVIATIONS = /* @__PURE__ */ new Set([
|
|
|
366
774
|
"rps",
|
|
367
775
|
"tps",
|
|
368
776
|
"wpm",
|
|
369
|
-
// Testing & i18n
|
|
370
777
|
"po",
|
|
371
778
|
"e2e",
|
|
372
779
|
"a11y",
|
|
373
780
|
"i18n",
|
|
374
781
|
"l10n",
|
|
375
782
|
"spy",
|
|
376
|
-
// Domain-specific abbreviations (context-aware)
|
|
377
783
|
"sk",
|
|
378
784
|
"fy",
|
|
379
785
|
"faq",
|
|
@@ -384,7 +790,6 @@ var ACCEPTABLE_ABBREVIATIONS = /* @__PURE__ */ new Set([
|
|
|
384
790
|
"kpi",
|
|
385
791
|
"ttl",
|
|
386
792
|
"pct",
|
|
387
|
-
// Technical abbreviations
|
|
388
793
|
"mac",
|
|
389
794
|
"hex",
|
|
390
795
|
"esm",
|
|
@@ -392,30 +797,27 @@ var ACCEPTABLE_ABBREVIATIONS = /* @__PURE__ */ new Set([
|
|
|
392
797
|
"rec",
|
|
393
798
|
"loc",
|
|
394
799
|
"dup",
|
|
395
|
-
// Boolean helpers (these are intentional short names)
|
|
396
800
|
"is",
|
|
397
801
|
"has",
|
|
398
802
|
"can",
|
|
399
803
|
"did",
|
|
400
804
|
"was",
|
|
401
805
|
"are",
|
|
402
|
-
// Date/Time context (when in date contexts)
|
|
403
806
|
"d",
|
|
404
807
|
"t",
|
|
405
808
|
"dt",
|
|
406
|
-
// Coverage metrics (industry standard: statements/branches/functions/lines)
|
|
407
809
|
"s",
|
|
408
810
|
"b",
|
|
409
811
|
"f",
|
|
410
812
|
"l",
|
|
411
|
-
//
|
|
813
|
+
// Coverage metrics
|
|
412
814
|
"vid",
|
|
413
815
|
"pic",
|
|
414
816
|
"img",
|
|
415
817
|
"doc",
|
|
416
818
|
"msg"
|
|
417
819
|
]);
|
|
418
|
-
async function
|
|
820
|
+
async function analyzeNamingAST(files) {
|
|
419
821
|
const issues = [];
|
|
420
822
|
const rootDir = files.length > 0 ? (0, import_path.dirname)(files[0]) : process.cwd();
|
|
421
823
|
const config = (0, import_core.loadConfig)(rootDir);
|
|
@@ -423,188 +825,223 @@ async function analyzeNaming(files) {
|
|
|
423
825
|
const customAbbreviations = new Set(consistencyConfig?.acceptedAbbreviations || []);
|
|
424
826
|
const customShortWords = new Set(consistencyConfig?.shortWords || []);
|
|
425
827
|
const disabledChecks = new Set(consistencyConfig?.disableChecks || []);
|
|
828
|
+
const allAbbreviations = /* @__PURE__ */ new Set([...ACCEPTABLE_ABBREVIATIONS, ...customAbbreviations]);
|
|
829
|
+
const allShortWords = /* @__PURE__ */ new Set([...COMMON_SHORT_WORDS, ...customShortWords]);
|
|
426
830
|
for (const file of files) {
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
831
|
+
try {
|
|
832
|
+
const ast = parseFile(file);
|
|
833
|
+
if (!ast) continue;
|
|
834
|
+
const fileIssues = analyzeFileNamingAST(
|
|
835
|
+
file,
|
|
836
|
+
ast,
|
|
837
|
+
allAbbreviations,
|
|
838
|
+
allShortWords,
|
|
839
|
+
disabledChecks
|
|
840
|
+
);
|
|
841
|
+
issues.push(...fileIssues);
|
|
842
|
+
} catch (error) {
|
|
843
|
+
console.warn(`Skipping ${file} due to parse error:`, error);
|
|
844
|
+
}
|
|
430
845
|
}
|
|
431
846
|
return issues;
|
|
432
847
|
}
|
|
433
|
-
function
|
|
848
|
+
function analyzeFileNamingAST(file, ast, allAbbreviations, allShortWords, disabledChecks) {
|
|
434
849
|
const issues = [];
|
|
435
|
-
const
|
|
436
|
-
const
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
usageCount += matches.length;
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
return usageCount >= 2 && usageCount <= 3;
|
|
456
|
-
};
|
|
457
|
-
lines.forEach((line, index) => {
|
|
458
|
-
const lineNumber = index + 1;
|
|
459
|
-
const contextWindow = getContextWindow(index);
|
|
460
|
-
if (!disabledChecks.has("single-letter")) {
|
|
461
|
-
const singleLetterMatches = line.matchAll(/\b(?:const|let|var)\s+([a-hm-z])\s*=/gi);
|
|
462
|
-
for (const match of singleLetterMatches) {
|
|
463
|
-
const letter = match[1].toLowerCase();
|
|
464
|
-
const isCoverageContext = /coverage|summary|metrics|pct|percent/i.test(line) || /\.(?:statements|branches|functions|lines)\.pct/i.test(line);
|
|
465
|
-
if (isCoverageContext && ["s", "b", "f", "l"].includes(letter)) {
|
|
466
|
-
continue;
|
|
467
|
-
}
|
|
468
|
-
const isInLoopContext = line.includes("for") || /\.(map|filter|forEach|reduce|find|some|every)\s*\(/.test(line) || line.includes("=>") || // Arrow function
|
|
469
|
-
/\w+\s*=>\s*/.test(line);
|
|
470
|
-
const isI18nContext = line.includes("useTranslation") || line.includes("i18n.t") || /\bt\s*\(['"]/.test(line);
|
|
471
|
-
const isArrowFunctionParam = /\(\s*[a-z]\s*(?:,\s*[a-z]\s*)*\)\s*=>/.test(line) || // (s) => or (a, b) =>
|
|
472
|
-
/[a-z]\s*=>/.test(line) || // s => on same line
|
|
473
|
-
// Multi-line arrow function detection: look for pattern in context window
|
|
474
|
-
new RegExp(`\\b${letter}\\s*\\)\\s*$`).test(line) && /=>/.test(contextWindow) || // (s)\n =>
|
|
475
|
-
new RegExp(`\\.(?:map|filter|forEach|reduce|find|some|every)\\s*\\(\\s*$`).test(lines[index - 1] || "") && /=>/.test(contextWindow);
|
|
476
|
-
const isShortLived = isShortLivedVariable(letter, index);
|
|
477
|
-
if (!isInLoopContext && !isI18nContext && !isArrowFunctionParam && !isShortLived && !["x", "y", "z", "i", "j", "k", "l", "n", "m"].includes(letter)) {
|
|
478
|
-
if (isTestFile && ["a", "b", "c", "d", "e", "f", "s"].includes(letter)) {
|
|
479
|
-
continue;
|
|
850
|
+
const scopeTracker = new ScopeTracker(ast);
|
|
851
|
+
const context = buildCodeContext(file, ast);
|
|
852
|
+
const ancestors = [];
|
|
853
|
+
traverseAST(ast, {
|
|
854
|
+
enter: (node, parent) => {
|
|
855
|
+
ancestors.push(node);
|
|
856
|
+
if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") {
|
|
857
|
+
scopeTracker.enterScope("function", node);
|
|
858
|
+
if ("params" in node) {
|
|
859
|
+
for (const param of node.params) {
|
|
860
|
+
if (param.type === "Identifier") {
|
|
861
|
+
scopeTracker.declareVariable(param.name, param, getLineNumber(param), {
|
|
862
|
+
isParameter: true
|
|
863
|
+
});
|
|
864
|
+
} else if (param.type === "ObjectPattern" || param.type === "ArrayPattern") {
|
|
865
|
+
extractIdentifiersFromPattern(param, scopeTracker, true);
|
|
866
|
+
}
|
|
480
867
|
}
|
|
481
|
-
issues.push({
|
|
482
|
-
file,
|
|
483
|
-
line: lineNumber,
|
|
484
|
-
type: "poor-naming",
|
|
485
|
-
identifier: match[1],
|
|
486
|
-
severity: "minor",
|
|
487
|
-
suggestion: `Use descriptive variable name instead of single letter '${match[1]}'`
|
|
488
|
-
});
|
|
489
868
|
}
|
|
869
|
+
} else if (node.type === "BlockStatement") {
|
|
870
|
+
scopeTracker.enterScope("block", node);
|
|
871
|
+
} else if (isLoopStatement(node)) {
|
|
872
|
+
scopeTracker.enterScope("loop", node);
|
|
873
|
+
} else if (node.type === "ClassDeclaration") {
|
|
874
|
+
scopeTracker.enterScope("class", node);
|
|
490
875
|
}
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
new RegExp(`\\.(?:map|filter|forEach|reduce|find|some|every)\\s*\\(\\s*$`).test(lines[index - 1] || "") && new RegExp(`^\\s*${abbrev}\\s*=>`).test(line);
|
|
507
|
-
if (isArrowFunctionParam) {
|
|
508
|
-
continue;
|
|
876
|
+
if (node.type === "VariableDeclarator") {
|
|
877
|
+
if (node.id.type === "Identifier") {
|
|
878
|
+
const isInCoverage = isCoverageContext(node, ancestors);
|
|
879
|
+
scopeTracker.declareVariable(
|
|
880
|
+
node.id.name,
|
|
881
|
+
node.id,
|
|
882
|
+
getLineNumber(node.id),
|
|
883
|
+
{
|
|
884
|
+
type: "typeAnnotation" in node.id ? node.id.typeAnnotation : null,
|
|
885
|
+
isDestructured: false,
|
|
886
|
+
isLoopVariable: scopeTracker.getCurrentScopeType() === "loop"
|
|
887
|
+
}
|
|
888
|
+
);
|
|
889
|
+
} else if (node.id.type === "ObjectPattern" || node.id.type === "ArrayPattern") {
|
|
890
|
+
extractIdentifiersFromPattern(node.id, scopeTracker, false, ancestors);
|
|
509
891
|
}
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
const isUserContext = /user|auth|account/i.test(line);
|
|
516
|
-
if (isUserContext && abbrev === "u") {
|
|
517
|
-
continue;
|
|
892
|
+
}
|
|
893
|
+
if (node.type === "Identifier" && parent) {
|
|
894
|
+
if (parent.type !== "VariableDeclarator" || parent.id !== node) {
|
|
895
|
+
if (parent.type !== "FunctionDeclaration" || parent.id !== node) {
|
|
896
|
+
scopeTracker.addReference(node.name, node);
|
|
518
897
|
}
|
|
519
898
|
}
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
suggestion: `Consider using full word instead of abbreviation '${match[1]}'`
|
|
527
|
-
});
|
|
899
|
+
}
|
|
900
|
+
},
|
|
901
|
+
leave: (node) => {
|
|
902
|
+
ancestors.pop();
|
|
903
|
+
if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression" || node.type === "BlockStatement" || isLoopStatement(node) || node.type === "ClassDeclaration") {
|
|
904
|
+
scopeTracker.exitScope();
|
|
528
905
|
}
|
|
529
906
|
}
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
907
|
+
});
|
|
908
|
+
const allVariables = scopeTracker.getAllVariables();
|
|
909
|
+
for (const varInfo of allVariables) {
|
|
910
|
+
const name = varInfo.name;
|
|
911
|
+
const line = varInfo.declarationLine;
|
|
912
|
+
if (disabledChecks.has("single-letter") && name.length === 1) continue;
|
|
913
|
+
if (disabledChecks.has("abbreviation") && name.length <= 3) continue;
|
|
914
|
+
const isInCoverage = ["s", "b", "f", "l"].includes(name) && varInfo.isDestructured;
|
|
915
|
+
if (isInCoverage) continue;
|
|
916
|
+
const functionComplexity = varInfo.node.type === "Identifier" && "parent" in varInfo.node ? calculateComplexity(varInfo.node) : context.complexity;
|
|
917
|
+
if (isAcceptableInContext(name, context, {
|
|
918
|
+
isLoopVariable: varInfo.isLoopVariable || allAbbreviations.has(name),
|
|
919
|
+
isParameter: varInfo.isParameter,
|
|
920
|
+
isDestructured: varInfo.isDestructured,
|
|
921
|
+
complexity: functionComplexity
|
|
922
|
+
})) {
|
|
923
|
+
continue;
|
|
924
|
+
}
|
|
925
|
+
if (name.length === 1 && !allAbbreviations.has(name) && !allShortWords.has(name)) {
|
|
926
|
+
const isShortLived = scopeTracker.isShortLived(varInfo, 5);
|
|
927
|
+
if (!isShortLived) {
|
|
534
928
|
issues.push({
|
|
535
929
|
file,
|
|
536
|
-
line
|
|
537
|
-
type: "
|
|
538
|
-
identifier:
|
|
539
|
-
severity: "minor",
|
|
540
|
-
suggestion: `Use
|
|
930
|
+
line,
|
|
931
|
+
type: "poor-naming",
|
|
932
|
+
identifier: name,
|
|
933
|
+
severity: adjustSeverity("minor", context, "poor-naming"),
|
|
934
|
+
suggestion: `Use descriptive variable name instead of single letter '${name}'`
|
|
541
935
|
});
|
|
542
936
|
}
|
|
937
|
+
continue;
|
|
543
938
|
}
|
|
544
|
-
if (
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
if (!name.match(/^(is|has|should|can|will|did)/i)) {
|
|
939
|
+
if (name.length >= 2 && name.length <= 3) {
|
|
940
|
+
if (!allShortWords.has(name.toLowerCase()) && !allAbbreviations.has(name.toLowerCase())) {
|
|
941
|
+
const isShortLived = scopeTracker.isShortLived(varInfo, 5);
|
|
942
|
+
if (!isShortLived) {
|
|
549
943
|
issues.push({
|
|
550
944
|
file,
|
|
551
|
-
line
|
|
552
|
-
type: "
|
|
945
|
+
line,
|
|
946
|
+
type: "abbreviation",
|
|
553
947
|
identifier: name,
|
|
554
|
-
severity: "info",
|
|
555
|
-
suggestion: `
|
|
948
|
+
severity: adjustSeverity("info", context, "abbreviation"),
|
|
949
|
+
suggestion: `Consider using full word instead of abbreviation '${name}'`
|
|
556
950
|
});
|
|
557
951
|
}
|
|
558
952
|
}
|
|
953
|
+
continue;
|
|
559
954
|
}
|
|
560
|
-
if (!disabledChecks.has("
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
}
|
|
572
|
-
const isFactoryPattern = name.match(/(Factory|Builder|Creator|Generator|Provider|Adapter|Mock)$/);
|
|
573
|
-
const isEventHandler = name.match(/^on[A-Z]/);
|
|
574
|
-
const isDescriptiveLong = name.length > 15;
|
|
575
|
-
const isReactHook = name.match(/^use[A-Z]/);
|
|
576
|
-
const isDescriptivePattern = name.match(/^(default|total|count|sum|avg|max|min|initial|current|previous|next)\w+/) || name.match(/\w+(Count|Total|Sum|Average|List|Map|Set|Config|Settings|Options|Props|Data|Info|Details|State|Status|Response|Result)$/);
|
|
577
|
-
const isHelperPattern = name.match(/^(to|from|with|without|for|as|into)\w+/) || // toMetadata, withLogger, forPath
|
|
578
|
-
name.match(/^\w+(To|From|With|Without|For|As|Into)\w*$/);
|
|
579
|
-
const isUtilityName = ["cn", "proxy", "sitemap", "robots", "gtag"].includes(name);
|
|
580
|
-
const capitalCount = (name.match(/[A-Z]/g) || []).length;
|
|
581
|
-
const isCompoundWord = capitalCount >= 3;
|
|
582
|
-
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)/);
|
|
583
|
-
if (!hasActionVerb && !isFactoryPattern && !isEventHandler && !isDescriptiveLong && !isDescriptivePattern && !isCompoundWord && !isHelperPattern && !isUtilityName && !isReactHook) {
|
|
584
|
-
issues.push({
|
|
585
|
-
file,
|
|
586
|
-
line: lineNumber,
|
|
587
|
-
type: "unclear",
|
|
588
|
-
identifier: name,
|
|
589
|
-
severity: "info",
|
|
590
|
-
suggestion: `Function '${name}' should start with an action verb (get, set, create, etc.)`
|
|
591
|
-
});
|
|
592
|
-
}
|
|
955
|
+
if (!disabledChecks.has("convention-mix") && file.match(/\.(ts|tsx|js|jsx)$/)) {
|
|
956
|
+
if (name.includes("_") && !name.startsWith("_") && name.toLowerCase() === name) {
|
|
957
|
+
const camelCase = name.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
958
|
+
issues.push({
|
|
959
|
+
file,
|
|
960
|
+
line,
|
|
961
|
+
type: "convention-mix",
|
|
962
|
+
identifier: name,
|
|
963
|
+
severity: adjustSeverity("minor", context, "convention-mix"),
|
|
964
|
+
suggestion: `Use camelCase '${camelCase}' instead of snake_case in TypeScript/JavaScript`
|
|
965
|
+
});
|
|
593
966
|
}
|
|
594
967
|
}
|
|
595
|
-
}
|
|
968
|
+
}
|
|
969
|
+
if (!disabledChecks.has("unclear")) {
|
|
970
|
+
traverseAST(ast, {
|
|
971
|
+
enter: (node) => {
|
|
972
|
+
if (node.type === "FunctionDeclaration" || node.type === "MethodDefinition") {
|
|
973
|
+
const name = getFunctionName(node);
|
|
974
|
+
if (!name) return;
|
|
975
|
+
const line = getLineNumber(node);
|
|
976
|
+
if (["main", "init", "setup", "bootstrap"].includes(name)) return;
|
|
977
|
+
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|count|detect|select)/);
|
|
978
|
+
const isFactoryPattern = name.match(/(Factory|Builder|Creator|Generator|Provider|Adapter|Mock)$/);
|
|
979
|
+
const isEventHandler = name.match(/^on[A-Z]/);
|
|
980
|
+
const isDescriptiveLong = name.length > 15;
|
|
981
|
+
const isReactHook = name.match(/^use[A-Z]/);
|
|
982
|
+
const isHelperPattern = name.match(/^(to|from|with|without|for|as|into)\w+/) || name.match(/^\w+(To|From|With|Without|For|As|Into)\w*$/);
|
|
983
|
+
const isUtilityName = ["cn", "proxy", "sitemap", "robots", "gtag"].includes(name);
|
|
984
|
+
const isLanguageKeyword = ["constructor", "toString", "valueOf", "toJSON"].includes(name);
|
|
985
|
+
const isFrameworkPattern = name.match(/^(goto|fill|click|select|submit|wait|expect)\w*/);
|
|
986
|
+
const isDescriptivePattern = name.match(/^(default|total|count|sum|avg|max|min|initial|current|previous|next)\w+/) || name.match(/\w+(Count|Total|Sum|Average|List|Map|Set|Config|Settings|Options|Props|Data|Info|Details|State|Status|Response|Result)$/);
|
|
987
|
+
const capitalCount = (name.match(/[A-Z]/g) || []).length;
|
|
988
|
+
const isCompoundWord = capitalCount >= 3;
|
|
989
|
+
if (!hasActionVerb && !isFactoryPattern && !isEventHandler && !isDescriptiveLong && !isReactHook && !isHelperPattern && !isUtilityName && !isDescriptivePattern && !isCompoundWord && !isLanguageKeyword && !isFrameworkPattern) {
|
|
990
|
+
issues.push({
|
|
991
|
+
file,
|
|
992
|
+
line,
|
|
993
|
+
type: "unclear",
|
|
994
|
+
identifier: name,
|
|
995
|
+
severity: adjustSeverity("info", context, "unclear"),
|
|
996
|
+
suggestion: `Function '${name}' should start with an action verb (get, set, create, etc.)`
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
596
1003
|
return issues;
|
|
597
1004
|
}
|
|
598
|
-
function
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
1005
|
+
function extractIdentifiersFromPattern(pattern, scopeTracker, isParameter, ancestors) {
|
|
1006
|
+
if (pattern.type === "ObjectPattern") {
|
|
1007
|
+
for (const prop of pattern.properties) {
|
|
1008
|
+
if (prop.type === "Property" && prop.value.type === "Identifier") {
|
|
1009
|
+
scopeTracker.declareVariable(
|
|
1010
|
+
prop.value.name,
|
|
1011
|
+
prop.value,
|
|
1012
|
+
getLineNumber(prop.value),
|
|
1013
|
+
{
|
|
1014
|
+
isParameter,
|
|
1015
|
+
isDestructured: true
|
|
1016
|
+
}
|
|
1017
|
+
);
|
|
1018
|
+
} else if (prop.type === "RestElement" && prop.argument.type === "Identifier") {
|
|
1019
|
+
scopeTracker.declareVariable(
|
|
1020
|
+
prop.argument.name,
|
|
1021
|
+
prop.argument,
|
|
1022
|
+
getLineNumber(prop.argument),
|
|
1023
|
+
{
|
|
1024
|
+
isParameter,
|
|
1025
|
+
isDestructured: true
|
|
1026
|
+
}
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
} else if (pattern.type === "ArrayPattern") {
|
|
1031
|
+
for (const element of pattern.elements) {
|
|
1032
|
+
if (element && element.type === "Identifier") {
|
|
1033
|
+
scopeTracker.declareVariable(
|
|
1034
|
+
element.name,
|
|
1035
|
+
element,
|
|
1036
|
+
getLineNumber(element),
|
|
1037
|
+
{
|
|
1038
|
+
isParameter,
|
|
1039
|
+
isDestructured: true
|
|
1040
|
+
}
|
|
1041
|
+
);
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
606
1044
|
}
|
|
607
|
-
return { dominantConvention: "camelCase", conventionScore: 0.9 };
|
|
608
1045
|
}
|
|
609
1046
|
|
|
610
1047
|
// src/analyzers/patterns.ts
|
|
@@ -763,7 +1200,7 @@ async function analyzeConsistency(options) {
|
|
|
763
1200
|
...scanOptions
|
|
764
1201
|
} = options;
|
|
765
1202
|
const filePaths = await (0, import_core3.scanFiles)(scanOptions);
|
|
766
|
-
const namingIssues = checkNaming ? await
|
|
1203
|
+
const namingIssues = checkNaming ? await analyzeNamingAST(filePaths) : [];
|
|
767
1204
|
const patternIssues = checkPatterns ? await analyzePatterns(filePaths) : [];
|
|
768
1205
|
const results = [];
|
|
769
1206
|
const fileIssuesMap = /* @__PURE__ */ new Map();
|
|
@@ -822,7 +1259,6 @@ async function analyzeConsistency(options) {
|
|
|
822
1259
|
});
|
|
823
1260
|
}
|
|
824
1261
|
const recommendations = generateRecommendations(namingIssues, patternIssues);
|
|
825
|
-
const conventionAnalysis = detectNamingConventions(filePaths, namingIssues);
|
|
826
1262
|
return {
|
|
827
1263
|
summary: {
|
|
828
1264
|
totalIssues: namingIssues.length + patternIssues.length,
|
|
@@ -885,10 +1321,588 @@ function generateRecommendations(namingIssues, patternIssues) {
|
|
|
885
1321
|
}
|
|
886
1322
|
return recommendations;
|
|
887
1323
|
}
|
|
1324
|
+
|
|
1325
|
+
// src/analyzers/naming.ts
|
|
1326
|
+
var import_core4 = require("@aiready/core");
|
|
1327
|
+
var import_path2 = require("path");
|
|
1328
|
+
var COMMON_SHORT_WORDS2 = /* @__PURE__ */ new Set([
|
|
1329
|
+
// Full English words (1-3 letters)
|
|
1330
|
+
"day",
|
|
1331
|
+
"key",
|
|
1332
|
+
"net",
|
|
1333
|
+
"to",
|
|
1334
|
+
"go",
|
|
1335
|
+
"for",
|
|
1336
|
+
"not",
|
|
1337
|
+
"new",
|
|
1338
|
+
"old",
|
|
1339
|
+
"top",
|
|
1340
|
+
"end",
|
|
1341
|
+
"run",
|
|
1342
|
+
"try",
|
|
1343
|
+
"use",
|
|
1344
|
+
"get",
|
|
1345
|
+
"set",
|
|
1346
|
+
"add",
|
|
1347
|
+
"put",
|
|
1348
|
+
"map",
|
|
1349
|
+
"log",
|
|
1350
|
+
"row",
|
|
1351
|
+
"col",
|
|
1352
|
+
"tab",
|
|
1353
|
+
"box",
|
|
1354
|
+
"div",
|
|
1355
|
+
"nav",
|
|
1356
|
+
"tag",
|
|
1357
|
+
"any",
|
|
1358
|
+
"all",
|
|
1359
|
+
"one",
|
|
1360
|
+
"two",
|
|
1361
|
+
"out",
|
|
1362
|
+
"off",
|
|
1363
|
+
"on",
|
|
1364
|
+
"yes",
|
|
1365
|
+
"no",
|
|
1366
|
+
"now",
|
|
1367
|
+
"max",
|
|
1368
|
+
"min",
|
|
1369
|
+
"sum",
|
|
1370
|
+
"avg",
|
|
1371
|
+
"ref",
|
|
1372
|
+
"src",
|
|
1373
|
+
"dst",
|
|
1374
|
+
"raw",
|
|
1375
|
+
"def",
|
|
1376
|
+
"sub",
|
|
1377
|
+
"pub",
|
|
1378
|
+
"pre",
|
|
1379
|
+
"mid",
|
|
1380
|
+
"alt",
|
|
1381
|
+
"opt",
|
|
1382
|
+
"tmp",
|
|
1383
|
+
"ext",
|
|
1384
|
+
"sep",
|
|
1385
|
+
// Prepositions and conjunctions
|
|
1386
|
+
"and",
|
|
1387
|
+
"from",
|
|
1388
|
+
"how",
|
|
1389
|
+
"pad",
|
|
1390
|
+
"bar",
|
|
1391
|
+
"non",
|
|
1392
|
+
// Additional full words commonly flagged
|
|
1393
|
+
"tax",
|
|
1394
|
+
"cat",
|
|
1395
|
+
"dog",
|
|
1396
|
+
"car",
|
|
1397
|
+
"bus",
|
|
1398
|
+
"web",
|
|
1399
|
+
"app",
|
|
1400
|
+
"war",
|
|
1401
|
+
"law",
|
|
1402
|
+
"pay",
|
|
1403
|
+
"buy",
|
|
1404
|
+
"win",
|
|
1405
|
+
"cut",
|
|
1406
|
+
"hit",
|
|
1407
|
+
"hot",
|
|
1408
|
+
"pop",
|
|
1409
|
+
"job",
|
|
1410
|
+
"age",
|
|
1411
|
+
"act",
|
|
1412
|
+
"let",
|
|
1413
|
+
"lot",
|
|
1414
|
+
"bad",
|
|
1415
|
+
"big",
|
|
1416
|
+
"far",
|
|
1417
|
+
"few",
|
|
1418
|
+
"own",
|
|
1419
|
+
"per",
|
|
1420
|
+
"red",
|
|
1421
|
+
"low",
|
|
1422
|
+
"see",
|
|
1423
|
+
"six",
|
|
1424
|
+
"ten",
|
|
1425
|
+
"way",
|
|
1426
|
+
"who",
|
|
1427
|
+
"why",
|
|
1428
|
+
"yet",
|
|
1429
|
+
"via",
|
|
1430
|
+
"due",
|
|
1431
|
+
"fee",
|
|
1432
|
+
"fun",
|
|
1433
|
+
"gas",
|
|
1434
|
+
"gay",
|
|
1435
|
+
"god",
|
|
1436
|
+
"gun",
|
|
1437
|
+
"guy",
|
|
1438
|
+
"ice",
|
|
1439
|
+
"ill",
|
|
1440
|
+
"kid",
|
|
1441
|
+
"mad",
|
|
1442
|
+
"man",
|
|
1443
|
+
"mix",
|
|
1444
|
+
"mom",
|
|
1445
|
+
"mrs",
|
|
1446
|
+
"nor",
|
|
1447
|
+
"odd",
|
|
1448
|
+
"oil",
|
|
1449
|
+
"pan",
|
|
1450
|
+
"pet",
|
|
1451
|
+
"pit",
|
|
1452
|
+
"pot",
|
|
1453
|
+
"pow",
|
|
1454
|
+
"pro",
|
|
1455
|
+
"raw",
|
|
1456
|
+
"rep",
|
|
1457
|
+
"rid",
|
|
1458
|
+
"sad",
|
|
1459
|
+
"sea",
|
|
1460
|
+
"sit",
|
|
1461
|
+
"sky",
|
|
1462
|
+
"son",
|
|
1463
|
+
"tea",
|
|
1464
|
+
"tie",
|
|
1465
|
+
"tip",
|
|
1466
|
+
"van",
|
|
1467
|
+
"war",
|
|
1468
|
+
"win",
|
|
1469
|
+
"won"
|
|
1470
|
+
]);
|
|
1471
|
+
var ACCEPTABLE_ABBREVIATIONS2 = /* @__PURE__ */ new Set([
|
|
1472
|
+
// Standard identifiers
|
|
1473
|
+
"id",
|
|
1474
|
+
"uid",
|
|
1475
|
+
"gid",
|
|
1476
|
+
"pid",
|
|
1477
|
+
// Loop counters and iterators
|
|
1478
|
+
"i",
|
|
1479
|
+
"j",
|
|
1480
|
+
"k",
|
|
1481
|
+
"n",
|
|
1482
|
+
"m",
|
|
1483
|
+
// Web/Network
|
|
1484
|
+
"url",
|
|
1485
|
+
"uri",
|
|
1486
|
+
"api",
|
|
1487
|
+
"cdn",
|
|
1488
|
+
"dns",
|
|
1489
|
+
"ip",
|
|
1490
|
+
"tcp",
|
|
1491
|
+
"udp",
|
|
1492
|
+
"http",
|
|
1493
|
+
"ssl",
|
|
1494
|
+
"tls",
|
|
1495
|
+
"utm",
|
|
1496
|
+
"seo",
|
|
1497
|
+
"rss",
|
|
1498
|
+
"xhr",
|
|
1499
|
+
"ajax",
|
|
1500
|
+
"cors",
|
|
1501
|
+
"ws",
|
|
1502
|
+
"wss",
|
|
1503
|
+
// Data formats
|
|
1504
|
+
"json",
|
|
1505
|
+
"xml",
|
|
1506
|
+
"yaml",
|
|
1507
|
+
"csv",
|
|
1508
|
+
"html",
|
|
1509
|
+
"css",
|
|
1510
|
+
"svg",
|
|
1511
|
+
"pdf",
|
|
1512
|
+
// File types & extensions
|
|
1513
|
+
"img",
|
|
1514
|
+
"txt",
|
|
1515
|
+
"doc",
|
|
1516
|
+
"docx",
|
|
1517
|
+
"xlsx",
|
|
1518
|
+
"ppt",
|
|
1519
|
+
"md",
|
|
1520
|
+
"rst",
|
|
1521
|
+
"jpg",
|
|
1522
|
+
"png",
|
|
1523
|
+
"gif",
|
|
1524
|
+
// Databases
|
|
1525
|
+
"db",
|
|
1526
|
+
"sql",
|
|
1527
|
+
"orm",
|
|
1528
|
+
"dao",
|
|
1529
|
+
"dto",
|
|
1530
|
+
"ddb",
|
|
1531
|
+
"rds",
|
|
1532
|
+
"nosql",
|
|
1533
|
+
// File system
|
|
1534
|
+
"fs",
|
|
1535
|
+
"dir",
|
|
1536
|
+
"tmp",
|
|
1537
|
+
"src",
|
|
1538
|
+
"dst",
|
|
1539
|
+
"bin",
|
|
1540
|
+
"lib",
|
|
1541
|
+
"pkg",
|
|
1542
|
+
// Operating system
|
|
1543
|
+
"os",
|
|
1544
|
+
"env",
|
|
1545
|
+
"arg",
|
|
1546
|
+
"cli",
|
|
1547
|
+
"cmd",
|
|
1548
|
+
"exe",
|
|
1549
|
+
"cwd",
|
|
1550
|
+
"pwd",
|
|
1551
|
+
// UI/UX
|
|
1552
|
+
"ui",
|
|
1553
|
+
"ux",
|
|
1554
|
+
"gui",
|
|
1555
|
+
"dom",
|
|
1556
|
+
"ref",
|
|
1557
|
+
// Request/Response
|
|
1558
|
+
"req",
|
|
1559
|
+
"res",
|
|
1560
|
+
"ctx",
|
|
1561
|
+
"err",
|
|
1562
|
+
"msg",
|
|
1563
|
+
"auth",
|
|
1564
|
+
// Mathematics/Computing
|
|
1565
|
+
"max",
|
|
1566
|
+
"min",
|
|
1567
|
+
"avg",
|
|
1568
|
+
"sum",
|
|
1569
|
+
"abs",
|
|
1570
|
+
"cos",
|
|
1571
|
+
"sin",
|
|
1572
|
+
"tan",
|
|
1573
|
+
"log",
|
|
1574
|
+
"exp",
|
|
1575
|
+
"pow",
|
|
1576
|
+
"sqrt",
|
|
1577
|
+
"std",
|
|
1578
|
+
"var",
|
|
1579
|
+
"int",
|
|
1580
|
+
"num",
|
|
1581
|
+
"idx",
|
|
1582
|
+
// Time
|
|
1583
|
+
"now",
|
|
1584
|
+
"utc",
|
|
1585
|
+
"tz",
|
|
1586
|
+
"ms",
|
|
1587
|
+
"sec",
|
|
1588
|
+
"hr",
|
|
1589
|
+
"min",
|
|
1590
|
+
"yr",
|
|
1591
|
+
"mo",
|
|
1592
|
+
// Common patterns
|
|
1593
|
+
"app",
|
|
1594
|
+
"cfg",
|
|
1595
|
+
"config",
|
|
1596
|
+
"init",
|
|
1597
|
+
"len",
|
|
1598
|
+
"val",
|
|
1599
|
+
"str",
|
|
1600
|
+
"obj",
|
|
1601
|
+
"arr",
|
|
1602
|
+
"gen",
|
|
1603
|
+
"def",
|
|
1604
|
+
"raw",
|
|
1605
|
+
"new",
|
|
1606
|
+
"old",
|
|
1607
|
+
"pre",
|
|
1608
|
+
"post",
|
|
1609
|
+
"sub",
|
|
1610
|
+
"pub",
|
|
1611
|
+
// Programming/Framework specific
|
|
1612
|
+
"ts",
|
|
1613
|
+
"js",
|
|
1614
|
+
"jsx",
|
|
1615
|
+
"tsx",
|
|
1616
|
+
"py",
|
|
1617
|
+
"rb",
|
|
1618
|
+
"vue",
|
|
1619
|
+
"re",
|
|
1620
|
+
"fn",
|
|
1621
|
+
"fns",
|
|
1622
|
+
"mod",
|
|
1623
|
+
"opts",
|
|
1624
|
+
"dev",
|
|
1625
|
+
// Cloud/Infrastructure
|
|
1626
|
+
"s3",
|
|
1627
|
+
"ec2",
|
|
1628
|
+
"sqs",
|
|
1629
|
+
"sns",
|
|
1630
|
+
"vpc",
|
|
1631
|
+
"ami",
|
|
1632
|
+
"iam",
|
|
1633
|
+
"acl",
|
|
1634
|
+
"elb",
|
|
1635
|
+
"alb",
|
|
1636
|
+
"nlb",
|
|
1637
|
+
"aws",
|
|
1638
|
+
"ses",
|
|
1639
|
+
"gst",
|
|
1640
|
+
"cdk",
|
|
1641
|
+
"btn",
|
|
1642
|
+
"buf",
|
|
1643
|
+
"agg",
|
|
1644
|
+
"ocr",
|
|
1645
|
+
"ai",
|
|
1646
|
+
"cf",
|
|
1647
|
+
"cfn",
|
|
1648
|
+
"ga",
|
|
1649
|
+
// Metrics/Performance
|
|
1650
|
+
"fcp",
|
|
1651
|
+
"lcp",
|
|
1652
|
+
"cls",
|
|
1653
|
+
"ttfb",
|
|
1654
|
+
"tti",
|
|
1655
|
+
"fid",
|
|
1656
|
+
"fps",
|
|
1657
|
+
"qps",
|
|
1658
|
+
"rps",
|
|
1659
|
+
"tps",
|
|
1660
|
+
"wpm",
|
|
1661
|
+
// Testing & i18n
|
|
1662
|
+
"po",
|
|
1663
|
+
"e2e",
|
|
1664
|
+
"a11y",
|
|
1665
|
+
"i18n",
|
|
1666
|
+
"l10n",
|
|
1667
|
+
"spy",
|
|
1668
|
+
// Domain-specific abbreviations (context-aware)
|
|
1669
|
+
"sk",
|
|
1670
|
+
"fy",
|
|
1671
|
+
"faq",
|
|
1672
|
+
"og",
|
|
1673
|
+
"seo",
|
|
1674
|
+
"cta",
|
|
1675
|
+
"roi",
|
|
1676
|
+
"kpi",
|
|
1677
|
+
"ttl",
|
|
1678
|
+
"pct",
|
|
1679
|
+
// Technical abbreviations
|
|
1680
|
+
"mac",
|
|
1681
|
+
"hex",
|
|
1682
|
+
"esm",
|
|
1683
|
+
"git",
|
|
1684
|
+
"rec",
|
|
1685
|
+
"loc",
|
|
1686
|
+
"dup",
|
|
1687
|
+
// Boolean helpers (these are intentional short names)
|
|
1688
|
+
"is",
|
|
1689
|
+
"has",
|
|
1690
|
+
"can",
|
|
1691
|
+
"did",
|
|
1692
|
+
"was",
|
|
1693
|
+
"are",
|
|
1694
|
+
// Date/Time context (when in date contexts)
|
|
1695
|
+
"d",
|
|
1696
|
+
"t",
|
|
1697
|
+
"dt",
|
|
1698
|
+
// Coverage metrics (industry standard: statements/branches/functions/lines)
|
|
1699
|
+
"s",
|
|
1700
|
+
"b",
|
|
1701
|
+
"f",
|
|
1702
|
+
"l",
|
|
1703
|
+
// Common media/content abbreviations
|
|
1704
|
+
"vid",
|
|
1705
|
+
"pic",
|
|
1706
|
+
"img",
|
|
1707
|
+
"doc",
|
|
1708
|
+
"msg"
|
|
1709
|
+
]);
|
|
1710
|
+
async function analyzeNaming(files) {
|
|
1711
|
+
const issues = [];
|
|
1712
|
+
const rootDir = files.length > 0 ? (0, import_path2.dirname)(files[0]) : process.cwd();
|
|
1713
|
+
const config = (0, import_core4.loadConfig)(rootDir);
|
|
1714
|
+
const consistencyConfig = config?.tools?.["consistency"];
|
|
1715
|
+
const customAbbreviations = new Set(consistencyConfig?.acceptedAbbreviations || []);
|
|
1716
|
+
const customShortWords = new Set(consistencyConfig?.shortWords || []);
|
|
1717
|
+
const disabledChecks = new Set(consistencyConfig?.disableChecks || []);
|
|
1718
|
+
for (const file of files) {
|
|
1719
|
+
const content = await (0, import_core4.readFileContent)(file);
|
|
1720
|
+
const fileIssues = analyzeFileNaming(file, content, customAbbreviations, customShortWords, disabledChecks);
|
|
1721
|
+
issues.push(...fileIssues);
|
|
1722
|
+
}
|
|
1723
|
+
return issues;
|
|
1724
|
+
}
|
|
1725
|
+
function analyzeFileNaming(file, content, customAbbreviations, customShortWords, disabledChecks) {
|
|
1726
|
+
const issues = [];
|
|
1727
|
+
const isTestFile = file.match(/\.(test|spec)\.(ts|tsx|js|jsx)$/);
|
|
1728
|
+
const lines = content.split("\n");
|
|
1729
|
+
const allAbbreviations = /* @__PURE__ */ new Set([...ACCEPTABLE_ABBREVIATIONS2, ...customAbbreviations]);
|
|
1730
|
+
const allShortWords = /* @__PURE__ */ new Set([...COMMON_SHORT_WORDS2, ...customShortWords]);
|
|
1731
|
+
const getContextWindow = (index, windowSize = 3) => {
|
|
1732
|
+
const start = Math.max(0, index - windowSize);
|
|
1733
|
+
const end = Math.min(lines.length, index + windowSize + 1);
|
|
1734
|
+
return lines.slice(start, end).join("\n");
|
|
1735
|
+
};
|
|
1736
|
+
const isShortLivedVariable = (varName, declarationIndex) => {
|
|
1737
|
+
const searchRange = 5;
|
|
1738
|
+
const endIndex = Math.min(lines.length, declarationIndex + searchRange + 1);
|
|
1739
|
+
let usageCount = 0;
|
|
1740
|
+
for (let i = declarationIndex; i < endIndex; i++) {
|
|
1741
|
+
const regex = new RegExp(`\\b${varName}\\b`, "g");
|
|
1742
|
+
const matches = lines[i].match(regex);
|
|
1743
|
+
if (matches) {
|
|
1744
|
+
usageCount += matches.length;
|
|
1745
|
+
}
|
|
1746
|
+
}
|
|
1747
|
+
return usageCount >= 2 && usageCount <= 3;
|
|
1748
|
+
};
|
|
1749
|
+
lines.forEach((line, index) => {
|
|
1750
|
+
const lineNumber = index + 1;
|
|
1751
|
+
const contextWindow = getContextWindow(index);
|
|
1752
|
+
if (!disabledChecks.has("single-letter")) {
|
|
1753
|
+
const singleLetterMatches = line.matchAll(/\b(?:const|let|var)\s+([a-hm-z])\s*=/gi);
|
|
1754
|
+
for (const match of singleLetterMatches) {
|
|
1755
|
+
const letter = match[1].toLowerCase();
|
|
1756
|
+
const isCoverageContext2 = /coverage|summary|metrics|pct|percent/i.test(line) || /\.(?:statements|branches|functions|lines)\.pct/i.test(line);
|
|
1757
|
+
if (isCoverageContext2 && ["s", "b", "f", "l"].includes(letter)) {
|
|
1758
|
+
continue;
|
|
1759
|
+
}
|
|
1760
|
+
const isInLoopContext = line.includes("for") || /\.(map|filter|forEach|reduce|find|some|every)\s*\(/.test(line) || line.includes("=>") || // Arrow function
|
|
1761
|
+
/\w+\s*=>\s*/.test(line);
|
|
1762
|
+
const isI18nContext = line.includes("useTranslation") || line.includes("i18n.t") || /\bt\s*\(['"]/.test(line);
|
|
1763
|
+
const isArrowFunctionParam = /\(\s*[a-z]\s*(?:,\s*[a-z]\s*)*\)\s*=>/.test(line) || // (s) => or (a, b) =>
|
|
1764
|
+
/[a-z]\s*=>/.test(line) || // s => on same line
|
|
1765
|
+
// Multi-line arrow function detection: look for pattern in context window
|
|
1766
|
+
new RegExp(`\\b${letter}\\s*\\)\\s*$`).test(line) && /=>/.test(contextWindow) || // (s)\n =>
|
|
1767
|
+
new RegExp(`\\.(?:map|filter|forEach|reduce|find|some|every)\\s*\\(\\s*$`).test(lines[index - 1] || "") && /=>/.test(contextWindow);
|
|
1768
|
+
const isShortLived = isShortLivedVariable(letter, index);
|
|
1769
|
+
if (!isInLoopContext && !isI18nContext && !isArrowFunctionParam && !isShortLived && !["x", "y", "z", "i", "j", "k", "l", "n", "m"].includes(letter)) {
|
|
1770
|
+
if (isTestFile && ["a", "b", "c", "d", "e", "f", "s"].includes(letter)) {
|
|
1771
|
+
continue;
|
|
1772
|
+
}
|
|
1773
|
+
issues.push({
|
|
1774
|
+
file,
|
|
1775
|
+
line: lineNumber,
|
|
1776
|
+
type: "poor-naming",
|
|
1777
|
+
identifier: match[1],
|
|
1778
|
+
severity: "minor",
|
|
1779
|
+
suggestion: `Use descriptive variable name instead of single letter '${match[1]}'`
|
|
1780
|
+
});
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
if (!disabledChecks.has("abbreviation")) {
|
|
1785
|
+
const abbreviationMatches = line.matchAll(/\b(?:const|let|var)\s+([a-z]{1,3})(?=[A-Z]|_|\s*=)/g);
|
|
1786
|
+
for (const match of abbreviationMatches) {
|
|
1787
|
+
const abbrev = match[1].toLowerCase();
|
|
1788
|
+
if (allShortWords.has(abbrev)) {
|
|
1789
|
+
continue;
|
|
1790
|
+
}
|
|
1791
|
+
if (allAbbreviations.has(abbrev)) {
|
|
1792
|
+
continue;
|
|
1793
|
+
}
|
|
1794
|
+
const isArrowFunctionParam = /\(\s*[a-z]\s*(?:,\s*[a-z]\s*)*\)\s*=>/.test(line) || // (s) => or (a, b) =>
|
|
1795
|
+
new RegExp(`\\b${abbrev}\\s*=>`).test(line) || // s => on same line
|
|
1796
|
+
// Multi-line arrow function: check context window
|
|
1797
|
+
new RegExp(`\\b${abbrev}\\s*\\)\\s*$`).test(line) && /=>/.test(contextWindow) || // (s)\n =>
|
|
1798
|
+
new RegExp(`\\.(?:map|filter|forEach|reduce|find|some|every)\\s*\\(\\s*$`).test(lines[index - 1] || "") && new RegExp(`^\\s*${abbrev}\\s*=>`).test(line);
|
|
1799
|
+
if (isArrowFunctionParam) {
|
|
1800
|
+
continue;
|
|
1801
|
+
}
|
|
1802
|
+
if (abbrev.length <= 2) {
|
|
1803
|
+
const isDateTimeContext = /date|time|day|hour|minute|second|timestamp/i.test(line);
|
|
1804
|
+
if (isDateTimeContext && ["d", "t", "dt"].includes(abbrev)) {
|
|
1805
|
+
continue;
|
|
1806
|
+
}
|
|
1807
|
+
const isUserContext = /user|auth|account/i.test(line);
|
|
1808
|
+
if (isUserContext && abbrev === "u") {
|
|
1809
|
+
continue;
|
|
1810
|
+
}
|
|
1811
|
+
}
|
|
1812
|
+
issues.push({
|
|
1813
|
+
file,
|
|
1814
|
+
line: lineNumber,
|
|
1815
|
+
type: "abbreviation",
|
|
1816
|
+
identifier: match[1],
|
|
1817
|
+
severity: "info",
|
|
1818
|
+
suggestion: `Consider using full word instead of abbreviation '${match[1]}'`
|
|
1819
|
+
});
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
if (!disabledChecks.has("convention-mix") && file.match(/\.(ts|tsx|js|jsx)$/)) {
|
|
1823
|
+
const camelCaseVars = line.match(/\b(?:const|let|var)\s+([a-z][a-zA-Z0-9]*)\s*=/);
|
|
1824
|
+
const snakeCaseVars = line.match(/\b(?:const|let|var)\s+([a-z][a-z0-9]*_[a-z0-9_]*)\s*=/);
|
|
1825
|
+
if (snakeCaseVars) {
|
|
1826
|
+
issues.push({
|
|
1827
|
+
file,
|
|
1828
|
+
line: lineNumber,
|
|
1829
|
+
type: "convention-mix",
|
|
1830
|
+
identifier: snakeCaseVars[1],
|
|
1831
|
+
severity: "minor",
|
|
1832
|
+
suggestion: `Use camelCase '${snakeCaseToCamelCase(snakeCaseVars[1])}' instead of snake_case in TypeScript/JavaScript`
|
|
1833
|
+
});
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
if (!disabledChecks.has("unclear")) {
|
|
1837
|
+
const booleanMatches = line.matchAll(/\b(?:const|let|var)\s+([a-z][a-zA-Z0-9]*)\s*:\s*boolean/gi);
|
|
1838
|
+
for (const match of booleanMatches) {
|
|
1839
|
+
const name = match[1];
|
|
1840
|
+
if (!name.match(/^(is|has|should|can|will|did)/i)) {
|
|
1841
|
+
issues.push({
|
|
1842
|
+
file,
|
|
1843
|
+
line: lineNumber,
|
|
1844
|
+
type: "unclear",
|
|
1845
|
+
identifier: name,
|
|
1846
|
+
severity: "info",
|
|
1847
|
+
suggestion: `Boolean variable '${name}' should start with is/has/should/can for clarity`
|
|
1848
|
+
});
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
}
|
|
1852
|
+
if (!disabledChecks.has("unclear")) {
|
|
1853
|
+
const functionMatches = line.matchAll(/function\s+([a-z][a-zA-Z0-9]*)/g);
|
|
1854
|
+
for (const match of functionMatches) {
|
|
1855
|
+
const name = match[1];
|
|
1856
|
+
const isKeyword = ["for", "if", "else", "while", "do", "switch", "case", "break", "continue", "return", "throw", "try", "catch", "finally", "with", "yield", "await"].includes(name);
|
|
1857
|
+
if (isKeyword) {
|
|
1858
|
+
continue;
|
|
1859
|
+
}
|
|
1860
|
+
const isEntryPoint = ["main", "init", "setup", "bootstrap"].includes(name);
|
|
1861
|
+
if (isEntryPoint) {
|
|
1862
|
+
continue;
|
|
1863
|
+
}
|
|
1864
|
+
const isFactoryPattern = name.match(/(Factory|Builder|Creator|Generator|Provider|Adapter|Mock)$/);
|
|
1865
|
+
const isEventHandler = name.match(/^on[A-Z]/);
|
|
1866
|
+
const isDescriptiveLong = name.length > 15;
|
|
1867
|
+
const isReactHook = name.match(/^use[A-Z]/);
|
|
1868
|
+
const isDescriptivePattern = name.match(/^(default|total|count|sum|avg|max|min|initial|current|previous|next)\w+/) || name.match(/\w+(Count|Total|Sum|Average|List|Map|Set|Config|Settings|Options|Props|Data|Info|Details|State|Status|Response|Result)$/);
|
|
1869
|
+
const isHelperPattern = name.match(/^(to|from|with|without|for|as|into)\w+/) || // toMetadata, withLogger, forPath
|
|
1870
|
+
name.match(/^\w+(To|From|With|Without|For|As|Into)\w*$/);
|
|
1871
|
+
const isUtilityName = ["cn", "proxy", "sitemap", "robots", "gtag"].includes(name);
|
|
1872
|
+
const capitalCount = (name.match(/[A-Z]/g) || []).length;
|
|
1873
|
+
const isCompoundWord = capitalCount >= 3;
|
|
1874
|
+
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)/);
|
|
1875
|
+
if (!hasActionVerb && !isFactoryPattern && !isEventHandler && !isDescriptiveLong && !isDescriptivePattern && !isCompoundWord && !isHelperPattern && !isUtilityName && !isReactHook) {
|
|
1876
|
+
issues.push({
|
|
1877
|
+
file,
|
|
1878
|
+
line: lineNumber,
|
|
1879
|
+
type: "unclear",
|
|
1880
|
+
identifier: name,
|
|
1881
|
+
severity: "info",
|
|
1882
|
+
suggestion: `Function '${name}' should start with an action verb (get, set, create, etc.)`
|
|
1883
|
+
});
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
});
|
|
1888
|
+
return issues;
|
|
1889
|
+
}
|
|
1890
|
+
function snakeCaseToCamelCase(str) {
|
|
1891
|
+
return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
1892
|
+
}
|
|
1893
|
+
function detectNamingConventions(files, allIssues) {
|
|
1894
|
+
const camelCaseCount = allIssues.filter((i) => i.type === "convention-mix").length;
|
|
1895
|
+
const totalChecks = files.length * 10;
|
|
1896
|
+
if (camelCaseCount / totalChecks > 0.3) {
|
|
1897
|
+
return { dominantConvention: "mixed", conventionScore: 0.5 };
|
|
1898
|
+
}
|
|
1899
|
+
return { dominantConvention: "camelCase", conventionScore: 0.9 };
|
|
1900
|
+
}
|
|
888
1901
|
// Annotate the CommonJS export names for ESM import in node:
|
|
889
1902
|
0 && (module.exports = {
|
|
890
1903
|
analyzeConsistency,
|
|
891
1904
|
analyzeNaming,
|
|
1905
|
+
analyzeNamingAST,
|
|
892
1906
|
analyzePatterns,
|
|
893
1907
|
detectNamingConventions
|
|
894
1908
|
});
|