@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/dist/cli.js CHANGED
@@ -29,11 +29,436 @@ var import_commander = require("commander");
29
29
  // src/analyzer.ts
30
30
  var import_core3 = require("@aiready/core");
31
31
 
32
- // src/analyzers/naming.ts
32
+ // src/analyzers/naming-ast.ts
33
33
  var import_core = require("@aiready/core");
34
34
  var import_path = require("path");
35
+
36
+ // src/utils/ast-parser.ts
37
+ var import_typescript_estree = require("@typescript-eslint/typescript-estree");
38
+ var import_fs = require("fs");
39
+ function parseFile(filePath, content) {
40
+ try {
41
+ const code = content ?? (0, import_fs.readFileSync)(filePath, "utf-8");
42
+ const isTypeScript = filePath.match(/\.tsx?$/);
43
+ return (0, import_typescript_estree.parse)(code, {
44
+ jsx: filePath.match(/\.[jt]sx$/i) !== null,
45
+ loc: true,
46
+ range: true,
47
+ comment: false,
48
+ tokens: false,
49
+ // Relaxed parsing for JavaScript files
50
+ sourceType: "module",
51
+ ecmaVersion: "latest",
52
+ // Only use TypeScript parser features for .ts/.tsx files
53
+ filePath: isTypeScript ? filePath : void 0
54
+ });
55
+ } catch (error) {
56
+ console.warn(`Failed to parse ${filePath}:`, error instanceof Error ? error.message : error);
57
+ return null;
58
+ }
59
+ }
60
+ function traverseAST(node, visitor, parent = null) {
61
+ if (!node) return;
62
+ visitor.enter?.(node, parent);
63
+ for (const key of Object.keys(node)) {
64
+ const value = node[key];
65
+ if (Array.isArray(value)) {
66
+ for (const child of value) {
67
+ if (child && typeof child === "object" && "type" in child) {
68
+ traverseAST(child, visitor, node);
69
+ }
70
+ }
71
+ } else if (value && typeof value === "object" && "type" in value) {
72
+ traverseAST(value, visitor, node);
73
+ }
74
+ }
75
+ visitor.leave?.(node, parent);
76
+ }
77
+ function isLoopStatement(node) {
78
+ return [
79
+ "ForStatement",
80
+ "ForInStatement",
81
+ "ForOfStatement",
82
+ "WhileStatement",
83
+ "DoWhileStatement"
84
+ ].includes(node.type);
85
+ }
86
+ function getFunctionName(node) {
87
+ switch (node.type) {
88
+ case "FunctionDeclaration":
89
+ return node.id?.name ?? null;
90
+ case "FunctionExpression":
91
+ return node.id?.name ?? null;
92
+ case "ArrowFunctionExpression":
93
+ return null;
94
+ // Arrow functions don't have names directly
95
+ case "MethodDefinition":
96
+ if (node.key.type === "Identifier") {
97
+ return node.key.name;
98
+ }
99
+ return null;
100
+ default:
101
+ return null;
102
+ }
103
+ }
104
+ function getLineNumber(node) {
105
+ return node.loc?.start.line ?? 0;
106
+ }
107
+ function isCoverageContext(node, ancestors) {
108
+ const coveragePatterns = /coverage|summary|metrics|pct|percent|statements|branches|functions|lines/i;
109
+ if (node.type === "Identifier" && coveragePatterns.test(node.name)) {
110
+ return true;
111
+ }
112
+ for (const ancestor of ancestors.slice(-3)) {
113
+ if (ancestor.type === "MemberExpression") {
114
+ const memberExpr = ancestor;
115
+ if (memberExpr.object.type === "Identifier" && coveragePatterns.test(memberExpr.object.name)) {
116
+ return true;
117
+ }
118
+ }
119
+ if (ancestor.type === "ObjectPattern" || ancestor.type === "ObjectExpression") {
120
+ const parent = ancestors[ancestors.indexOf(ancestor) - 1];
121
+ if (parent?.type === "VariableDeclarator") {
122
+ const varDecl = parent;
123
+ if (varDecl.id.type === "Identifier" && coveragePatterns.test(varDecl.id.name)) {
124
+ return true;
125
+ }
126
+ }
127
+ }
128
+ }
129
+ return false;
130
+ }
131
+
132
+ // src/utils/scope-tracker.ts
133
+ var ScopeTracker = class {
134
+ constructor(rootNode) {
135
+ this.allScopes = [];
136
+ this.rootScope = {
137
+ type: "global",
138
+ node: rootNode,
139
+ parent: null,
140
+ children: [],
141
+ variables: /* @__PURE__ */ new Map()
142
+ };
143
+ this.currentScope = this.rootScope;
144
+ this.allScopes.push(this.rootScope);
145
+ }
146
+ /**
147
+ * Enter a new scope
148
+ */
149
+ enterScope(type, node) {
150
+ const newScope = {
151
+ type,
152
+ node,
153
+ parent: this.currentScope,
154
+ children: [],
155
+ variables: /* @__PURE__ */ new Map()
156
+ };
157
+ this.currentScope.children.push(newScope);
158
+ this.currentScope = newScope;
159
+ this.allScopes.push(newScope);
160
+ }
161
+ /**
162
+ * Exit current scope and return to parent
163
+ */
164
+ exitScope() {
165
+ if (this.currentScope.parent) {
166
+ this.currentScope = this.currentScope.parent;
167
+ }
168
+ }
169
+ /**
170
+ * Declare a variable in the current scope
171
+ */
172
+ declareVariable(name, node, line, options = {}) {
173
+ const varInfo = {
174
+ name,
175
+ node,
176
+ declarationLine: line,
177
+ references: [],
178
+ type: options.type,
179
+ isParameter: options.isParameter ?? false,
180
+ isDestructured: options.isDestructured ?? false,
181
+ isLoopVariable: options.isLoopVariable ?? false
182
+ };
183
+ this.currentScope.variables.set(name, varInfo);
184
+ }
185
+ /**
186
+ * Add a reference to a variable
187
+ */
188
+ addReference(name, node) {
189
+ const varInfo = this.findVariable(name);
190
+ if (varInfo) {
191
+ varInfo.references.push(node);
192
+ }
193
+ }
194
+ /**
195
+ * Find a variable in current or parent scopes
196
+ */
197
+ findVariable(name) {
198
+ let scope = this.currentScope;
199
+ while (scope) {
200
+ const varInfo = scope.variables.get(name);
201
+ if (varInfo) {
202
+ return varInfo;
203
+ }
204
+ scope = scope.parent;
205
+ }
206
+ return null;
207
+ }
208
+ /**
209
+ * Get all variables in current scope (not including parent scopes)
210
+ */
211
+ getCurrentScopeVariables() {
212
+ return Array.from(this.currentScope.variables.values());
213
+ }
214
+ /**
215
+ * Get all variables across all scopes
216
+ */
217
+ getAllVariables() {
218
+ const allVars = [];
219
+ for (const scope of this.allScopes) {
220
+ allVars.push(...Array.from(scope.variables.values()));
221
+ }
222
+ return allVars;
223
+ }
224
+ /**
225
+ * Calculate actual usage count (references minus declaration)
226
+ */
227
+ getUsageCount(varInfo) {
228
+ return varInfo.references.length;
229
+ }
230
+ /**
231
+ * Check if a variable is short-lived (used within N lines)
232
+ */
233
+ isShortLived(varInfo, maxLines = 5) {
234
+ if (varInfo.references.length === 0) {
235
+ return false;
236
+ }
237
+ const declarationLine = varInfo.declarationLine;
238
+ const maxUsageLine = Math.max(
239
+ ...varInfo.references.map((ref) => ref.loc?.start.line ?? declarationLine)
240
+ );
241
+ return maxUsageLine - declarationLine <= maxLines;
242
+ }
243
+ /**
244
+ * Check if a variable is used in a limited scope (e.g., only in one callback)
245
+ */
246
+ isLocallyScoped(varInfo) {
247
+ if (varInfo.references.length === 0) return false;
248
+ const lines = varInfo.references.map((ref) => ref.loc?.start.line ?? 0);
249
+ const minLine = Math.min(...lines);
250
+ const maxLine = Math.max(...lines);
251
+ return maxLine - minLine <= 3;
252
+ }
253
+ /**
254
+ * Get current scope type
255
+ */
256
+ getCurrentScopeType() {
257
+ return this.currentScope.type;
258
+ }
259
+ /**
260
+ * Check if currently in a loop scope
261
+ */
262
+ isInLoop() {
263
+ let scope = this.currentScope;
264
+ while (scope) {
265
+ if (scope.type === "loop") {
266
+ return true;
267
+ }
268
+ scope = scope.parent;
269
+ }
270
+ return false;
271
+ }
272
+ /**
273
+ * Check if currently in a function scope
274
+ */
275
+ isInFunction() {
276
+ let scope = this.currentScope;
277
+ while (scope) {
278
+ if (scope.type === "function") {
279
+ return true;
280
+ }
281
+ scope = scope.parent;
282
+ }
283
+ return false;
284
+ }
285
+ /**
286
+ * Get the root scope
287
+ */
288
+ getRootScope() {
289
+ return this.rootScope;
290
+ }
291
+ };
292
+
293
+ // src/utils/context-detector.ts
294
+ function detectFileType(filePath, ast) {
295
+ const path = filePath.toLowerCase();
296
+ if (path.match(/\.(test|spec)\.(ts|tsx|js|jsx)$/) || path.includes("__tests__")) {
297
+ return "test";
298
+ }
299
+ if (path.endsWith(".d.ts") || path.includes("types")) {
300
+ return "types";
301
+ }
302
+ if (path.match(/config|\.config\.|rc\.|setup/) || path.includes("configuration")) {
303
+ return "config";
304
+ }
305
+ return "production";
306
+ }
307
+ function detectCodeLayer(ast) {
308
+ let hasAPIIndicators = 0;
309
+ let hasBusinessIndicators = 0;
310
+ let hasDataIndicators = 0;
311
+ let hasUtilityIndicators = 0;
312
+ traverseAST(ast, {
313
+ enter: (node) => {
314
+ if (node.type === "ImportDeclaration") {
315
+ const source = node.source.value;
316
+ if (source.match(/express|fastify|koa|@nestjs|axios|fetch|http/i)) {
317
+ hasAPIIndicators++;
318
+ }
319
+ if (source.match(/database|prisma|typeorm|sequelize|mongoose|pg|mysql/i)) {
320
+ hasDataIndicators++;
321
+ }
322
+ }
323
+ if (node.type === "FunctionDeclaration" && node.id) {
324
+ const name = node.id.name;
325
+ if (name.match(/^(get|post|put|delete|patch|handle|api|route|controller)/i)) {
326
+ hasAPIIndicators++;
327
+ }
328
+ if (name.match(/^(calculate|process|validate|transform|compute|analyze)/i)) {
329
+ hasBusinessIndicators++;
330
+ }
331
+ if (name.match(/^(find|create|update|delete|save|fetch|query|insert)/i)) {
332
+ hasDataIndicators++;
333
+ }
334
+ if (name.match(/^(format|parse|convert|normalize|sanitize|encode|decode)/i)) {
335
+ hasUtilityIndicators++;
336
+ }
337
+ }
338
+ if (node.type === "ExportNamedDeclaration" || node.type === "ExportDefaultDeclaration") {
339
+ if (node.type === "ExportNamedDeclaration" && node.declaration) {
340
+ if (node.declaration.type === "FunctionDeclaration" && node.declaration.id) {
341
+ const name = node.declaration.id.name;
342
+ if (name.match(/handler|route|api|controller/i)) {
343
+ hasAPIIndicators += 2;
344
+ }
345
+ }
346
+ }
347
+ }
348
+ }
349
+ });
350
+ const scores = {
351
+ api: hasAPIIndicators,
352
+ business: hasBusinessIndicators,
353
+ data: hasDataIndicators,
354
+ utility: hasUtilityIndicators
355
+ };
356
+ const maxScore = Math.max(...Object.values(scores));
357
+ if (maxScore === 0) {
358
+ return "unknown";
359
+ }
360
+ if (scores.api === maxScore) return "api";
361
+ if (scores.data === maxScore) return "data";
362
+ if (scores.business === maxScore) return "business";
363
+ if (scores.utility === maxScore) return "utility";
364
+ return "unknown";
365
+ }
366
+ function calculateComplexity(node) {
367
+ let complexity = 1;
368
+ traverseAST(node, {
369
+ enter: (childNode) => {
370
+ switch (childNode.type) {
371
+ case "IfStatement":
372
+ case "ConditionalExpression":
373
+ // ternary
374
+ case "SwitchCase":
375
+ case "ForStatement":
376
+ case "ForInStatement":
377
+ case "ForOfStatement":
378
+ case "WhileStatement":
379
+ case "DoWhileStatement":
380
+ case "CatchClause":
381
+ complexity++;
382
+ break;
383
+ case "LogicalExpression":
384
+ if (childNode.operator === "&&" || childNode.operator === "||") {
385
+ complexity++;
386
+ }
387
+ break;
388
+ }
389
+ }
390
+ });
391
+ return complexity;
392
+ }
393
+ function buildCodeContext(filePath, ast) {
394
+ const fileType = detectFileType(filePath, ast);
395
+ const codeLayer = detectCodeLayer(ast);
396
+ let totalComplexity = 0;
397
+ let functionCount = 0;
398
+ traverseAST(ast, {
399
+ enter: (node) => {
400
+ if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") {
401
+ totalComplexity += calculateComplexity(node);
402
+ functionCount++;
403
+ }
404
+ }
405
+ });
406
+ const avgComplexity = functionCount > 0 ? totalComplexity / functionCount : 1;
407
+ return {
408
+ fileType,
409
+ codeLayer,
410
+ complexity: Math.round(avgComplexity),
411
+ isTestFile: fileType === "test",
412
+ isTypeDefinition: fileType === "types"
413
+ };
414
+ }
415
+ function adjustSeverity(baseSeverity, context, issueType) {
416
+ if (context.isTestFile) {
417
+ if (baseSeverity === "minor") return "info";
418
+ if (baseSeverity === "major") return "minor";
419
+ }
420
+ if (context.isTypeDefinition) {
421
+ if (baseSeverity === "minor") return "info";
422
+ }
423
+ if (context.codeLayer === "api") {
424
+ if (baseSeverity === "info" && issueType === "unclear") return "minor";
425
+ if (baseSeverity === "minor" && issueType === "unclear") return "major";
426
+ }
427
+ if (context.complexity > 10) {
428
+ if (baseSeverity === "info") return "minor";
429
+ }
430
+ if (context.codeLayer === "utility") {
431
+ if (baseSeverity === "minor" && issueType === "abbreviation") return "info";
432
+ }
433
+ return baseSeverity;
434
+ }
435
+ function isAcceptableInContext(name, context, options) {
436
+ if (options.isLoopVariable && ["i", "j", "k", "l", "n", "m"].includes(name)) {
437
+ return true;
438
+ }
439
+ if (context.isTestFile) {
440
+ if (["a", "b", "c", "x", "y", "z"].includes(name) && options.isParameter) {
441
+ return true;
442
+ }
443
+ }
444
+ if (context.codeLayer === "utility" && ["x", "y", "z"].includes(name)) {
445
+ return true;
446
+ }
447
+ if (options.isDestructured) {
448
+ if (["s", "b", "f", "l"].includes(name)) {
449
+ return true;
450
+ }
451
+ }
452
+ if (options.isParameter && (options.complexity ?? context.complexity) < 3) {
453
+ if (name.length >= 2) {
454
+ return true;
455
+ }
456
+ }
457
+ return false;
458
+ }
459
+
460
+ // src/analyzers/naming-ast.ts
35
461
  var COMMON_SHORT_WORDS = /* @__PURE__ */ new Set([
36
- // Full English words (1-3 letters)
37
462
  "day",
38
463
  "key",
39
464
  "net",
@@ -89,14 +514,12 @@ var COMMON_SHORT_WORDS = /* @__PURE__ */ new Set([
89
514
  "tmp",
90
515
  "ext",
91
516
  "sep",
92
- // Prepositions and conjunctions
93
517
  "and",
94
518
  "from",
95
519
  "how",
96
520
  "pad",
97
521
  "bar",
98
522
  "non",
99
- // Additional full words commonly flagged
100
523
  "tax",
101
524
  "cat",
102
525
  "dog",
@@ -176,18 +599,15 @@ var COMMON_SHORT_WORDS = /* @__PURE__ */ new Set([
176
599
  "won"
177
600
  ]);
178
601
  var ACCEPTABLE_ABBREVIATIONS = /* @__PURE__ */ new Set([
179
- // Standard identifiers
180
602
  "id",
181
603
  "uid",
182
604
  "gid",
183
605
  "pid",
184
- // Loop counters and iterators
185
606
  "i",
186
607
  "j",
187
608
  "k",
188
609
  "n",
189
610
  "m",
190
- // Web/Network
191
611
  "url",
192
612
  "uri",
193
613
  "api",
@@ -207,7 +627,6 @@ var ACCEPTABLE_ABBREVIATIONS = /* @__PURE__ */ new Set([
207
627
  "cors",
208
628
  "ws",
209
629
  "wss",
210
- // Data formats
211
630
  "json",
212
631
  "xml",
213
632
  "yaml",
@@ -216,7 +635,6 @@ var ACCEPTABLE_ABBREVIATIONS = /* @__PURE__ */ new Set([
216
635
  "css",
217
636
  "svg",
218
637
  "pdf",
219
- // File types & extensions
220
638
  "img",
221
639
  "txt",
222
640
  "doc",
@@ -228,7 +646,6 @@ var ACCEPTABLE_ABBREVIATIONS = /* @__PURE__ */ new Set([
228
646
  "jpg",
229
647
  "png",
230
648
  "gif",
231
- // Databases
232
649
  "db",
233
650
  "sql",
234
651
  "orm",
@@ -237,7 +654,6 @@ var ACCEPTABLE_ABBREVIATIONS = /* @__PURE__ */ new Set([
237
654
  "ddb",
238
655
  "rds",
239
656
  "nosql",
240
- // File system
241
657
  "fs",
242
658
  "dir",
243
659
  "tmp",
@@ -246,7 +662,6 @@ var ACCEPTABLE_ABBREVIATIONS = /* @__PURE__ */ new Set([
246
662
  "bin",
247
663
  "lib",
248
664
  "pkg",
249
- // Operating system
250
665
  "os",
251
666
  "env",
252
667
  "arg",
@@ -255,20 +670,17 @@ var ACCEPTABLE_ABBREVIATIONS = /* @__PURE__ */ new Set([
255
670
  "exe",
256
671
  "cwd",
257
672
  "pwd",
258
- // UI/UX
259
673
  "ui",
260
674
  "ux",
261
675
  "gui",
262
676
  "dom",
263
677
  "ref",
264
- // Request/Response
265
678
  "req",
266
679
  "res",
267
680
  "ctx",
268
681
  "err",
269
682
  "msg",
270
683
  "auth",
271
- // Mathematics/Computing
272
684
  "max",
273
685
  "min",
274
686
  "avg",
@@ -286,7 +698,6 @@ var ACCEPTABLE_ABBREVIATIONS = /* @__PURE__ */ new Set([
286
698
  "int",
287
699
  "num",
288
700
  "idx",
289
- // Time
290
701
  "now",
291
702
  "utc",
292
703
  "tz",
@@ -296,7 +707,6 @@ var ACCEPTABLE_ABBREVIATIONS = /* @__PURE__ */ new Set([
296
707
  "min",
297
708
  "yr",
298
709
  "mo",
299
- // Common patterns
300
710
  "app",
301
711
  "cfg",
302
712
  "config",
@@ -315,7 +725,6 @@ var ACCEPTABLE_ABBREVIATIONS = /* @__PURE__ */ new Set([
315
725
  "post",
316
726
  "sub",
317
727
  "pub",
318
- // Programming/Framework specific
319
728
  "ts",
320
729
  "js",
321
730
  "jsx",
@@ -329,7 +738,6 @@ var ACCEPTABLE_ABBREVIATIONS = /* @__PURE__ */ new Set([
329
738
  "mod",
330
739
  "opts",
331
740
  "dev",
332
- // Cloud/Infrastructure
333
741
  "s3",
334
742
  "ec2",
335
743
  "sqs",
@@ -353,7 +761,6 @@ var ACCEPTABLE_ABBREVIATIONS = /* @__PURE__ */ new Set([
353
761
  "cf",
354
762
  "cfn",
355
763
  "ga",
356
- // Metrics/Performance
357
764
  "fcp",
358
765
  "lcp",
359
766
  "cls",
@@ -365,14 +772,12 @@ var ACCEPTABLE_ABBREVIATIONS = /* @__PURE__ */ new Set([
365
772
  "rps",
366
773
  "tps",
367
774
  "wpm",
368
- // Testing & i18n
369
775
  "po",
370
776
  "e2e",
371
777
  "a11y",
372
778
  "i18n",
373
779
  "l10n",
374
780
  "spy",
375
- // Domain-specific abbreviations (context-aware)
376
781
  "sk",
377
782
  "fy",
378
783
  "faq",
@@ -383,7 +788,6 @@ var ACCEPTABLE_ABBREVIATIONS = /* @__PURE__ */ new Set([
383
788
  "kpi",
384
789
  "ttl",
385
790
  "pct",
386
- // Technical abbreviations
387
791
  "mac",
388
792
  "hex",
389
793
  "esm",
@@ -391,19 +795,27 @@ var ACCEPTABLE_ABBREVIATIONS = /* @__PURE__ */ new Set([
391
795
  "rec",
392
796
  "loc",
393
797
  "dup",
394
- // Boolean helpers (these are intentional short names)
395
798
  "is",
396
799
  "has",
397
800
  "can",
398
801
  "did",
399
802
  "was",
400
803
  "are",
401
- // Date/Time context (when in date contexts)
402
804
  "d",
403
805
  "t",
404
- "dt"
806
+ "dt",
807
+ "s",
808
+ "b",
809
+ "f",
810
+ "l",
811
+ // Coverage metrics
812
+ "vid",
813
+ "pic",
814
+ "img",
815
+ "doc",
816
+ "msg"
405
817
  ]);
406
- async function analyzeNaming(files) {
818
+ async function analyzeNamingAST(files) {
407
819
  const issues = [];
408
820
  const rootDir = files.length > 0 ? (0, import_path.dirname)(files[0]) : process.cwd();
409
821
  const config = (0, import_core.loadConfig)(rootDir);
@@ -411,184 +823,221 @@ async function analyzeNaming(files) {
411
823
  const customAbbreviations = new Set(consistencyConfig?.acceptedAbbreviations || []);
412
824
  const customShortWords = new Set(consistencyConfig?.shortWords || []);
413
825
  const disabledChecks = new Set(consistencyConfig?.disableChecks || []);
826
+ const allAbbreviations = /* @__PURE__ */ new Set([...ACCEPTABLE_ABBREVIATIONS, ...customAbbreviations]);
827
+ const allShortWords = /* @__PURE__ */ new Set([...COMMON_SHORT_WORDS, ...customShortWords]);
414
828
  for (const file of files) {
415
- const content = await (0, import_core.readFileContent)(file);
416
- const fileIssues = analyzeFileNaming(file, content, customAbbreviations, customShortWords, disabledChecks);
417
- issues.push(...fileIssues);
829
+ try {
830
+ const ast = parseFile(file);
831
+ if (!ast) continue;
832
+ const fileIssues = analyzeFileNamingAST(
833
+ file,
834
+ ast,
835
+ allAbbreviations,
836
+ allShortWords,
837
+ disabledChecks
838
+ );
839
+ issues.push(...fileIssues);
840
+ } catch (error) {
841
+ console.warn(`Skipping ${file} due to parse error:`, error);
842
+ }
418
843
  }
419
844
  return issues;
420
845
  }
421
- function analyzeFileNaming(file, content, customAbbreviations, customShortWords, disabledChecks) {
846
+ function analyzeFileNamingAST(file, ast, allAbbreviations, allShortWords, disabledChecks) {
422
847
  const issues = [];
423
- const isTestFile = file.match(/\.(test|spec)\.(ts|tsx|js|jsx)$/);
424
- const lines = content.split("\n");
425
- const allAbbreviations = /* @__PURE__ */ new Set([...ACCEPTABLE_ABBREVIATIONS, ...customAbbreviations]);
426
- const allShortWords = /* @__PURE__ */ new Set([...COMMON_SHORT_WORDS, ...customShortWords]);
427
- const getContextWindow = (index, windowSize = 3) => {
428
- const start = Math.max(0, index - windowSize);
429
- const end = Math.min(lines.length, index + windowSize + 1);
430
- return lines.slice(start, end).join("\n");
431
- };
432
- const isShortLivedVariable = (varName, declarationIndex) => {
433
- const searchRange = 5;
434
- const endIndex = Math.min(lines.length, declarationIndex + searchRange + 1);
435
- let usageCount = 0;
436
- for (let i = declarationIndex; i < endIndex; i++) {
437
- const regex = new RegExp(`\\b${varName}\\b`, "g");
438
- const matches = lines[i].match(regex);
439
- if (matches) {
440
- usageCount += matches.length;
441
- }
442
- }
443
- return usageCount >= 2 && usageCount <= 3;
444
- };
445
- lines.forEach((line, index) => {
446
- const lineNumber = index + 1;
447
- const contextWindow = getContextWindow(index);
448
- if (!disabledChecks.has("single-letter")) {
449
- const singleLetterMatches = line.matchAll(/\b(?:const|let|var)\s+([a-hm-z])\s*=/gi);
450
- for (const match of singleLetterMatches) {
451
- const letter = match[1].toLowerCase();
452
- const isInLoopContext = line.includes("for") || /\.(map|filter|forEach|reduce|find|some|every)\s*\(/.test(line) || line.includes("=>") || // Arrow function
453
- /\w+\s*=>\s*/.test(line);
454
- const isI18nContext = line.includes("useTranslation") || line.includes("i18n.t") || /\bt\s*\(['"]/.test(line);
455
- const isArrowFunctionParam = /\(\s*[a-z]\s*(?:,\s*[a-z]\s*)*\)\s*=>/.test(line) || // (s) => or (a, b) =>
456
- /[a-z]\s*=>/.test(line) || // s => on same line
457
- // Multi-line arrow function detection: look for pattern in context window
458
- new RegExp(`\\b${letter}\\s*\\)\\s*$`).test(line) && /=>/.test(contextWindow) || // (s)\n =>
459
- new RegExp(`\\.(?:map|filter|forEach|reduce|find|some|every)\\s*\\(\\s*$`).test(lines[index - 1] || "") && /=>/.test(contextWindow);
460
- const isShortLived = isShortLivedVariable(letter, index);
461
- if (!isInLoopContext && !isI18nContext && !isArrowFunctionParam && !isShortLived && !["x", "y", "z", "i", "j", "k", "l", "n", "m"].includes(letter)) {
462
- if (isTestFile && ["a", "b", "c", "d", "e", "f", "s"].includes(letter)) {
463
- continue;
848
+ const scopeTracker = new ScopeTracker(ast);
849
+ const context = buildCodeContext(file, ast);
850
+ const ancestors = [];
851
+ traverseAST(ast, {
852
+ enter: (node, parent) => {
853
+ ancestors.push(node);
854
+ if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") {
855
+ scopeTracker.enterScope("function", node);
856
+ if ("params" in node) {
857
+ for (const param of node.params) {
858
+ if (param.type === "Identifier") {
859
+ scopeTracker.declareVariable(param.name, param, getLineNumber(param), {
860
+ isParameter: true
861
+ });
862
+ } else if (param.type === "ObjectPattern" || param.type === "ArrayPattern") {
863
+ extractIdentifiersFromPattern(param, scopeTracker, true);
864
+ }
464
865
  }
465
- issues.push({
466
- file,
467
- line: lineNumber,
468
- type: "poor-naming",
469
- identifier: match[1],
470
- severity: "minor",
471
- suggestion: `Use descriptive variable name instead of single letter '${match[1]}'`
472
- });
473
866
  }
867
+ } else if (node.type === "BlockStatement") {
868
+ scopeTracker.enterScope("block", node);
869
+ } else if (isLoopStatement(node)) {
870
+ scopeTracker.enterScope("loop", node);
871
+ } else if (node.type === "ClassDeclaration") {
872
+ scopeTracker.enterScope("class", node);
474
873
  }
475
- }
476
- if (!disabledChecks.has("abbreviation")) {
477
- const abbreviationMatches = line.matchAll(/\b(?:const|let|var)\s+([a-z]{1,3})(?=[A-Z]|_|\s*=)/g);
478
- for (const match of abbreviationMatches) {
479
- const abbrev = match[1].toLowerCase();
480
- if (allShortWords.has(abbrev)) {
481
- continue;
482
- }
483
- if (allAbbreviations.has(abbrev)) {
484
- continue;
485
- }
486
- const isArrowFunctionParam = /\(\s*[a-z]\s*(?:,\s*[a-z]\s*)*\)\s*=>/.test(line) || // (s) => or (a, b) =>
487
- new RegExp(`\\b${abbrev}\\s*=>`).test(line) || // s => on same line
488
- // Multi-line arrow function: check context window
489
- new RegExp(`\\b${abbrev}\\s*\\)\\s*$`).test(line) && /=>/.test(contextWindow) || // (s)\n =>
490
- new RegExp(`\\.(?:map|filter|forEach|reduce|find|some|every)\\s*\\(\\s*$`).test(lines[index - 1] || "") && new RegExp(`^\\s*${abbrev}\\s*=>`).test(line);
491
- if (isArrowFunctionParam) {
492
- continue;
874
+ if (node.type === "VariableDeclarator") {
875
+ if (node.id.type === "Identifier") {
876
+ const isInCoverage = isCoverageContext(node, ancestors);
877
+ scopeTracker.declareVariable(
878
+ node.id.name,
879
+ node.id,
880
+ getLineNumber(node.id),
881
+ {
882
+ type: "typeAnnotation" in node.id ? node.id.typeAnnotation : null,
883
+ isDestructured: false,
884
+ isLoopVariable: scopeTracker.getCurrentScopeType() === "loop"
885
+ }
886
+ );
887
+ } else if (node.id.type === "ObjectPattern" || node.id.type === "ArrayPattern") {
888
+ extractIdentifiersFromPattern(node.id, scopeTracker, false, ancestors);
493
889
  }
494
- if (abbrev.length <= 2) {
495
- const isDateTimeContext = /date|time|day|hour|minute|second|timestamp/i.test(line);
496
- if (isDateTimeContext && ["d", "t", "dt"].includes(abbrev)) {
497
- continue;
498
- }
499
- const isUserContext = /user|auth|account/i.test(line);
500
- if (isUserContext && abbrev === "u") {
501
- continue;
890
+ }
891
+ if (node.type === "Identifier" && parent) {
892
+ if (parent.type !== "VariableDeclarator" || parent.id !== node) {
893
+ if (parent.type !== "FunctionDeclaration" || parent.id !== node) {
894
+ scopeTracker.addReference(node.name, node);
502
895
  }
503
896
  }
504
- issues.push({
505
- file,
506
- line: lineNumber,
507
- type: "abbreviation",
508
- identifier: match[1],
509
- severity: "info",
510
- suggestion: `Consider using full word instead of abbreviation '${match[1]}'`
511
- });
897
+ }
898
+ },
899
+ leave: (node) => {
900
+ ancestors.pop();
901
+ if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression" || node.type === "BlockStatement" || isLoopStatement(node) || node.type === "ClassDeclaration") {
902
+ scopeTracker.exitScope();
512
903
  }
513
904
  }
514
- if (!disabledChecks.has("convention-mix") && file.match(/\.(ts|tsx|js|jsx)$/)) {
515
- const camelCaseVars = line.match(/\b(?:const|let|var)\s+([a-z][a-zA-Z0-9]*)\s*=/);
516
- const snakeCaseVars = line.match(/\b(?:const|let|var)\s+([a-z][a-z0-9]*_[a-z0-9_]*)\s*=/);
517
- if (snakeCaseVars) {
905
+ });
906
+ const allVariables = scopeTracker.getAllVariables();
907
+ for (const varInfo of allVariables) {
908
+ const name = varInfo.name;
909
+ const line = varInfo.declarationLine;
910
+ if (disabledChecks.has("single-letter") && name.length === 1) continue;
911
+ if (disabledChecks.has("abbreviation") && name.length <= 3) continue;
912
+ const isInCoverage = ["s", "b", "f", "l"].includes(name) && varInfo.isDestructured;
913
+ if (isInCoverage) continue;
914
+ const functionComplexity = varInfo.node.type === "Identifier" && "parent" in varInfo.node ? calculateComplexity(varInfo.node) : context.complexity;
915
+ if (isAcceptableInContext(name, context, {
916
+ isLoopVariable: varInfo.isLoopVariable || allAbbreviations.has(name),
917
+ isParameter: varInfo.isParameter,
918
+ isDestructured: varInfo.isDestructured,
919
+ complexity: functionComplexity
920
+ })) {
921
+ continue;
922
+ }
923
+ if (name.length === 1 && !allAbbreviations.has(name) && !allShortWords.has(name)) {
924
+ const isShortLived = scopeTracker.isShortLived(varInfo, 5);
925
+ if (!isShortLived) {
518
926
  issues.push({
519
927
  file,
520
- line: lineNumber,
521
- type: "convention-mix",
522
- identifier: snakeCaseVars[1],
523
- severity: "minor",
524
- suggestion: `Use camelCase '${snakeCaseToCamelCase(snakeCaseVars[1])}' instead of snake_case in TypeScript/JavaScript`
928
+ line,
929
+ type: "poor-naming",
930
+ identifier: name,
931
+ severity: adjustSeverity("minor", context, "poor-naming"),
932
+ suggestion: `Use descriptive variable name instead of single letter '${name}'`
525
933
  });
526
934
  }
935
+ continue;
527
936
  }
528
- if (!disabledChecks.has("unclear")) {
529
- const booleanMatches = line.matchAll(/\b(?:const|let|var)\s+([a-z][a-zA-Z0-9]*)\s*:\s*boolean/gi);
530
- for (const match of booleanMatches) {
531
- const name = match[1];
532
- if (!name.match(/^(is|has|should|can|will|did)/i)) {
937
+ if (name.length >= 2 && name.length <= 3) {
938
+ if (!allShortWords.has(name.toLowerCase()) && !allAbbreviations.has(name.toLowerCase())) {
939
+ const isShortLived = scopeTracker.isShortLived(varInfo, 5);
940
+ if (!isShortLived) {
533
941
  issues.push({
534
942
  file,
535
- line: lineNumber,
536
- type: "unclear",
943
+ line,
944
+ type: "abbreviation",
537
945
  identifier: name,
538
- severity: "info",
539
- suggestion: `Boolean variable '${name}' should start with is/has/should/can for clarity`
946
+ severity: adjustSeverity("info", context, "abbreviation"),
947
+ suggestion: `Consider using full word instead of abbreviation '${name}'`
540
948
  });
541
949
  }
542
950
  }
951
+ continue;
543
952
  }
544
- if (!disabledChecks.has("unclear")) {
545
- const functionMatches = line.matchAll(/function\s+([a-z][a-zA-Z0-9]*)/g);
546
- for (const match of functionMatches) {
547
- const name = match[1];
548
- const isKeyword = ["for", "if", "else", "while", "do", "switch", "case", "break", "continue", "return", "throw", "try", "catch", "finally", "with", "yield", "await"].includes(name);
549
- if (isKeyword) {
550
- continue;
551
- }
552
- const isEntryPoint = ["main", "init", "setup", "bootstrap"].includes(name);
553
- if (isEntryPoint) {
554
- continue;
555
- }
556
- const isFactoryPattern = name.match(/(Factory|Builder|Creator|Generator|Provider|Adapter|Mock)$/);
557
- const isEventHandler = name.match(/^on[A-Z]/);
558
- const isDescriptiveLong = name.length > 15;
559
- const isReactHook = name.match(/^use[A-Z]/);
560
- 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)$/);
561
- const isHelperPattern = name.match(/^(to|from|with|without|for|as|into)\w+/) || // toMetadata, withLogger, forPath
562
- name.match(/^\w+(To|From|With|Without|For|As|Into)\w*$/);
563
- const isUtilityName = ["cn", "proxy", "sitemap", "robots", "gtag"].includes(name);
564
- const capitalCount = (name.match(/[A-Z]/g) || []).length;
565
- const isCompoundWord = capitalCount >= 3;
566
- 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)/);
567
- if (!hasActionVerb && !isFactoryPattern && !isEventHandler && !isDescriptiveLong && !isDescriptivePattern && !isCompoundWord && !isHelperPattern && !isUtilityName && !isReactHook) {
568
- issues.push({
569
- file,
570
- line: lineNumber,
571
- type: "unclear",
572
- identifier: name,
573
- severity: "info",
574
- suggestion: `Function '${name}' should start with an action verb (get, set, create, etc.)`
575
- });
576
- }
953
+ if (!disabledChecks.has("convention-mix") && file.match(/\.(ts|tsx|js|jsx)$/)) {
954
+ if (name.includes("_") && !name.startsWith("_") && name.toLowerCase() === name) {
955
+ const camelCase = name.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
956
+ issues.push({
957
+ file,
958
+ line,
959
+ type: "convention-mix",
960
+ identifier: name,
961
+ severity: adjustSeverity("minor", context, "convention-mix"),
962
+ suggestion: `Use camelCase '${camelCase}' instead of snake_case in TypeScript/JavaScript`
963
+ });
577
964
  }
578
965
  }
579
- });
966
+ }
967
+ if (!disabledChecks.has("unclear")) {
968
+ traverseAST(ast, {
969
+ enter: (node) => {
970
+ if (node.type === "FunctionDeclaration" || node.type === "MethodDefinition") {
971
+ const name = getFunctionName(node);
972
+ if (!name) return;
973
+ const line = getLineNumber(node);
974
+ if (["main", "init", "setup", "bootstrap"].includes(name)) return;
975
+ 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)/);
976
+ const isFactoryPattern = name.match(/(Factory|Builder|Creator|Generator|Provider|Adapter|Mock)$/);
977
+ const isEventHandler = name.match(/^on[A-Z]/);
978
+ const isDescriptiveLong = name.length > 15;
979
+ const isReactHook = name.match(/^use[A-Z]/);
980
+ const isHelperPattern = name.match(/^(to|from|with|without|for|as|into)\w+/);
981
+ const isUtilityName = ["cn", "proxy", "sitemap", "robots", "gtag"].includes(name);
982
+ 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)$/);
983
+ const capitalCount = (name.match(/[A-Z]/g) || []).length;
984
+ const isCompoundWord = capitalCount >= 3;
985
+ if (!hasActionVerb && !isFactoryPattern && !isEventHandler && !isDescriptiveLong && !isReactHook && !isHelperPattern && !isUtilityName && !isDescriptivePattern && !isCompoundWord) {
986
+ issues.push({
987
+ file,
988
+ line,
989
+ type: "unclear",
990
+ identifier: name,
991
+ severity: adjustSeverity("info", context, "unclear"),
992
+ suggestion: `Function '${name}' should start with an action verb (get, set, create, etc.)`
993
+ });
994
+ }
995
+ }
996
+ }
997
+ });
998
+ }
580
999
  return issues;
581
1000
  }
582
- function snakeCaseToCamelCase(str) {
583
- return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
584
- }
585
- function detectNamingConventions(files, allIssues) {
586
- const camelCaseCount = allIssues.filter((i) => i.type === "convention-mix").length;
587
- const totalChecks = files.length * 10;
588
- if (camelCaseCount / totalChecks > 0.3) {
589
- return { dominantConvention: "mixed", conventionScore: 0.5 };
1001
+ function extractIdentifiersFromPattern(pattern, scopeTracker, isParameter, ancestors) {
1002
+ if (pattern.type === "ObjectPattern") {
1003
+ for (const prop of pattern.properties) {
1004
+ if (prop.type === "Property" && prop.value.type === "Identifier") {
1005
+ scopeTracker.declareVariable(
1006
+ prop.value.name,
1007
+ prop.value,
1008
+ getLineNumber(prop.value),
1009
+ {
1010
+ isParameter,
1011
+ isDestructured: true
1012
+ }
1013
+ );
1014
+ } else if (prop.type === "RestElement" && prop.argument.type === "Identifier") {
1015
+ scopeTracker.declareVariable(
1016
+ prop.argument.name,
1017
+ prop.argument,
1018
+ getLineNumber(prop.argument),
1019
+ {
1020
+ isParameter,
1021
+ isDestructured: true
1022
+ }
1023
+ );
1024
+ }
1025
+ }
1026
+ } else if (pattern.type === "ArrayPattern") {
1027
+ for (const element of pattern.elements) {
1028
+ if (element && element.type === "Identifier") {
1029
+ scopeTracker.declareVariable(
1030
+ element.name,
1031
+ element,
1032
+ getLineNumber(element),
1033
+ {
1034
+ isParameter,
1035
+ isDestructured: true
1036
+ }
1037
+ );
1038
+ }
1039
+ }
590
1040
  }
591
- return { dominantConvention: "camelCase", conventionScore: 0.9 };
592
1041
  }
593
1042
 
594
1043
  // src/analyzers/patterns.ts
@@ -747,7 +1196,7 @@ async function analyzeConsistency(options) {
747
1196
  ...scanOptions
748
1197
  } = options;
749
1198
  const filePaths = await (0, import_core3.scanFiles)(scanOptions);
750
- const namingIssues = checkNaming ? await analyzeNaming(filePaths) : [];
1199
+ const namingIssues = checkNaming ? await analyzeNamingAST(filePaths) : [];
751
1200
  const patternIssues = checkPatterns ? await analyzePatterns(filePaths) : [];
752
1201
  const results = [];
753
1202
  const fileIssuesMap = /* @__PURE__ */ new Map();
@@ -806,7 +1255,6 @@ async function analyzeConsistency(options) {
806
1255
  });
807
1256
  }
808
1257
  const recommendations = generateRecommendations(namingIssues, patternIssues);
809
- const conventionAnalysis = detectNamingConventions(filePaths, namingIssues);
810
1258
  return {
811
1259
  summary: {
812
1260
  totalIssues: namingIssues.length + patternIssues.length,
@@ -872,7 +1320,7 @@ function generateRecommendations(namingIssues, patternIssues) {
872
1320
 
873
1321
  // src/cli.ts
874
1322
  var import_chalk = __toESM(require("chalk"));
875
- var import_fs = require("fs");
1323
+ var import_fs2 = require("fs");
876
1324
  var import_path2 = require("path");
877
1325
  var import_core4 = require("@aiready/core");
878
1326
  var program = new import_commander.Command();
@@ -923,10 +1371,10 @@ EXAMPLES:
923
1371
  directory
924
1372
  );
925
1373
  const dir = (0, import_path2.dirname)(outputPath);
926
- if (!(0, import_fs.existsSync)(dir)) {
927
- (0, import_fs.mkdirSync)(dir, { recursive: true });
1374
+ if (!(0, import_fs2.existsSync)(dir)) {
1375
+ (0, import_fs2.mkdirSync)(dir, { recursive: true });
928
1376
  }
929
- (0, import_fs.writeFileSync)(outputPath, output);
1377
+ (0, import_fs2.writeFileSync)(outputPath, output);
930
1378
  console.log(import_chalk.default.green(`\u2713 Report saved to ${outputPath}`));
931
1379
  } else if (options.output === "markdown") {
932
1380
  const markdown = generateMarkdownReport(report, elapsedTime);
@@ -936,10 +1384,10 @@ EXAMPLES:
936
1384
  directory
937
1385
  );
938
1386
  const dir = (0, import_path2.dirname)(outputPath);
939
- if (!(0, import_fs.existsSync)(dir)) {
940
- (0, import_fs.mkdirSync)(dir, { recursive: true });
1387
+ if (!(0, import_fs2.existsSync)(dir)) {
1388
+ (0, import_fs2.mkdirSync)(dir, { recursive: true });
941
1389
  }
942
- (0, import_fs.writeFileSync)(outputPath, markdown);
1390
+ (0, import_fs2.writeFileSync)(outputPath, markdown);
943
1391
  console.log(import_chalk.default.green(`\u2713 Report saved to ${outputPath}`));
944
1392
  } else {
945
1393
  displayConsoleReport(report, elapsedTime);