@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.
@@ -1,384 +1,71 @@
1
- import { readFileContent } from '@aiready/core';
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
- * Analyzes naming conventions and quality
6
+ * Legacy regex-based naming analyzer
7
+ * (Used as fallback or for languages without AST support)
12
8
  */
13
- export async function analyzeNaming(files: string[]): Promise<NamingIssue[]> {
9
+ export async function analyzeNaming(filePaths: string[]): Promise<NamingIssue[]> {
14
10
  const issues: NamingIssue[] = [];
15
11
 
16
- // Load and merge configuration
17
- const { customAbbreviations, customShortWords, disabledChecks } =
18
- await loadNamingConfig(files);
19
-
20
- for (const file of files) {
21
- const content = await readFileContent(file);
22
- const fileIssues = analyzeFileNaming(
23
- file,
24
- content,
25
- customAbbreviations,
26
- customShortWords,
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: lineNumber,
24
+ file: filePath,
25
+ line: index + 1,
158
26
  type: 'poor-naming',
159
- identifier: match[1],
160
- severity: 'minor',
161
- suggestion: `Use descriptive variable name instead of single letter '${match[1]}'`,
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
- // Check for arrow function parameter context (with multi-line detection)
186
- const isArrowFunctionParam =
187
- /\(\s*[a-z]\s*(?:,\s*[a-z]\s*)*\)\s*=>/.test(line) || // (s) => or (a, b) =>
188
- new RegExp(`\\b${abbrev}\\s*=>`).test(line) || // s => on same line
189
- // Multi-line arrow function: check context window
190
- (new RegExp(`\\b${abbrev}\\s*\\)\\s*$`).test(line) &&
191
- /=>/.test(contextWindow)) || // (s)\n =>
192
- (new RegExp(
193
- `\\.(?:map|filter|forEach|reduce|find|some|every)\\s*\\(\\s*$`
194
- ).test(lines[index - 1] || '') &&
195
- new RegExp(`^\\s*${abbrev}\\s*=>`).test(line)); // .map(\n s =>
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
- issues.push({
217
- file,
218
- line: lineNumber,
219
- type: 'abbreviation',
220
- identifier: match[1],
221
- severity: 'info',
222
- suggestion: `Consider using full word instead of abbreviation '${match[1]}'`,
223
- });
224
- }
225
- }
226
-
227
- // Check for snake_case vs camelCase mixing in TypeScript/JavaScript
228
- if (
229
- !disabledChecks.has('convention-mix') &&
230
- file.match(/\.(ts|tsx|js|jsx)$/)
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
  }