@aiready/consistency 0.16.2 → 0.16.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +10 -10
- package/.turbo/turbo-test.log +12 -10
- package/dist/chunk-AASFXGUR.mjs +1622 -0
- package/dist/chunk-AR7DIZLP.mjs +827 -0
- package/dist/chunk-BMILMNKJ.mjs +1633 -0
- package/dist/chunk-HJCP36VW.mjs +821 -0
- package/dist/chunk-QOIPVP6P.mjs +1607 -0
- package/dist/chunk-RMEQWG52.mjs +1633 -0
- package/dist/chunk-XVW5DKJQ.mjs +1619 -0
- package/dist/cli.js +277 -1019
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +14 -14
- package/dist/index.d.ts +14 -14
- package/dist/index.js +320 -1242
- package/dist/index.mjs +58 -230
- package/package.json +2 -2
- package/src/__tests__/contract.test.ts +18 -2
- package/src/analyzer.ts +49 -28
- package/src/analyzers/naming-ast.ts +188 -328
- package/src/analyzers/naming.ts +52 -365
- package/src/analyzers/patterns.ts +51 -228
- package/src/types.ts +10 -10
- package/src/utils/context-detector.ts +23 -10
package/src/analyzers/naming.ts
CHANGED
|
@@ -1,384 +1,71 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { Severity } from '@aiready/core';
|
|
2
3
|
import type { NamingIssue } from '../types';
|
|
3
|
-
import {
|
|
4
|
-
COMMON_SHORT_WORDS,
|
|
5
|
-
ACCEPTABLE_ABBREVIATIONS,
|
|
6
|
-
snakeCaseToCamelCase,
|
|
7
|
-
} from './naming-constants';
|
|
8
|
-
import { loadNamingConfig } from '../utils/config-loader';
|
|
9
4
|
|
|
10
5
|
/**
|
|
11
|
-
*
|
|
6
|
+
* Legacy regex-based naming analyzer
|
|
7
|
+
* (Used as fallback or for languages without AST support)
|
|
12
8
|
*/
|
|
13
|
-
export async function analyzeNaming(
|
|
9
|
+
export async function analyzeNaming(filePaths: string[]): Promise<NamingIssue[]> {
|
|
14
10
|
const issues: NamingIssue[] = [];
|
|
15
11
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
disabledChecks
|
|
28
|
-
);
|
|
29
|
-
issues.push(...fileIssues);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return issues;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function analyzeFileNaming(
|
|
36
|
-
file: string,
|
|
37
|
-
content: string,
|
|
38
|
-
customAbbreviations: Set<string>,
|
|
39
|
-
customShortWords: Set<string>,
|
|
40
|
-
disabledChecks: Set<string>
|
|
41
|
-
): NamingIssue[] {
|
|
42
|
-
const issues: NamingIssue[] = [];
|
|
43
|
-
|
|
44
|
-
// Check if this is a test file (more lenient rules)
|
|
45
|
-
const isTestFile = file.match(/\.(test|spec)\.(ts|tsx|js|jsx)$/);
|
|
46
|
-
|
|
47
|
-
// Split into lines for line number tracking
|
|
48
|
-
const lines = content.split('\n');
|
|
49
|
-
|
|
50
|
-
// Merge custom sets with defaults
|
|
51
|
-
const allAbbreviations = new Set([
|
|
52
|
-
...ACCEPTABLE_ABBREVIATIONS,
|
|
53
|
-
...customAbbreviations,
|
|
54
|
-
]);
|
|
55
|
-
const allShortWords = new Set([...COMMON_SHORT_WORDS, ...customShortWords]);
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Helper: Get context window around a line (for multi-line pattern detection)
|
|
59
|
-
*/
|
|
60
|
-
const getContextWindow = (index: number, windowSize: number = 3): string => {
|
|
61
|
-
const start = Math.max(0, index - windowSize);
|
|
62
|
-
const end = Math.min(lines.length, index + windowSize + 1);
|
|
63
|
-
return lines.slice(start, end).join('\n');
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Helper: Check if a variable is short-lived (used only within 3-5 lines)
|
|
68
|
-
*/
|
|
69
|
-
const isShortLivedVariable = (
|
|
70
|
-
varName: string,
|
|
71
|
-
declarationIndex: number
|
|
72
|
-
): boolean => {
|
|
73
|
-
const searchRange = 5; // Check 5 lines after declaration
|
|
74
|
-
const endIndex = Math.min(lines.length, declarationIndex + searchRange + 1);
|
|
75
|
-
|
|
76
|
-
let usageCount = 0;
|
|
77
|
-
for (let i = declarationIndex; i < endIndex; i++) {
|
|
78
|
-
// Match variable name as whole word
|
|
79
|
-
const regex = new RegExp(`\\b${varName}\\b`, 'g');
|
|
80
|
-
const matches = lines[i].match(regex);
|
|
81
|
-
if (matches) {
|
|
82
|
-
usageCount += matches.length;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// If variable is only used 2-3 times within 5 lines, it's short-lived
|
|
87
|
-
// (1 = declaration, 1-2 = actual usage)
|
|
88
|
-
return usageCount >= 2 && usageCount <= 3;
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
// Check for naming patterns
|
|
92
|
-
lines.forEach((line, index) => {
|
|
93
|
-
const lineNumber = index + 1;
|
|
94
|
-
const contextWindow = getContextWindow(index);
|
|
95
|
-
|
|
96
|
-
// Check for single letter variables (except i, j, k, l in loops/common contexts)
|
|
97
|
-
if (!disabledChecks.has('single-letter')) {
|
|
98
|
-
const singleLetterMatches = line.matchAll(
|
|
99
|
-
/\b(?:const|let|var)\s+([a-hm-z])\s*=/gi
|
|
100
|
-
);
|
|
101
|
-
for (const match of singleLetterMatches) {
|
|
102
|
-
const letter = match[1].toLowerCase();
|
|
103
|
-
|
|
104
|
-
// Coverage metrics context (s/b/f/l are standard for statements/branches/functions/lines)
|
|
105
|
-
const isCoverageContext =
|
|
106
|
-
/coverage|summary|metrics|pct|percent/i.test(line) ||
|
|
107
|
-
/\.(?:statements|branches|functions|lines)\.pct/i.test(line);
|
|
108
|
-
if (isCoverageContext && ['s', 'b', 'f', 'l'].includes(letter)) {
|
|
109
|
-
continue;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// Enhanced loop/iterator context detection
|
|
113
|
-
const isInLoopContext =
|
|
114
|
-
line.includes('for') ||
|
|
115
|
-
/\.(map|filter|forEach|reduce|find|some|every)\s*\(/.test(line) ||
|
|
116
|
-
line.includes('=>') || // Arrow function
|
|
117
|
-
/\w+\s*=>\s*/.test(line); // Callback pattern
|
|
118
|
-
|
|
119
|
-
// Check for i18n/translation context
|
|
120
|
-
const isI18nContext =
|
|
121
|
-
line.includes('useTranslation') ||
|
|
122
|
-
line.includes('i18n.t') ||
|
|
123
|
-
/\bt\s*\(['"]/.test(line); // t('key') pattern
|
|
124
|
-
|
|
125
|
-
// Check for arrow function parameter (improved detection with context window)
|
|
126
|
-
const isArrowFunctionParam =
|
|
127
|
-
/\(\s*[a-z]\s*(?:,\s*[a-z]\s*)*\)\s*=>/.test(line) || // (s) => or (a, b) =>
|
|
128
|
-
/[a-z]\s*=>/.test(line) || // s => on same line
|
|
129
|
-
// Multi-line arrow function detection: look for pattern in context window
|
|
130
|
-
(new RegExp(`\\b${letter}\\s*\\)\\s*$`).test(line) &&
|
|
131
|
-
/=>/.test(contextWindow)) || // (s)\n =>
|
|
132
|
-
(new RegExp(
|
|
133
|
-
`\\.(?:map|filter|forEach|reduce|find|some|every)\\s*\\(\\s*$`
|
|
134
|
-
).test(lines[index - 1] || '') &&
|
|
135
|
-
/=>/.test(contextWindow)); // .map(\n s =>
|
|
136
|
-
|
|
137
|
-
// Check if variable is short-lived (comparison/temporary contexts)
|
|
138
|
-
const isShortLived = isShortLivedVariable(letter, index);
|
|
139
|
-
|
|
140
|
-
if (
|
|
141
|
-
!isInLoopContext &&
|
|
142
|
-
!isI18nContext &&
|
|
143
|
-
!isArrowFunctionParam &&
|
|
144
|
-
!isShortLived &&
|
|
145
|
-
!['x', 'y', 'z', 'i', 'j', 'k', 'l', 'n', 'm'].includes(letter)
|
|
146
|
-
) {
|
|
147
|
-
// Skip in test files unless it's really unclear
|
|
148
|
-
if (
|
|
149
|
-
isTestFile &&
|
|
150
|
-
['a', 'b', 'c', 'd', 'e', 'f', 's'].includes(letter)
|
|
151
|
-
) {
|
|
152
|
-
continue;
|
|
153
|
-
}
|
|
154
|
-
|
|
12
|
+
for (const filePath of filePaths) {
|
|
13
|
+
try {
|
|
14
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
15
|
+
const lines = content.split('\n');
|
|
16
|
+
|
|
17
|
+
lines.forEach((line, index) => {
|
|
18
|
+
// Simple regex patterns for naming issues
|
|
19
|
+
|
|
20
|
+
// 1. Single letter variables (except common ones)
|
|
21
|
+
const singleLetterMatch = line.match(/\b(const|let|var)\s+([a-hj-km-np-zA-Z])\s*=/);
|
|
22
|
+
if (singleLetterMatch) {
|
|
155
23
|
issues.push({
|
|
156
|
-
file,
|
|
157
|
-
line:
|
|
24
|
+
file: filePath,
|
|
25
|
+
line: index + 1,
|
|
158
26
|
type: 'poor-naming',
|
|
159
|
-
identifier:
|
|
160
|
-
severity:
|
|
161
|
-
suggestion:
|
|
27
|
+
identifier: singleLetterMatch[2],
|
|
28
|
+
severity: Severity.Minor,
|
|
29
|
+
suggestion: 'Use a more descriptive name than a single letter',
|
|
162
30
|
});
|
|
163
31
|
}
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// Check for overly abbreviated variables
|
|
168
|
-
if (!disabledChecks.has('abbreviation')) {
|
|
169
|
-
const abbreviationMatches = line.matchAll(
|
|
170
|
-
/\b(?:const|let|var)\s+([a-z]{1,3})(?=[A-Z]|_|\s*=)/g
|
|
171
|
-
);
|
|
172
|
-
for (const match of abbreviationMatches) {
|
|
173
|
-
const abbrev = match[1].toLowerCase();
|
|
174
|
-
|
|
175
|
-
// Skip if it's a common short English word (full word, not abbreviation)
|
|
176
|
-
if (allShortWords.has(abbrev)) {
|
|
177
|
-
continue;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Skip acceptable abbreviations (including custom ones)
|
|
181
|
-
if (allAbbreviations.has(abbrev)) {
|
|
182
|
-
continue;
|
|
183
|
-
}
|
|
184
32
|
|
|
185
|
-
//
|
|
186
|
-
|
|
187
|
-
/\(\s
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
if (isArrowFunctionParam) {
|
|
198
|
-
continue;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// For very short names (1-2 letters), check for date/time context
|
|
202
|
-
if (abbrev.length <= 2) {
|
|
203
|
-
const isDateTimeContext =
|
|
204
|
-
/date|time|day|hour|minute|second|timestamp/i.test(line);
|
|
205
|
-
if (isDateTimeContext && ['d', 't', 'dt'].includes(abbrev)) {
|
|
206
|
-
continue;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Check for user/auth context
|
|
210
|
-
const isUserContext = /user|auth|account/i.test(line);
|
|
211
|
-
if (isUserContext && abbrev === 'u') {
|
|
212
|
-
continue;
|
|
33
|
+
// 2. Snake case in TS/JS files
|
|
34
|
+
if (filePath.match(/\.(ts|tsx|js|jsx)$/)) {
|
|
35
|
+
const snakeCaseMatch = line.match(/\b(const|let|var|function)\s+([a-z]+_[a-z0-9_]+)\b/);
|
|
36
|
+
if (snakeCaseMatch) {
|
|
37
|
+
issues.push({
|
|
38
|
+
file: filePath,
|
|
39
|
+
line: index + 1,
|
|
40
|
+
type: 'convention-mix',
|
|
41
|
+
identifier: snakeCaseMatch[2],
|
|
42
|
+
severity: Severity.Info,
|
|
43
|
+
suggestion: 'Use camelCase instead of snake_case in TypeScript/JavaScript',
|
|
44
|
+
});
|
|
213
45
|
}
|
|
214
46
|
}
|
|
215
47
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
) {
|
|
232
|
-
const camelCaseVars = line.match(
|
|
233
|
-
/\b(?:const|let|var)\s+([a-z][a-zA-Z0-9]*)\s*=/
|
|
234
|
-
);
|
|
235
|
-
// Variable present for future checks; reference to avoid lint warnings
|
|
236
|
-
void camelCaseVars;
|
|
237
|
-
const snakeCaseVars = line.match(
|
|
238
|
-
/\b(?:const|let|var)\s+([a-z][a-z0-9]*_[a-z0-9_]*)\s*=/
|
|
239
|
-
);
|
|
240
|
-
|
|
241
|
-
if (snakeCaseVars) {
|
|
242
|
-
issues.push({
|
|
243
|
-
file,
|
|
244
|
-
line: lineNumber,
|
|
245
|
-
type: 'convention-mix',
|
|
246
|
-
identifier: snakeCaseVars[1],
|
|
247
|
-
severity: 'minor',
|
|
248
|
-
suggestion: `Use camelCase '${snakeCaseToCamelCase(snakeCaseVars[1])}' instead of snake_case in TypeScript/JavaScript`,
|
|
249
|
-
});
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// Check for unclear boolean names (should start with is/has/should/can)
|
|
254
|
-
if (!disabledChecks.has('unclear')) {
|
|
255
|
-
const booleanMatches = line.matchAll(
|
|
256
|
-
/\b(?:const|let|var)\s+([a-z][a-zA-Z0-9]*)\s*:\s*boolean/gi
|
|
257
|
-
);
|
|
258
|
-
for (const match of booleanMatches) {
|
|
259
|
-
const name = match[1];
|
|
260
|
-
if (!name.match(/^(is|has|should|can|will|did)/i)) {
|
|
261
|
-
issues.push({
|
|
262
|
-
file,
|
|
263
|
-
line: lineNumber,
|
|
264
|
-
type: 'unclear',
|
|
265
|
-
identifier: name,
|
|
266
|
-
severity: 'info',
|
|
267
|
-
suggestion: `Boolean variable '${name}' should start with is/has/should/can for clarity`,
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
// Check for function names that don't indicate action
|
|
274
|
-
if (!disabledChecks.has('unclear')) {
|
|
275
|
-
const functionMatches = line.matchAll(/function\s+([a-z][a-zA-Z0-9]*)/g);
|
|
276
|
-
for (const match of functionMatches) {
|
|
277
|
-
const name = match[1];
|
|
278
|
-
|
|
279
|
-
// Skip JavaScript/TypeScript keywords that shouldn't be function names
|
|
280
|
-
const isKeyword = [
|
|
281
|
-
'for',
|
|
282
|
-
'if',
|
|
283
|
-
'else',
|
|
284
|
-
'while',
|
|
285
|
-
'do',
|
|
286
|
-
'switch',
|
|
287
|
-
'case',
|
|
288
|
-
'break',
|
|
289
|
-
'continue',
|
|
290
|
-
'return',
|
|
291
|
-
'throw',
|
|
292
|
-
'try',
|
|
293
|
-
'catch',
|
|
294
|
-
'finally',
|
|
295
|
-
'with',
|
|
296
|
-
'yield',
|
|
297
|
-
'await',
|
|
298
|
-
].includes(name);
|
|
299
|
-
if (isKeyword) {
|
|
300
|
-
continue;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// Skip common entry point names
|
|
304
|
-
const isEntryPoint = ['main', 'init', 'setup', 'bootstrap'].includes(
|
|
305
|
-
name
|
|
306
|
-
);
|
|
307
|
-
if (isEntryPoint) {
|
|
308
|
-
continue;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
// Functions should typically start with verbs, but allow:
|
|
312
|
-
// 1. Factory/builder patterns (ends with Factory, Builder, etc.)
|
|
313
|
-
// 2. Descriptive compound names that explain what they return
|
|
314
|
-
// 3. Event handlers (onClick, onSubmit, etc.)
|
|
315
|
-
// 4. Descriptive aggregate/collection patterns
|
|
316
|
-
// 5. Very long descriptive names (>15 chars)
|
|
317
|
-
// 6. Compound words with 3+ capitals
|
|
318
|
-
// 7. Helper/utility functions (common patterns)
|
|
319
|
-
// 8. React hooks (useX pattern)
|
|
320
|
-
|
|
321
|
-
const isFactoryPattern = name.match(
|
|
322
|
-
/(Factory|Builder|Creator|Generator|Provider|Adapter|Mock)$/
|
|
323
|
-
);
|
|
324
|
-
const isEventHandler = name.match(/^on[A-Z]/);
|
|
325
|
-
const isDescriptiveLong = name.length > 15; // Reduced from 20 to 15
|
|
326
|
-
const isReactHook = name.match(/^use[A-Z]/); // React hooks
|
|
327
|
-
|
|
328
|
-
// Check for descriptive patterns
|
|
329
|
-
const isDescriptivePattern =
|
|
330
|
-
name.match(
|
|
331
|
-
/^(default|total|count|sum|avg|max|min|initial|current|previous|next)\w+/
|
|
332
|
-
) ||
|
|
333
|
-
name.match(
|
|
334
|
-
/\w+(Count|Total|Sum|Average|List|Map|Set|Config|Settings|Options|Props|Data|Info|Details|State|Status|Response|Result)$/
|
|
335
|
-
);
|
|
336
|
-
|
|
337
|
-
// Helper/utility function patterns
|
|
338
|
-
const isHelperPattern =
|
|
339
|
-
name.match(/^(to|from|with|without|for|as|into)\w+/) || // toMetadata, withLogger, forPath
|
|
340
|
-
name.match(/^\w+(To|From|With|Without|For|As|Into)\w*$/); // metadataTo, pathFrom
|
|
341
|
-
|
|
342
|
-
// Common utility names that are descriptive
|
|
343
|
-
const isUtilityName = [
|
|
344
|
-
'cn',
|
|
345
|
-
'proxy',
|
|
346
|
-
'sitemap',
|
|
347
|
-
'robots',
|
|
348
|
-
'gtag',
|
|
349
|
-
].includes(name);
|
|
350
|
-
|
|
351
|
-
// Count capital letters for compound detection
|
|
352
|
-
const capitalCount = (name.match(/[A-Z]/g) || []).length;
|
|
353
|
-
const isCompoundWord = capitalCount >= 3; // daysSinceLastCommit has 4 capitals
|
|
354
|
-
|
|
355
|
-
const hasActionVerb = name.match(
|
|
356
|
-
/^(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)/
|
|
357
|
-
);
|
|
358
|
-
|
|
359
|
-
if (
|
|
360
|
-
!hasActionVerb &&
|
|
361
|
-
!isFactoryPattern &&
|
|
362
|
-
!isEventHandler &&
|
|
363
|
-
!isDescriptiveLong &&
|
|
364
|
-
!isDescriptivePattern &&
|
|
365
|
-
!isCompoundWord &&
|
|
366
|
-
!isHelperPattern &&
|
|
367
|
-
!isUtilityName &&
|
|
368
|
-
!isReactHook
|
|
369
|
-
) {
|
|
370
|
-
issues.push({
|
|
371
|
-
file,
|
|
372
|
-
line: lineNumber,
|
|
373
|
-
type: 'unclear',
|
|
374
|
-
identifier: name,
|
|
375
|
-
severity: 'info',
|
|
376
|
-
suggestion: `Function '${name}' should start with an action verb (get, set, create, etc.)`,
|
|
377
|
-
});
|
|
48
|
+
// 3. Very short names
|
|
49
|
+
const shortNameMatch = line.match(/\b(const|let|var)\s+([a-zA-Z0-9]{2,3})\s*=/);
|
|
50
|
+
if (shortNameMatch) {
|
|
51
|
+
const name = shortNameMatch[2].toLowerCase();
|
|
52
|
+
const vagueNames = ['obj', 'val', 'tmp', 'res', 'ret', 'data'];
|
|
53
|
+
if (vagueNames.includes(name)) {
|
|
54
|
+
issues.push({
|
|
55
|
+
file: filePath,
|
|
56
|
+
line: index + 1,
|
|
57
|
+
type: 'poor-naming',
|
|
58
|
+
identifier: name,
|
|
59
|
+
severity: Severity.Minor,
|
|
60
|
+
suggestion: `Avoid vague names like '${name}'`,
|
|
61
|
+
});
|
|
62
|
+
}
|
|
378
63
|
}
|
|
379
|
-
}
|
|
64
|
+
});
|
|
65
|
+
} catch (err) {
|
|
66
|
+
void err;
|
|
380
67
|
}
|
|
381
|
-
}
|
|
68
|
+
}
|
|
382
69
|
|
|
383
70
|
return issues;
|
|
384
71
|
}
|