@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,4 +1,5 @@
1
1
  import { TSESTree } from '@typescript-eslint/typescript-estree';
2
+ import { Severity } from '@aiready/core';
2
3
  import type { NamingIssue } from '../types';
3
4
  import {
4
5
  parseFile,
@@ -8,369 +9,241 @@ import {
8
9
  isCoverageContext,
9
10
  isLoopStatement,
10
11
  } from '../utils/ast-parser';
11
- import { ScopeTracker } from '../utils/scope-tracker';
12
12
  import {
13
13
  buildCodeContext,
14
- adjustSeverity,
15
14
  isAcceptableInContext,
16
- calculateComplexity,
15
+ adjustSeverity,
17
16
  } from '../utils/context-detector';
18
- import { loadNamingConfig } from '../utils/config-loader';
19
17
 
20
18
  /**
21
- * AST-based naming analyzer
22
- * Only supports TypeScript/JavaScript files (.ts, .tsx, .js, .jsx)
19
+ * Advanced naming analyzer using TypeScript AST
23
20
  */
24
21
  export async function analyzeNamingAST(
25
- files: string[]
22
+ filePaths: string[]
26
23
  ): Promise<NamingIssue[]> {
27
- const issues: NamingIssue[] = [];
24
+ const allIssues: NamingIssue[] = [];
28
25
 
29
- // Load and merge configuration
30
- const { allAbbreviations, allShortWords, disabledChecks } =
31
- await loadNamingConfig(files);
32
-
33
- // Filter to only JS/TS files that the TypeScript parser can handle
34
- const supportedFiles = files.filter((file) =>
35
- /\.(js|jsx|ts|tsx)$/i.test(file)
36
- );
37
-
38
- for (const file of supportedFiles) {
26
+ for (const filePath of filePaths) {
39
27
  try {
40
- const ast = parseFile(file);
41
- if (!ast) continue; // Skip files that fail to parse
42
-
43
- const fileIssues = analyzeFileNamingAST(
44
- file,
45
- ast,
46
- allAbbreviations,
47
- allShortWords,
48
- disabledChecks
49
- );
50
- issues.push(...fileIssues);
51
- } catch (error) {
52
- console.warn(`Skipping ${file} due to parse error:`, error);
28
+ const ast = parseFile(filePath);
29
+ if (!ast) continue;
30
+
31
+ const context = buildCodeContext(filePath, ast);
32
+ const issues = analyzeIdentifiers(ast, filePath, context);
33
+ allIssues.push(...issues);
34
+ } catch (err) {
35
+ void err;
53
36
  }
54
37
  }
55
38
 
56
- return issues;
39
+ return allIssues;
57
40
  }
58
41
 
59
- function analyzeFileNamingAST(
60
- file: string,
42
+ /**
43
+ * Traverse AST and find naming issues in identifiers
44
+ */
45
+ function analyzeIdentifiers(
61
46
  ast: TSESTree.Program,
62
- allAbbreviations: Set<string>,
63
- allShortWords: Set<string>,
64
- disabledChecks: Set<string>
47
+ filePath: string,
48
+ context: any
65
49
  ): NamingIssue[] {
66
50
  const issues: NamingIssue[] = [];
67
- const scopeTracker = new ScopeTracker(ast);
68
- const context = buildCodeContext(file, ast);
69
- const ancestors: TSESTree.Node[] = [];
51
+ const scopeTracker = new ScopeTracker();
70
52
 
71
- // First pass: Build scope tree and track all variables
72
53
  traverseAST(ast, {
73
- enter: (node, parent) => {
74
- ancestors.push(node);
54
+ enter: (node) => {
55
+ // 1. Variable Declarations
56
+ if (node.type === 'VariableDeclarator' && node.id.type === 'Identifier') {
57
+ const isParameter = false;
58
+ const isLoopVariable = isLoopStatement(node.parent?.parent);
59
+ scopeTracker.declareVariable(
60
+ node.id.name,
61
+ node.id,
62
+ getLineNumber(node.id),
63
+ { isParameter, isLoopVariable }
64
+ );
65
+ }
75
66
 
76
- // Enter scopes
67
+ // 2. Function Parameters
77
68
  if (
78
69
  node.type === 'FunctionDeclaration' ||
79
70
  node.type === 'FunctionExpression' ||
80
71
  node.type === 'ArrowFunctionExpression'
81
72
  ) {
82
- scopeTracker.enterScope('function', node);
83
-
84
- // Track parameters
85
- if ('params' in node) {
86
- for (const param of node.params) {
87
- if (param.type === 'Identifier') {
88
- scopeTracker.declareVariable(
89
- param.name,
90
- param,
91
- getLineNumber(param),
92
- {
93
- isParameter: true,
94
- }
95
- );
96
- } else if (
97
- param.type === 'ObjectPattern' ||
98
- param.type === 'ArrayPattern'
99
- ) {
100
- // Handle destructured parameters
101
- extractIdentifiersFromPattern(param, scopeTracker, true);
102
- }
103
- }
104
- }
105
- } else if (node.type === 'BlockStatement') {
106
- scopeTracker.enterScope('block', node);
107
- } else if (isLoopStatement(node)) {
108
- scopeTracker.enterScope('loop', node);
109
- } else if (node.type === 'ClassDeclaration') {
110
- scopeTracker.enterScope('class', node);
111
- }
112
-
113
- // Track variable declarations
114
- if (node.type === 'VariableDeclarator') {
115
- if (node.id.type === 'Identifier') {
116
- void isCoverageContext(node, ancestors);
117
- scopeTracker.declareVariable(
118
- node.id.name,
119
- node.id,
120
- getLineNumber(node.id),
121
- {
122
- type:
123
- 'typeAnnotation' in node.id
124
- ? (node.id as any).typeAnnotation
125
- : null,
126
- isDestructured: false,
127
- isLoopVariable: scopeTracker.getCurrentScopeType() === 'loop',
128
- }
129
- );
130
- } else if (
131
- node.id.type === 'ObjectPattern' ||
132
- node.id.type === 'ArrayPattern'
133
- ) {
134
- extractIdentifiersFromPattern(
135
- node.id,
136
- scopeTracker,
137
- false,
138
- ancestors
139
- );
140
- }
141
- }
142
-
143
- // Track references
144
- if (node.type === 'Identifier' && parent) {
145
- // Only count as reference if it's not a declaration
146
- if (parent.type !== 'VariableDeclarator' || parent.id !== node) {
147
- if (parent.type !== 'FunctionDeclaration' || parent.id !== node) {
148
- scopeTracker.addReference(node.name, node);
73
+ node.params.forEach((param) => {
74
+ if (param.type === 'Identifier') {
75
+ scopeTracker.declareVariable(
76
+ param.name,
77
+ param,
78
+ getLineNumber(param),
79
+ { isParameter: true }
80
+ );
81
+ } else if (param.type === 'ObjectPattern') {
82
+ // Handle destructured parameters: { id, name }
83
+ extractDestructuredIdentifiers(param, true, scopeTracker);
149
84
  }
150
- }
85
+ });
151
86
  }
152
- },
153
- leave: (node) => {
154
- ancestors.pop();
155
87
 
156
- // Exit scopes
88
+ // 3. Class/Interface/Type names
157
89
  if (
158
- node.type === 'FunctionDeclaration' ||
159
- node.type === 'FunctionExpression' ||
160
- node.type === 'ArrowFunctionExpression' ||
161
- node.type === 'BlockStatement' ||
162
- isLoopStatement(node) ||
163
- node.type === 'ClassDeclaration'
90
+ (node.type === 'ClassDeclaration' ||
91
+ node.type === 'TSInterfaceDeclaration' ||
92
+ node.type === 'TSTypeAliasDeclaration') &&
93
+ node.id
164
94
  ) {
165
- scopeTracker.exitScope();
95
+ checkNamingConvention(
96
+ node.id.name,
97
+ 'PascalCase',
98
+ node.id,
99
+ filePath,
100
+ issues,
101
+ context
102
+ );
166
103
  }
167
104
  },
168
105
  });
169
106
 
170
- // Second pass: Analyze all variables
171
- const allVariables = scopeTracker.getAllVariables();
172
-
173
- for (const varInfo of allVariables) {
174
- const name = varInfo.name;
175
- const line = varInfo.declarationLine;
176
-
177
- // Skip if checks are disabled
178
- if (disabledChecks.has('single-letter') && name.length === 1) continue;
179
- if (disabledChecks.has('abbreviation') && name.length <= 3) continue;
180
-
181
- // Check coverage context
182
- const isInCoverage =
183
- ['s', 'b', 'f', 'l'].includes(name) && varInfo.isDestructured;
184
- if (isInCoverage) continue;
185
-
186
- // Check if acceptable in context
187
- const functionComplexity =
188
- varInfo.node.type === 'Identifier' && 'parent' in varInfo.node
189
- ? calculateComplexity(varInfo.node as any)
190
- : context.complexity;
191
-
192
- if (
193
- isAcceptableInContext(name, context, {
194
- isLoopVariable: varInfo.isLoopVariable || allAbbreviations.has(name),
195
- isParameter: varInfo.isParameter,
196
- isDestructured: varInfo.isDestructured,
197
- complexity: functionComplexity,
198
- })
199
- ) {
200
- continue;
201
- }
202
-
203
- // Single letter check
204
- if (
205
- name.length === 1 &&
206
- !allAbbreviations.has(name) &&
207
- !allShortWords.has(name)
208
- ) {
209
- // Check if short-lived
210
- const isShortLived = scopeTracker.isShortLived(varInfo, 5);
211
- if (!isShortLived) {
212
- issues.push({
213
- file,
214
- line,
215
- type: 'poor-naming',
216
- identifier: name,
217
- severity: adjustSeverity('minor', context, 'poor-naming'),
218
- suggestion: `Use descriptive variable name instead of single letter '${name}'`,
219
- });
220
- }
221
- continue;
222
- }
223
-
224
- // Abbreviation check (2-3 letters)
225
- if (name.length >= 2 && name.length <= 3) {
226
- if (
227
- !allShortWords.has(name.toLowerCase()) &&
228
- !allAbbreviations.has(name.toLowerCase())
229
- ) {
230
- // Check if short-lived for abbreviations too
231
- const isShortLived = scopeTracker.isShortLived(varInfo, 5);
232
- if (!isShortLived) {
233
- issues.push({
234
- file,
235
- line,
236
- type: 'abbreviation',
237
- identifier: name,
238
- severity: adjustSeverity('info', context, 'abbreviation'),
239
- suggestion: `Consider using full word instead of abbreviation '${name}'`,
240
- });
241
- }
242
- }
243
- continue;
244
- }
245
-
246
- // Snake_case check for TypeScript/JavaScript
247
- if (
248
- !disabledChecks.has('convention-mix') &&
249
- file.match(/\.(ts|tsx|js|jsx)$/)
250
- ) {
251
- if (
252
- name.includes('_') &&
253
- !name.startsWith('_') &&
254
- name.toLowerCase() === name
255
- ) {
256
- const camelCase = name.replace(/_([a-z])/g, (_, letter) =>
257
- letter.toUpperCase()
258
- );
259
- issues.push({
260
- file,
261
- line,
262
- type: 'convention-mix',
263
- identifier: name,
264
- severity: adjustSeverity('minor', context, 'convention-mix'),
265
- suggestion: `Use camelCase '${camelCase}' instead of snake_case in TypeScript/JavaScript`,
266
- });
267
- }
268
- }
107
+ // Check all collected variables
108
+ for (const varInfo of scopeTracker.getVariables()) {
109
+ checkVariableNaming(varInfo, filePath, issues, context);
269
110
  }
270
111
 
271
- // Third pass: Analyze function names
272
- if (!disabledChecks.has('unclear')) {
273
- traverseAST(ast, {
274
- enter: (node) => {
275
- if (
276
- node.type === 'FunctionDeclaration' ||
277
- node.type === 'MethodDefinition'
278
- ) {
279
- const name = getFunctionName(node);
280
- if (!name) return;
281
-
282
- const line = getLineNumber(node);
112
+ return issues;
113
+ }
283
114
 
284
- // Skip entry points
285
- if (['main', 'init', 'setup', 'bootstrap'].includes(name)) return;
115
+ /**
116
+ * Check if a name follows a specific convention
117
+ */
118
+ function checkNamingConvention(
119
+ name: string,
120
+ convention: 'camelCase' | 'PascalCase' | 'UPPER_CASE',
121
+ node: TSESTree.Node,
122
+ file: string,
123
+ issues: NamingIssue[],
124
+ context: any
125
+ ) {
126
+ let isValid = true;
127
+ if (convention === 'PascalCase') {
128
+ isValid = /^[A-Z][a-zA-Z0-9]*$/.test(name);
129
+ } else if (convention === 'camelCase') {
130
+ isValid = /^[a-z][a-zA-Z0-9]*$/.test(name);
131
+ } else if (convention === 'UPPER_CASE') {
132
+ isValid = /^[A-Z][A-Z0-9_]*$/.test(name);
133
+ }
286
134
 
287
- // Check for action verbs and patterns
288
- const hasActionVerb = name.match(
289
- /^(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)/
290
- );
135
+ if (!isValid) {
136
+ const severity = adjustSeverity(Severity.Info, context, 'convention-mix');
137
+ issues.push({
138
+ file,
139
+ line: getLineNumber(node),
140
+ type: 'convention-mix',
141
+ identifier: name,
142
+ severity,
143
+ suggestion: `Follow ${convention} for this identifier`,
144
+ });
145
+ }
146
+ }
291
147
 
292
- const isFactoryPattern = name.match(
293
- /(Factory|Builder|Creator|Generator|Provider|Adapter|Mock)$/
294
- );
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 =
299
- name.match(/^(to|from|with|without|for|as|into)\w+/) ||
300
- name.match(/^\w+(To|From|With|Without|For|As|Into)\w*$/); // xForY, xToY patterns
301
- const isUtilityName = [
302
- 'cn',
303
- 'proxy',
304
- 'sitemap',
305
- 'robots',
306
- 'gtag',
307
- ].includes(name);
308
- const isLanguageKeyword = [
309
- 'constructor',
310
- 'toString',
311
- 'valueOf',
312
- 'toJSON',
313
- ].includes(name);
314
- const isFrameworkPattern = name.match(
315
- /^(goto|fill|click|select|submit|wait|expect)\w*/
316
- ); // Page Object Model, test framework patterns
148
+ /**
149
+ * Advanced variable naming checks
150
+ */
151
+ function checkVariableNaming(
152
+ varInfo: any,
153
+ file: string,
154
+ issues: NamingIssue[],
155
+ context: any
156
+ ) {
157
+ const { name, node, line, options } = varInfo;
158
+
159
+ // Skip very common small names if they are in acceptable context
160
+ if (isAcceptableInContext(name, context, options)) {
161
+ return;
162
+ }
317
163
 
318
- // Descriptive patterns: countX, totalX, etc.
319
- const isDescriptivePattern =
320
- name.match(
321
- /^(default|total|count|sum|avg|max|min|initial|current|previous|next)\w+/
322
- ) ||
323
- name.match(
324
- /\w+(Count|Total|Sum|Average|List|Map|Set|Config|Settings|Options|Props|Data|Info|Details|State|Status|Response|Result)$/
325
- );
164
+ // 1. Single letter names
165
+ if (name.length === 1 && !options.isLoopVariable) {
166
+ const severity = adjustSeverity(Severity.Minor, context, 'poor-naming');
167
+ issues.push({
168
+ file,
169
+ line,
170
+ type: 'poor-naming',
171
+ identifier: name,
172
+ severity,
173
+ suggestion: 'Use a more descriptive name than a single letter',
174
+ });
175
+ }
326
176
 
327
- // Count capital letters for compound detection
328
- const capitalCount = (name.match(/[A-Z]/g) || []).length;
329
- const isCompoundWord = capitalCount >= 3; // daysSinceLastCommit has 4 capitals
177
+ // 2. Vague names
178
+ const vagueNames = [
179
+ 'data',
180
+ 'info',
181
+ 'item',
182
+ 'obj',
183
+ 'val',
184
+ 'tmp',
185
+ 'temp',
186
+ 'thing',
187
+ 'stuff',
188
+ ];
189
+ if (vagueNames.includes(name.toLowerCase())) {
190
+ const severity = adjustSeverity(Severity.Minor, context, 'poor-naming');
191
+ issues.push({
192
+ file,
193
+ line,
194
+ type: 'poor-naming',
195
+ identifier: name,
196
+ severity,
197
+ suggestion: `Avoid vague names like '${name}'. What does this data represent?`,
198
+ });
199
+ }
330
200
 
331
- if (
332
- !hasActionVerb &&
333
- !isFactoryPattern &&
334
- !isEventHandler &&
335
- !isDescriptiveLong &&
336
- !isReactHook &&
337
- !isHelperPattern &&
338
- !isUtilityName &&
339
- !isDescriptivePattern &&
340
- !isCompoundWord &&
341
- !isLanguageKeyword &&
342
- !isFrameworkPattern
343
- ) {
344
- issues.push({
345
- file,
346
- line,
347
- type: 'unclear',
348
- identifier: name,
349
- severity: adjustSeverity('info', context, 'unclear'),
350
- suggestion: `Function '${name}' should start with an action verb (get, set, create, etc.)`,
351
- });
352
- }
353
- }
354
- },
201
+ // 3. Abbreviations
202
+ if (
203
+ name.length > 1 &&
204
+ name.length <= 3 &&
205
+ !options.isLoopVariable &&
206
+ !isCommonAbbreviation(name)
207
+ ) {
208
+ const severity = adjustSeverity(Severity.Info, context, 'abbreviation');
209
+ issues.push({
210
+ file,
211
+ line,
212
+ type: 'abbreviation',
213
+ identifier: name,
214
+ severity,
215
+ suggestion: 'Avoid non-standard abbreviations',
355
216
  });
356
217
  }
218
+ }
357
219
 
358
- return issues;
220
+ function isCommonAbbreviation(name: string): boolean {
221
+ const common = ['id', 'db', 'fs', 'os', 'ip', 'ui', 'ux', 'api', 'env', 'url'];
222
+ return common.includes(name.toLowerCase());
359
223
  }
360
224
 
361
225
  /**
362
- * Extract all identifiers from a destructuring pattern
226
+ * Simple scope-aware variable tracker
363
227
  */
364
- function extractIdentifiersFromPattern(
365
- pattern: TSESTree.ObjectPattern | TSESTree.ArrayPattern,
366
- scopeTracker: ScopeTracker,
228
+ class ScopeTracker {
229
+ private variables: any[] = [];
230
+
231
+ declareVariable(name: string, node: TSESTree.Node, line: number, options = {}) {
232
+ this.variables.push({ name, node, line, options });
233
+ }
234
+
235
+ getVariables() {
236
+ return this.variables;
237
+ }
238
+ }
239
+
240
+ function extractDestructuredIdentifiers(
241
+ node: TSESTree.ObjectPattern | TSESTree.ArrayPattern,
367
242
  isParameter: boolean,
368
- ancestors?: TSESTree.Node[]
369
- ): void {
370
- // Ancestors parameter is accepted for future use; reference it to avoid lint warnings
371
- void ancestors;
372
- if (pattern.type === 'ObjectPattern') {
373
- for (const prop of pattern.properties) {
243
+ scopeTracker: ScopeTracker
244
+ ) {
245
+ if (node.type === 'ObjectPattern') {
246
+ node.properties.forEach((prop) => {
374
247
  if (prop.type === 'Property' && prop.value.type === 'Identifier') {
375
248
  scopeTracker.declareVariable(
376
249
  prop.value.name,
@@ -381,24 +254,11 @@ function extractIdentifiersFromPattern(
381
254
  isDestructured: true,
382
255
  }
383
256
  );
384
- } else if (
385
- prop.type === 'RestElement' &&
386
- prop.argument.type === 'Identifier'
387
- ) {
388
- scopeTracker.declareVariable(
389
- prop.argument.name,
390
- prop.argument,
391
- getLineNumber(prop.argument),
392
- {
393
- isParameter,
394
- isDestructured: true,
395
- }
396
- );
397
257
  }
398
- }
399
- } else if (pattern.type === 'ArrayPattern') {
400
- for (const element of pattern.elements) {
401
- if (element && element.type === 'Identifier') {
258
+ });
259
+ } else if (node.type === 'ArrayPattern') {
260
+ for (const element of node.elements) {
261
+ if (element?.type === 'Identifier') {
402
262
  scopeTracker.declareVariable(
403
263
  element.name,
404
264
  element,