@aiready/consistency 0.5.0 → 0.6.0

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.
@@ -0,0 +1,375 @@
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)/);
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
+ const isUtilityName = ['cn', 'proxy', 'sitemap', 'robots', 'gtag'].includes(name);
300
+
301
+ // Descriptive patterns: countX, totalX, etc.
302
+ const isDescriptivePattern = name.match(/^(default|total|count|sum|avg|max|min|initial|current|previous|next)\w+/) ||
303
+ name.match(/\w+(Count|Total|Sum|Average|List|Map|Set|Config|Settings|Options|Props|Data|Info|Details|State|Status|Response|Result)$/);
304
+
305
+ // Count capital letters for compound detection
306
+ const capitalCount = (name.match(/[A-Z]/g) || []).length;
307
+ const isCompoundWord = capitalCount >= 3; // daysSinceLastCommit has 4 capitals
308
+
309
+ if (!hasActionVerb && !isFactoryPattern && !isEventHandler && !isDescriptiveLong && !isReactHook && !isHelperPattern && !isUtilityName && !isDescriptivePattern && !isCompoundWord) {
310
+ issues.push({
311
+ file,
312
+ line,
313
+ type: 'unclear',
314
+ identifier: name,
315
+ severity: adjustSeverity('info', context, 'unclear'),
316
+ suggestion: `Function '${name}' should start with an action verb (get, set, create, etc.)`,
317
+ });
318
+ }
319
+ }
320
+ },
321
+ });
322
+ }
323
+
324
+ return issues;
325
+ }
326
+
327
+ /**
328
+ * Extract all identifiers from a destructuring pattern
329
+ */
330
+ function extractIdentifiersFromPattern(
331
+ pattern: TSESTree.ObjectPattern | TSESTree.ArrayPattern,
332
+ scopeTracker: ScopeTracker,
333
+ isParameter: boolean,
334
+ ancestors?: TSESTree.Node[]
335
+ ): void {
336
+ if (pattern.type === 'ObjectPattern') {
337
+ for (const prop of pattern.properties) {
338
+ if (prop.type === 'Property' && prop.value.type === 'Identifier') {
339
+ scopeTracker.declareVariable(
340
+ prop.value.name,
341
+ prop.value,
342
+ getLineNumber(prop.value),
343
+ {
344
+ isParameter,
345
+ isDestructured: true,
346
+ }
347
+ );
348
+ } else if (prop.type === 'RestElement' && prop.argument.type === 'Identifier') {
349
+ scopeTracker.declareVariable(
350
+ prop.argument.name,
351
+ prop.argument,
352
+ getLineNumber(prop.argument),
353
+ {
354
+ isParameter,
355
+ isDestructured: true,
356
+ }
357
+ );
358
+ }
359
+ }
360
+ } else if (pattern.type === 'ArrayPattern') {
361
+ for (const element of pattern.elements) {
362
+ if (element && element.type === 'Identifier') {
363
+ scopeTracker.declareVariable(
364
+ element.name,
365
+ element,
366
+ getLineNumber(element),
367
+ {
368
+ isParameter,
369
+ isDestructured: true,
370
+ }
371
+ );
372
+ }
373
+ }
374
+ }
375
+ }
package/src/index.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export { analyzeConsistency } from './analyzer';
2
- export { analyzeNaming, detectNamingConventions } from './analyzers/naming';
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
+ }