@aiready/consistency 0.5.0 → 0.6.1

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