@aiready/consistency 0.3.3 → 0.3.4

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.
@@ -0,0 +1,661 @@
1
+ // src/analyzers/naming.ts
2
+ import { readFileContent } from "@aiready/core";
3
+ var COMMON_SHORT_WORDS = /* @__PURE__ */ new Set([
4
+ // Full English words (1-3 letters)
5
+ "day",
6
+ "key",
7
+ "net",
8
+ "to",
9
+ "go",
10
+ "for",
11
+ "not",
12
+ "new",
13
+ "old",
14
+ "top",
15
+ "end",
16
+ "run",
17
+ "try",
18
+ "use",
19
+ "get",
20
+ "set",
21
+ "add",
22
+ "put",
23
+ "map",
24
+ "log",
25
+ "row",
26
+ "col",
27
+ "tab",
28
+ "box",
29
+ "div",
30
+ "nav",
31
+ "tag",
32
+ "any",
33
+ "all",
34
+ "one",
35
+ "two",
36
+ "out",
37
+ "off",
38
+ "on",
39
+ "yes",
40
+ "no",
41
+ "now",
42
+ "max",
43
+ "min",
44
+ "sum",
45
+ "avg",
46
+ "ref",
47
+ "src",
48
+ "dst",
49
+ "raw",
50
+ "def",
51
+ "sub",
52
+ "pub",
53
+ "pre",
54
+ "mid",
55
+ "alt",
56
+ "opt",
57
+ "tmp",
58
+ "ext",
59
+ "sep"
60
+ ]);
61
+ var ACCEPTABLE_ABBREVIATIONS = /* @__PURE__ */ new Set([
62
+ // Standard identifiers
63
+ "id",
64
+ "uid",
65
+ "gid",
66
+ "pid",
67
+ // Web/Network
68
+ "url",
69
+ "uri",
70
+ "api",
71
+ "cdn",
72
+ "dns",
73
+ "ip",
74
+ "tcp",
75
+ "udp",
76
+ "http",
77
+ "ssl",
78
+ "tls",
79
+ "utm",
80
+ "seo",
81
+ "rss",
82
+ "xhr",
83
+ "ajax",
84
+ "cors",
85
+ "ws",
86
+ "wss",
87
+ // Data formats
88
+ "json",
89
+ "xml",
90
+ "yaml",
91
+ "csv",
92
+ "html",
93
+ "css",
94
+ "svg",
95
+ "pdf",
96
+ // File types & extensions
97
+ "img",
98
+ "txt",
99
+ "doc",
100
+ "docx",
101
+ "xlsx",
102
+ "ppt",
103
+ "md",
104
+ "rst",
105
+ "jpg",
106
+ "png",
107
+ "gif",
108
+ // Databases
109
+ "db",
110
+ "sql",
111
+ "orm",
112
+ "dao",
113
+ "dto",
114
+ "ddb",
115
+ "rds",
116
+ "nosql",
117
+ // File system
118
+ "fs",
119
+ "dir",
120
+ "tmp",
121
+ "src",
122
+ "dst",
123
+ "bin",
124
+ "lib",
125
+ "pkg",
126
+ // Operating system
127
+ "os",
128
+ "env",
129
+ "arg",
130
+ "cli",
131
+ "cmd",
132
+ "exe",
133
+ "cwd",
134
+ "pwd",
135
+ // UI/UX
136
+ "ui",
137
+ "ux",
138
+ "gui",
139
+ "dom",
140
+ "ref",
141
+ // Request/Response
142
+ "req",
143
+ "res",
144
+ "ctx",
145
+ "err",
146
+ "msg",
147
+ "auth",
148
+ // Mathematics/Computing
149
+ "max",
150
+ "min",
151
+ "avg",
152
+ "sum",
153
+ "abs",
154
+ "cos",
155
+ "sin",
156
+ "tan",
157
+ "log",
158
+ "exp",
159
+ "pow",
160
+ "sqrt",
161
+ "std",
162
+ "var",
163
+ "int",
164
+ "num",
165
+ "idx",
166
+ // Time
167
+ "now",
168
+ "utc",
169
+ "tz",
170
+ "ms",
171
+ "sec",
172
+ "hr",
173
+ "min",
174
+ "yr",
175
+ "mo",
176
+ // Common patterns
177
+ "app",
178
+ "cfg",
179
+ "config",
180
+ "init",
181
+ "len",
182
+ "val",
183
+ "str",
184
+ "obj",
185
+ "arr",
186
+ "gen",
187
+ "def",
188
+ "raw",
189
+ "new",
190
+ "old",
191
+ "pre",
192
+ "post",
193
+ "sub",
194
+ "pub",
195
+ // Programming/Framework specific
196
+ "ts",
197
+ "js",
198
+ "jsx",
199
+ "tsx",
200
+ "py",
201
+ "rb",
202
+ "vue",
203
+ "re",
204
+ "fn",
205
+ "fns",
206
+ "mod",
207
+ "opts",
208
+ // Cloud/Infrastructure
209
+ "s3",
210
+ "ec2",
211
+ "sqs",
212
+ "sns",
213
+ "vpc",
214
+ "ami",
215
+ "iam",
216
+ "acl",
217
+ "elb",
218
+ "alb",
219
+ "nlb",
220
+ // Metrics/Performance
221
+ "fcp",
222
+ "lcp",
223
+ "cls",
224
+ "ttfb",
225
+ "tti",
226
+ "fid",
227
+ "fps",
228
+ "qps",
229
+ "rps",
230
+ "tps",
231
+ // Testing & i18n
232
+ "po",
233
+ "e2e",
234
+ "a11y",
235
+ "i18n",
236
+ "l10n",
237
+ // Boolean helpers (these are intentional short names)
238
+ "is",
239
+ "has",
240
+ "can",
241
+ "did",
242
+ "was",
243
+ "are",
244
+ // Date/Time context (when in date contexts)
245
+ "d",
246
+ "t",
247
+ "dt"
248
+ ]);
249
+ async function analyzeNaming(files) {
250
+ const issues = [];
251
+ for (const file of files) {
252
+ const content = await readFileContent(file);
253
+ const fileIssues = analyzeFileNaming(file, content);
254
+ issues.push(...fileIssues);
255
+ }
256
+ return issues;
257
+ }
258
+ function analyzeFileNaming(file, content) {
259
+ const issues = [];
260
+ const isTestFile = file.match(/\.(test|spec)\.(ts|tsx|js|jsx)$/);
261
+ const lines = content.split("\n");
262
+ lines.forEach((line, index) => {
263
+ const lineNumber = index + 1;
264
+ const singleLetterMatches = line.matchAll(/\b(?:const|let|var)\s+([a-hm-z])\s*=/gi);
265
+ for (const match of singleLetterMatches) {
266
+ const letter = match[1].toLowerCase();
267
+ const isInLoopContext = line.includes("for") || /\.(map|filter|forEach|reduce|find|some|every)\s*\(/.test(line) || line.includes("=>") || // Arrow function
268
+ /\w+\s*=>\s*/.test(line);
269
+ const isI18nContext = line.includes("useTranslation") || line.includes("i18n.t") || /\bt\s*\(['"]/.test(line);
270
+ if (!isInLoopContext && !isI18nContext && !["x", "y", "z", "i", "j", "k", "l", "n", "m"].includes(letter)) {
271
+ if (isTestFile && ["a", "b", "c", "d", "e", "f"].includes(letter)) {
272
+ continue;
273
+ }
274
+ issues.push({
275
+ file,
276
+ line: lineNumber,
277
+ type: "poor-naming",
278
+ identifier: match[1],
279
+ severity: "minor",
280
+ suggestion: `Use descriptive variable name instead of single letter '${match[1]}'`
281
+ });
282
+ }
283
+ }
284
+ const abbreviationMatches = line.matchAll(/\b(?:const|let|var)\s+([a-z]{1,3})(?=[A-Z]|_|\s*=)/g);
285
+ for (const match of abbreviationMatches) {
286
+ const abbrev = match[1].toLowerCase();
287
+ if (COMMON_SHORT_WORDS.has(abbrev)) {
288
+ continue;
289
+ }
290
+ if (ACCEPTABLE_ABBREVIATIONS.has(abbrev)) {
291
+ continue;
292
+ }
293
+ if (abbrev.length <= 2) {
294
+ const isDateTimeContext = /date|time|day|hour|minute|second|timestamp/i.test(line);
295
+ if (isDateTimeContext && ["d", "t", "dt"].includes(abbrev)) {
296
+ continue;
297
+ }
298
+ const isUserContext = /user|auth|account/i.test(line);
299
+ if (isUserContext && abbrev === "u") {
300
+ continue;
301
+ }
302
+ }
303
+ issues.push({
304
+ file,
305
+ line: lineNumber,
306
+ type: "abbreviation",
307
+ identifier: match[1],
308
+ severity: "info",
309
+ suggestion: `Consider using full word instead of abbreviation '${match[1]}'`
310
+ });
311
+ }
312
+ if (file.match(/\.(ts|tsx|js|jsx)$/)) {
313
+ const camelCaseVars = line.match(/\b(?:const|let|var)\s+([a-z][a-zA-Z0-9]*)\s*=/);
314
+ const snakeCaseVars = line.match(/\b(?:const|let|var)\s+([a-z][a-z0-9]*_[a-z0-9_]*)\s*=/);
315
+ if (snakeCaseVars) {
316
+ issues.push({
317
+ file,
318
+ line: lineNumber,
319
+ type: "convention-mix",
320
+ identifier: snakeCaseVars[1],
321
+ severity: "minor",
322
+ suggestion: `Use camelCase '${snakeCaseToCamelCase(snakeCaseVars[1])}' instead of snake_case in TypeScript/JavaScript`
323
+ });
324
+ }
325
+ }
326
+ const booleanMatches = line.matchAll(/\b(?:const|let|var)\s+([a-z][a-zA-Z0-9]*)\s*:\s*boolean/gi);
327
+ for (const match of booleanMatches) {
328
+ const name = match[1];
329
+ if (!name.match(/^(is|has|should|can|will|did)/i)) {
330
+ issues.push({
331
+ file,
332
+ line: lineNumber,
333
+ type: "unclear",
334
+ identifier: name,
335
+ severity: "info",
336
+ suggestion: `Boolean variable '${name}' should start with is/has/should/can for clarity`
337
+ });
338
+ }
339
+ }
340
+ const functionMatches = line.matchAll(/function\s+([a-z][a-zA-Z0-9]*)/g);
341
+ for (const match of functionMatches) {
342
+ const name = match[1];
343
+ const isFactoryPattern = name.match(/(Factory|Builder|Creator|Generator)$/);
344
+ const isEventHandler = name.match(/^on[A-Z]/);
345
+ const isDescriptiveLong = name.length > 15;
346
+ 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)$/);
347
+ const capitalCount = (name.match(/[A-Z]/g) || []).length;
348
+ const isCompoundWord = capitalCount >= 3;
349
+ 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)/);
350
+ if (!hasActionVerb && !isFactoryPattern && !isEventHandler && !isDescriptiveLong && !isDescriptivePattern && !isCompoundWord) {
351
+ issues.push({
352
+ file,
353
+ line: lineNumber,
354
+ type: "unclear",
355
+ identifier: name,
356
+ severity: "info",
357
+ suggestion: `Function '${name}' should start with an action verb (get, set, create, etc.)`
358
+ });
359
+ }
360
+ }
361
+ });
362
+ return issues;
363
+ }
364
+ function snakeCaseToCamelCase(str) {
365
+ return str.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
366
+ }
367
+ function detectNamingConventions(files, allIssues) {
368
+ const camelCaseCount = allIssues.filter((i) => i.type === "convention-mix").length;
369
+ const totalChecks = files.length * 10;
370
+ if (camelCaseCount / totalChecks > 0.3) {
371
+ return { dominantConvention: "mixed", conventionScore: 0.5 };
372
+ }
373
+ return { dominantConvention: "camelCase", conventionScore: 0.9 };
374
+ }
375
+
376
+ // src/analyzers/patterns.ts
377
+ import { readFileContent as readFileContent2 } from "@aiready/core";
378
+ async function analyzePatterns(files) {
379
+ const issues = [];
380
+ const errorHandlingIssues = await analyzeErrorHandling(files);
381
+ issues.push(...errorHandlingIssues);
382
+ const asyncIssues = await analyzeAsyncPatterns(files);
383
+ issues.push(...asyncIssues);
384
+ const importIssues = await analyzeImportStyles(files);
385
+ issues.push(...importIssues);
386
+ return issues;
387
+ }
388
+ async function analyzeErrorHandling(files) {
389
+ const patterns = {
390
+ tryCatch: [],
391
+ throwsError: [],
392
+ returnsNull: [],
393
+ returnsError: []
394
+ };
395
+ for (const file of files) {
396
+ const content = await readFileContent2(file);
397
+ if (content.includes("try {") || content.includes("} catch")) {
398
+ patterns.tryCatch.push(file);
399
+ }
400
+ if (content.match(/throw new \w*Error/)) {
401
+ patterns.throwsError.push(file);
402
+ }
403
+ if (content.match(/return null/)) {
404
+ patterns.returnsNull.push(file);
405
+ }
406
+ if (content.match(/return \{ error:/)) {
407
+ patterns.returnsError.push(file);
408
+ }
409
+ }
410
+ const issues = [];
411
+ const strategiesUsed = Object.values(patterns).filter((p) => p.length > 0).length;
412
+ if (strategiesUsed > 2) {
413
+ issues.push({
414
+ files: [.../* @__PURE__ */ new Set([
415
+ ...patterns.tryCatch,
416
+ ...patterns.throwsError,
417
+ ...patterns.returnsNull,
418
+ ...patterns.returnsError
419
+ ])],
420
+ type: "error-handling",
421
+ description: "Inconsistent error handling strategies across codebase",
422
+ examples: [
423
+ patterns.tryCatch.length > 0 ? `Try-catch used in ${patterns.tryCatch.length} files` : "",
424
+ patterns.throwsError.length > 0 ? `Throws errors in ${patterns.throwsError.length} files` : "",
425
+ patterns.returnsNull.length > 0 ? `Returns null in ${patterns.returnsNull.length} files` : "",
426
+ patterns.returnsError.length > 0 ? `Returns error objects in ${patterns.returnsError.length} files` : ""
427
+ ].filter((e) => e),
428
+ severity: "major"
429
+ });
430
+ }
431
+ return issues;
432
+ }
433
+ async function analyzeAsyncPatterns(files) {
434
+ const patterns = {
435
+ asyncAwait: [],
436
+ promises: [],
437
+ callbacks: []
438
+ };
439
+ for (const file of files) {
440
+ const content = await readFileContent2(file);
441
+ if (content.match(/async\s+(function|\(|[a-zA-Z])/)) {
442
+ patterns.asyncAwait.push(file);
443
+ }
444
+ if (content.match(/\.then\(/) || content.match(/\.catch\(/)) {
445
+ patterns.promises.push(file);
446
+ }
447
+ if (content.match(/callback\s*\(/) || content.match(/\(\s*err\s*,/)) {
448
+ patterns.callbacks.push(file);
449
+ }
450
+ }
451
+ const issues = [];
452
+ if (patterns.callbacks.length > 0 && patterns.asyncAwait.length > 0) {
453
+ issues.push({
454
+ files: [...patterns.callbacks, ...patterns.asyncAwait],
455
+ type: "async-style",
456
+ description: "Mixed async patterns: callbacks and async/await",
457
+ examples: [
458
+ `Callbacks found in: ${patterns.callbacks.slice(0, 3).join(", ")}`,
459
+ `Async/await used in: ${patterns.asyncAwait.slice(0, 3).join(", ")}`
460
+ ],
461
+ severity: "minor"
462
+ });
463
+ }
464
+ if (patterns.promises.length > patterns.asyncAwait.length * 0.3 && patterns.asyncAwait.length > 0) {
465
+ issues.push({
466
+ files: patterns.promises,
467
+ type: "async-style",
468
+ description: "Consider using async/await instead of promise chains for consistency",
469
+ examples: patterns.promises.slice(0, 5),
470
+ severity: "info"
471
+ });
472
+ }
473
+ return issues;
474
+ }
475
+ async function analyzeImportStyles(files) {
476
+ const patterns = {
477
+ esModules: [],
478
+ commonJS: [],
479
+ mixed: []
480
+ };
481
+ for (const file of files) {
482
+ const content = await readFileContent2(file);
483
+ const hasESM = content.match(/^import\s+/m);
484
+ const hasCJS = content.match(/require\s*\(/);
485
+ if (hasESM && hasCJS) {
486
+ patterns.mixed.push(file);
487
+ } else if (hasESM) {
488
+ patterns.esModules.push(file);
489
+ } else if (hasCJS) {
490
+ patterns.commonJS.push(file);
491
+ }
492
+ }
493
+ const issues = [];
494
+ if (patterns.mixed.length > 0) {
495
+ issues.push({
496
+ files: patterns.mixed,
497
+ type: "import-style",
498
+ description: "Mixed ES modules and CommonJS imports in same files",
499
+ examples: patterns.mixed.slice(0, 5),
500
+ severity: "major"
501
+ });
502
+ }
503
+ if (patterns.esModules.length > 0 && patterns.commonJS.length > 0) {
504
+ const ratio = patterns.commonJS.length / (patterns.esModules.length + patterns.commonJS.length);
505
+ if (ratio > 0.2 && ratio < 0.8) {
506
+ issues.push({
507
+ files: [...patterns.esModules, ...patterns.commonJS],
508
+ type: "import-style",
509
+ description: "Inconsistent import styles across project",
510
+ examples: [
511
+ `ES modules: ${patterns.esModules.length} files`,
512
+ `CommonJS: ${patterns.commonJS.length} files`
513
+ ],
514
+ severity: "minor"
515
+ });
516
+ }
517
+ }
518
+ return issues;
519
+ }
520
+
521
+ // src/analyzer.ts
522
+ import { scanFiles } from "@aiready/core";
523
+ async function analyzeConsistency(options) {
524
+ const {
525
+ checkNaming = true,
526
+ checkPatterns = true,
527
+ checkArchitecture = false,
528
+ // Not implemented yet
529
+ minSeverity = "info",
530
+ ...scanOptions
531
+ } = options;
532
+ const filePaths = await scanFiles(scanOptions);
533
+ const namingIssues = checkNaming ? await analyzeNaming(filePaths) : [];
534
+ const patternIssues = checkPatterns ? await analyzePatterns(filePaths) : [];
535
+ const results = [];
536
+ const fileIssuesMap = /* @__PURE__ */ new Map();
537
+ for (const issue of namingIssues) {
538
+ if (!shouldIncludeSeverity(issue.severity, minSeverity)) {
539
+ continue;
540
+ }
541
+ const consistencyIssue = {
542
+ type: issue.type === "convention-mix" ? "naming-inconsistency" : "naming-quality",
543
+ category: "naming",
544
+ severity: issue.severity,
545
+ message: `${issue.type}: ${issue.identifier}`,
546
+ location: {
547
+ file: issue.file,
548
+ line: issue.line,
549
+ column: issue.column
550
+ },
551
+ suggestion: issue.suggestion
552
+ };
553
+ if (!fileIssuesMap.has(issue.file)) {
554
+ fileIssuesMap.set(issue.file, []);
555
+ }
556
+ fileIssuesMap.get(issue.file).push(consistencyIssue);
557
+ }
558
+ for (const issue of patternIssues) {
559
+ if (!shouldIncludeSeverity(issue.severity, minSeverity)) {
560
+ continue;
561
+ }
562
+ const consistencyIssue = {
563
+ type: "pattern-inconsistency",
564
+ category: "patterns",
565
+ severity: issue.severity,
566
+ message: issue.description,
567
+ location: {
568
+ file: issue.files[0] || "multiple files",
569
+ line: 1
570
+ },
571
+ examples: issue.examples,
572
+ suggestion: `Standardize ${issue.type} patterns across ${issue.files.length} files`
573
+ };
574
+ const firstFile = issue.files[0];
575
+ if (firstFile && !fileIssuesMap.has(firstFile)) {
576
+ fileIssuesMap.set(firstFile, []);
577
+ }
578
+ if (firstFile) {
579
+ fileIssuesMap.get(firstFile).push(consistencyIssue);
580
+ }
581
+ }
582
+ for (const [fileName, issues] of fileIssuesMap) {
583
+ results.push({
584
+ fileName,
585
+ issues,
586
+ metrics: {
587
+ consistencyScore: calculateConsistencyScore(issues)
588
+ }
589
+ });
590
+ }
591
+ const recommendations = generateRecommendations(namingIssues, patternIssues);
592
+ const conventionAnalysis = detectNamingConventions(filePaths, namingIssues);
593
+ return {
594
+ summary: {
595
+ totalIssues: namingIssues.length + patternIssues.length,
596
+ namingIssues: namingIssues.length,
597
+ patternIssues: patternIssues.length,
598
+ architectureIssues: 0,
599
+ filesAnalyzed: filePaths.length
600
+ },
601
+ results,
602
+ recommendations
603
+ };
604
+ }
605
+ function shouldIncludeSeverity(severity, minSeverity) {
606
+ const severityLevels = { info: 0, minor: 1, major: 2, critical: 3 };
607
+ return severityLevels[severity] >= severityLevels[minSeverity];
608
+ }
609
+ function calculateConsistencyScore(issues) {
610
+ const weights = { critical: 10, major: 5, minor: 2, info: 1 };
611
+ const totalWeight = issues.reduce((sum, issue) => sum + weights[issue.severity], 0);
612
+ return Math.max(0, 1 - totalWeight / 100);
613
+ }
614
+ function generateRecommendations(namingIssues, patternIssues) {
615
+ const recommendations = [];
616
+ if (namingIssues.length > 0) {
617
+ const conventionMixCount = namingIssues.filter((i) => i.type === "convention-mix").length;
618
+ if (conventionMixCount > 0) {
619
+ recommendations.push(
620
+ `Standardize naming conventions: Found ${conventionMixCount} snake_case variables in TypeScript/JavaScript (use camelCase)`
621
+ );
622
+ }
623
+ const poorNamingCount = namingIssues.filter((i) => i.type === "poor-naming").length;
624
+ if (poorNamingCount > 0) {
625
+ recommendations.push(
626
+ `Improve variable naming: Found ${poorNamingCount} single-letter or unclear variable names`
627
+ );
628
+ }
629
+ }
630
+ if (patternIssues.length > 0) {
631
+ const errorHandlingIssues = patternIssues.filter((i) => i.type === "error-handling");
632
+ if (errorHandlingIssues.length > 0) {
633
+ recommendations.push(
634
+ "Standardize error handling strategy across the codebase (prefer try-catch with typed errors)"
635
+ );
636
+ }
637
+ const asyncIssues = patternIssues.filter((i) => i.type === "async-style");
638
+ if (asyncIssues.length > 0) {
639
+ recommendations.push(
640
+ "Use async/await consistently instead of mixing with promise chains or callbacks"
641
+ );
642
+ }
643
+ const importIssues = patternIssues.filter((i) => i.type === "import-style");
644
+ if (importIssues.length > 0) {
645
+ recommendations.push(
646
+ "Use ES modules consistently across the project (avoid mixing with CommonJS)"
647
+ );
648
+ }
649
+ }
650
+ if (recommendations.length === 0) {
651
+ recommendations.push("No major consistency issues found! Your codebase follows good practices.");
652
+ }
653
+ return recommendations;
654
+ }
655
+
656
+ export {
657
+ analyzeNaming,
658
+ detectNamingConventions,
659
+ analyzePatterns,
660
+ analyzeConsistency
661
+ };