@aiready/consistency 0.5.0 → 0.6.1
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 +24 -0
- package/.turbo/turbo-test.log +123 -0
- package/dist/chunk-HAOJLJNB.mjs +1290 -0
- package/dist/chunk-IVRBV7SE.mjs +1295 -0
- package/dist/chunk-LD3CHHU2.mjs +1297 -0
- package/dist/chunk-VODCPPET.mjs +1292 -0
- package/dist/chunk-WGH4TGZ3.mjs +1288 -0
- package/dist/cli.js +624 -189
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +6 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.js +1196 -182
- package/dist/index.mjs +581 -4
- package/package.json +14 -13
- package/src/analyzer.ts +4 -4
- package/src/analyzers/naming-ast.ts +378 -0
- package/src/index.ts +2 -1
- package/src/utils/ast-parser.ts +181 -0
- package/src/utils/context-detector.ts +278 -0
- package/src/utils/scope-tracker.ts +221 -0
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
import { TSESTree } from '@typescript-eslint/typescript-estree';
|
|
2
|
+
import { loadConfig } from '@aiready/core';
|
|
3
|
+
import { dirname } from 'path';
|
|
4
|
+
import type { NamingIssue } from '../types';
|
|
5
|
+
import {
|
|
6
|
+
parseFile,
|
|
7
|
+
traverseAST,
|
|
8
|
+
getFunctionName,
|
|
9
|
+
getLineNumber,
|
|
10
|
+
isCoverageContext,
|
|
11
|
+
isLoopStatement,
|
|
12
|
+
isCallback,
|
|
13
|
+
} from '../utils/ast-parser';
|
|
14
|
+
import { ScopeTracker } from '../utils/scope-tracker';
|
|
15
|
+
import {
|
|
16
|
+
buildCodeContext,
|
|
17
|
+
adjustSeverity,
|
|
18
|
+
isAcceptableInContext,
|
|
19
|
+
calculateComplexity,
|
|
20
|
+
} from '../utils/context-detector';
|
|
21
|
+
|
|
22
|
+
// Common short English words that are NOT abbreviations (full, valid words)
|
|
23
|
+
const COMMON_SHORT_WORDS = new Set([
|
|
24
|
+
'day', 'key', 'net', 'to', 'go', 'for', 'not', 'new', 'old', 'top', 'end',
|
|
25
|
+
'run', 'try', 'use', 'get', 'set', 'add', 'put', 'map', 'log', 'row', 'col',
|
|
26
|
+
'tab', 'box', 'div', 'nav', 'tag', 'any', 'all', 'one', 'two', 'out', 'off',
|
|
27
|
+
'on', 'yes', 'no', 'now', 'max', 'min', 'sum', 'avg', 'ref', 'src', 'dst',
|
|
28
|
+
'raw', 'def', 'sub', 'pub', 'pre', 'mid', 'alt', 'opt', 'tmp', 'ext', 'sep',
|
|
29
|
+
'and', 'from', 'how', 'pad', 'bar', 'non',
|
|
30
|
+
'tax', 'cat', 'dog', 'car', 'bus', 'web', 'app', 'war', 'law', 'pay', 'buy',
|
|
31
|
+
'win', 'cut', 'hit', 'hot', 'pop', 'job', 'age', 'act', 'let', 'lot', 'bad',
|
|
32
|
+
'big', 'far', 'few', 'own', 'per', 'red', 'low', 'see', 'six', 'ten', 'way',
|
|
33
|
+
'who', 'why', 'yet', 'via', 'due', 'fee', 'fun', 'gas', 'gay', 'god', 'gun',
|
|
34
|
+
'guy', 'ice', 'ill', 'kid', 'mad', 'man', 'mix', 'mom', 'mrs', 'nor', 'odd',
|
|
35
|
+
'oil', 'pan', 'pet', 'pit', 'pot', 'pow', 'pro', 'raw', 'rep', 'rid', 'sad',
|
|
36
|
+
'sea', 'sit', 'sky', 'son', 'tea', 'tie', 'tip', 'van', 'war', 'win', 'won'
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
// Comprehensive list of acceptable abbreviations and acronyms
|
|
40
|
+
const ACCEPTABLE_ABBREVIATIONS = new Set([
|
|
41
|
+
'id', 'uid', 'gid', 'pid', 'i', 'j', 'k', 'n', 'm',
|
|
42
|
+
'url', 'uri', 'api', 'cdn', 'dns', 'ip', 'tcp', 'udp', 'http', 'ssl', 'tls',
|
|
43
|
+
'utm', 'seo', 'rss', 'xhr', 'ajax', 'cors', 'ws', 'wss',
|
|
44
|
+
'json', 'xml', 'yaml', 'csv', 'html', 'css', 'svg', 'pdf',
|
|
45
|
+
'img', 'txt', 'doc', 'docx', 'xlsx', 'ppt', 'md', 'rst', 'jpg', 'png', 'gif',
|
|
46
|
+
'db', 'sql', 'orm', 'dao', 'dto', 'ddb', 'rds', 'nosql',
|
|
47
|
+
'fs', 'dir', 'tmp', 'src', 'dst', 'bin', 'lib', 'pkg',
|
|
48
|
+
'os', 'env', 'arg', 'cli', 'cmd', 'exe', 'cwd', 'pwd',
|
|
49
|
+
'ui', 'ux', 'gui', 'dom', 'ref',
|
|
50
|
+
'req', 'res', 'ctx', 'err', 'msg', 'auth',
|
|
51
|
+
'max', 'min', 'avg', 'sum', 'abs', 'cos', 'sin', 'tan', 'log', 'exp',
|
|
52
|
+
'pow', 'sqrt', 'std', 'var', 'int', 'num', 'idx',
|
|
53
|
+
'now', 'utc', 'tz', 'ms', 'sec', 'hr', 'min', 'yr', 'mo',
|
|
54
|
+
'app', 'cfg', 'config', 'init', 'len', 'val', 'str', 'obj', 'arr',
|
|
55
|
+
'gen', 'def', 'raw', 'new', 'old', 'pre', 'post', 'sub', 'pub',
|
|
56
|
+
'ts', 'js', 'jsx', 'tsx', 'py', 'rb', 'vue', 're', 'fn', 'fns', 'mod', 'opts', 'dev',
|
|
57
|
+
's3', 'ec2', 'sqs', 'sns', 'vpc', 'ami', 'iam', 'acl', 'elb', 'alb', 'nlb', 'aws',
|
|
58
|
+
'ses', 'gst', 'cdk', 'btn', 'buf', 'agg', 'ocr', 'ai', 'cf', 'cfn', 'ga',
|
|
59
|
+
'fcp', 'lcp', 'cls', 'ttfb', 'tti', 'fid', 'fps', 'qps', 'rps', 'tps', 'wpm',
|
|
60
|
+
'po', 'e2e', 'a11y', 'i18n', 'l10n', 'spy',
|
|
61
|
+
'sk', 'fy', 'faq', 'og', 'seo', 'cta', 'roi', 'kpi', 'ttl', 'pct',
|
|
62
|
+
'mac', 'hex', 'esm', 'git', 'rec', 'loc', 'dup',
|
|
63
|
+
'is', 'has', 'can', 'did', 'was', 'are',
|
|
64
|
+
'd', 't', 'dt',
|
|
65
|
+
's', 'b', 'f', 'l', // Coverage metrics
|
|
66
|
+
'vid', 'pic', 'img', 'doc', 'msg'
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* AST-based naming analyzer
|
|
71
|
+
*/
|
|
72
|
+
export async function analyzeNamingAST(files: string[]): Promise<NamingIssue[]> {
|
|
73
|
+
const issues: NamingIssue[] = [];
|
|
74
|
+
|
|
75
|
+
// Load config
|
|
76
|
+
const rootDir = files.length > 0 ? dirname(files[0]) : process.cwd();
|
|
77
|
+
const config = loadConfig(rootDir);
|
|
78
|
+
const consistencyConfig = config?.tools?.['consistency'];
|
|
79
|
+
|
|
80
|
+
// Merge custom configuration
|
|
81
|
+
const customAbbreviations = new Set(consistencyConfig?.acceptedAbbreviations || []);
|
|
82
|
+
const customShortWords = new Set(consistencyConfig?.shortWords || []);
|
|
83
|
+
const disabledChecks = new Set(consistencyConfig?.disableChecks || []);
|
|
84
|
+
|
|
85
|
+
const allAbbreviations = new Set([...ACCEPTABLE_ABBREVIATIONS, ...customAbbreviations]);
|
|
86
|
+
const allShortWords = new Set([...COMMON_SHORT_WORDS, ...customShortWords]);
|
|
87
|
+
|
|
88
|
+
for (const file of files) {
|
|
89
|
+
try {
|
|
90
|
+
const ast = parseFile(file);
|
|
91
|
+
if (!ast) continue; // Skip files that fail to parse
|
|
92
|
+
|
|
93
|
+
const fileIssues = analyzeFileNamingAST(
|
|
94
|
+
file,
|
|
95
|
+
ast,
|
|
96
|
+
allAbbreviations,
|
|
97
|
+
allShortWords,
|
|
98
|
+
disabledChecks
|
|
99
|
+
);
|
|
100
|
+
issues.push(...fileIssues);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.warn(`Skipping ${file} due to parse error:`, error);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return issues;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function analyzeFileNamingAST(
|
|
110
|
+
file: string,
|
|
111
|
+
ast: TSESTree.Program,
|
|
112
|
+
allAbbreviations: Set<string>,
|
|
113
|
+
allShortWords: Set<string>,
|
|
114
|
+
disabledChecks: Set<string>
|
|
115
|
+
): NamingIssue[] {
|
|
116
|
+
const issues: NamingIssue[] = [];
|
|
117
|
+
const scopeTracker = new ScopeTracker(ast);
|
|
118
|
+
const context = buildCodeContext(file, ast);
|
|
119
|
+
const ancestors: TSESTree.Node[] = [];
|
|
120
|
+
|
|
121
|
+
// First pass: Build scope tree and track all variables
|
|
122
|
+
traverseAST(ast, {
|
|
123
|
+
enter: (node, parent) => {
|
|
124
|
+
ancestors.push(node);
|
|
125
|
+
|
|
126
|
+
// Enter scopes
|
|
127
|
+
if (node.type === 'FunctionDeclaration' || node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') {
|
|
128
|
+
scopeTracker.enterScope('function', node);
|
|
129
|
+
|
|
130
|
+
// Track parameters
|
|
131
|
+
if ('params' in node) {
|
|
132
|
+
for (const param of node.params) {
|
|
133
|
+
if (param.type === 'Identifier') {
|
|
134
|
+
scopeTracker.declareVariable(param.name, param, getLineNumber(param), {
|
|
135
|
+
isParameter: true,
|
|
136
|
+
});
|
|
137
|
+
} else if (param.type === 'ObjectPattern' || param.type === 'ArrayPattern') {
|
|
138
|
+
// Handle destructured parameters
|
|
139
|
+
extractIdentifiersFromPattern(param, scopeTracker, true);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
} else if (node.type === 'BlockStatement') {
|
|
144
|
+
scopeTracker.enterScope('block', node);
|
|
145
|
+
} else if (isLoopStatement(node)) {
|
|
146
|
+
scopeTracker.enterScope('loop', node);
|
|
147
|
+
} else if (node.type === 'ClassDeclaration') {
|
|
148
|
+
scopeTracker.enterScope('class', node);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Track variable declarations
|
|
152
|
+
if (node.type === 'VariableDeclarator') {
|
|
153
|
+
if (node.id.type === 'Identifier') {
|
|
154
|
+
const isInCoverage = isCoverageContext(node, ancestors);
|
|
155
|
+
scopeTracker.declareVariable(
|
|
156
|
+
node.id.name,
|
|
157
|
+
node.id,
|
|
158
|
+
getLineNumber(node.id),
|
|
159
|
+
{
|
|
160
|
+
type: ('typeAnnotation' in node.id) ? (node.id as any).typeAnnotation : null,
|
|
161
|
+
isDestructured: false,
|
|
162
|
+
isLoopVariable: scopeTracker.getCurrentScopeType() === 'loop',
|
|
163
|
+
}
|
|
164
|
+
);
|
|
165
|
+
} else if (node.id.type === 'ObjectPattern' || node.id.type === 'ArrayPattern') {
|
|
166
|
+
extractIdentifiersFromPattern(node.id, scopeTracker, false, ancestors);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Track references
|
|
171
|
+
if (node.type === 'Identifier' && parent) {
|
|
172
|
+
// Only count as reference if it's not a declaration
|
|
173
|
+
if (parent.type !== 'VariableDeclarator' || parent.id !== node) {
|
|
174
|
+
if (parent.type !== 'FunctionDeclaration' || parent.id !== node) {
|
|
175
|
+
scopeTracker.addReference(node.name, node);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
leave: (node) => {
|
|
181
|
+
ancestors.pop();
|
|
182
|
+
|
|
183
|
+
// Exit scopes
|
|
184
|
+
if (
|
|
185
|
+
node.type === 'FunctionDeclaration' ||
|
|
186
|
+
node.type === 'FunctionExpression' ||
|
|
187
|
+
node.type === 'ArrowFunctionExpression' ||
|
|
188
|
+
node.type === 'BlockStatement' ||
|
|
189
|
+
isLoopStatement(node) ||
|
|
190
|
+
node.type === 'ClassDeclaration'
|
|
191
|
+
) {
|
|
192
|
+
scopeTracker.exitScope();
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// Second pass: Analyze all variables
|
|
198
|
+
const allVariables = scopeTracker.getAllVariables();
|
|
199
|
+
|
|
200
|
+
for (const varInfo of allVariables) {
|
|
201
|
+
const name = varInfo.name;
|
|
202
|
+
const line = varInfo.declarationLine;
|
|
203
|
+
|
|
204
|
+
// Skip if checks are disabled
|
|
205
|
+
if (disabledChecks.has('single-letter') && name.length === 1) continue;
|
|
206
|
+
if (disabledChecks.has('abbreviation') && name.length <= 3) continue;
|
|
207
|
+
|
|
208
|
+
// Check coverage context
|
|
209
|
+
const isInCoverage = ['s', 'b', 'f', 'l'].includes(name) && varInfo.isDestructured;
|
|
210
|
+
if (isInCoverage) continue;
|
|
211
|
+
|
|
212
|
+
// Check if acceptable in context
|
|
213
|
+
const functionComplexity = varInfo.node.type === 'Identifier' && 'parent' in varInfo.node
|
|
214
|
+
? calculateComplexity(varInfo.node as any)
|
|
215
|
+
: context.complexity;
|
|
216
|
+
|
|
217
|
+
if (isAcceptableInContext(name, context, {
|
|
218
|
+
isLoopVariable: varInfo.isLoopVariable || allAbbreviations.has(name),
|
|
219
|
+
isParameter: varInfo.isParameter,
|
|
220
|
+
isDestructured: varInfo.isDestructured,
|
|
221
|
+
complexity: functionComplexity,
|
|
222
|
+
})) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Single letter check
|
|
227
|
+
if (name.length === 1 && !allAbbreviations.has(name) && !allShortWords.has(name)) {
|
|
228
|
+
// Check if short-lived
|
|
229
|
+
const isShortLived = scopeTracker.isShortLived(varInfo, 5);
|
|
230
|
+
if (!isShortLived) {
|
|
231
|
+
issues.push({
|
|
232
|
+
file,
|
|
233
|
+
line,
|
|
234
|
+
type: 'poor-naming',
|
|
235
|
+
identifier: name,
|
|
236
|
+
severity: adjustSeverity('minor', context, 'poor-naming'),
|
|
237
|
+
suggestion: `Use descriptive variable name instead of single letter '${name}'`,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Abbreviation check (2-3 letters)
|
|
244
|
+
if (name.length >= 2 && name.length <= 3) {
|
|
245
|
+
if (!allShortWords.has(name.toLowerCase()) && !allAbbreviations.has(name.toLowerCase())) {
|
|
246
|
+
// Check if short-lived for abbreviations too
|
|
247
|
+
const isShortLived = scopeTracker.isShortLived(varInfo, 5);
|
|
248
|
+
if (!isShortLived) {
|
|
249
|
+
issues.push({
|
|
250
|
+
file,
|
|
251
|
+
line,
|
|
252
|
+
type: 'abbreviation',
|
|
253
|
+
identifier: name,
|
|
254
|
+
severity: adjustSeverity('info', context, 'abbreviation'),
|
|
255
|
+
suggestion: `Consider using full word instead of abbreviation '${name}'`,
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Snake_case check for TypeScript/JavaScript
|
|
263
|
+
if (!disabledChecks.has('convention-mix') && file.match(/\.(ts|tsx|js|jsx)$/)) {
|
|
264
|
+
if (name.includes('_') && !name.startsWith('_') && name.toLowerCase() === name) {
|
|
265
|
+
const camelCase = name.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase());
|
|
266
|
+
issues.push({
|
|
267
|
+
file,
|
|
268
|
+
line,
|
|
269
|
+
type: 'convention-mix',
|
|
270
|
+
identifier: name,
|
|
271
|
+
severity: adjustSeverity('minor', context, 'convention-mix'),
|
|
272
|
+
suggestion: `Use camelCase '${camelCase}' instead of snake_case in TypeScript/JavaScript`,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Third pass: Analyze function names
|
|
279
|
+
if (!disabledChecks.has('unclear')) {
|
|
280
|
+
traverseAST(ast, {
|
|
281
|
+
enter: (node) => {
|
|
282
|
+
if (node.type === 'FunctionDeclaration' || node.type === 'MethodDefinition') {
|
|
283
|
+
const name = getFunctionName(node);
|
|
284
|
+
if (!name) return;
|
|
285
|
+
|
|
286
|
+
const line = getLineNumber(node);
|
|
287
|
+
|
|
288
|
+
// Skip entry points
|
|
289
|
+
if (['main', 'init', 'setup', 'bootstrap'].includes(name)) return;
|
|
290
|
+
|
|
291
|
+
// Check for action verbs and patterns
|
|
292
|
+
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|track|store|persist|upsert|derive|classify|combine|discover|activate|require|assert|expect|mask|escape|sign|put|list|complete|page|safe|mock|pick|pluralize|text|count|detect|select)/);
|
|
293
|
+
|
|
294
|
+
const isFactoryPattern = name.match(/(Factory|Builder|Creator|Generator|Provider|Adapter|Mock)$/);
|
|
295
|
+
const isEventHandler = name.match(/^on[A-Z]/);
|
|
296
|
+
const isDescriptiveLong = name.length > 15;
|
|
297
|
+
const isReactHook = name.match(/^use[A-Z]/);
|
|
298
|
+
const isHelperPattern = name.match(/^(to|from|with|without|for|as|into)\w+/) ||
|
|
299
|
+
name.match(/^\w+(To|From|With|Without|For|As|Into)\w*$/); // xForY, xToY patterns
|
|
300
|
+
const isUtilityName = ['cn', 'proxy', 'sitemap', 'robots', 'gtag'].includes(name);
|
|
301
|
+
const isLanguageKeyword = ['constructor', 'toString', 'valueOf', 'toJSON'].includes(name);
|
|
302
|
+
const isFrameworkPattern = name.match(/^(goto|fill|click|select|submit|wait|expect)\w*/); // Page Object Model, test framework patterns
|
|
303
|
+
|
|
304
|
+
// Descriptive patterns: countX, totalX, etc.
|
|
305
|
+
const isDescriptivePattern = name.match(/^(default|total|count|sum|avg|max|min|initial|current|previous|next)\w+/) ||
|
|
306
|
+
name.match(/\w+(Count|Total|Sum|Average|List|Map|Set|Config|Settings|Options|Props|Data|Info|Details|State|Status|Response|Result)$/);
|
|
307
|
+
|
|
308
|
+
// Count capital letters for compound detection
|
|
309
|
+
const capitalCount = (name.match(/[A-Z]/g) || []).length;
|
|
310
|
+
const isCompoundWord = capitalCount >= 3; // daysSinceLastCommit has 4 capitals
|
|
311
|
+
|
|
312
|
+
if (!hasActionVerb && !isFactoryPattern && !isEventHandler && !isDescriptiveLong && !isReactHook && !isHelperPattern && !isUtilityName && !isDescriptivePattern && !isCompoundWord && !isLanguageKeyword && !isFrameworkPattern) {
|
|
313
|
+
issues.push({
|
|
314
|
+
file,
|
|
315
|
+
line,
|
|
316
|
+
type: 'unclear',
|
|
317
|
+
identifier: name,
|
|
318
|
+
severity: adjustSeverity('info', context, 'unclear'),
|
|
319
|
+
suggestion: `Function '${name}' should start with an action verb (get, set, create, etc.)`,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return issues;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Extract all identifiers from a destructuring pattern
|
|
332
|
+
*/
|
|
333
|
+
function extractIdentifiersFromPattern(
|
|
334
|
+
pattern: TSESTree.ObjectPattern | TSESTree.ArrayPattern,
|
|
335
|
+
scopeTracker: ScopeTracker,
|
|
336
|
+
isParameter: boolean,
|
|
337
|
+
ancestors?: TSESTree.Node[]
|
|
338
|
+
): void {
|
|
339
|
+
if (pattern.type === 'ObjectPattern') {
|
|
340
|
+
for (const prop of pattern.properties) {
|
|
341
|
+
if (prop.type === 'Property' && prop.value.type === 'Identifier') {
|
|
342
|
+
scopeTracker.declareVariable(
|
|
343
|
+
prop.value.name,
|
|
344
|
+
prop.value,
|
|
345
|
+
getLineNumber(prop.value),
|
|
346
|
+
{
|
|
347
|
+
isParameter,
|
|
348
|
+
isDestructured: true,
|
|
349
|
+
}
|
|
350
|
+
);
|
|
351
|
+
} else if (prop.type === 'RestElement' && prop.argument.type === 'Identifier') {
|
|
352
|
+
scopeTracker.declareVariable(
|
|
353
|
+
prop.argument.name,
|
|
354
|
+
prop.argument,
|
|
355
|
+
getLineNumber(prop.argument),
|
|
356
|
+
{
|
|
357
|
+
isParameter,
|
|
358
|
+
isDestructured: true,
|
|
359
|
+
}
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
} else if (pattern.type === 'ArrayPattern') {
|
|
364
|
+
for (const element of pattern.elements) {
|
|
365
|
+
if (element && element.type === 'Identifier') {
|
|
366
|
+
scopeTracker.declareVariable(
|
|
367
|
+
element.name,
|
|
368
|
+
element,
|
|
369
|
+
getLineNumber(element),
|
|
370
|
+
{
|
|
371
|
+
isParameter,
|
|
372
|
+
isDestructured: true,
|
|
373
|
+
}
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { analyzeConsistency } from './analyzer';
|
|
2
|
-
export {
|
|
2
|
+
export { analyzeNamingAST } from './analyzers/naming-ast';
|
|
3
|
+
export { analyzeNaming, detectNamingConventions } from './analyzers/naming'; // Legacy regex version
|
|
3
4
|
export { analyzePatterns } from './analyzers/patterns';
|
|
4
5
|
export type {
|
|
5
6
|
ConsistencyOptions,
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import { parse, TSESTree } from '@typescript-eslint/typescript-estree';
|
|
2
|
+
import { readFileSync } from 'fs';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Parse a file into an AST
|
|
6
|
+
*/
|
|
7
|
+
export function parseFile(filePath: string, content?: string): TSESTree.Program | null {
|
|
8
|
+
try {
|
|
9
|
+
const code = content ?? readFileSync(filePath, 'utf-8');
|
|
10
|
+
const isTypeScript = filePath.match(/\.tsx?$/);
|
|
11
|
+
|
|
12
|
+
return parse(code, {
|
|
13
|
+
jsx: filePath.match(/\.[jt]sx$/i) !== null,
|
|
14
|
+
loc: true,
|
|
15
|
+
range: true,
|
|
16
|
+
comment: false,
|
|
17
|
+
tokens: false,
|
|
18
|
+
// Relaxed parsing for JavaScript files
|
|
19
|
+
sourceType: 'module',
|
|
20
|
+
ecmaVersion: 'latest',
|
|
21
|
+
// Only use TypeScript parser features for .ts/.tsx files
|
|
22
|
+
filePath: isTypeScript ? filePath : undefined,
|
|
23
|
+
});
|
|
24
|
+
} catch (error) {
|
|
25
|
+
// If TypeScript parsing fails, return null (file might have syntax errors)
|
|
26
|
+
console.warn(`Failed to parse ${filePath}:`, error instanceof Error ? error.message : error);
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Traverse AST nodes with a visitor pattern
|
|
33
|
+
*/
|
|
34
|
+
export function traverseAST(
|
|
35
|
+
node: TSESTree.Node,
|
|
36
|
+
visitor: {
|
|
37
|
+
enter?: (node: TSESTree.Node, parent: TSESTree.Node | null) => void;
|
|
38
|
+
leave?: (node: TSESTree.Node, parent: TSESTree.Node | null) => void;
|
|
39
|
+
},
|
|
40
|
+
parent: TSESTree.Node | null = null
|
|
41
|
+
): void {
|
|
42
|
+
if (!node) return;
|
|
43
|
+
|
|
44
|
+
visitor.enter?.(node, parent);
|
|
45
|
+
|
|
46
|
+
// Visit children
|
|
47
|
+
for (const key of Object.keys(node)) {
|
|
48
|
+
const value = (node as any)[key];
|
|
49
|
+
|
|
50
|
+
if (Array.isArray(value)) {
|
|
51
|
+
for (const child of value) {
|
|
52
|
+
if (child && typeof child === 'object' && 'type' in child) {
|
|
53
|
+
traverseAST(child as TSESTree.Node, visitor, node);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
} else if (value && typeof value === 'object' && 'type' in value) {
|
|
57
|
+
traverseAST(value as TSESTree.Node, visitor, node);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
visitor.leave?.(node, parent);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Check if a node is within a specific type of ancestor
|
|
66
|
+
*/
|
|
67
|
+
export function hasAncestor(
|
|
68
|
+
node: TSESTree.Node,
|
|
69
|
+
ancestorTypes: string[],
|
|
70
|
+
ancestors: TSESTree.Node[]
|
|
71
|
+
): boolean {
|
|
72
|
+
return ancestors.some(ancestor => ancestorTypes.includes(ancestor.type));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get the name of an identifier or pattern
|
|
77
|
+
*/
|
|
78
|
+
export function getIdentifierName(node: TSESTree.Node): string | null {
|
|
79
|
+
if (node.type === 'Identifier') {
|
|
80
|
+
return node.name;
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Check if a node is a loop
|
|
87
|
+
*/
|
|
88
|
+
export function isLoopStatement(node: TSESTree.Node): boolean {
|
|
89
|
+
return [
|
|
90
|
+
'ForStatement',
|
|
91
|
+
'ForInStatement',
|
|
92
|
+
'ForOfStatement',
|
|
93
|
+
'WhileStatement',
|
|
94
|
+
'DoWhileStatement',
|
|
95
|
+
].includes(node.type);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Check if a node is an arrow function or callback
|
|
100
|
+
*/
|
|
101
|
+
export function isCallback(node: TSESTree.Node): boolean {
|
|
102
|
+
if (node.type === 'ArrowFunctionExpression') {
|
|
103
|
+
return true;
|
|
104
|
+
}
|
|
105
|
+
if (node.type === 'FunctionExpression') {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Extract function/method name from various declaration types
|
|
113
|
+
*/
|
|
114
|
+
export function getFunctionName(node: TSESTree.Node): string | null {
|
|
115
|
+
switch (node.type) {
|
|
116
|
+
case 'FunctionDeclaration':
|
|
117
|
+
return node.id?.name ?? null;
|
|
118
|
+
case 'FunctionExpression':
|
|
119
|
+
return node.id?.name ?? null;
|
|
120
|
+
case 'ArrowFunctionExpression':
|
|
121
|
+
return null; // Arrow functions don't have names directly
|
|
122
|
+
case 'MethodDefinition':
|
|
123
|
+
if (node.key.type === 'Identifier') {
|
|
124
|
+
return node.key.name;
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
default:
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Check if a variable declaration is in a destructuring pattern
|
|
134
|
+
*/
|
|
135
|
+
export function isInDestructuring(node: TSESTree.Node): boolean {
|
|
136
|
+
if (!node) return false;
|
|
137
|
+
|
|
138
|
+
return node.type === 'ObjectPattern' || node.type === 'ArrayPattern';
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Get the line number from a node
|
|
143
|
+
*/
|
|
144
|
+
export function getLineNumber(node: TSESTree.Node): number {
|
|
145
|
+
return node.loc?.start.line ?? 0;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Check if a node represents a coverage metric context
|
|
150
|
+
*/
|
|
151
|
+
export function isCoverageContext(node: TSESTree.Node, ancestors: TSESTree.Node[]): boolean {
|
|
152
|
+
// Check if any ancestor or the node itself references coverage-related properties
|
|
153
|
+
const coveragePatterns = /coverage|summary|metrics|pct|percent|statements|branches|functions|lines/i;
|
|
154
|
+
|
|
155
|
+
// Check variable name
|
|
156
|
+
if (node.type === 'Identifier' && coveragePatterns.test(node.name)) {
|
|
157
|
+
return true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Check if it's a property of something coverage-related
|
|
161
|
+
for (const ancestor of ancestors.slice(-3)) { // Check last 3 ancestors
|
|
162
|
+
if (ancestor.type === 'MemberExpression') {
|
|
163
|
+
const memberExpr = ancestor as TSESTree.MemberExpression;
|
|
164
|
+
if (memberExpr.object.type === 'Identifier' && coveragePatterns.test(memberExpr.object.name)) {
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
if (ancestor.type === 'ObjectPattern' || ancestor.type === 'ObjectExpression') {
|
|
169
|
+
// Check if parent variable has coverage-related name
|
|
170
|
+
const parent = ancestors[ancestors.indexOf(ancestor) - 1];
|
|
171
|
+
if (parent?.type === 'VariableDeclarator') {
|
|
172
|
+
const varDecl = parent as TSESTree.VariableDeclarator;
|
|
173
|
+
if (varDecl.id.type === 'Identifier' && coveragePatterns.test(varDecl.id.name)) {
|
|
174
|
+
return true;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
return false;
|
|
181
|
+
}
|