@aiready/consistency 0.4.1 → 0.6.0
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 +10 -10
- package/.turbo/turbo-test.log +49 -7
- package/PHASE5-RESULTS.md +277 -0
- package/dist/chunk-HAOJLJNB.mjs +1290 -0
- package/dist/chunk-IVRBV7SE.mjs +1295 -0
- package/dist/chunk-TXHPUU7A.mjs +863 -0
- package/dist/chunk-VODCPPET.mjs +1292 -0
- package/dist/chunk-WGH4TGZ3.mjs +1288 -0
- package/dist/cli.js +632 -184
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +6 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +1204 -177
- package/dist/index.mjs +581 -4
- package/package.json +3 -1
- package/src/analyzer.ts +4 -4
- package/src/analyzers/naming-ast.ts +375 -0
- package/src/analyzers/naming.ts +12 -1
- 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,19 +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
|
-
"dt"
|
|
808
|
+
"dt",
|
|
809
|
+
"s",
|
|
810
|
+
"b",
|
|
811
|
+
"f",
|
|
812
|
+
"l",
|
|
813
|
+
// Coverage metrics
|
|
814
|
+
"vid",
|
|
815
|
+
"pic",
|
|
816
|
+
"img",
|
|
817
|
+
"doc",
|
|
818
|
+
"msg"
|
|
406
819
|
]);
|
|
407
|
-
async function
|
|
820
|
+
async function analyzeNamingAST(files) {
|
|
408
821
|
const issues = [];
|
|
409
822
|
const rootDir = files.length > 0 ? (0, import_path.dirname)(files[0]) : process.cwd();
|
|
410
823
|
const config = (0, import_core.loadConfig)(rootDir);
|
|
@@ -412,184 +825,221 @@ async function analyzeNaming(files) {
|
|
|
412
825
|
const customAbbreviations = new Set(consistencyConfig?.acceptedAbbreviations || []);
|
|
413
826
|
const customShortWords = new Set(consistencyConfig?.shortWords || []);
|
|
414
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]);
|
|
415
830
|
for (const file of files) {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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
|
+
}
|
|
419
845
|
}
|
|
420
846
|
return issues;
|
|
421
847
|
}
|
|
422
|
-
function
|
|
848
|
+
function analyzeFileNamingAST(file, ast, allAbbreviations, allShortWords, disabledChecks) {
|
|
423
849
|
const issues = [];
|
|
424
|
-
const
|
|
425
|
-
const
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
usageCount += matches.length;
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
return usageCount >= 2 && usageCount <= 3;
|
|
445
|
-
};
|
|
446
|
-
lines.forEach((line, index) => {
|
|
447
|
-
const lineNumber = index + 1;
|
|
448
|
-
const contextWindow = getContextWindow(index);
|
|
449
|
-
if (!disabledChecks.has("single-letter")) {
|
|
450
|
-
const singleLetterMatches = line.matchAll(/\b(?:const|let|var)\s+([a-hm-z])\s*=/gi);
|
|
451
|
-
for (const match of singleLetterMatches) {
|
|
452
|
-
const letter = match[1].toLowerCase();
|
|
453
|
-
const isInLoopContext = line.includes("for") || /\.(map|filter|forEach|reduce|find|some|every)\s*\(/.test(line) || line.includes("=>") || // Arrow function
|
|
454
|
-
/\w+\s*=>\s*/.test(line);
|
|
455
|
-
const isI18nContext = line.includes("useTranslation") || line.includes("i18n.t") || /\bt\s*\(['"]/.test(line);
|
|
456
|
-
const isArrowFunctionParam = /\(\s*[a-z]\s*(?:,\s*[a-z]\s*)*\)\s*=>/.test(line) || // (s) => or (a, b) =>
|
|
457
|
-
/[a-z]\s*=>/.test(line) || // s => on same line
|
|
458
|
-
// Multi-line arrow function detection: look for pattern in context window
|
|
459
|
-
new RegExp(`\\b${letter}\\s*\\)\\s*$`).test(line) && /=>/.test(contextWindow) || // (s)\n =>
|
|
460
|
-
new RegExp(`\\.(?:map|filter|forEach|reduce|find|some|every)\\s*\\(\\s*$`).test(lines[index - 1] || "") && /=>/.test(contextWindow);
|
|
461
|
-
const isShortLived = isShortLivedVariable(letter, index);
|
|
462
|
-
if (!isInLoopContext && !isI18nContext && !isArrowFunctionParam && !isShortLived && !["x", "y", "z", "i", "j", "k", "l", "n", "m"].includes(letter)) {
|
|
463
|
-
if (isTestFile && ["a", "b", "c", "d", "e", "f", "s"].includes(letter)) {
|
|
464
|
-
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
|
+
}
|
|
465
867
|
}
|
|
466
|
-
issues.push({
|
|
467
|
-
file,
|
|
468
|
-
line: lineNumber,
|
|
469
|
-
type: "poor-naming",
|
|
470
|
-
identifier: match[1],
|
|
471
|
-
severity: "minor",
|
|
472
|
-
suggestion: `Use descriptive variable name instead of single letter '${match[1]}'`
|
|
473
|
-
});
|
|
474
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);
|
|
475
875
|
}
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
new RegExp(`\\.(?:map|filter|forEach|reduce|find|some|every)\\s*\\(\\s*$`).test(lines[index - 1] || "") && new RegExp(`^\\s*${abbrev}\\s*=>`).test(line);
|
|
492
|
-
if (isArrowFunctionParam) {
|
|
493
|
-
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);
|
|
494
891
|
}
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
const isUserContext = /user|auth|account/i.test(line);
|
|
501
|
-
if (isUserContext && abbrev === "u") {
|
|
502
|
-
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);
|
|
503
897
|
}
|
|
504
898
|
}
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
suggestion: `Consider using full word instead of abbreviation '${match[1]}'`
|
|
512
|
-
});
|
|
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();
|
|
513
905
|
}
|
|
514
906
|
}
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
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) {
|
|
519
928
|
issues.push({
|
|
520
929
|
file,
|
|
521
|
-
line
|
|
522
|
-
type: "
|
|
523
|
-
identifier:
|
|
524
|
-
severity: "minor",
|
|
525
|
-
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}'`
|
|
526
935
|
});
|
|
527
936
|
}
|
|
937
|
+
continue;
|
|
528
938
|
}
|
|
529
|
-
if (
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
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) {
|
|
534
943
|
issues.push({
|
|
535
944
|
file,
|
|
536
|
-
line
|
|
537
|
-
type: "
|
|
945
|
+
line,
|
|
946
|
+
type: "abbreviation",
|
|
538
947
|
identifier: name,
|
|
539
|
-
severity: "info",
|
|
540
|
-
suggestion: `
|
|
948
|
+
severity: adjustSeverity("info", context, "abbreviation"),
|
|
949
|
+
suggestion: `Consider using full word instead of abbreviation '${name}'`
|
|
541
950
|
});
|
|
542
951
|
}
|
|
543
952
|
}
|
|
953
|
+
continue;
|
|
544
954
|
}
|
|
545
|
-
if (!disabledChecks.has("
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
}
|
|
557
|
-
const isFactoryPattern = name.match(/(Factory|Builder|Creator|Generator|Provider|Adapter|Mock)$/);
|
|
558
|
-
const isEventHandler = name.match(/^on[A-Z]/);
|
|
559
|
-
const isDescriptiveLong = name.length > 15;
|
|
560
|
-
const isReactHook = name.match(/^use[A-Z]/);
|
|
561
|
-
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)$/);
|
|
562
|
-
const isHelperPattern = name.match(/^(to|from|with|without|for|as|into)\w+/) || // toMetadata, withLogger, forPath
|
|
563
|
-
name.match(/^\w+(To|From|With|Without|For|As|Into)\w*$/);
|
|
564
|
-
const isUtilityName = ["cn", "proxy", "sitemap", "robots", "gtag"].includes(name);
|
|
565
|
-
const capitalCount = (name.match(/[A-Z]/g) || []).length;
|
|
566
|
-
const isCompoundWord = capitalCount >= 3;
|
|
567
|
-
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)/);
|
|
568
|
-
if (!hasActionVerb && !isFactoryPattern && !isEventHandler && !isDescriptiveLong && !isDescriptivePattern && !isCompoundWord && !isHelperPattern && !isUtilityName && !isReactHook) {
|
|
569
|
-
issues.push({
|
|
570
|
-
file,
|
|
571
|
-
line: lineNumber,
|
|
572
|
-
type: "unclear",
|
|
573
|
-
identifier: name,
|
|
574
|
-
severity: "info",
|
|
575
|
-
suggestion: `Function '${name}' should start with an action verb (get, set, create, etc.)`
|
|
576
|
-
});
|
|
577
|
-
}
|
|
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
|
+
});
|
|
578
966
|
}
|
|
579
967
|
}
|
|
580
|
-
}
|
|
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)/);
|
|
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+/);
|
|
983
|
+
const isUtilityName = ["cn", "proxy", "sitemap", "robots", "gtag"].includes(name);
|
|
984
|
+
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)$/);
|
|
985
|
+
const capitalCount = (name.match(/[A-Z]/g) || []).length;
|
|
986
|
+
const isCompoundWord = capitalCount >= 3;
|
|
987
|
+
if (!hasActionVerb && !isFactoryPattern && !isEventHandler && !isDescriptiveLong && !isReactHook && !isHelperPattern && !isUtilityName && !isDescriptivePattern && !isCompoundWord) {
|
|
988
|
+
issues.push({
|
|
989
|
+
file,
|
|
990
|
+
line,
|
|
991
|
+
type: "unclear",
|
|
992
|
+
identifier: name,
|
|
993
|
+
severity: adjustSeverity("info", context, "unclear"),
|
|
994
|
+
suggestion: `Function '${name}' should start with an action verb (get, set, create, etc.)`
|
|
995
|
+
});
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
581
1001
|
return issues;
|
|
582
1002
|
}
|
|
583
|
-
function
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
1003
|
+
function extractIdentifiersFromPattern(pattern, scopeTracker, isParameter, ancestors) {
|
|
1004
|
+
if (pattern.type === "ObjectPattern") {
|
|
1005
|
+
for (const prop of pattern.properties) {
|
|
1006
|
+
if (prop.type === "Property" && prop.value.type === "Identifier") {
|
|
1007
|
+
scopeTracker.declareVariable(
|
|
1008
|
+
prop.value.name,
|
|
1009
|
+
prop.value,
|
|
1010
|
+
getLineNumber(prop.value),
|
|
1011
|
+
{
|
|
1012
|
+
isParameter,
|
|
1013
|
+
isDestructured: true
|
|
1014
|
+
}
|
|
1015
|
+
);
|
|
1016
|
+
} else if (prop.type === "RestElement" && prop.argument.type === "Identifier") {
|
|
1017
|
+
scopeTracker.declareVariable(
|
|
1018
|
+
prop.argument.name,
|
|
1019
|
+
prop.argument,
|
|
1020
|
+
getLineNumber(prop.argument),
|
|
1021
|
+
{
|
|
1022
|
+
isParameter,
|
|
1023
|
+
isDestructured: true
|
|
1024
|
+
}
|
|
1025
|
+
);
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
} else if (pattern.type === "ArrayPattern") {
|
|
1029
|
+
for (const element of pattern.elements) {
|
|
1030
|
+
if (element && element.type === "Identifier") {
|
|
1031
|
+
scopeTracker.declareVariable(
|
|
1032
|
+
element.name,
|
|
1033
|
+
element,
|
|
1034
|
+
getLineNumber(element),
|
|
1035
|
+
{
|
|
1036
|
+
isParameter,
|
|
1037
|
+
isDestructured: true
|
|
1038
|
+
}
|
|
1039
|
+
);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
591
1042
|
}
|
|
592
|
-
return { dominantConvention: "camelCase", conventionScore: 0.9 };
|
|
593
1043
|
}
|
|
594
1044
|
|
|
595
1045
|
// src/analyzers/patterns.ts
|
|
@@ -748,7 +1198,7 @@ async function analyzeConsistency(options) {
|
|
|
748
1198
|
...scanOptions
|
|
749
1199
|
} = options;
|
|
750
1200
|
const filePaths = await (0, import_core3.scanFiles)(scanOptions);
|
|
751
|
-
const namingIssues = checkNaming ? await
|
|
1201
|
+
const namingIssues = checkNaming ? await analyzeNamingAST(filePaths) : [];
|
|
752
1202
|
const patternIssues = checkPatterns ? await analyzePatterns(filePaths) : [];
|
|
753
1203
|
const results = [];
|
|
754
1204
|
const fileIssuesMap = /* @__PURE__ */ new Map();
|
|
@@ -807,7 +1257,6 @@ async function analyzeConsistency(options) {
|
|
|
807
1257
|
});
|
|
808
1258
|
}
|
|
809
1259
|
const recommendations = generateRecommendations(namingIssues, patternIssues);
|
|
810
|
-
const conventionAnalysis = detectNamingConventions(filePaths, namingIssues);
|
|
811
1260
|
return {
|
|
812
1261
|
summary: {
|
|
813
1262
|
totalIssues: namingIssues.length + patternIssues.length,
|
|
@@ -870,10 +1319,588 @@ function generateRecommendations(namingIssues, patternIssues) {
|
|
|
870
1319
|
}
|
|
871
1320
|
return recommendations;
|
|
872
1321
|
}
|
|
1322
|
+
|
|
1323
|
+
// src/analyzers/naming.ts
|
|
1324
|
+
var import_core4 = require("@aiready/core");
|
|
1325
|
+
var import_path2 = require("path");
|
|
1326
|
+
var COMMON_SHORT_WORDS2 = /* @__PURE__ */ new Set([
|
|
1327
|
+
// Full English words (1-3 letters)
|
|
1328
|
+
"day",
|
|
1329
|
+
"key",
|
|
1330
|
+
"net",
|
|
1331
|
+
"to",
|
|
1332
|
+
"go",
|
|
1333
|
+
"for",
|
|
1334
|
+
"not",
|
|
1335
|
+
"new",
|
|
1336
|
+
"old",
|
|
1337
|
+
"top",
|
|
1338
|
+
"end",
|
|
1339
|
+
"run",
|
|
1340
|
+
"try",
|
|
1341
|
+
"use",
|
|
1342
|
+
"get",
|
|
1343
|
+
"set",
|
|
1344
|
+
"add",
|
|
1345
|
+
"put",
|
|
1346
|
+
"map",
|
|
1347
|
+
"log",
|
|
1348
|
+
"row",
|
|
1349
|
+
"col",
|
|
1350
|
+
"tab",
|
|
1351
|
+
"box",
|
|
1352
|
+
"div",
|
|
1353
|
+
"nav",
|
|
1354
|
+
"tag",
|
|
1355
|
+
"any",
|
|
1356
|
+
"all",
|
|
1357
|
+
"one",
|
|
1358
|
+
"two",
|
|
1359
|
+
"out",
|
|
1360
|
+
"off",
|
|
1361
|
+
"on",
|
|
1362
|
+
"yes",
|
|
1363
|
+
"no",
|
|
1364
|
+
"now",
|
|
1365
|
+
"max",
|
|
1366
|
+
"min",
|
|
1367
|
+
"sum",
|
|
1368
|
+
"avg",
|
|
1369
|
+
"ref",
|
|
1370
|
+
"src",
|
|
1371
|
+
"dst",
|
|
1372
|
+
"raw",
|
|
1373
|
+
"def",
|
|
1374
|
+
"sub",
|
|
1375
|
+
"pub",
|
|
1376
|
+
"pre",
|
|
1377
|
+
"mid",
|
|
1378
|
+
"alt",
|
|
1379
|
+
"opt",
|
|
1380
|
+
"tmp",
|
|
1381
|
+
"ext",
|
|
1382
|
+
"sep",
|
|
1383
|
+
// Prepositions and conjunctions
|
|
1384
|
+
"and",
|
|
1385
|
+
"from",
|
|
1386
|
+
"how",
|
|
1387
|
+
"pad",
|
|
1388
|
+
"bar",
|
|
1389
|
+
"non",
|
|
1390
|
+
// Additional full words commonly flagged
|
|
1391
|
+
"tax",
|
|
1392
|
+
"cat",
|
|
1393
|
+
"dog",
|
|
1394
|
+
"car",
|
|
1395
|
+
"bus",
|
|
1396
|
+
"web",
|
|
1397
|
+
"app",
|
|
1398
|
+
"war",
|
|
1399
|
+
"law",
|
|
1400
|
+
"pay",
|
|
1401
|
+
"buy",
|
|
1402
|
+
"win",
|
|
1403
|
+
"cut",
|
|
1404
|
+
"hit",
|
|
1405
|
+
"hot",
|
|
1406
|
+
"pop",
|
|
1407
|
+
"job",
|
|
1408
|
+
"age",
|
|
1409
|
+
"act",
|
|
1410
|
+
"let",
|
|
1411
|
+
"lot",
|
|
1412
|
+
"bad",
|
|
1413
|
+
"big",
|
|
1414
|
+
"far",
|
|
1415
|
+
"few",
|
|
1416
|
+
"own",
|
|
1417
|
+
"per",
|
|
1418
|
+
"red",
|
|
1419
|
+
"low",
|
|
1420
|
+
"see",
|
|
1421
|
+
"six",
|
|
1422
|
+
"ten",
|
|
1423
|
+
"way",
|
|
1424
|
+
"who",
|
|
1425
|
+
"why",
|
|
1426
|
+
"yet",
|
|
1427
|
+
"via",
|
|
1428
|
+
"due",
|
|
1429
|
+
"fee",
|
|
1430
|
+
"fun",
|
|
1431
|
+
"gas",
|
|
1432
|
+
"gay",
|
|
1433
|
+
"god",
|
|
1434
|
+
"gun",
|
|
1435
|
+
"guy",
|
|
1436
|
+
"ice",
|
|
1437
|
+
"ill",
|
|
1438
|
+
"kid",
|
|
1439
|
+
"mad",
|
|
1440
|
+
"man",
|
|
1441
|
+
"mix",
|
|
1442
|
+
"mom",
|
|
1443
|
+
"mrs",
|
|
1444
|
+
"nor",
|
|
1445
|
+
"odd",
|
|
1446
|
+
"oil",
|
|
1447
|
+
"pan",
|
|
1448
|
+
"pet",
|
|
1449
|
+
"pit",
|
|
1450
|
+
"pot",
|
|
1451
|
+
"pow",
|
|
1452
|
+
"pro",
|
|
1453
|
+
"raw",
|
|
1454
|
+
"rep",
|
|
1455
|
+
"rid",
|
|
1456
|
+
"sad",
|
|
1457
|
+
"sea",
|
|
1458
|
+
"sit",
|
|
1459
|
+
"sky",
|
|
1460
|
+
"son",
|
|
1461
|
+
"tea",
|
|
1462
|
+
"tie",
|
|
1463
|
+
"tip",
|
|
1464
|
+
"van",
|
|
1465
|
+
"war",
|
|
1466
|
+
"win",
|
|
1467
|
+
"won"
|
|
1468
|
+
]);
|
|
1469
|
+
var ACCEPTABLE_ABBREVIATIONS2 = /* @__PURE__ */ new Set([
|
|
1470
|
+
// Standard identifiers
|
|
1471
|
+
"id",
|
|
1472
|
+
"uid",
|
|
1473
|
+
"gid",
|
|
1474
|
+
"pid",
|
|
1475
|
+
// Loop counters and iterators
|
|
1476
|
+
"i",
|
|
1477
|
+
"j",
|
|
1478
|
+
"k",
|
|
1479
|
+
"n",
|
|
1480
|
+
"m",
|
|
1481
|
+
// Web/Network
|
|
1482
|
+
"url",
|
|
1483
|
+
"uri",
|
|
1484
|
+
"api",
|
|
1485
|
+
"cdn",
|
|
1486
|
+
"dns",
|
|
1487
|
+
"ip",
|
|
1488
|
+
"tcp",
|
|
1489
|
+
"udp",
|
|
1490
|
+
"http",
|
|
1491
|
+
"ssl",
|
|
1492
|
+
"tls",
|
|
1493
|
+
"utm",
|
|
1494
|
+
"seo",
|
|
1495
|
+
"rss",
|
|
1496
|
+
"xhr",
|
|
1497
|
+
"ajax",
|
|
1498
|
+
"cors",
|
|
1499
|
+
"ws",
|
|
1500
|
+
"wss",
|
|
1501
|
+
// Data formats
|
|
1502
|
+
"json",
|
|
1503
|
+
"xml",
|
|
1504
|
+
"yaml",
|
|
1505
|
+
"csv",
|
|
1506
|
+
"html",
|
|
1507
|
+
"css",
|
|
1508
|
+
"svg",
|
|
1509
|
+
"pdf",
|
|
1510
|
+
// File types & extensions
|
|
1511
|
+
"img",
|
|
1512
|
+
"txt",
|
|
1513
|
+
"doc",
|
|
1514
|
+
"docx",
|
|
1515
|
+
"xlsx",
|
|
1516
|
+
"ppt",
|
|
1517
|
+
"md",
|
|
1518
|
+
"rst",
|
|
1519
|
+
"jpg",
|
|
1520
|
+
"png",
|
|
1521
|
+
"gif",
|
|
1522
|
+
// Databases
|
|
1523
|
+
"db",
|
|
1524
|
+
"sql",
|
|
1525
|
+
"orm",
|
|
1526
|
+
"dao",
|
|
1527
|
+
"dto",
|
|
1528
|
+
"ddb",
|
|
1529
|
+
"rds",
|
|
1530
|
+
"nosql",
|
|
1531
|
+
// File system
|
|
1532
|
+
"fs",
|
|
1533
|
+
"dir",
|
|
1534
|
+
"tmp",
|
|
1535
|
+
"src",
|
|
1536
|
+
"dst",
|
|
1537
|
+
"bin",
|
|
1538
|
+
"lib",
|
|
1539
|
+
"pkg",
|
|
1540
|
+
// Operating system
|
|
1541
|
+
"os",
|
|
1542
|
+
"env",
|
|
1543
|
+
"arg",
|
|
1544
|
+
"cli",
|
|
1545
|
+
"cmd",
|
|
1546
|
+
"exe",
|
|
1547
|
+
"cwd",
|
|
1548
|
+
"pwd",
|
|
1549
|
+
// UI/UX
|
|
1550
|
+
"ui",
|
|
1551
|
+
"ux",
|
|
1552
|
+
"gui",
|
|
1553
|
+
"dom",
|
|
1554
|
+
"ref",
|
|
1555
|
+
// Request/Response
|
|
1556
|
+
"req",
|
|
1557
|
+
"res",
|
|
1558
|
+
"ctx",
|
|
1559
|
+
"err",
|
|
1560
|
+
"msg",
|
|
1561
|
+
"auth",
|
|
1562
|
+
// Mathematics/Computing
|
|
1563
|
+
"max",
|
|
1564
|
+
"min",
|
|
1565
|
+
"avg",
|
|
1566
|
+
"sum",
|
|
1567
|
+
"abs",
|
|
1568
|
+
"cos",
|
|
1569
|
+
"sin",
|
|
1570
|
+
"tan",
|
|
1571
|
+
"log",
|
|
1572
|
+
"exp",
|
|
1573
|
+
"pow",
|
|
1574
|
+
"sqrt",
|
|
1575
|
+
"std",
|
|
1576
|
+
"var",
|
|
1577
|
+
"int",
|
|
1578
|
+
"num",
|
|
1579
|
+
"idx",
|
|
1580
|
+
// Time
|
|
1581
|
+
"now",
|
|
1582
|
+
"utc",
|
|
1583
|
+
"tz",
|
|
1584
|
+
"ms",
|
|
1585
|
+
"sec",
|
|
1586
|
+
"hr",
|
|
1587
|
+
"min",
|
|
1588
|
+
"yr",
|
|
1589
|
+
"mo",
|
|
1590
|
+
// Common patterns
|
|
1591
|
+
"app",
|
|
1592
|
+
"cfg",
|
|
1593
|
+
"config",
|
|
1594
|
+
"init",
|
|
1595
|
+
"len",
|
|
1596
|
+
"val",
|
|
1597
|
+
"str",
|
|
1598
|
+
"obj",
|
|
1599
|
+
"arr",
|
|
1600
|
+
"gen",
|
|
1601
|
+
"def",
|
|
1602
|
+
"raw",
|
|
1603
|
+
"new",
|
|
1604
|
+
"old",
|
|
1605
|
+
"pre",
|
|
1606
|
+
"post",
|
|
1607
|
+
"sub",
|
|
1608
|
+
"pub",
|
|
1609
|
+
// Programming/Framework specific
|
|
1610
|
+
"ts",
|
|
1611
|
+
"js",
|
|
1612
|
+
"jsx",
|
|
1613
|
+
"tsx",
|
|
1614
|
+
"py",
|
|
1615
|
+
"rb",
|
|
1616
|
+
"vue",
|
|
1617
|
+
"re",
|
|
1618
|
+
"fn",
|
|
1619
|
+
"fns",
|
|
1620
|
+
"mod",
|
|
1621
|
+
"opts",
|
|
1622
|
+
"dev",
|
|
1623
|
+
// Cloud/Infrastructure
|
|
1624
|
+
"s3",
|
|
1625
|
+
"ec2",
|
|
1626
|
+
"sqs",
|
|
1627
|
+
"sns",
|
|
1628
|
+
"vpc",
|
|
1629
|
+
"ami",
|
|
1630
|
+
"iam",
|
|
1631
|
+
"acl",
|
|
1632
|
+
"elb",
|
|
1633
|
+
"alb",
|
|
1634
|
+
"nlb",
|
|
1635
|
+
"aws",
|
|
1636
|
+
"ses",
|
|
1637
|
+
"gst",
|
|
1638
|
+
"cdk",
|
|
1639
|
+
"btn",
|
|
1640
|
+
"buf",
|
|
1641
|
+
"agg",
|
|
1642
|
+
"ocr",
|
|
1643
|
+
"ai",
|
|
1644
|
+
"cf",
|
|
1645
|
+
"cfn",
|
|
1646
|
+
"ga",
|
|
1647
|
+
// Metrics/Performance
|
|
1648
|
+
"fcp",
|
|
1649
|
+
"lcp",
|
|
1650
|
+
"cls",
|
|
1651
|
+
"ttfb",
|
|
1652
|
+
"tti",
|
|
1653
|
+
"fid",
|
|
1654
|
+
"fps",
|
|
1655
|
+
"qps",
|
|
1656
|
+
"rps",
|
|
1657
|
+
"tps",
|
|
1658
|
+
"wpm",
|
|
1659
|
+
// Testing & i18n
|
|
1660
|
+
"po",
|
|
1661
|
+
"e2e",
|
|
1662
|
+
"a11y",
|
|
1663
|
+
"i18n",
|
|
1664
|
+
"l10n",
|
|
1665
|
+
"spy",
|
|
1666
|
+
// Domain-specific abbreviations (context-aware)
|
|
1667
|
+
"sk",
|
|
1668
|
+
"fy",
|
|
1669
|
+
"faq",
|
|
1670
|
+
"og",
|
|
1671
|
+
"seo",
|
|
1672
|
+
"cta",
|
|
1673
|
+
"roi",
|
|
1674
|
+
"kpi",
|
|
1675
|
+
"ttl",
|
|
1676
|
+
"pct",
|
|
1677
|
+
// Technical abbreviations
|
|
1678
|
+
"mac",
|
|
1679
|
+
"hex",
|
|
1680
|
+
"esm",
|
|
1681
|
+
"git",
|
|
1682
|
+
"rec",
|
|
1683
|
+
"loc",
|
|
1684
|
+
"dup",
|
|
1685
|
+
// Boolean helpers (these are intentional short names)
|
|
1686
|
+
"is",
|
|
1687
|
+
"has",
|
|
1688
|
+
"can",
|
|
1689
|
+
"did",
|
|
1690
|
+
"was",
|
|
1691
|
+
"are",
|
|
1692
|
+
// Date/Time context (when in date contexts)
|
|
1693
|
+
"d",
|
|
1694
|
+
"t",
|
|
1695
|
+
"dt",
|
|
1696
|
+
// Coverage metrics (industry standard: statements/branches/functions/lines)
|
|
1697
|
+
"s",
|
|
1698
|
+
"b",
|
|
1699
|
+
"f",
|
|
1700
|
+
"l",
|
|
1701
|
+
// Common media/content abbreviations
|
|
1702
|
+
"vid",
|
|
1703
|
+
"pic",
|
|
1704
|
+
"img",
|
|
1705
|
+
"doc",
|
|
1706
|
+
"msg"
|
|
1707
|
+
]);
|
|
1708
|
+
async function analyzeNaming(files) {
|
|
1709
|
+
const issues = [];
|
|
1710
|
+
const rootDir = files.length > 0 ? (0, import_path2.dirname)(files[0]) : process.cwd();
|
|
1711
|
+
const config = (0, import_core4.loadConfig)(rootDir);
|
|
1712
|
+
const consistencyConfig = config?.tools?.["consistency"];
|
|
1713
|
+
const customAbbreviations = new Set(consistencyConfig?.acceptedAbbreviations || []);
|
|
1714
|
+
const customShortWords = new Set(consistencyConfig?.shortWords || []);
|
|
1715
|
+
const disabledChecks = new Set(consistencyConfig?.disableChecks || []);
|
|
1716
|
+
for (const file of files) {
|
|
1717
|
+
const content = await (0, import_core4.readFileContent)(file);
|
|
1718
|
+
const fileIssues = analyzeFileNaming(file, content, customAbbreviations, customShortWords, disabledChecks);
|
|
1719
|
+
issues.push(...fileIssues);
|
|
1720
|
+
}
|
|
1721
|
+
return issues;
|
|
1722
|
+
}
|
|
1723
|
+
function analyzeFileNaming(file, content, customAbbreviations, customShortWords, disabledChecks) {
|
|
1724
|
+
const issues = [];
|
|
1725
|
+
const isTestFile = file.match(/\.(test|spec)\.(ts|tsx|js|jsx)$/);
|
|
1726
|
+
const lines = content.split("\n");
|
|
1727
|
+
const allAbbreviations = /* @__PURE__ */ new Set([...ACCEPTABLE_ABBREVIATIONS2, ...customAbbreviations]);
|
|
1728
|
+
const allShortWords = /* @__PURE__ */ new Set([...COMMON_SHORT_WORDS2, ...customShortWords]);
|
|
1729
|
+
const getContextWindow = (index, windowSize = 3) => {
|
|
1730
|
+
const start = Math.max(0, index - windowSize);
|
|
1731
|
+
const end = Math.min(lines.length, index + windowSize + 1);
|
|
1732
|
+
return lines.slice(start, end).join("\n");
|
|
1733
|
+
};
|
|
1734
|
+
const isShortLivedVariable = (varName, declarationIndex) => {
|
|
1735
|
+
const searchRange = 5;
|
|
1736
|
+
const endIndex = Math.min(lines.length, declarationIndex + searchRange + 1);
|
|
1737
|
+
let usageCount = 0;
|
|
1738
|
+
for (let i = declarationIndex; i < endIndex; i++) {
|
|
1739
|
+
const regex = new RegExp(`\\b${varName}\\b`, "g");
|
|
1740
|
+
const matches = lines[i].match(regex);
|
|
1741
|
+
if (matches) {
|
|
1742
|
+
usageCount += matches.length;
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
return usageCount >= 2 && usageCount <= 3;
|
|
1746
|
+
};
|
|
1747
|
+
lines.forEach((line, index) => {
|
|
1748
|
+
const lineNumber = index + 1;
|
|
1749
|
+
const contextWindow = getContextWindow(index);
|
|
1750
|
+
if (!disabledChecks.has("single-letter")) {
|
|
1751
|
+
const singleLetterMatches = line.matchAll(/\b(?:const|let|var)\s+([a-hm-z])\s*=/gi);
|
|
1752
|
+
for (const match of singleLetterMatches) {
|
|
1753
|
+
const letter = match[1].toLowerCase();
|
|
1754
|
+
const isCoverageContext2 = /coverage|summary|metrics|pct|percent/i.test(line) || /\.(?:statements|branches|functions|lines)\.pct/i.test(line);
|
|
1755
|
+
if (isCoverageContext2 && ["s", "b", "f", "l"].includes(letter)) {
|
|
1756
|
+
continue;
|
|
1757
|
+
}
|
|
1758
|
+
const isInLoopContext = line.includes("for") || /\.(map|filter|forEach|reduce|find|some|every)\s*\(/.test(line) || line.includes("=>") || // Arrow function
|
|
1759
|
+
/\w+\s*=>\s*/.test(line);
|
|
1760
|
+
const isI18nContext = line.includes("useTranslation") || line.includes("i18n.t") || /\bt\s*\(['"]/.test(line);
|
|
1761
|
+
const isArrowFunctionParam = /\(\s*[a-z]\s*(?:,\s*[a-z]\s*)*\)\s*=>/.test(line) || // (s) => or (a, b) =>
|
|
1762
|
+
/[a-z]\s*=>/.test(line) || // s => on same line
|
|
1763
|
+
// Multi-line arrow function detection: look for pattern in context window
|
|
1764
|
+
new RegExp(`\\b${letter}\\s*\\)\\s*$`).test(line) && /=>/.test(contextWindow) || // (s)\n =>
|
|
1765
|
+
new RegExp(`\\.(?:map|filter|forEach|reduce|find|some|every)\\s*\\(\\s*$`).test(lines[index - 1] || "") && /=>/.test(contextWindow);
|
|
1766
|
+
const isShortLived = isShortLivedVariable(letter, index);
|
|
1767
|
+
if (!isInLoopContext && !isI18nContext && !isArrowFunctionParam && !isShortLived && !["x", "y", "z", "i", "j", "k", "l", "n", "m"].includes(letter)) {
|
|
1768
|
+
if (isTestFile && ["a", "b", "c", "d", "e", "f", "s"].includes(letter)) {
|
|
1769
|
+
continue;
|
|
1770
|
+
}
|
|
1771
|
+
issues.push({
|
|
1772
|
+
file,
|
|
1773
|
+
line: lineNumber,
|
|
1774
|
+
type: "poor-naming",
|
|
1775
|
+
identifier: match[1],
|
|
1776
|
+
severity: "minor",
|
|
1777
|
+
suggestion: `Use descriptive variable name instead of single letter '${match[1]}'`
|
|
1778
|
+
});
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
if (!disabledChecks.has("abbreviation")) {
|
|
1783
|
+
const abbreviationMatches = line.matchAll(/\b(?:const|let|var)\s+([a-z]{1,3})(?=[A-Z]|_|\s*=)/g);
|
|
1784
|
+
for (const match of abbreviationMatches) {
|
|
1785
|
+
const abbrev = match[1].toLowerCase();
|
|
1786
|
+
if (allShortWords.has(abbrev)) {
|
|
1787
|
+
continue;
|
|
1788
|
+
}
|
|
1789
|
+
if (allAbbreviations.has(abbrev)) {
|
|
1790
|
+
continue;
|
|
1791
|
+
}
|
|
1792
|
+
const isArrowFunctionParam = /\(\s*[a-z]\s*(?:,\s*[a-z]\s*)*\)\s*=>/.test(line) || // (s) => or (a, b) =>
|
|
1793
|
+
new RegExp(`\\b${abbrev}\\s*=>`).test(line) || // s => on same line
|
|
1794
|
+
// Multi-line arrow function: check context window
|
|
1795
|
+
new RegExp(`\\b${abbrev}\\s*\\)\\s*$`).test(line) && /=>/.test(contextWindow) || // (s)\n =>
|
|
1796
|
+
new RegExp(`\\.(?:map|filter|forEach|reduce|find|some|every)\\s*\\(\\s*$`).test(lines[index - 1] || "") && new RegExp(`^\\s*${abbrev}\\s*=>`).test(line);
|
|
1797
|
+
if (isArrowFunctionParam) {
|
|
1798
|
+
continue;
|
|
1799
|
+
}
|
|
1800
|
+
if (abbrev.length <= 2) {
|
|
1801
|
+
const isDateTimeContext = /date|time|day|hour|minute|second|timestamp/i.test(line);
|
|
1802
|
+
if (isDateTimeContext && ["d", "t", "dt"].includes(abbrev)) {
|
|
1803
|
+
continue;
|
|
1804
|
+
}
|
|
1805
|
+
const isUserContext = /user|auth|account/i.test(line);
|
|
1806
|
+
if (isUserContext && abbrev === "u") {
|
|
1807
|
+
continue;
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
issues.push({
|
|
1811
|
+
file,
|
|
1812
|
+
line: lineNumber,
|
|
1813
|
+
type: "abbreviation",
|
|
1814
|
+
identifier: match[1],
|
|
1815
|
+
severity: "info",
|
|
1816
|
+
suggestion: `Consider using full word instead of abbreviation '${match[1]}'`
|
|
1817
|
+
});
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
if (!disabledChecks.has("convention-mix") && file.match(/\.(ts|tsx|js|jsx)$/)) {
|
|
1821
|
+
const camelCaseVars = line.match(/\b(?:const|let|var)\s+([a-z][a-zA-Z0-9]*)\s*=/);
|
|
1822
|
+
const snakeCaseVars = line.match(/\b(?:const|let|var)\s+([a-z][a-z0-9]*_[a-z0-9_]*)\s*=/);
|
|
1823
|
+
if (snakeCaseVars) {
|
|
1824
|
+
issues.push({
|
|
1825
|
+
file,
|
|
1826
|
+
line: lineNumber,
|
|
1827
|
+
type: "convention-mix",
|
|
1828
|
+
identifier: snakeCaseVars[1],
|
|
1829
|
+
severity: "minor",
|
|
1830
|
+
suggestion: `Use camelCase '${snakeCaseToCamelCase(snakeCaseVars[1])}' instead of snake_case in TypeScript/JavaScript`
|
|
1831
|
+
});
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
if (!disabledChecks.has("unclear")) {
|
|
1835
|
+
const booleanMatches = line.matchAll(/\b(?:const|let|var)\s+([a-z][a-zA-Z0-9]*)\s*:\s*boolean/gi);
|
|
1836
|
+
for (const match of booleanMatches) {
|
|
1837
|
+
const name = match[1];
|
|
1838
|
+
if (!name.match(/^(is|has|should|can|will|did)/i)) {
|
|
1839
|
+
issues.push({
|
|
1840
|
+
file,
|
|
1841
|
+
line: lineNumber,
|
|
1842
|
+
type: "unclear",
|
|
1843
|
+
identifier: name,
|
|
1844
|
+
severity: "info",
|
|
1845
|
+
suggestion: `Boolean variable '${name}' should start with is/has/should/can for clarity`
|
|
1846
|
+
});
|
|
1847
|
+
}
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
if (!disabledChecks.has("unclear")) {
|
|
1851
|
+
const functionMatches = line.matchAll(/function\s+([a-z][a-zA-Z0-9]*)/g);
|
|
1852
|
+
for (const match of functionMatches) {
|
|
1853
|
+
const name = match[1];
|
|
1854
|
+
const isKeyword = ["for", "if", "else", "while", "do", "switch", "case", "break", "continue", "return", "throw", "try", "catch", "finally", "with", "yield", "await"].includes(name);
|
|
1855
|
+
if (isKeyword) {
|
|
1856
|
+
continue;
|
|
1857
|
+
}
|
|
1858
|
+
const isEntryPoint = ["main", "init", "setup", "bootstrap"].includes(name);
|
|
1859
|
+
if (isEntryPoint) {
|
|
1860
|
+
continue;
|
|
1861
|
+
}
|
|
1862
|
+
const isFactoryPattern = name.match(/(Factory|Builder|Creator|Generator|Provider|Adapter|Mock)$/);
|
|
1863
|
+
const isEventHandler = name.match(/^on[A-Z]/);
|
|
1864
|
+
const isDescriptiveLong = name.length > 15;
|
|
1865
|
+
const isReactHook = name.match(/^use[A-Z]/);
|
|
1866
|
+
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)$/);
|
|
1867
|
+
const isHelperPattern = name.match(/^(to|from|with|without|for|as|into)\w+/) || // toMetadata, withLogger, forPath
|
|
1868
|
+
name.match(/^\w+(To|From|With|Without|For|As|Into)\w*$/);
|
|
1869
|
+
const isUtilityName = ["cn", "proxy", "sitemap", "robots", "gtag"].includes(name);
|
|
1870
|
+
const capitalCount = (name.match(/[A-Z]/g) || []).length;
|
|
1871
|
+
const isCompoundWord = capitalCount >= 3;
|
|
1872
|
+
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)/);
|
|
1873
|
+
if (!hasActionVerb && !isFactoryPattern && !isEventHandler && !isDescriptiveLong && !isDescriptivePattern && !isCompoundWord && !isHelperPattern && !isUtilityName && !isReactHook) {
|
|
1874
|
+
issues.push({
|
|
1875
|
+
file,
|
|
1876
|
+
line: lineNumber,
|
|
1877
|
+
type: "unclear",
|
|
1878
|
+
identifier: name,
|
|
1879
|
+
severity: "info",
|
|
1880
|
+
suggestion: `Function '${name}' should start with an action verb (get, set, create, etc.)`
|
|
1881
|
+
});
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
});
|
|
1886
|
+
return issues;
|
|
1887
|
+
}
|
|
1888
|
+
function snakeCaseToCamelCase(str) {
|
|
1889
|
+
return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
1890
|
+
}
|
|
1891
|
+
function detectNamingConventions(files, allIssues) {
|
|
1892
|
+
const camelCaseCount = allIssues.filter((i) => i.type === "convention-mix").length;
|
|
1893
|
+
const totalChecks = files.length * 10;
|
|
1894
|
+
if (camelCaseCount / totalChecks > 0.3) {
|
|
1895
|
+
return { dominantConvention: "mixed", conventionScore: 0.5 };
|
|
1896
|
+
}
|
|
1897
|
+
return { dominantConvention: "camelCase", conventionScore: 0.9 };
|
|
1898
|
+
}
|
|
873
1899
|
// Annotate the CommonJS export names for ESM import in node:
|
|
874
1900
|
0 && (module.exports = {
|
|
875
1901
|
analyzeConsistency,
|
|
876
1902
|
analyzeNaming,
|
|
1903
|
+
analyzeNamingAST,
|
|
877
1904
|
analyzePatterns,
|
|
878
1905
|
detectNamingConventions
|
|
879
1906
|
});
|