@aiready/consistency 0.3.3 → 0.3.5

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,35 +1,70 @@
1
- import { readFileContent } from '@aiready/core';
1
+ import { readFileContent, loadConfig } from '@aiready/core';
2
2
  import type { NamingIssue } from '../types';
3
+ import { dirname } from 'path';
4
+
5
+ // Common short English words that are NOT abbreviations (full, valid words)
6
+ const COMMON_SHORT_WORDS = new Set([
7
+ // Full English words (1-3 letters)
8
+ 'day', 'key', 'net', 'to', 'go', 'for', 'not', 'new', 'old', 'top', 'end',
9
+ 'run', 'try', 'use', 'get', 'set', 'add', 'put', 'map', 'log', 'row', 'col',
10
+ 'tab', 'box', 'div', 'nav', 'tag', 'any', 'all', 'one', 'two', 'out', 'off',
11
+ 'on', 'yes', 'no', 'now', 'max', 'min', 'sum', 'avg', 'ref', 'src', 'dst',
12
+ 'raw', 'def', 'sub', 'pub', 'pre', 'mid', 'alt', 'opt', 'tmp', 'ext', 'sep',
13
+ // Additional full words commonly flagged
14
+ 'tax', 'cat', 'dog', 'car', 'bus', 'web', 'app', 'war', 'law', 'pay', 'buy',
15
+ 'win', 'cut', 'hit', 'hot', 'pop', 'job', 'age', 'act', 'let', 'lot', 'bad',
16
+ 'big', 'far', 'few', 'own', 'per', 'red', 'low', 'see', 'six', 'ten', 'way',
17
+ 'who', 'why', 'yet', 'via', 'due', 'fee', 'fun', 'gas', 'gay', 'god', 'gun',
18
+ 'guy', 'ice', 'ill', 'kid', 'mad', 'man', 'mix', 'mom', 'mrs', 'nor', 'odd',
19
+ 'oil', 'pan', 'pet', 'pit', 'pot', 'pow', 'pro', 'raw', 'rep', 'rid', 'sad',
20
+ 'sea', 'sit', 'sky', 'son', 'tea', 'tie', 'tip', 'van', 'war', 'win', 'won'
21
+ ]);
3
22
 
4
23
  // Comprehensive list of acceptable abbreviations and acronyms
5
24
  const ACCEPTABLE_ABBREVIATIONS = new Set([
6
25
  // Standard identifiers
7
26
  'id', 'uid', 'gid', 'pid',
27
+ // Loop counters and iterators
28
+ 'i', 'j', 'k', 'n', 'm',
8
29
  // Web/Network
9
30
  'url', 'uri', 'api', 'cdn', 'dns', 'ip', 'tcp', 'udp', 'http', 'ssl', 'tls',
10
- 'utm', 'seo', 'rss', 'xhr', 'ajax',
31
+ 'utm', 'seo', 'rss', 'xhr', 'ajax', 'cors', 'ws', 'wss',
11
32
  // Data formats
12
33
  'json', 'xml', 'yaml', 'csv', 'html', 'css', 'svg', 'pdf',
34
+ // File types & extensions
35
+ 'img', 'txt', 'doc', 'docx', 'xlsx', 'ppt', 'md', 'rst', 'jpg', 'png', 'gif',
13
36
  // Databases
14
- 'db', 'sql', 'orm', 'dao', 'dto',
37
+ 'db', 'sql', 'orm', 'dao', 'dto', 'ddb', 'rds', 'nosql',
15
38
  // File system
16
39
  'fs', 'dir', 'tmp', 'src', 'dst', 'bin', 'lib', 'pkg',
17
40
  // Operating system
18
- 'os', 'env', 'arg', 'cli', 'cmd', 'exe',
41
+ 'os', 'env', 'arg', 'cli', 'cmd', 'exe', 'cwd', 'pwd',
19
42
  // UI/UX
20
43
  'ui', 'ux', 'gui', 'dom', 'ref',
21
44
  // Request/Response
22
- 'req', 'res', 'ctx', 'err', 'msg',
45
+ 'req', 'res', 'ctx', 'err', 'msg', 'auth',
23
46
  // Mathematics/Computing
24
47
  'max', 'min', 'avg', 'sum', 'abs', 'cos', 'sin', 'tan', 'log', 'exp',
25
- 'pow', 'sqrt', 'std', 'var', 'int', 'num',
48
+ 'pow', 'sqrt', 'std', 'var', 'int', 'num', 'idx',
26
49
  // Time
27
- 'now', 'utc', 'tz', 'ms', 'sec',
50
+ 'now', 'utc', 'tz', 'ms', 'sec', 'hr', 'min', 'yr', 'mo',
28
51
  // Common patterns
29
52
  'app', 'cfg', 'config', 'init', 'len', 'val', 'str', 'obj', 'arr',
30
53
  'gen', 'def', 'raw', 'new', 'old', 'pre', 'post', 'sub', 'pub',
54
+ // Programming/Framework specific
55
+ 'ts', 'js', 'jsx', 'tsx', 'py', 'rb', 'vue', 're', 'fn', 'fns', 'mod', 'opts', 'dev',
56
+ // Cloud/Infrastructure
57
+ 's3', 'ec2', 'sqs', 'sns', 'vpc', 'ami', 'iam', 'acl', 'elb', 'alb', 'nlb', 'aws',
58
+ // Metrics/Performance
59
+ 'fcp', 'lcp', 'cls', 'ttfb', 'tti', 'fid', 'fps', 'qps', 'rps', 'tps',
60
+ // Testing & i18n
61
+ 'po', 'e2e', 'a11y', 'i18n', 'l10n',
62
+ // Domain-specific abbreviations (context-aware)
63
+ 'sk', 'fy', 'faq', 'og', 'seo', 'cta', 'roi', 'kpi',
31
64
  // Boolean helpers (these are intentional short names)
32
- 'is', 'has', 'can', 'did', 'was', 'are'
65
+ 'is', 'has', 'can', 'did', 'was', 'are',
66
+ // Date/Time context (when in date contexts)
67
+ 'd', 't', 'dt'
33
68
  ]);
34
69
 
35
70
  /**
@@ -38,51 +73,171 @@ const ACCEPTABLE_ABBREVIATIONS = new Set([
38
73
  export async function analyzeNaming(files: string[]): Promise<NamingIssue[]> {
39
74
  const issues: NamingIssue[] = [];
40
75
 
76
+ // Load config from the first file's directory (or project root)
77
+ const rootDir = files.length > 0 ? dirname(files[0]) : process.cwd();
78
+ const config = loadConfig(rootDir);
79
+ const consistencyConfig = config?.tools?.['consistency'];
80
+
81
+ // Merge custom abbreviations and short words with defaults
82
+ const customAbbreviations = new Set(consistencyConfig?.acceptedAbbreviations || []);
83
+ const customShortWords = new Set(consistencyConfig?.shortWords || []);
84
+ const disabledChecks = new Set(consistencyConfig?.disableChecks || []);
85
+
41
86
  for (const file of files) {
42
87
  const content = await readFileContent(file);
43
- const fileIssues = analyzeFileNaming(file, content);
88
+ const fileIssues = analyzeFileNaming(file, content, customAbbreviations, customShortWords, disabledChecks);
44
89
  issues.push(...fileIssues);
45
90
  }
46
91
 
47
92
  return issues;
48
93
  }
49
94
 
50
- function analyzeFileNaming(file: string, content: string): NamingIssue[] {
95
+ function analyzeFileNaming(
96
+ file: string,
97
+ content: string,
98
+ customAbbreviations: Set<string>,
99
+ customShortWords: Set<string>,
100
+ disabledChecks: Set<string>
101
+ ): NamingIssue[] {
51
102
  const issues: NamingIssue[] = [];
52
103
 
104
+ // Check if this is a test file (more lenient rules)
105
+ const isTestFile = file.match(/\.(test|spec)\.(ts|tsx|js|jsx)$/);
106
+
53
107
  // Split into lines for line number tracking
54
108
  const lines = content.split('\n');
55
109
 
110
+ // Merge custom sets with defaults
111
+ const allAbbreviations = new Set([...ACCEPTABLE_ABBREVIATIONS, ...customAbbreviations]);
112
+ const allShortWords = new Set([...COMMON_SHORT_WORDS, ...customShortWords]);
113
+
114
+ /**
115
+ * Helper: Get context window around a line (for multi-line pattern detection)
116
+ */
117
+ const getContextWindow = (index: number, windowSize: number = 3): string => {
118
+ const start = Math.max(0, index - windowSize);
119
+ const end = Math.min(lines.length, index + windowSize + 1);
120
+ return lines.slice(start, end).join('\n');
121
+ };
122
+
123
+ /**
124
+ * Helper: Check if a variable is short-lived (used only within 3-5 lines)
125
+ */
126
+ const isShortLivedVariable = (varName: string, declarationIndex: number): boolean => {
127
+ const searchRange = 5; // Check 5 lines after declaration
128
+ const endIndex = Math.min(lines.length, declarationIndex + searchRange + 1);
129
+
130
+ let usageCount = 0;
131
+ for (let i = declarationIndex; i < endIndex; i++) {
132
+ // Match variable name as whole word
133
+ const regex = new RegExp(`\\b${varName}\\b`, 'g');
134
+ const matches = lines[i].match(regex);
135
+ if (matches) {
136
+ usageCount += matches.length;
137
+ }
138
+ }
139
+
140
+ // If variable is only used 2-3 times within 5 lines, it's short-lived
141
+ // (1 = declaration, 1-2 = actual usage)
142
+ return usageCount >= 2 && usageCount <= 3;
143
+ };
144
+
56
145
  // Check for naming patterns
57
146
  lines.forEach((line, index) => {
58
147
  const lineNumber = index + 1;
148
+ const contextWindow = getContextWindow(index);
59
149
 
60
150
  // Check for single letter variables (except i, j, k, l in loops/common contexts)
61
- const singleLetterMatches = line.matchAll(/\b(?:const|let|var)\s+([a-hm-z])\s*=/gi);
62
- for (const match of singleLetterMatches) {
63
- const letter = match[1].toLowerCase();
64
- // Skip if it's in a loop context or common iterator
65
- const isInLoopContext = line.includes('for') || line.includes('.map') ||
66
- line.includes('.filter') || line.includes('.forEach') ||
67
- line.includes('.reduce');
68
- if (!isInLoopContext && !['x', 'y', 'z', 'i', 'j', 'k', 'l', 'n', 'm'].includes(letter)) {
69
- issues.push({
70
- file,
71
- line: lineNumber,
72
- type: 'poor-naming',
73
- identifier: match[1],
74
- severity: 'minor',
75
- suggestion: `Use descriptive variable name instead of single letter '${match[1]}'`
76
- });
151
+ if (!disabledChecks.has('single-letter')) {
152
+ const singleLetterMatches = line.matchAll(/\b(?:const|let|var)\s+([a-hm-z])\s*=/gi);
153
+ for (const match of singleLetterMatches) {
154
+ const letter = match[1].toLowerCase();
155
+
156
+ // Enhanced loop/iterator context detection
157
+ const isInLoopContext =
158
+ line.includes('for') ||
159
+ /\.(map|filter|forEach|reduce|find|some|every)\s*\(/.test(line) ||
160
+ line.includes('=>') || // Arrow function
161
+ /\w+\s*=>\s*/.test(line); // Callback pattern
162
+
163
+ // Check for i18n/translation context
164
+ const isI18nContext =
165
+ line.includes('useTranslation') ||
166
+ line.includes('i18n.t') ||
167
+ /\bt\s*\(['"]/.test(line); // t('key') pattern
168
+
169
+ // Check for arrow function parameter (improved detection with context window)
170
+ const isArrowFunctionParam =
171
+ /\(\s*[a-z]\s*(?:,\s*[a-z]\s*)*\)\s*=>/.test(line) || // (s) => or (a, b) =>
172
+ /[a-z]\s*=>/.test(line) || // s => on same line
173
+ // Multi-line arrow function detection: look for pattern in context window
174
+ new RegExp(`\\b${letter}\\s*\\)\\s*$`).test(line) && /=>/.test(contextWindow) || // (s)\n =>
175
+ new RegExp(`\\.(?:map|filter|forEach|reduce|find|some|every)\\s*\\(\\s*$`).test(lines[index - 1] || '') && /=>/.test(contextWindow); // .map(\n s =>
176
+
177
+ // Check if variable is short-lived (comparison/temporary contexts)
178
+ const isShortLived = isShortLivedVariable(letter, index);
179
+
180
+ if (!isInLoopContext && !isI18nContext && !isArrowFunctionParam && !isShortLived && !['x', 'y', 'z', 'i', 'j', 'k', 'l', 'n', 'm'].includes(letter)) {
181
+ // Skip in test files unless it's really unclear
182
+ if (isTestFile && ['a', 'b', 'c', 'd', 'e', 'f', 's'].includes(letter)) {
183
+ continue;
184
+ }
185
+
186
+ issues.push({
187
+ file,
188
+ line: lineNumber,
189
+ type: 'poor-naming',
190
+ identifier: match[1],
191
+ severity: 'minor',
192
+ suggestion: `Use descriptive variable name instead of single letter '${match[1]}'`
193
+ });
194
+ }
77
195
  }
78
196
  }
79
197
 
80
198
  // Check for overly abbreviated variables
81
- const abbreviationMatches = line.matchAll(/\b(?:const|let|var)\s+([a-z]{1,3})(?=[A-Z]|_|\s*=)/g);
82
- for (const match of abbreviationMatches) {
83
- const abbrev = match[1].toLowerCase();
84
- // Skip acceptable abbreviations
85
- if (!ACCEPTABLE_ABBREVIATIONS.has(abbrev)) {
199
+ if (!disabledChecks.has('abbreviation')) {
200
+ const abbreviationMatches = line.matchAll(/\b(?:const|let|var)\s+([a-z]{1,3})(?=[A-Z]|_|\s*=)/g);
201
+ for (const match of abbreviationMatches) {
202
+ const abbrev = match[1].toLowerCase();
203
+
204
+ // Skip if it's a common short English word (full word, not abbreviation)
205
+ if (allShortWords.has(abbrev)) {
206
+ continue;
207
+ }
208
+
209
+ // Skip acceptable abbreviations (including custom ones)
210
+ if (allAbbreviations.has(abbrev)) {
211
+ continue;
212
+ }
213
+
214
+ // Check for arrow function parameter context (with multi-line detection)
215
+ const isArrowFunctionParam =
216
+ /\(\s*[a-z]\s*(?:,\s*[a-z]\s*)*\)\s*=>/.test(line) || // (s) => or (a, b) =>
217
+ new RegExp(`\\b${abbrev}\\s*=>`).test(line) || // s => on same line
218
+ // Multi-line arrow function: check context window
219
+ (new RegExp(`\\b${abbrev}\\s*\\)\\s*$`).test(line) && /=>/.test(contextWindow)) || // (s)\n =>
220
+ (new RegExp(`\\.(?:map|filter|forEach|reduce|find|some|every)\\s*\\(\\s*$`).test(lines[index - 1] || '') &&
221
+ new RegExp(`^\\s*${abbrev}\\s*=>`).test(line)); // .map(\n s =>
222
+
223
+ if (isArrowFunctionParam) {
224
+ continue;
225
+ }
226
+
227
+ // For very short names (1-2 letters), check for date/time context
228
+ if (abbrev.length <= 2) {
229
+ const isDateTimeContext = /date|time|day|hour|minute|second|timestamp/i.test(line);
230
+ if (isDateTimeContext && ['d', 't', 'dt'].includes(abbrev)) {
231
+ continue;
232
+ }
233
+
234
+ // Check for user/auth context
235
+ const isUserContext = /user|auth|account/i.test(line);
236
+ if (isUserContext && abbrev === 'u') {
237
+ continue;
238
+ }
239
+ }
240
+
86
241
  issues.push({
87
242
  file,
88
243
  line: lineNumber,
@@ -95,7 +250,7 @@ function analyzeFileNaming(file: string, content: string): NamingIssue[] {
95
250
  }
96
251
 
97
252
  // Check for snake_case vs camelCase mixing in TypeScript/JavaScript
98
- if (file.match(/\.(ts|tsx|js|jsx)$/)) {
253
+ if (!disabledChecks.has('convention-mix') && file.match(/\.(ts|tsx|js|jsx)$/)) {
99
254
  const camelCaseVars = line.match(/\b(?:const|let|var)\s+([a-z][a-zA-Z0-9]*)\s*=/);
100
255
  const snakeCaseVars = line.match(/\b(?:const|let|var)\s+([a-z][a-z0-9]*_[a-z0-9_]*)\s*=/);
101
256
 
@@ -112,43 +267,73 @@ function analyzeFileNaming(file: string, content: string): NamingIssue[] {
112
267
  }
113
268
 
114
269
  // Check for unclear boolean names (should start with is/has/should/can)
115
- const booleanMatches = line.matchAll(/\b(?:const|let|var)\s+([a-z][a-zA-Z0-9]*)\s*:\s*boolean/gi);
116
- for (const match of booleanMatches) {
117
- const name = match[1];
118
- if (!name.match(/^(is|has|should|can|will|did)/i)) {
119
- issues.push({
120
- file,
121
- line: lineNumber,
122
- type: 'unclear',
123
- identifier: name,
124
- severity: 'info',
125
- suggestion: `Boolean variable '${name}' should start with is/has/should/can for clarity`
126
- });
270
+ if (!disabledChecks.has('unclear')) {
271
+ const booleanMatches = line.matchAll(/\b(?:const|let|var)\s+([a-z][a-zA-Z0-9]*)\s*:\s*boolean/gi);
272
+ for (const match of booleanMatches) {
273
+ const name = match[1];
274
+ if (!name.match(/^(is|has|should|can|will|did)/i)) {
275
+ issues.push({
276
+ file,
277
+ line: lineNumber,
278
+ type: 'unclear',
279
+ identifier: name,
280
+ severity: 'info',
281
+ suggestion: `Boolean variable '${name}' should start with is/has/should/can for clarity`
282
+ });
283
+ }
127
284
  }
128
285
  }
129
286
 
130
287
  // Check for function names that don't indicate action
131
- const functionMatches = line.matchAll(/function\s+([a-z][a-zA-Z0-9]*)/g);
132
- for (const match of functionMatches) {
133
- const name = match[1];
134
- // Functions should typically start with verbs, but allow:
135
- // 1. Factory/builder patterns (ends with Factory, Builder, etc.)
136
- // 2. Descriptive compound names that explain what they return
137
- // 3. Event handlers (onClick, onSubmit, etc.)
138
- const isFactoryPattern = name.match(/(Factory|Builder|Creator|Generator)$/);
139
- const isEventHandler = name.match(/^on[A-Z]/);
140
- const isDescriptiveLong = name.length > 20; // Long names are usually descriptive enough
141
- 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)/);
142
-
143
- if (!hasActionVerb && !isFactoryPattern && !isEventHandler && !isDescriptiveLong) {
144
- issues.push({
145
- file,
146
- line: lineNumber,
147
- type: 'unclear',
148
- identifier: name,
149
- severity: 'info',
150
- suggestion: `Function '${name}' should start with an action verb (get, set, create, etc.)`
151
- });
288
+ if (!disabledChecks.has('unclear')) {
289
+ const functionMatches = line.matchAll(/function\s+([a-z][a-zA-Z0-9]*)/g);
290
+ for (const match of functionMatches) {
291
+ const name = match[1];
292
+
293
+ // Skip JavaScript/TypeScript keywords that shouldn't be function names
294
+ const isKeyword = ['for', 'if', 'else', 'while', 'do', 'switch', 'case', 'break', 'continue', 'return', 'throw', 'try', 'catch', 'finally', 'with', 'yield', 'await'].includes(name);
295
+ if (isKeyword) {
296
+ continue;
297
+ }
298
+
299
+ // Skip common entry point names
300
+ const isEntryPoint = ['main', 'init', 'setup', 'bootstrap'].includes(name);
301
+ if (isEntryPoint) {
302
+ continue;
303
+ }
304
+
305
+ // Functions should typically start with verbs, but allow:
306
+ // 1. Factory/builder patterns (ends with Factory, Builder, etc.)
307
+ // 2. Descriptive compound names that explain what they return
308
+ // 3. Event handlers (onClick, onSubmit, etc.)
309
+ // 4. Descriptive aggregate/collection patterns
310
+ // 5. Very long descriptive names (>15 chars)
311
+ // 6. Compound words with 3+ capitals
312
+
313
+ const isFactoryPattern = name.match(/(Factory|Builder|Creator|Generator)$/);
314
+ const isEventHandler = name.match(/^on[A-Z]/);
315
+ const isDescriptiveLong = name.length > 15; // Reduced from 20 to 15
316
+
317
+ // Check for descriptive patterns
318
+ const isDescriptivePattern = name.match(/^(default|total|count|sum|avg|max|min|initial|current|previous|next)\w+/) ||
319
+ name.match(/\w+(Count|Total|Sum|Average|List|Map|Set|Config|Settings|Options|Props)$/);
320
+
321
+ // Count capital letters for compound detection
322
+ const capitalCount = (name.match(/[A-Z]/g) || []).length;
323
+ const isCompoundWord = capitalCount >= 3; // daysSinceLastCommit has 4 capitals
324
+
325
+ 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)/);
326
+
327
+ if (!hasActionVerb && !isFactoryPattern && !isEventHandler && !isDescriptiveLong && !isDescriptivePattern && !isCompoundWord) {
328
+ issues.push({
329
+ file,
330
+ line: lineNumber,
331
+ type: 'unclear',
332
+ identifier: name,
333
+ severity: 'info',
334
+ suggestion: `Function '${name}' should start with an action verb (get, set, create, etc.)`
335
+ });
336
+ }
152
337
  }
153
338
  }
154
339
  });