@aiready/consistency 0.5.0 → 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,30 +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
806
  "dt",
405
- // Coverage metrics (industry standard: statements/branches/functions/lines)
406
807
  "s",
407
808
  "b",
408
809
  "f",
409
810
  "l",
410
- // Common media/content abbreviations
811
+ // Coverage metrics
411
812
  "vid",
412
813
  "pic",
413
814
  "img",
414
815
  "doc",
415
816
  "msg"
416
817
  ]);
417
- async function analyzeNaming(files) {
818
+ async function analyzeNamingAST(files) {
418
819
  const issues = [];
419
820
  const rootDir = files.length > 0 ? (0, import_path.dirname)(files[0]) : process.cwd();
420
821
  const config = (0, import_core.loadConfig)(rootDir);
@@ -422,188 +823,221 @@ async function analyzeNaming(files) {
422
823
  const customAbbreviations = new Set(consistencyConfig?.acceptedAbbreviations || []);
423
824
  const customShortWords = new Set(consistencyConfig?.shortWords || []);
424
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]);
425
828
  for (const file of files) {
426
- const content = await (0, import_core.readFileContent)(file);
427
- const fileIssues = analyzeFileNaming(file, content, customAbbreviations, customShortWords, disabledChecks);
428
- 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
+ }
429
843
  }
430
844
  return issues;
431
845
  }
432
- function analyzeFileNaming(file, content, customAbbreviations, customShortWords, disabledChecks) {
846
+ function analyzeFileNamingAST(file, ast, allAbbreviations, allShortWords, disabledChecks) {
433
847
  const issues = [];
434
- const isTestFile = file.match(/\.(test|spec)\.(ts|tsx|js|jsx)$/);
435
- const lines = content.split("\n");
436
- const allAbbreviations = /* @__PURE__ */ new Set([...ACCEPTABLE_ABBREVIATIONS, ...customAbbreviations]);
437
- const allShortWords = /* @__PURE__ */ new Set([...COMMON_SHORT_WORDS, ...customShortWords]);
438
- const getContextWindow = (index, windowSize = 3) => {
439
- const start = Math.max(0, index - windowSize);
440
- const end = Math.min(lines.length, index + windowSize + 1);
441
- return lines.slice(start, end).join("\n");
442
- };
443
- const isShortLivedVariable = (varName, declarationIndex) => {
444
- const searchRange = 5;
445
- const endIndex = Math.min(lines.length, declarationIndex + searchRange + 1);
446
- let usageCount = 0;
447
- for (let i = declarationIndex; i < endIndex; i++) {
448
- const regex = new RegExp(`\\b${varName}\\b`, "g");
449
- const matches = lines[i].match(regex);
450
- if (matches) {
451
- usageCount += matches.length;
452
- }
453
- }
454
- return usageCount >= 2 && usageCount <= 3;
455
- };
456
- lines.forEach((line, index) => {
457
- const lineNumber = index + 1;
458
- const contextWindow = getContextWindow(index);
459
- if (!disabledChecks.has("single-letter")) {
460
- const singleLetterMatches = line.matchAll(/\b(?:const|let|var)\s+([a-hm-z])\s*=/gi);
461
- for (const match of singleLetterMatches) {
462
- const letter = match[1].toLowerCase();
463
- const isCoverageContext = /coverage|summary|metrics|pct|percent/i.test(line) || /\.(?:statements|branches|functions|lines)\.pct/i.test(line);
464
- if (isCoverageContext && ["s", "b", "f", "l"].includes(letter)) {
465
- continue;
466
- }
467
- const isInLoopContext = line.includes("for") || /\.(map|filter|forEach|reduce|find|some|every)\s*\(/.test(line) || line.includes("=>") || // Arrow function
468
- /\w+\s*=>\s*/.test(line);
469
- const isI18nContext = line.includes("useTranslation") || line.includes("i18n.t") || /\bt\s*\(['"]/.test(line);
470
- const isArrowFunctionParam = /\(\s*[a-z]\s*(?:,\s*[a-z]\s*)*\)\s*=>/.test(line) || // (s) => or (a, b) =>
471
- /[a-z]\s*=>/.test(line) || // s => on same line
472
- // Multi-line arrow function detection: look for pattern in context window
473
- new RegExp(`\\b${letter}\\s*\\)\\s*$`).test(line) && /=>/.test(contextWindow) || // (s)\n =>
474
- new RegExp(`\\.(?:map|filter|forEach|reduce|find|some|every)\\s*\\(\\s*$`).test(lines[index - 1] || "") && /=>/.test(contextWindow);
475
- const isShortLived = isShortLivedVariable(letter, index);
476
- if (!isInLoopContext && !isI18nContext && !isArrowFunctionParam && !isShortLived && !["x", "y", "z", "i", "j", "k", "l", "n", "m"].includes(letter)) {
477
- if (isTestFile && ["a", "b", "c", "d", "e", "f", "s"].includes(letter)) {
478
- 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
+ }
479
865
  }
480
- issues.push({
481
- file,
482
- line: lineNumber,
483
- type: "poor-naming",
484
- identifier: match[1],
485
- severity: "minor",
486
- suggestion: `Use descriptive variable name instead of single letter '${match[1]}'`
487
- });
488
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);
489
873
  }
490
- }
491
- if (!disabledChecks.has("abbreviation")) {
492
- const abbreviationMatches = line.matchAll(/\b(?:const|let|var)\s+([a-z]{1,3})(?=[A-Z]|_|\s*=)/g);
493
- for (const match of abbreviationMatches) {
494
- const abbrev = match[1].toLowerCase();
495
- if (allShortWords.has(abbrev)) {
496
- continue;
497
- }
498
- if (allAbbreviations.has(abbrev)) {
499
- continue;
500
- }
501
- const isArrowFunctionParam = /\(\s*[a-z]\s*(?:,\s*[a-z]\s*)*\)\s*=>/.test(line) || // (s) => or (a, b) =>
502
- new RegExp(`\\b${abbrev}\\s*=>`).test(line) || // s => on same line
503
- // Multi-line arrow function: check context window
504
- new RegExp(`\\b${abbrev}\\s*\\)\\s*$`).test(line) && /=>/.test(contextWindow) || // (s)\n =>
505
- new RegExp(`\\.(?:map|filter|forEach|reduce|find|some|every)\\s*\\(\\s*$`).test(lines[index - 1] || "") && new RegExp(`^\\s*${abbrev}\\s*=>`).test(line);
506
- if (isArrowFunctionParam) {
507
- 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);
508
889
  }
509
- if (abbrev.length <= 2) {
510
- const isDateTimeContext = /date|time|day|hour|minute|second|timestamp/i.test(line);
511
- if (isDateTimeContext && ["d", "t", "dt"].includes(abbrev)) {
512
- continue;
513
- }
514
- const isUserContext = /user|auth|account/i.test(line);
515
- if (isUserContext && abbrev === "u") {
516
- 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);
517
895
  }
518
896
  }
519
- issues.push({
520
- file,
521
- line: lineNumber,
522
- type: "abbreviation",
523
- identifier: match[1],
524
- severity: "info",
525
- suggestion: `Consider using full word instead of abbreviation '${match[1]}'`
526
- });
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();
527
903
  }
528
904
  }
529
- if (!disabledChecks.has("convention-mix") && file.match(/\.(ts|tsx|js|jsx)$/)) {
530
- const camelCaseVars = line.match(/\b(?:const|let|var)\s+([a-z][a-zA-Z0-9]*)\s*=/);
531
- const snakeCaseVars = line.match(/\b(?:const|let|var)\s+([a-z][a-z0-9]*_[a-z0-9_]*)\s*=/);
532
- 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) {
533
926
  issues.push({
534
927
  file,
535
- line: lineNumber,
536
- type: "convention-mix",
537
- identifier: snakeCaseVars[1],
538
- severity: "minor",
539
- 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}'`
540
933
  });
541
934
  }
935
+ continue;
542
936
  }
543
- if (!disabledChecks.has("unclear")) {
544
- const booleanMatches = line.matchAll(/\b(?:const|let|var)\s+([a-z][a-zA-Z0-9]*)\s*:\s*boolean/gi);
545
- for (const match of booleanMatches) {
546
- const name = match[1];
547
- 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) {
548
941
  issues.push({
549
942
  file,
550
- line: lineNumber,
551
- type: "unclear",
943
+ line,
944
+ type: "abbreviation",
552
945
  identifier: name,
553
- severity: "info",
554
- 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}'`
555
948
  });
556
949
  }
557
950
  }
951
+ continue;
558
952
  }
559
- if (!disabledChecks.has("unclear")) {
560
- const functionMatches = line.matchAll(/function\s+([a-z][a-zA-Z0-9]*)/g);
561
- for (const match of functionMatches) {
562
- const name = match[1];
563
- const isKeyword = ["for", "if", "else", "while", "do", "switch", "case", "break", "continue", "return", "throw", "try", "catch", "finally", "with", "yield", "await"].includes(name);
564
- if (isKeyword) {
565
- continue;
566
- }
567
- const isEntryPoint = ["main", "init", "setup", "bootstrap"].includes(name);
568
- if (isEntryPoint) {
569
- continue;
570
- }
571
- const isFactoryPattern = name.match(/(Factory|Builder|Creator|Generator|Provider|Adapter|Mock)$/);
572
- const isEventHandler = name.match(/^on[A-Z]/);
573
- const isDescriptiveLong = name.length > 15;
574
- const isReactHook = name.match(/^use[A-Z]/);
575
- 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)$/);
576
- const isHelperPattern = name.match(/^(to|from|with|without|for|as|into)\w+/) || // toMetadata, withLogger, forPath
577
- name.match(/^\w+(To|From|With|Without|For|As|Into)\w*$/);
578
- const isUtilityName = ["cn", "proxy", "sitemap", "robots", "gtag"].includes(name);
579
- const capitalCount = (name.match(/[A-Z]/g) || []).length;
580
- const isCompoundWord = capitalCount >= 3;
581
- 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)/);
582
- if (!hasActionVerb && !isFactoryPattern && !isEventHandler && !isDescriptiveLong && !isDescriptivePattern && !isCompoundWord && !isHelperPattern && !isUtilityName && !isReactHook) {
583
- issues.push({
584
- file,
585
- line: lineNumber,
586
- type: "unclear",
587
- identifier: name,
588
- severity: "info",
589
- suggestion: `Function '${name}' should start with an action verb (get, set, create, etc.)`
590
- });
591
- }
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
+ });
592
964
  }
593
965
  }
594
- });
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
+ }
595
999
  return issues;
596
1000
  }
597
- function snakeCaseToCamelCase(str) {
598
- return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
599
- }
600
- function detectNamingConventions(files, allIssues) {
601
- const camelCaseCount = allIssues.filter((i) => i.type === "convention-mix").length;
602
- const totalChecks = files.length * 10;
603
- if (camelCaseCount / totalChecks > 0.3) {
604
- 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
+ }
605
1040
  }
606
- return { dominantConvention: "camelCase", conventionScore: 0.9 };
607
1041
  }
608
1042
 
609
1043
  // src/analyzers/patterns.ts
@@ -762,7 +1196,7 @@ async function analyzeConsistency(options) {
762
1196
  ...scanOptions
763
1197
  } = options;
764
1198
  const filePaths = await (0, import_core3.scanFiles)(scanOptions);
765
- const namingIssues = checkNaming ? await analyzeNaming(filePaths) : [];
1199
+ const namingIssues = checkNaming ? await analyzeNamingAST(filePaths) : [];
766
1200
  const patternIssues = checkPatterns ? await analyzePatterns(filePaths) : [];
767
1201
  const results = [];
768
1202
  const fileIssuesMap = /* @__PURE__ */ new Map();
@@ -821,7 +1255,6 @@ async function analyzeConsistency(options) {
821
1255
  });
822
1256
  }
823
1257
  const recommendations = generateRecommendations(namingIssues, patternIssues);
824
- const conventionAnalysis = detectNamingConventions(filePaths, namingIssues);
825
1258
  return {
826
1259
  summary: {
827
1260
  totalIssues: namingIssues.length + patternIssues.length,
@@ -887,7 +1320,7 @@ function generateRecommendations(namingIssues, patternIssues) {
887
1320
 
888
1321
  // src/cli.ts
889
1322
  var import_chalk = __toESM(require("chalk"));
890
- var import_fs = require("fs");
1323
+ var import_fs2 = require("fs");
891
1324
  var import_path2 = require("path");
892
1325
  var import_core4 = require("@aiready/core");
893
1326
  var program = new import_commander.Command();
@@ -938,10 +1371,10 @@ EXAMPLES:
938
1371
  directory
939
1372
  );
940
1373
  const dir = (0, import_path2.dirname)(outputPath);
941
- if (!(0, import_fs.existsSync)(dir)) {
942
- (0, import_fs.mkdirSync)(dir, { recursive: true });
1374
+ if (!(0, import_fs2.existsSync)(dir)) {
1375
+ (0, import_fs2.mkdirSync)(dir, { recursive: true });
943
1376
  }
944
- (0, import_fs.writeFileSync)(outputPath, output);
1377
+ (0, import_fs2.writeFileSync)(outputPath, output);
945
1378
  console.log(import_chalk.default.green(`\u2713 Report saved to ${outputPath}`));
946
1379
  } else if (options.output === "markdown") {
947
1380
  const markdown = generateMarkdownReport(report, elapsedTime);
@@ -951,10 +1384,10 @@ EXAMPLES:
951
1384
  directory
952
1385
  );
953
1386
  const dir = (0, import_path2.dirname)(outputPath);
954
- if (!(0, import_fs.existsSync)(dir)) {
955
- (0, import_fs.mkdirSync)(dir, { recursive: true });
1387
+ if (!(0, import_fs2.existsSync)(dir)) {
1388
+ (0, import_fs2.mkdirSync)(dir, { recursive: true });
956
1389
  }
957
- (0, import_fs.writeFileSync)(outputPath, markdown);
1390
+ (0, import_fs2.writeFileSync)(outputPath, markdown);
958
1391
  console.log(import_chalk.default.green(`\u2713 Report saved to ${outputPath}`));
959
1392
  } else {
960
1393
  displayConsoleReport(report, elapsedTime);