@aiready/consistency 0.8.30 → 0.8.32
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/.github/FUNDING.yml +2 -2
- package/.turbo/turbo-build.log +9 -9
- package/.turbo/turbo-test.log +11 -11
- package/CONTRIBUTING.md +10 -3
- package/dist/chunk-EIQ5K6OO.mjs +1579 -0
- package/dist/chunk-J5IFYDVU.mjs +1579 -0
- package/dist/cli.js +136 -37
- package/dist/cli.mjs +28 -5
- package/dist/index.js +191 -50
- package/dist/index.mjs +76 -19
- package/package.json +2 -2
- package/src/__tests__/analyzer.test.ts +63 -8
- package/src/__tests__/language-filter.test.ts +6 -9
- package/src/__tests__/scoring.test.ts +35 -10
- package/src/analyzer.ts +60 -29
- package/src/analyzers/naming-ast.ts +134 -46
- package/src/analyzers/naming-constants.ts +356 -42
- package/src/analyzers/naming-python.ts +27 -9
- package/src/analyzers/naming.ts +160 -68
- package/src/analyzers/patterns.ts +59 -35
- package/src/cli.ts +42 -18
- package/src/scoring.ts +35 -31
- package/src/types.ts +1 -1
- package/src/utils/ast-parser.ts +32 -14
- package/src/utils/config-loader.ts +8 -3
- package/src/utils/context-detector.ts +76 -43
- package/src/utils/scope-tracker.ts +11 -11
package/src/analyzers/naming.ts
CHANGED
|
@@ -14,11 +14,18 @@ export async function analyzeNaming(files: string[]): Promise<NamingIssue[]> {
|
|
|
14
14
|
const issues: NamingIssue[] = [];
|
|
15
15
|
|
|
16
16
|
// Load and merge configuration
|
|
17
|
-
const { customAbbreviations, customShortWords, disabledChecks } =
|
|
17
|
+
const { customAbbreviations, customShortWords, disabledChecks } =
|
|
18
|
+
await loadNamingConfig(files);
|
|
18
19
|
|
|
19
20
|
for (const file of files) {
|
|
20
21
|
const content = await readFileContent(file);
|
|
21
|
-
const fileIssues = analyzeFileNaming(
|
|
22
|
+
const fileIssues = analyzeFileNaming(
|
|
23
|
+
file,
|
|
24
|
+
content,
|
|
25
|
+
customAbbreviations,
|
|
26
|
+
customShortWords,
|
|
27
|
+
disabledChecks
|
|
28
|
+
);
|
|
22
29
|
issues.push(...fileIssues);
|
|
23
30
|
}
|
|
24
31
|
|
|
@@ -26,8 +33,8 @@ export async function analyzeNaming(files: string[]): Promise<NamingIssue[]> {
|
|
|
26
33
|
}
|
|
27
34
|
|
|
28
35
|
function analyzeFileNaming(
|
|
29
|
-
file: string,
|
|
30
|
-
content: string,
|
|
36
|
+
file: string,
|
|
37
|
+
content: string,
|
|
31
38
|
customAbbreviations: Set<string>,
|
|
32
39
|
customShortWords: Set<string>,
|
|
33
40
|
disabledChecks: Set<string>
|
|
@@ -41,7 +48,10 @@ function analyzeFileNaming(
|
|
|
41
48
|
const lines = content.split('\n');
|
|
42
49
|
|
|
43
50
|
// Merge custom sets with defaults
|
|
44
|
-
const allAbbreviations = new Set([
|
|
51
|
+
const allAbbreviations = new Set([
|
|
52
|
+
...ACCEPTABLE_ABBREVIATIONS,
|
|
53
|
+
...customAbbreviations,
|
|
54
|
+
]);
|
|
45
55
|
const allShortWords = new Set([...COMMON_SHORT_WORDS, ...customShortWords]);
|
|
46
56
|
|
|
47
57
|
/**
|
|
@@ -56,10 +66,13 @@ function analyzeFileNaming(
|
|
|
56
66
|
/**
|
|
57
67
|
* Helper: Check if a variable is short-lived (used only within 3-5 lines)
|
|
58
68
|
*/
|
|
59
|
-
const isShortLivedVariable = (
|
|
69
|
+
const isShortLivedVariable = (
|
|
70
|
+
varName: string,
|
|
71
|
+
declarationIndex: number
|
|
72
|
+
): boolean => {
|
|
60
73
|
const searchRange = 5; // Check 5 lines after declaration
|
|
61
74
|
const endIndex = Math.min(lines.length, declarationIndex + searchRange + 1);
|
|
62
|
-
|
|
75
|
+
|
|
63
76
|
let usageCount = 0;
|
|
64
77
|
for (let i = declarationIndex; i < endIndex; i++) {
|
|
65
78
|
// Match variable name as whole word
|
|
@@ -69,7 +82,7 @@ function analyzeFileNaming(
|
|
|
69
82
|
usageCount += matches.length;
|
|
70
83
|
}
|
|
71
84
|
}
|
|
72
|
-
|
|
85
|
+
|
|
73
86
|
// If variable is only used 2-3 times within 5 lines, it's short-lived
|
|
74
87
|
// (1 = declaration, 1-2 = actual usage)
|
|
75
88
|
return usageCount >= 2 && usageCount <= 3;
|
|
@@ -82,54 +95,70 @@ function analyzeFileNaming(
|
|
|
82
95
|
|
|
83
96
|
// Check for single letter variables (except i, j, k, l in loops/common contexts)
|
|
84
97
|
if (!disabledChecks.has('single-letter')) {
|
|
85
|
-
const singleLetterMatches = line.matchAll(
|
|
98
|
+
const singleLetterMatches = line.matchAll(
|
|
99
|
+
/\b(?:const|let|var)\s+([a-hm-z])\s*=/gi
|
|
100
|
+
);
|
|
86
101
|
for (const match of singleLetterMatches) {
|
|
87
102
|
const letter = match[1].toLowerCase();
|
|
88
|
-
|
|
103
|
+
|
|
89
104
|
// Coverage metrics context (s/b/f/l are standard for statements/branches/functions/lines)
|
|
90
|
-
const isCoverageContext =
|
|
105
|
+
const isCoverageContext =
|
|
106
|
+
/coverage|summary|metrics|pct|percent/i.test(line) ||
|
|
91
107
|
/\.(?:statements|branches|functions|lines)\.pct/i.test(line);
|
|
92
108
|
if (isCoverageContext && ['s', 'b', 'f', 'l'].includes(letter)) {
|
|
93
109
|
continue;
|
|
94
110
|
}
|
|
95
|
-
|
|
111
|
+
|
|
96
112
|
// Enhanced loop/iterator context detection
|
|
97
|
-
const isInLoopContext =
|
|
98
|
-
line.includes('for') ||
|
|
113
|
+
const isInLoopContext =
|
|
114
|
+
line.includes('for') ||
|
|
99
115
|
/\.(map|filter|forEach|reduce|find|some|every)\s*\(/.test(line) ||
|
|
100
116
|
line.includes('=>') || // Arrow function
|
|
101
117
|
/\w+\s*=>\s*/.test(line); // Callback pattern
|
|
102
|
-
|
|
118
|
+
|
|
103
119
|
// Check for i18n/translation context
|
|
104
|
-
const isI18nContext =
|
|
120
|
+
const isI18nContext =
|
|
105
121
|
line.includes('useTranslation') ||
|
|
106
122
|
line.includes('i18n.t') ||
|
|
107
123
|
/\bt\s*\(['"]/.test(line); // t('key') pattern
|
|
108
|
-
|
|
124
|
+
|
|
109
125
|
// Check for arrow function parameter (improved detection with context window)
|
|
110
|
-
const isArrowFunctionParam =
|
|
126
|
+
const isArrowFunctionParam =
|
|
111
127
|
/\(\s*[a-z]\s*(?:,\s*[a-z]\s*)*\)\s*=>/.test(line) || // (s) => or (a, b) =>
|
|
112
128
|
/[a-z]\s*=>/.test(line) || // s => on same line
|
|
113
129
|
// Multi-line arrow function detection: look for pattern in context window
|
|
114
|
-
new RegExp(`\\b${letter}\\s*\\)\\s*$`).test(line) &&
|
|
115
|
-
|
|
116
|
-
|
|
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
|
+
|
|
117
137
|
// Check if variable is short-lived (comparison/temporary contexts)
|
|
118
138
|
const isShortLived = isShortLivedVariable(letter, index);
|
|
119
|
-
|
|
120
|
-
if (
|
|
139
|
+
|
|
140
|
+
if (
|
|
141
|
+
!isInLoopContext &&
|
|
142
|
+
!isI18nContext &&
|
|
143
|
+
!isArrowFunctionParam &&
|
|
144
|
+
!isShortLived &&
|
|
145
|
+
!['x', 'y', 'z', 'i', 'j', 'k', 'l', 'n', 'm'].includes(letter)
|
|
146
|
+
) {
|
|
121
147
|
// Skip in test files unless it's really unclear
|
|
122
|
-
if (
|
|
148
|
+
if (
|
|
149
|
+
isTestFile &&
|
|
150
|
+
['a', 'b', 'c', 'd', 'e', 'f', 's'].includes(letter)
|
|
151
|
+
) {
|
|
123
152
|
continue;
|
|
124
153
|
}
|
|
125
|
-
|
|
154
|
+
|
|
126
155
|
issues.push({
|
|
127
156
|
file,
|
|
128
157
|
line: lineNumber,
|
|
129
158
|
type: 'poor-naming',
|
|
130
159
|
identifier: match[1],
|
|
131
160
|
severity: 'minor',
|
|
132
|
-
suggestion: `Use descriptive variable name instead of single letter '${match[1]}'
|
|
161
|
+
suggestion: `Use descriptive variable name instead of single letter '${match[1]}'`,
|
|
133
162
|
});
|
|
134
163
|
}
|
|
135
164
|
}
|
|
@@ -137,63 +166,78 @@ function analyzeFileNaming(
|
|
|
137
166
|
|
|
138
167
|
// Check for overly abbreviated variables
|
|
139
168
|
if (!disabledChecks.has('abbreviation')) {
|
|
140
|
-
const abbreviationMatches = line.matchAll(
|
|
169
|
+
const abbreviationMatches = line.matchAll(
|
|
170
|
+
/\b(?:const|let|var)\s+([a-z]{1,3})(?=[A-Z]|_|\s*=)/g
|
|
171
|
+
);
|
|
141
172
|
for (const match of abbreviationMatches) {
|
|
142
173
|
const abbrev = match[1].toLowerCase();
|
|
143
|
-
|
|
174
|
+
|
|
144
175
|
// Skip if it's a common short English word (full word, not abbreviation)
|
|
145
176
|
if (allShortWords.has(abbrev)) {
|
|
146
177
|
continue;
|
|
147
178
|
}
|
|
148
|
-
|
|
179
|
+
|
|
149
180
|
// Skip acceptable abbreviations (including custom ones)
|
|
150
181
|
if (allAbbreviations.has(abbrev)) {
|
|
151
182
|
continue;
|
|
152
183
|
}
|
|
153
|
-
|
|
184
|
+
|
|
154
185
|
// Check for arrow function parameter context (with multi-line detection)
|
|
155
|
-
const isArrowFunctionParam =
|
|
186
|
+
const isArrowFunctionParam =
|
|
156
187
|
/\(\s*[a-z]\s*(?:,\s*[a-z]\s*)*\)\s*=>/.test(line) || // (s) => or (a, b) =>
|
|
157
188
|
new RegExp(`\\b${abbrev}\\s*=>`).test(line) || // s => on same line
|
|
158
189
|
// Multi-line arrow function: check context window
|
|
159
|
-
(new RegExp(`\\b${abbrev}\\s*\\)\\s*$`).test(line) &&
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
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
|
+
|
|
163
197
|
if (isArrowFunctionParam) {
|
|
164
198
|
continue;
|
|
165
199
|
}
|
|
166
|
-
|
|
200
|
+
|
|
167
201
|
// For very short names (1-2 letters), check for date/time context
|
|
168
202
|
if (abbrev.length <= 2) {
|
|
169
|
-
const isDateTimeContext =
|
|
203
|
+
const isDateTimeContext =
|
|
204
|
+
/date|time|day|hour|minute|second|timestamp/i.test(line);
|
|
170
205
|
if (isDateTimeContext && ['d', 't', 'dt'].includes(abbrev)) {
|
|
171
206
|
continue;
|
|
172
207
|
}
|
|
173
|
-
|
|
208
|
+
|
|
174
209
|
// Check for user/auth context
|
|
175
210
|
const isUserContext = /user|auth|account/i.test(line);
|
|
176
211
|
if (isUserContext && abbrev === 'u') {
|
|
177
212
|
continue;
|
|
178
213
|
}
|
|
179
214
|
}
|
|
180
|
-
|
|
215
|
+
|
|
181
216
|
issues.push({
|
|
182
217
|
file,
|
|
183
218
|
line: lineNumber,
|
|
184
219
|
type: 'abbreviation',
|
|
185
220
|
identifier: match[1],
|
|
186
221
|
severity: 'info',
|
|
187
|
-
suggestion: `Consider using full word instead of abbreviation '${match[1]}'
|
|
222
|
+
suggestion: `Consider using full word instead of abbreviation '${match[1]}'`,
|
|
188
223
|
});
|
|
189
224
|
}
|
|
190
225
|
}
|
|
191
226
|
|
|
192
227
|
// Check for snake_case vs camelCase mixing in TypeScript/JavaScript
|
|
193
|
-
if (
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
+
|
|
197
241
|
if (snakeCaseVars) {
|
|
198
242
|
issues.push({
|
|
199
243
|
file,
|
|
@@ -201,14 +245,16 @@ function analyzeFileNaming(
|
|
|
201
245
|
type: 'convention-mix',
|
|
202
246
|
identifier: snakeCaseVars[1],
|
|
203
247
|
severity: 'minor',
|
|
204
|
-
suggestion: `Use camelCase '${snakeCaseToCamelCase(snakeCaseVars[1])}' instead of snake_case in TypeScript/JavaScript
|
|
248
|
+
suggestion: `Use camelCase '${snakeCaseToCamelCase(snakeCaseVars[1])}' instead of snake_case in TypeScript/JavaScript`,
|
|
205
249
|
});
|
|
206
250
|
}
|
|
207
251
|
}
|
|
208
252
|
|
|
209
253
|
// Check for unclear boolean names (should start with is/has/should/can)
|
|
210
254
|
if (!disabledChecks.has('unclear')) {
|
|
211
|
-
const booleanMatches = line.matchAll(
|
|
255
|
+
const booleanMatches = line.matchAll(
|
|
256
|
+
/\b(?:const|let|var)\s+([a-z][a-zA-Z0-9]*)\s*:\s*boolean/gi
|
|
257
|
+
);
|
|
212
258
|
for (const match of booleanMatches) {
|
|
213
259
|
const name = match[1];
|
|
214
260
|
if (!name.match(/^(is|has|should|can|will|did)/i)) {
|
|
@@ -218,7 +264,7 @@ function analyzeFileNaming(
|
|
|
218
264
|
type: 'unclear',
|
|
219
265
|
identifier: name,
|
|
220
266
|
severity: 'info',
|
|
221
|
-
suggestion: `Boolean variable '${name}' should start with is/has/should/can for clarity
|
|
267
|
+
suggestion: `Boolean variable '${name}' should start with is/has/should/can for clarity`,
|
|
222
268
|
});
|
|
223
269
|
}
|
|
224
270
|
}
|
|
@@ -229,19 +275,39 @@ function analyzeFileNaming(
|
|
|
229
275
|
const functionMatches = line.matchAll(/function\s+([a-z][a-zA-Z0-9]*)/g);
|
|
230
276
|
for (const match of functionMatches) {
|
|
231
277
|
const name = match[1];
|
|
232
|
-
|
|
278
|
+
|
|
233
279
|
// Skip JavaScript/TypeScript keywords that shouldn't be function names
|
|
234
|
-
const isKeyword = [
|
|
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);
|
|
235
299
|
if (isKeyword) {
|
|
236
300
|
continue;
|
|
237
301
|
}
|
|
238
|
-
|
|
302
|
+
|
|
239
303
|
// Skip common entry point names
|
|
240
|
-
const isEntryPoint = ['main', 'init', 'setup', 'bootstrap'].includes(
|
|
304
|
+
const isEntryPoint = ['main', 'init', 'setup', 'bootstrap'].includes(
|
|
305
|
+
name
|
|
306
|
+
);
|
|
241
307
|
if (isEntryPoint) {
|
|
242
308
|
continue;
|
|
243
309
|
}
|
|
244
|
-
|
|
310
|
+
|
|
245
311
|
// Functions should typically start with verbs, but allow:
|
|
246
312
|
// 1. Factory/builder patterns (ends with Factory, Builder, etc.)
|
|
247
313
|
// 2. Descriptive compound names that explain what they return
|
|
@@ -251,37 +317,63 @@ function analyzeFileNaming(
|
|
|
251
317
|
// 6. Compound words with 3+ capitals
|
|
252
318
|
// 7. Helper/utility functions (common patterns)
|
|
253
319
|
// 8. React hooks (useX pattern)
|
|
254
|
-
|
|
255
|
-
const isFactoryPattern = name.match(
|
|
320
|
+
|
|
321
|
+
const isFactoryPattern = name.match(
|
|
322
|
+
/(Factory|Builder|Creator|Generator|Provider|Adapter|Mock)$/
|
|
323
|
+
);
|
|
256
324
|
const isEventHandler = name.match(/^on[A-Z]/);
|
|
257
325
|
const isDescriptiveLong = name.length > 15; // Reduced from 20 to 15
|
|
258
326
|
const isReactHook = name.match(/^use[A-Z]/); // React hooks
|
|
259
|
-
|
|
327
|
+
|
|
260
328
|
// Check for descriptive patterns
|
|
261
|
-
const isDescriptivePattern =
|
|
262
|
-
|
|
263
|
-
|
|
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
|
+
|
|
264
337
|
// Helper/utility function patterns
|
|
265
|
-
const isHelperPattern =
|
|
266
|
-
|
|
267
|
-
|
|
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
|
+
|
|
268
342
|
// Common utility names that are descriptive
|
|
269
|
-
const isUtilityName = [
|
|
270
|
-
|
|
343
|
+
const isUtilityName = [
|
|
344
|
+
'cn',
|
|
345
|
+
'proxy',
|
|
346
|
+
'sitemap',
|
|
347
|
+
'robots',
|
|
348
|
+
'gtag',
|
|
349
|
+
].includes(name);
|
|
350
|
+
|
|
271
351
|
// Count capital letters for compound detection
|
|
272
352
|
const capitalCount = (name.match(/[A-Z]/g) || []).length;
|
|
273
353
|
const isCompoundWord = capitalCount >= 3; // daysSinceLastCommit has 4 capitals
|
|
274
|
-
|
|
275
|
-
const hasActionVerb = name.match(
|
|
276
|
-
|
|
277
|
-
|
|
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
|
+
) {
|
|
278
370
|
issues.push({
|
|
279
371
|
file,
|
|
280
372
|
line: lineNumber,
|
|
281
373
|
type: 'unclear',
|
|
282
374
|
identifier: name,
|
|
283
375
|
severity: 'info',
|
|
284
|
-
suggestion: `Function '${name}' should start with an action verb (get, set, create, etc.)
|
|
376
|
+
suggestion: `Function '${name}' should start with an action verb (get, set, create, etc.)`,
|
|
285
377
|
});
|
|
286
378
|
}
|
|
287
379
|
}
|
|
@@ -4,7 +4,9 @@ import type { PatternIssue } from '../types';
|
|
|
4
4
|
/**
|
|
5
5
|
* Analyzes code pattern consistency
|
|
6
6
|
*/
|
|
7
|
-
export async function analyzePatterns(
|
|
7
|
+
export async function analyzePatterns(
|
|
8
|
+
files: string[]
|
|
9
|
+
): Promise<PatternIssue[]> {
|
|
8
10
|
const issues: PatternIssue[] = [];
|
|
9
11
|
|
|
10
12
|
// Analyze error handling patterns
|
|
@@ -27,12 +29,12 @@ async function analyzeErrorHandling(files: string[]): Promise<PatternIssue[]> {
|
|
|
27
29
|
tryCatch: [] as string[],
|
|
28
30
|
throwsError: [] as string[],
|
|
29
31
|
returnsNull: [] as string[],
|
|
30
|
-
returnsError: [] as string[]
|
|
32
|
+
returnsError: [] as string[],
|
|
31
33
|
};
|
|
32
34
|
|
|
33
35
|
for (const file of files) {
|
|
34
36
|
const content = await readFileContent(file);
|
|
35
|
-
|
|
37
|
+
|
|
36
38
|
if (content.includes('try {') || content.includes('} catch')) {
|
|
37
39
|
patterns.tryCatch.push(file);
|
|
38
40
|
}
|
|
@@ -50,24 +52,36 @@ async function analyzeErrorHandling(files: string[]): Promise<PatternIssue[]> {
|
|
|
50
52
|
const issues: PatternIssue[] = [];
|
|
51
53
|
|
|
52
54
|
// Check for mixed error handling strategies
|
|
53
|
-
const strategiesUsed = Object.values(patterns).filter(
|
|
55
|
+
const strategiesUsed = Object.values(patterns).filter(
|
|
56
|
+
(p) => p.length > 0
|
|
57
|
+
).length;
|
|
54
58
|
if (strategiesUsed > 2) {
|
|
55
59
|
issues.push({
|
|
56
|
-
files: [
|
|
57
|
-
...
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
files: [
|
|
61
|
+
...new Set([
|
|
62
|
+
...patterns.tryCatch,
|
|
63
|
+
...patterns.throwsError,
|
|
64
|
+
...patterns.returnsNull,
|
|
65
|
+
...patterns.returnsError,
|
|
66
|
+
]),
|
|
67
|
+
],
|
|
62
68
|
type: 'error-handling',
|
|
63
69
|
description: 'Inconsistent error handling strategies across codebase',
|
|
64
70
|
examples: [
|
|
65
|
-
patterns.tryCatch.length > 0
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
patterns.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
+
patterns.tryCatch.length > 0
|
|
72
|
+
? `Try-catch used in ${patterns.tryCatch.length} files`
|
|
73
|
+
: '',
|
|
74
|
+
patterns.throwsError.length > 0
|
|
75
|
+
? `Throws errors in ${patterns.throwsError.length} files`
|
|
76
|
+
: '',
|
|
77
|
+
patterns.returnsNull.length > 0
|
|
78
|
+
? `Returns null in ${patterns.returnsNull.length} files`
|
|
79
|
+
: '',
|
|
80
|
+
patterns.returnsError.length > 0
|
|
81
|
+
? `Returns error objects in ${patterns.returnsError.length} files`
|
|
82
|
+
: '',
|
|
83
|
+
].filter((e) => e),
|
|
84
|
+
severity: 'major',
|
|
71
85
|
});
|
|
72
86
|
}
|
|
73
87
|
|
|
@@ -78,12 +92,12 @@ async function analyzeAsyncPatterns(files: string[]): Promise<PatternIssue[]> {
|
|
|
78
92
|
const patterns = {
|
|
79
93
|
asyncAwait: [] as string[],
|
|
80
94
|
promises: [] as string[],
|
|
81
|
-
callbacks: [] as string[]
|
|
95
|
+
callbacks: [] as string[],
|
|
82
96
|
};
|
|
83
97
|
|
|
84
98
|
for (const file of files) {
|
|
85
99
|
const content = await readFileContent(file);
|
|
86
|
-
|
|
100
|
+
|
|
87
101
|
if (content.match(/async\s+(function|\(|[a-zA-Z])/)) {
|
|
88
102
|
patterns.asyncAwait.push(file);
|
|
89
103
|
}
|
|
@@ -105,20 +119,24 @@ async function analyzeAsyncPatterns(files: string[]): Promise<PatternIssue[]> {
|
|
|
105
119
|
description: 'Mixed async patterns: callbacks and async/await',
|
|
106
120
|
examples: [
|
|
107
121
|
`Callbacks found in: ${patterns.callbacks.slice(0, 3).join(', ')}`,
|
|
108
|
-
`Async/await used in: ${patterns.asyncAwait.slice(0, 3).join(', ')}
|
|
122
|
+
`Async/await used in: ${patterns.asyncAwait.slice(0, 3).join(', ')}`,
|
|
109
123
|
],
|
|
110
|
-
severity: 'minor'
|
|
124
|
+
severity: 'minor',
|
|
111
125
|
});
|
|
112
126
|
}
|
|
113
127
|
|
|
114
128
|
// Mixing .then() chains with async/await
|
|
115
|
-
if (
|
|
129
|
+
if (
|
|
130
|
+
patterns.promises.length > patterns.asyncAwait.length * 0.3 &&
|
|
131
|
+
patterns.asyncAwait.length > 0
|
|
132
|
+
) {
|
|
116
133
|
issues.push({
|
|
117
134
|
files: patterns.promises,
|
|
118
135
|
type: 'async-style',
|
|
119
|
-
description:
|
|
136
|
+
description:
|
|
137
|
+
'Consider using async/await instead of promise chains for consistency',
|
|
120
138
|
examples: patterns.promises.slice(0, 5),
|
|
121
|
-
severity: 'info'
|
|
139
|
+
severity: 'info',
|
|
122
140
|
});
|
|
123
141
|
}
|
|
124
142
|
|
|
@@ -129,19 +147,19 @@ async function analyzeImportStyles(files: string[]): Promise<PatternIssue[]> {
|
|
|
129
147
|
const patterns = {
|
|
130
148
|
esModules: [] as string[],
|
|
131
149
|
commonJS: [] as string[],
|
|
132
|
-
mixed: [] as string[]
|
|
150
|
+
mixed: [] as string[],
|
|
133
151
|
};
|
|
134
152
|
|
|
135
153
|
for (const file of files) {
|
|
136
154
|
const content = await readFileContent(file);
|
|
137
155
|
const hasESM = content.match(/^import\s+/m);
|
|
138
|
-
|
|
156
|
+
|
|
139
157
|
// Check for actual CommonJS require() calls, excluding:
|
|
140
158
|
// - String literals: "require('...') or 'require('...')
|
|
141
159
|
// - Regex patterns: /require\(/
|
|
142
160
|
// - Comments: // require( or /* require( */
|
|
143
161
|
const hasCJS = hasActualRequireCalls(content);
|
|
144
|
-
|
|
162
|
+
|
|
145
163
|
if (hasESM && hasCJS) {
|
|
146
164
|
patterns.mixed.push(file);
|
|
147
165
|
} else if (hasESM) {
|
|
@@ -160,13 +178,15 @@ async function analyzeImportStyles(files: string[]): Promise<PatternIssue[]> {
|
|
|
160
178
|
type: 'import-style',
|
|
161
179
|
description: 'Mixed ES modules and CommonJS imports in same files',
|
|
162
180
|
examples: patterns.mixed.slice(0, 5),
|
|
163
|
-
severity: 'major'
|
|
181
|
+
severity: 'major',
|
|
164
182
|
});
|
|
165
183
|
}
|
|
166
184
|
|
|
167
185
|
// Check for inconsistent styles across project
|
|
168
186
|
if (patterns.esModules.length > 0 && patterns.commonJS.length > 0) {
|
|
169
|
-
const ratio =
|
|
187
|
+
const ratio =
|
|
188
|
+
patterns.commonJS.length /
|
|
189
|
+
(patterns.esModules.length + patterns.commonJS.length);
|
|
170
190
|
if (ratio > 0.2 && ratio < 0.8) {
|
|
171
191
|
issues.push({
|
|
172
192
|
files: [...patterns.esModules, ...patterns.commonJS],
|
|
@@ -174,9 +194,9 @@ async function analyzeImportStyles(files: string[]): Promise<PatternIssue[]> {
|
|
|
174
194
|
description: 'Inconsistent import styles across project',
|
|
175
195
|
examples: [
|
|
176
196
|
`ES modules: ${patterns.esModules.length} files`,
|
|
177
|
-
`CommonJS: ${patterns.commonJS.length} files
|
|
197
|
+
`CommonJS: ${patterns.commonJS.length} files`,
|
|
178
198
|
],
|
|
179
|
-
severity: 'minor'
|
|
199
|
+
severity: 'minor',
|
|
180
200
|
});
|
|
181
201
|
}
|
|
182
202
|
}
|
|
@@ -196,20 +216,20 @@ function hasActualRequireCalls(content: string): boolean {
|
|
|
196
216
|
// Simple heuristic: remove obvious false positives
|
|
197
217
|
// 1. Remove single-line comments
|
|
198
218
|
let cleaned = content.replace(/\/\/.*$/gm, '');
|
|
199
|
-
|
|
219
|
+
|
|
200
220
|
// 2. Remove multi-line comments (non-greedy)
|
|
201
221
|
cleaned = cleaned.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
202
|
-
|
|
222
|
+
|
|
203
223
|
// 3. Remove string literals - use simpler regex to avoid backtracking
|
|
204
224
|
// Match strings but don't try to be perfect, just remove obvious ones
|
|
205
225
|
cleaned = cleaned.replace(/"[^"\n]*"/g, '""');
|
|
206
226
|
cleaned = cleaned.replace(/'[^'\n]*'/g, "''");
|
|
207
227
|
cleaned = cleaned.replace(/`[^`]*`/g, '``');
|
|
208
|
-
|
|
228
|
+
|
|
209
229
|
// 4. Simple regex detection: if we see /require in the line, likely a regex pattern
|
|
210
230
|
// Remove lines that look like regex patterns with require
|
|
211
231
|
cleaned = cleaned.replace(/\/[^/\n]*require[^/\n]*\/[gimsuvy]*/g, '');
|
|
212
|
-
|
|
232
|
+
|
|
213
233
|
// Now check for require( in the cleaned content
|
|
214
234
|
return /require\s*\(/.test(cleaned);
|
|
215
235
|
}
|
|
@@ -217,11 +237,15 @@ function hasActualRequireCalls(content: string): boolean {
|
|
|
217
237
|
/**
|
|
218
238
|
* Analyzes API design consistency
|
|
219
239
|
*/
|
|
220
|
-
export async function analyzeAPIDesign(
|
|
240
|
+
export async function analyzeAPIDesign(
|
|
241
|
+
files: string[]
|
|
242
|
+
): Promise<PatternIssue[]> {
|
|
221
243
|
// This would analyze:
|
|
222
244
|
// - Function parameter order consistency
|
|
223
245
|
// - Return type patterns
|
|
224
246
|
// - Options object vs individual parameters
|
|
225
247
|
// For now, return empty array
|
|
248
|
+
// Parameter currently unused; reference to avoid lint warnings
|
|
249
|
+
void files;
|
|
226
250
|
return [];
|
|
227
251
|
}
|