@sun-asterisk/sunlint 1.3.16 → 1.3.17
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/config/rule-analysis-strategies.js +3 -3
- package/config/rules/enhanced-rules-registry.json +40 -20
- package/core/cli-action-handler.js +2 -2
- package/core/config-merger.js +28 -6
- package/core/constants/defaults.js +1 -1
- package/core/file-targeting-service.js +72 -4
- package/core/output-service.js +21 -4
- package/engines/heuristic-engine.js +5 -0
- package/package.json +1 -1
- package/rules/common/C002_no_duplicate_code/README.md +115 -0
- package/rules/common/C002_no_duplicate_code/analyzer.js +615 -219
- package/rules/common/C002_no_duplicate_code/test-cases/api-handlers.ts +64 -0
- package/rules/common/C002_no_duplicate_code/test-cases/data-processor.ts +46 -0
- package/rules/common/C002_no_duplicate_code/test-cases/good-example.tsx +40 -0
- package/rules/common/C002_no_duplicate_code/test-cases/product-service.ts +57 -0
- package/rules/common/C002_no_duplicate_code/test-cases/user-service.ts +49 -0
- package/rules/common/C008/analyzer.js +40 -0
- package/rules/common/C008/config.json +20 -0
- package/rules/common/C008/ts-morph-analyzer.js +1067 -0
- package/rules/common/C018_no_throw_generic_error/analyzer.js +1 -1
- package/rules/common/C018_no_throw_generic_error/symbol-based-analyzer.js +27 -3
- package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +504 -162
- package/rules/common/C029_catch_block_logging/analyzer.js +499 -89
- package/rules/common/C033_separate_service_repository/README.md +131 -20
- package/rules/common/C033_separate_service_repository/analyzer.js +1 -1
- package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +417 -274
- package/rules/common/C041_no_sensitive_hardcode/analyzer.js +144 -254
- package/rules/common/C041_no_sensitive_hardcode/config.json +50 -0
- package/rules/common/C041_no_sensitive_hardcode/symbol-based-analyzer.js +575 -0
- package/rules/common/C067_no_hardcoded_config/analyzer.js +17 -16
- package/rules/common/C067_no_hardcoded_config/symbol-based-analyzer.js +3477 -659
- package/rules/docs/C002_no_duplicate_code.md +276 -11
- package/rules/index.js +5 -1
- package/rules/security/S006_no_plaintext_recovery_codes/analyzer.js +266 -88
- package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +805 -0
- package/rules/security/S010_no_insecure_encryption/README.md +78 -0
- package/rules/security/S010_no_insecure_encryption/analyzer.js +463 -398
- package/rules/security/S013_tls_enforcement/README.md +51 -0
- package/rules/security/S013_tls_enforcement/analyzer.js +99 -0
- package/rules/security/S013_tls_enforcement/config.json +41 -0
- package/rules/security/S013_tls_enforcement/symbol-based-analyzer.js +339 -0
- package/rules/security/S014_tls_version_enforcement/README.md +354 -0
- package/rules/security/S014_tls_version_enforcement/analyzer.js +118 -0
- package/rules/security/S014_tls_version_enforcement/config.json +56 -0
- package/rules/security/S014_tls_version_enforcement/symbol-based-analyzer.js +194 -0
- package/rules/security/S055_content_type_validation/analyzer.js +121 -279
- package/rules/security/S055_content_type_validation/symbol-based-analyzer.js +346 -0
- package/rules/tests/C002_no_duplicate_code.test.js +111 -22
- package/rules/common/C029_catch_block_logging/analyzer-smart-pipeline.js +0 -755
- package/rules/common/C041_no_sensitive_hardcode/ast-analyzer.js +0 -296
|
@@ -0,0 +1,1067 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C008 ts-morph Analyzer - Minimize Variable Scope (Declare Near Usage)
|
|
3
|
+
*
|
|
4
|
+
* Uses ts-morph AST analysis to detect variables declared far from first usage.
|
|
5
|
+
* 100% accurate scope detection with proper handling of:
|
|
6
|
+
* - Function boundaries
|
|
7
|
+
* - Nested scopes
|
|
8
|
+
* - React hooks (useState, useEffect, etc.)
|
|
9
|
+
* - CSS-in-JS (keyframes, styled, css)
|
|
10
|
+
* - Ternary expressions
|
|
11
|
+
* - Module-level constants
|
|
12
|
+
* - Top-of-block declarations
|
|
13
|
+
*
|
|
14
|
+
* Following Rule C005: Single responsibility
|
|
15
|
+
* Following Rule C006: Verb-noun naming
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const { Project, SyntaxKind, Node } = require('ts-morph');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
|
|
22
|
+
class C008TsMorphAnalyzer {
|
|
23
|
+
constructor(semanticEngine = null, options = {}) {
|
|
24
|
+
this.ruleId = 'C008';
|
|
25
|
+
this.ruleName = 'Minimize Variable Scope';
|
|
26
|
+
this.description = 'Variables should be declared close to their first usage';
|
|
27
|
+
this.semanticEngine = semanticEngine;
|
|
28
|
+
this.project = null;
|
|
29
|
+
this.verbose = false;
|
|
30
|
+
|
|
31
|
+
// Configuration
|
|
32
|
+
this.maxLineDistance = options.maxLineDistance || 10;
|
|
33
|
+
this.allowTopOfBlock = options.allowTopOfBlock !== undefined ? options.allowTopOfBlock : true;
|
|
34
|
+
this.ignoreConst = options.ignoreConst || false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async initialize(semanticEngine = null) {
|
|
38
|
+
if (semanticEngine) {
|
|
39
|
+
this.semanticEngine = semanticEngine;
|
|
40
|
+
}
|
|
41
|
+
this.verbose = semanticEngine?.verbose || false;
|
|
42
|
+
|
|
43
|
+
// Use semantic engine's project if available
|
|
44
|
+
if (this.semanticEngine?.project) {
|
|
45
|
+
this.project = this.semanticEngine.project;
|
|
46
|
+
if (this.verbose) {
|
|
47
|
+
console.log('[DEBUG] 🎯 C008: Using semantic engine project');
|
|
48
|
+
}
|
|
49
|
+
} else {
|
|
50
|
+
this.project = new Project({
|
|
51
|
+
compilerOptions: {
|
|
52
|
+
target: 99, // Latest
|
|
53
|
+
module: 99, // ESNext
|
|
54
|
+
allowJs: true,
|
|
55
|
+
checkJs: false,
|
|
56
|
+
jsx: 2, // React
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
if (this.verbose) {
|
|
60
|
+
console.log('[DEBUG] 🎯 C008: Created standalone ts-morph project');
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async analyze(files, language, options = {}) {
|
|
66
|
+
this.verbose = options.verbose || this.verbose;
|
|
67
|
+
|
|
68
|
+
if (!this.project) {
|
|
69
|
+
await this.initialize();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const violations = [];
|
|
73
|
+
|
|
74
|
+
for (const filePath of files) {
|
|
75
|
+
try {
|
|
76
|
+
const fileViolations = await this.analyzeFile(filePath, options);
|
|
77
|
+
violations.push(...fileViolations);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
if (this.verbose) {
|
|
80
|
+
console.warn(`[C008] Error analyzing ${filePath}:`, error.message);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return violations;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async analyzeFile(filePath, options = {}) {
|
|
89
|
+
// Get source file (may be in-memory or on disk)
|
|
90
|
+
let sourceFile = this.project.getSourceFile(filePath);
|
|
91
|
+
|
|
92
|
+
// If not found and file exists on disk, add it
|
|
93
|
+
if (!sourceFile && fs.existsSync(filePath)) {
|
|
94
|
+
sourceFile = this.project.addSourceFileAtPath(filePath);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!sourceFile) {
|
|
98
|
+
return []; // File not found in project
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const violations = [];
|
|
102
|
+
|
|
103
|
+
// Find all variable declarations
|
|
104
|
+
const variableDeclarations = sourceFile.getDescendantsOfKind(SyntaxKind.VariableDeclaration);
|
|
105
|
+
|
|
106
|
+
for (const declaration of variableDeclarations) {
|
|
107
|
+
const violation = this.checkVariableDeclaration(declaration, sourceFile);
|
|
108
|
+
if (violation) {
|
|
109
|
+
violations.push(violation);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return violations;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Check if variable declaration is too far from first usage
|
|
118
|
+
*/
|
|
119
|
+
checkVariableDeclaration(declaration, sourceFile) {
|
|
120
|
+
const variableName = declaration.getName();
|
|
121
|
+
|
|
122
|
+
// Skip destructured variables (e.g., const { a, b } = obj)
|
|
123
|
+
if (declaration.getParent().getKind() === SyntaxKind.ObjectBindingPattern ||
|
|
124
|
+
declaration.getParent().getKind() === SyntaxKind.ArrayBindingPattern) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const initializer = declaration.getInitializer();
|
|
129
|
+
if (!initializer) {
|
|
130
|
+
return null; // No initializer, skip
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Skip React hooks
|
|
134
|
+
if (this.isReactHook(initializer)) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Skip CSS-in-JS
|
|
139
|
+
if (this.isCSSInJS(initializer)) {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Skip function declarations
|
|
144
|
+
if (this.isFunctionDeclaration(initializer)) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Skip module-level constants
|
|
149
|
+
if (this.isModuleLevelConstant(declaration)) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Skip Redux Toolkit patterns (createSlice, createAsyncThunk, etc.)
|
|
154
|
+
if (this.isReduxPattern(declaration, initializer)) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Skip Redux initialState pattern
|
|
159
|
+
if (this.isReduxInitialState(declaration)) {
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Skip style configuration objects
|
|
164
|
+
if (this.isStyleConfig(declaration, initializer)) {
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Skip React component definitions
|
|
169
|
+
if (this.isReactComponentDefinition(declaration, initializer)) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Skip Storybook meta exports
|
|
174
|
+
if (this.isStoryBookMeta(declaration, sourceFile)) {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Skip module-level variables declared at top of file (global singletons)
|
|
179
|
+
if (this.isModuleLevelTopDeclaration(declaration)) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Find first usage
|
|
184
|
+
const firstUsage = this.findFirstUsage(declaration, sourceFile);
|
|
185
|
+
if (!firstUsage) {
|
|
186
|
+
return null; // Variable not used or only in JSX
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Skip variables used only in JSX (React render pattern)
|
|
190
|
+
if (this.isUsedOnlyInJSX(declaration, firstUsage, sourceFile)) {
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// Skip computed constants (UPPER_CASE + simple expression)
|
|
195
|
+
if (this.isComputedConstant(declaration, initializer)) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Skip React component top-level data declarations
|
|
200
|
+
if (this.isReactComponentTopDeclaration(declaration, sourceFile)) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Skip variables used immediately in return object
|
|
205
|
+
if (this.isUsedInReturnObject(declaration, firstUsage)) {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Calculate distance
|
|
210
|
+
const declLine = declaration.getStartLineNumber();
|
|
211
|
+
const usageLine = firstUsage.getStartLineNumber();
|
|
212
|
+
const distance = this.calculateDistance(declaration, firstUsage, sourceFile);
|
|
213
|
+
|
|
214
|
+
if (distance <= this.maxLineDistance) {
|
|
215
|
+
return null; // Within acceptable range
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Distance > maxLineDistance - This is a violation
|
|
219
|
+
// Note: allowTopOfBlock option removed as it was too aggressive
|
|
220
|
+
// and caused false negatives (skipping legitimate violations)
|
|
221
|
+
|
|
222
|
+
// Create violation
|
|
223
|
+
return {
|
|
224
|
+
ruleId: this.ruleId,
|
|
225
|
+
message: `Variable '${variableName}' (line ${declLine}) is declared ${distance} lines before first use (max: ${this.maxLineDistance}). Consider moving declaration closer to usage.`,
|
|
226
|
+
severity: 'warning',
|
|
227
|
+
filePath: sourceFile.getFilePath(),
|
|
228
|
+
location: {
|
|
229
|
+
start: {
|
|
230
|
+
line: declLine,
|
|
231
|
+
column: declaration.getStart() - declaration.getStartLinePos() + 1
|
|
232
|
+
},
|
|
233
|
+
end: {
|
|
234
|
+
line: declaration.getEndLineNumber(),
|
|
235
|
+
column: declaration.getEnd() - declaration.getStartLinePos() + 1
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
context: {
|
|
239
|
+
variable: variableName,
|
|
240
|
+
declaredAt: declLine,
|
|
241
|
+
usedAt: usageLine,
|
|
242
|
+
distance: distance
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Check if initializer is a React hook
|
|
249
|
+
*/
|
|
250
|
+
isReactHook(initializer) {
|
|
251
|
+
if (Node.isCallExpression(initializer)) {
|
|
252
|
+
const expression = initializer.getExpression();
|
|
253
|
+
const text = expression.getText();
|
|
254
|
+
|
|
255
|
+
// useState, useEffect, useRef, etc.
|
|
256
|
+
if (/^use[A-Z]/.test(text)) {
|
|
257
|
+
return true;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// Form.useWatch, etc.
|
|
261
|
+
if (/\.[A-Z].*\.use[A-Z]/.test(text)) {
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Check if initializer is CSS-in-JS (keyframes, styled, css)
|
|
271
|
+
*/
|
|
272
|
+
isCSSInJS(initializer) {
|
|
273
|
+
if (Node.isTaggedTemplateExpression(initializer)) {
|
|
274
|
+
const tag = initializer.getTag().getText();
|
|
275
|
+
return tag === 'keyframes' || tag === 'css' || tag.startsWith('styled.');
|
|
276
|
+
}
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Check if initializer is a function declaration
|
|
282
|
+
*/
|
|
283
|
+
isFunctionDeclaration(initializer) {
|
|
284
|
+
return Node.isArrowFunction(initializer) ||
|
|
285
|
+
Node.isFunctionExpression(initializer);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Check if variable is module-level constant (UPPER_CASE = literal/config)
|
|
290
|
+
* This covers:
|
|
291
|
+
* - const TYPE_DIGITAL_RELEASE = 'digital'
|
|
292
|
+
* - const DEFAULT_CONFIG = { ... }
|
|
293
|
+
* - const VALID_TYPES = ['a', 'b']
|
|
294
|
+
* - const MAX_RETRIES = 3
|
|
295
|
+
* - Pattern: Declare at top, export at bottom (e.g. storages.ts)
|
|
296
|
+
*/
|
|
297
|
+
isModuleLevelConstant(declaration) {
|
|
298
|
+
const variableStatement = declaration.getFirstAncestorByKind(SyntaxKind.VariableStatement);
|
|
299
|
+
if (!variableStatement) {
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Check if at module level (not inside function/class)
|
|
304
|
+
const parent = variableStatement.getParent();
|
|
305
|
+
if (!Node.isSourceFile(parent)) {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Check if const with UPPER_CASE naming
|
|
310
|
+
const declarationList = declaration.getParent();
|
|
311
|
+
if (declarationList.getDeclarationKind() !== 'const') {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const name = declaration.getName();
|
|
316
|
+
if (!/^[A-Z][A-Z0-9_]*$/.test(name)) {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// Check if exported (common pattern: declare at top, export at bottom)
|
|
321
|
+
const sourceFile = declaration.getSourceFile();
|
|
322
|
+
const exports = sourceFile.getExportSymbols();
|
|
323
|
+
const isExported = exports.some(exp => {
|
|
324
|
+
const expName = exp.getName();
|
|
325
|
+
return expName === name;
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
if (isExported) {
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// Check if constant-like value (literal or config object/array)
|
|
333
|
+
const initializer = declaration.getInitializer();
|
|
334
|
+
if (!initializer) {
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const kind = initializer.getKind();
|
|
339
|
+
|
|
340
|
+
// Simple literals
|
|
341
|
+
if (Node.isNumericLiteral(initializer) ||
|
|
342
|
+
Node.isStringLiteral(initializer) ||
|
|
343
|
+
kind === SyntaxKind.TrueKeyword ||
|
|
344
|
+
kind === SyntaxKind.FalseKeyword ||
|
|
345
|
+
kind === SyntaxKind.NullKeyword ||
|
|
346
|
+
kind === SyntaxKind.UndefinedKeyword) {
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// Template literals (const URL = `https://...`)
|
|
351
|
+
if (kind === SyntaxKind.TemplateExpression || kind === SyntaxKind.NoSubstitutionTemplateLiteral) {
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Object literals (const CONFIG = { ... })
|
|
356
|
+
if (kind === SyntaxKind.ObjectLiteralExpression) {
|
|
357
|
+
return true;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Array literals (const TYPES = ['a', 'b', 'c'])
|
|
361
|
+
if (kind === SyntaxKind.ArrayLiteralExpression) {
|
|
362
|
+
return true;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// Enum-like member access (const STATUS = MyEnum.Active)
|
|
366
|
+
if (kind === SyntaxKind.PropertyAccessExpression) {
|
|
367
|
+
return true;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* Check if variable is Redux Toolkit pattern
|
|
375
|
+
* Covers:
|
|
376
|
+
* - createAsyncThunk
|
|
377
|
+
* - createSlice
|
|
378
|
+
* - createAction
|
|
379
|
+
* - createReducer
|
|
380
|
+
*/
|
|
381
|
+
isReduxPattern(declaration, initializer) {
|
|
382
|
+
if (!Node.isCallExpression(initializer)) {
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const expression = initializer.getExpression();
|
|
387
|
+
const text = expression.getText();
|
|
388
|
+
|
|
389
|
+
// Redux Toolkit functions
|
|
390
|
+
const reduxPatterns = [
|
|
391
|
+
'createAsyncThunk',
|
|
392
|
+
'createSlice',
|
|
393
|
+
'createAction',
|
|
394
|
+
'createReducer',
|
|
395
|
+
'createSelector',
|
|
396
|
+
'createEntityAdapter',
|
|
397
|
+
];
|
|
398
|
+
|
|
399
|
+
return reduxPatterns.some(pattern => text.includes(pattern));
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Check if variable is Storybook meta export
|
|
404
|
+
*/
|
|
405
|
+
isStoryBookMeta(declaration, sourceFile) {
|
|
406
|
+
const name = declaration.getName();
|
|
407
|
+
|
|
408
|
+
// Check if variable name is 'meta' or 'Meta'
|
|
409
|
+
if (name !== 'meta' && name !== 'Meta') {
|
|
410
|
+
return false;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Check if file is a story file
|
|
414
|
+
const filePath = sourceFile.getFilePath();
|
|
415
|
+
if (!/\.stories\.(ts|tsx|js|jsx)$/.test(filePath)) {
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Check if it's exported as default
|
|
420
|
+
const variableStatement = declaration.getFirstAncestorByKind(SyntaxKind.VariableStatement);
|
|
421
|
+
if (!variableStatement) {
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// Check if there's a default export of this meta
|
|
426
|
+
const sourceFileNode = declaration.getSourceFile();
|
|
427
|
+
const defaultExport = sourceFileNode.getDefaultExportSymbol();
|
|
428
|
+
if (defaultExport) {
|
|
429
|
+
const declarations = defaultExport.getDeclarations();
|
|
430
|
+
for (const decl of declarations) {
|
|
431
|
+
if (decl.getText().includes(name)) {
|
|
432
|
+
return true;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
return false;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* Check if variable is Redux initialState pattern
|
|
442
|
+
* Pattern: const initialState: Type = { ... }
|
|
443
|
+
* This is a standard Redux Toolkit pattern where initialState
|
|
444
|
+
* must be defined before createSlice() call
|
|
445
|
+
*/
|
|
446
|
+
isReduxInitialState(declaration) {
|
|
447
|
+
const name = declaration.getName();
|
|
448
|
+
|
|
449
|
+
// Must be named 'initialState'
|
|
450
|
+
if (name !== 'initialState') {
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Check if in a Redux slice file
|
|
455
|
+
const sourceFile = declaration.getSourceFile();
|
|
456
|
+
const filePath = sourceFile.getFilePath();
|
|
457
|
+
const fileName = filePath.split('/').pop() || '';
|
|
458
|
+
|
|
459
|
+
// Check if it's a slice file or contains Redux patterns
|
|
460
|
+
if (fileName.includes('Slice.ts') || fileName.includes('slice.ts')) {
|
|
461
|
+
return true;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// Alternative: Check if file contains createSlice
|
|
465
|
+
const fileText = sourceFile.getFullText();
|
|
466
|
+
if (fileText.includes('createSlice')) {
|
|
467
|
+
return true;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Check if variable is a style configuration object
|
|
475
|
+
* Patterns:
|
|
476
|
+
* - const ButtonStyle: ComponentStyleConfig = { ... }
|
|
477
|
+
* - const xxxStyle = { baseStyle: {...}, variants: {...} }
|
|
478
|
+
* Used in Chakra UI, styled-components, theme configs
|
|
479
|
+
*/
|
|
480
|
+
isStyleConfig(declaration, initializer) {
|
|
481
|
+
const name = declaration.getName();
|
|
482
|
+
|
|
483
|
+
// Check if variable name ends with 'Style' or 'Styles'
|
|
484
|
+
if (name.endsWith('Style') || name.endsWith('Styles')) {
|
|
485
|
+
// Must be object literal
|
|
486
|
+
if (Node.isObjectLiteralExpression(initializer)) {
|
|
487
|
+
return true;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Check type annotation for ComponentStyleConfig or similar
|
|
492
|
+
const typeNode = declaration.getTypeNode();
|
|
493
|
+
if (typeNode) {
|
|
494
|
+
const typeText = typeNode.getText();
|
|
495
|
+
if (typeText.includes('StyleConfig') ||
|
|
496
|
+
typeText.includes('ComponentStyle') ||
|
|
497
|
+
typeText.includes('ThemeConfig')) {
|
|
498
|
+
return true;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
// Check if object contains common style config keys
|
|
503
|
+
if (Node.isObjectLiteralExpression(initializer)) {
|
|
504
|
+
const properties = initializer.getProperties();
|
|
505
|
+
const propNames = properties
|
|
506
|
+
.filter(p => Node.isPropertyAssignment(p))
|
|
507
|
+
.map(p => p.getName ? p.getName() : '');
|
|
508
|
+
|
|
509
|
+
// Common style config properties
|
|
510
|
+
const styleConfigKeys = ['baseStyle', 'variants', 'sizes', 'defaultProps'];
|
|
511
|
+
const hasStyleKeys = styleConfigKeys.some(key => propNames.includes(key));
|
|
512
|
+
|
|
513
|
+
if (hasStyleKeys) {
|
|
514
|
+
return true;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
return false;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Check if variable is a React component definition
|
|
523
|
+
* Patterns:
|
|
524
|
+
* - const Component = forwardRef<Props>(...)
|
|
525
|
+
* - const Component: React.FC<Props> = ...
|
|
526
|
+
*/
|
|
527
|
+
isReactComponentDefinition(declaration, initializer) {
|
|
528
|
+
// Check if assigned to forwardRef()
|
|
529
|
+
if (Node.isCallExpression(initializer)) {
|
|
530
|
+
const expression = initializer.getExpression();
|
|
531
|
+
const text = expression.getText();
|
|
532
|
+
|
|
533
|
+
if (text === 'forwardRef' || text.endsWith('.forwardRef')) {
|
|
534
|
+
return true;
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// Check type annotation for React.FC or React.FunctionComponent
|
|
539
|
+
const typeNode = declaration.getTypeNode();
|
|
540
|
+
if (typeNode) {
|
|
541
|
+
const typeText = typeNode.getText();
|
|
542
|
+
if (typeText.includes('React.FC') ||
|
|
543
|
+
typeText.includes('React.FunctionComponent') ||
|
|
544
|
+
typeText.includes('FunctionComponent')) {
|
|
545
|
+
return true;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return false;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Check if declaration is at top of function/block
|
|
554
|
+
*/
|
|
555
|
+
isTopOfBlock(declaration) {
|
|
556
|
+
const block = declaration.getFirstAncestorByKind(SyntaxKind.Block);
|
|
557
|
+
if (!block) {
|
|
558
|
+
return false;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// Check if block is function body
|
|
562
|
+
const blockParent = block.getParent();
|
|
563
|
+
const isFunctionBlock = Node.isFunctionDeclaration(blockParent) ||
|
|
564
|
+
Node.isFunctionExpression(blockParent) ||
|
|
565
|
+
Node.isArrowFunction(blockParent) ||
|
|
566
|
+
Node.isMethodDeclaration(blockParent);
|
|
567
|
+
|
|
568
|
+
if (!isFunctionBlock) {
|
|
569
|
+
return false;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// Count statements from block start
|
|
573
|
+
const statements = block.getStatements();
|
|
574
|
+
const declStatement = declaration.getFirstAncestorByKind(SyntaxKind.VariableStatement);
|
|
575
|
+
if (!declStatement) {
|
|
576
|
+
return false;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
const declIndex = statements.indexOf(declStatement);
|
|
580
|
+
|
|
581
|
+
// Allow within first 10 statements
|
|
582
|
+
return declIndex < 10;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Check if declaration is module-level AND at top of file
|
|
587
|
+
* Pattern: Global singletons like axios instances, services
|
|
588
|
+
* Example:
|
|
589
|
+
* const axiosApp = axios.create(...); // Line 29
|
|
590
|
+
* axiosApp.interceptors.request.use(...);
|
|
591
|
+
* export default axiosApp;
|
|
592
|
+
*/
|
|
593
|
+
isModuleLevelTopDeclaration(declaration) {
|
|
594
|
+
// Check if it's at module level (not inside function)
|
|
595
|
+
const scopeNode = this.getScopeNode(declaration);
|
|
596
|
+
if (!scopeNode || !Node.isSourceFile(scopeNode)) {
|
|
597
|
+
return false; // Not module-level
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Check if declared near top of file (within first 50 lines)
|
|
601
|
+
const declLine = declaration.getStartLineNumber();
|
|
602
|
+
const MAX_TOP_LINES = 50;
|
|
603
|
+
|
|
604
|
+
if (declLine > MAX_TOP_LINES) {
|
|
605
|
+
return false; // Too far from top
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Additional check: Common singleton patterns
|
|
609
|
+
const initializer = declaration.getInitializer();
|
|
610
|
+
if (!initializer) {
|
|
611
|
+
return false;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
const initText = initializer.getText();
|
|
615
|
+
|
|
616
|
+
// Check for common singleton patterns
|
|
617
|
+
const singletonPatterns = [
|
|
618
|
+
'axios.create',
|
|
619
|
+
'.create(',
|
|
620
|
+
'new ',
|
|
621
|
+
'createClient',
|
|
622
|
+
'createInstance',
|
|
623
|
+
];
|
|
624
|
+
|
|
625
|
+
const isSingletonPattern = singletonPatterns.some(pattern =>
|
|
626
|
+
initText.includes(pattern)
|
|
627
|
+
);
|
|
628
|
+
|
|
629
|
+
return isSingletonPattern;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Check if variable is used only in JSX (React render pattern)
|
|
634
|
+
* Pattern: const menuList = [...]; return <>{menuList.map(...)}</>
|
|
635
|
+
* These are data declarations for rendering, acceptable pattern
|
|
636
|
+
*/
|
|
637
|
+
isUsedOnlyInJSX(declaration, firstUsage, sourceFile) {
|
|
638
|
+
// Check if first usage is inside JSX
|
|
639
|
+
let current = firstUsage.getParent();
|
|
640
|
+
while (current && current !== sourceFile) {
|
|
641
|
+
if (Node.isJsxElement(current) ||
|
|
642
|
+
Node.isJsxSelfClosingElement(current) ||
|
|
643
|
+
Node.isJsxFragment(current) ||
|
|
644
|
+
Node.isJsxExpression(current)) {
|
|
645
|
+
return true; // Used in JSX
|
|
646
|
+
}
|
|
647
|
+
current = current.getParent();
|
|
648
|
+
}
|
|
649
|
+
return false;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Check if variable is a computed constant (UPPER_CASE + simple expression)
|
|
654
|
+
* Pattern: const SP_LANDSCAPE = window.orientation === 90 || ...
|
|
655
|
+
* These are derived constants, acceptable even if far from usage
|
|
656
|
+
*/
|
|
657
|
+
isComputedConstant(declaration, initializer) {
|
|
658
|
+
const name = declaration.getName();
|
|
659
|
+
|
|
660
|
+
// Check UPPER_CASE naming convention
|
|
661
|
+
if (!/^[A-Z][A-Z0-9_]*$/.test(name)) {
|
|
662
|
+
return false;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Check if it's a simple expression (comparison, logical, etc.)
|
|
666
|
+
if (Node.isBinaryExpression(initializer) ||
|
|
667
|
+
Node.isConditionalExpression(initializer) ||
|
|
668
|
+
Node.isPrefixUnaryExpression(initializer)) {
|
|
669
|
+
return true;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Check if it's accessing property/method (e.g., window.orientation)
|
|
673
|
+
if (Node.isPropertyAccessExpression(initializer) ||
|
|
674
|
+
Node.isElementAccessExpression(initializer)) {
|
|
675
|
+
return true;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
return false;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Check if variable is declared at top of React component function
|
|
683
|
+
* Pattern: Variables declared alongside hooks (useState, useEffect, etc.)
|
|
684
|
+
* Example:
|
|
685
|
+
* const Component = () => {
|
|
686
|
+
* const [state, setState] = useState(...); // Hook
|
|
687
|
+
* const router = useRouter(); // Hook
|
|
688
|
+
* const dataCheck = Storage.get(...); // Data declaration ← SKIP
|
|
689
|
+
*
|
|
690
|
+
* useEffect(() => { use(dataCheck); }, []); // Far away but acceptable
|
|
691
|
+
* return <div>{dataCheck}</div>;
|
|
692
|
+
* };
|
|
693
|
+
*/
|
|
694
|
+
isReactComponentTopDeclaration(declaration, sourceFile) {
|
|
695
|
+
// Check if inside a function (arrow or regular)
|
|
696
|
+
const parentFunc = declaration.getFirstAncestor(node =>
|
|
697
|
+
Node.isArrowFunction(node) ||
|
|
698
|
+
Node.isFunctionExpression(node) ||
|
|
699
|
+
Node.isFunctionDeclaration(node)
|
|
700
|
+
);
|
|
701
|
+
|
|
702
|
+
if (!parentFunc) {
|
|
703
|
+
return false; // Not in function
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Check if this function is a React component
|
|
707
|
+
// Heuristic: Has JSX return statement or uses React hooks
|
|
708
|
+
const funcBody = Node.isArrowFunction(parentFunc) || Node.isFunctionExpression(parentFunc)
|
|
709
|
+
? parentFunc.getBody()
|
|
710
|
+
: Node.isFunctionDeclaration(parentFunc)
|
|
711
|
+
? parentFunc.getBody()
|
|
712
|
+
: null;
|
|
713
|
+
|
|
714
|
+
if (!funcBody || !Node.isBlock(funcBody)) {
|
|
715
|
+
return false; // No block body
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// Check if function body has JSX or hooks
|
|
719
|
+
const hasJSX = funcBody.getDescendantsOfKind(SyntaxKind.JsxElement).length > 0 ||
|
|
720
|
+
funcBody.getDescendantsOfKind(SyntaxKind.JsxSelfClosingElement).length > 0 ||
|
|
721
|
+
funcBody.getDescendantsOfKind(SyntaxKind.JsxFragment).length > 0;
|
|
722
|
+
|
|
723
|
+
if (!hasJSX) {
|
|
724
|
+
return false; // Not a React component
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
// Check if declaration is in top statements of component (within first 30 statements)
|
|
728
|
+
const statements = funcBody.getStatements();
|
|
729
|
+
const declStatement = declaration.getFirstAncestorByKind(SyntaxKind.VariableStatement);
|
|
730
|
+
|
|
731
|
+
if (!declStatement) {
|
|
732
|
+
return false;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
const declIndex = statements.indexOf(declStatement);
|
|
736
|
+
if (declIndex === -1 || declIndex >= 30) {
|
|
737
|
+
return false; // Too far from top
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// Check if there are hooks nearby (useState, useEffect, etc.)
|
|
741
|
+
// If yes, this is likely a React component top declaration
|
|
742
|
+
const nearbyStatements = statements.slice(Math.max(0, declIndex - 5), Math.min(statements.length, declIndex + 5));
|
|
743
|
+
const hasNearbyHooks = nearbyStatements.some(stmt => {
|
|
744
|
+
const text = stmt.getText();
|
|
745
|
+
return /use[A-Z]\w+/.test(text); // Match useXxx hooks
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
return hasNearbyHooks;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
/**
|
|
752
|
+
* Check if variable is used in a return object/statement
|
|
753
|
+
* Pattern: const x = ...; return { x, ... } or return x;
|
|
754
|
+
*
|
|
755
|
+
* Example (SKIP):
|
|
756
|
+
* const dataset_attributes = !dataset ? undefined : { ...dataset };
|
|
757
|
+
* const other = ...;
|
|
758
|
+
* return { dataset_attributes, other };
|
|
759
|
+
*
|
|
760
|
+
* Logic: If variable is ONLY used in the final return statement,
|
|
761
|
+
* it's a "prepare and return" pattern - acceptable.
|
|
762
|
+
*/
|
|
763
|
+
isUsedInReturnObject(declaration, firstUsage) {
|
|
764
|
+
if (!firstUsage) {
|
|
765
|
+
return false;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
// Find the return statement containing this usage
|
|
769
|
+
const returnStmt = firstUsage.getFirstAncestor(node =>
|
|
770
|
+
Node.isReturnStatement(node)
|
|
771
|
+
);
|
|
772
|
+
|
|
773
|
+
if (!returnStmt) {
|
|
774
|
+
return false;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Get parent block
|
|
778
|
+
const declStatement = declaration.getFirstAncestorByKind(SyntaxKind.VariableStatement);
|
|
779
|
+
if (!declStatement) {
|
|
780
|
+
return false;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
const parentBlock = declStatement.getFirstAncestor(node => Node.isBlock(node));
|
|
784
|
+
if (!parentBlock) {
|
|
785
|
+
return false;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const statements = parentBlock.getStatements();
|
|
789
|
+
const returnIndex = statements.indexOf(returnStmt);
|
|
790
|
+
|
|
791
|
+
if (returnIndex === -1) {
|
|
792
|
+
return false;
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// Check if return is the last (or near-last) statement in block
|
|
796
|
+
// Pattern: Variables declared in middle of function, then used in final return
|
|
797
|
+
// This is acceptable "prepare data and return" pattern
|
|
798
|
+
const isNearEnd = returnIndex >= statements.length - 2;
|
|
799
|
+
|
|
800
|
+
if (!isNearEnd) {
|
|
801
|
+
return false; // Return is not at end, not a "prepare and return" pattern
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// Check if variable is ONLY used in this return (not used elsewhere)
|
|
805
|
+
const variableName = declaration.getName();
|
|
806
|
+
const allUsages = parentBlock.getDescendantsOfKind(SyntaxKind.Identifier)
|
|
807
|
+
.filter(id => {
|
|
808
|
+
if (id.getText() !== variableName || id === declaration.getNameNode()) {
|
|
809
|
+
return false;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
// Skip if it's another declaration
|
|
813
|
+
const parent = id.getParent();
|
|
814
|
+
if (Node.isVariableDeclaration(parent) || Node.isBindingElement(parent)) {
|
|
815
|
+
return false;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// Skip if it's part of property access (e.g., params.dataset_attributes)
|
|
819
|
+
// We only want standalone references
|
|
820
|
+
if (Node.isPropertyAccessExpression(parent) && parent.getNameNode() === id) {
|
|
821
|
+
return false; // This is a property name, not a variable reference
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
return true;
|
|
825
|
+
});
|
|
826
|
+
|
|
827
|
+
// Count usages outside the return statement
|
|
828
|
+
const usagesOutsideReturn = allUsages.filter(usage => {
|
|
829
|
+
const usageReturn = usage.getFirstAncestor(node => Node.isReturnStatement(node));
|
|
830
|
+
return usageReturn !== returnStmt;
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
// If variable is only used in the return, it's a "prepare and return" pattern
|
|
834
|
+
return usagesOutsideReturn.length === 0;
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Find first usage of variable
|
|
839
|
+
*/
|
|
840
|
+
findFirstUsage(declaration, sourceFile) {
|
|
841
|
+
const variableName = declaration.getName();
|
|
842
|
+
|
|
843
|
+
// Get containing function/block scope
|
|
844
|
+
const scopeNode = this.getScopeNode(declaration);
|
|
845
|
+
if (!scopeNode) {
|
|
846
|
+
return null;
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
const declPos = declaration.getEnd();
|
|
850
|
+
let firstUsage = null;
|
|
851
|
+
let firstUsagePos = Infinity;
|
|
852
|
+
|
|
853
|
+
// Find all identifiers with same name in scope
|
|
854
|
+
const identifiers = scopeNode.getDescendantsOfKind(SyntaxKind.Identifier);
|
|
855
|
+
|
|
856
|
+
for (const identifier of identifiers) {
|
|
857
|
+
if (identifier.getText() !== variableName) {
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
const pos = identifier.getStart();
|
|
862
|
+
|
|
863
|
+
// Skip if before declaration
|
|
864
|
+
if (pos <= declPos) {
|
|
865
|
+
continue;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
// Skip if it's the declaration itself
|
|
869
|
+
if (identifier.getParent() === declaration) {
|
|
870
|
+
continue;
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// Skip if it's another variable declaration (destructuring, etc.)
|
|
874
|
+
const parent = identifier.getParent();
|
|
875
|
+
if (Node.isVariableDeclaration(parent) || Node.isBindingElement(parent)) {
|
|
876
|
+
continue; // This is a declaration, not a usage
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
// Skip if it's an assignment target (property name in object literal)
|
|
880
|
+
if (Node.isPropertyAssignment(parent) && parent.getName() === variableName) {
|
|
881
|
+
// Check if it's {variableName} shorthand or {variableName: value}
|
|
882
|
+
if (parent.getInitializer()?.getText() === variableName) {
|
|
883
|
+
// It's shorthand {variableName}, this IS a usage
|
|
884
|
+
} else {
|
|
885
|
+
continue; // It's {variableName: value}, skip
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// Skip JSX attribute names (but not values)
|
|
890
|
+
if (Node.isJsxAttribute(parent)) {
|
|
891
|
+
const attrName = parent.getNameNode();
|
|
892
|
+
if (attrName === identifier) {
|
|
893
|
+
continue; // It's attribute name, skip
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
// This is a usage
|
|
898
|
+
if (pos < firstUsagePos) {
|
|
899
|
+
firstUsagePos = pos;
|
|
900
|
+
firstUsage = identifier;
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
return firstUsage;
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
/**
|
|
908
|
+
* Get scope node for variable (function/block it belongs to)
|
|
909
|
+
*/
|
|
910
|
+
getScopeNode(declaration) {
|
|
911
|
+
// Find containing function
|
|
912
|
+
const func = declaration.getFirstAncestor(node =>
|
|
913
|
+
Node.isFunctionDeclaration(node) ||
|
|
914
|
+
Node.isFunctionExpression(node) ||
|
|
915
|
+
Node.isArrowFunction(node) ||
|
|
916
|
+
Node.isMethodDeclaration(node) ||
|
|
917
|
+
Node.isConstructorDeclaration(node)
|
|
918
|
+
);
|
|
919
|
+
|
|
920
|
+
if (func) {
|
|
921
|
+
return func;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
// Module-level variable
|
|
925
|
+
return declaration.getSourceFile();
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
/**
|
|
929
|
+
* Calculate real distance between declaration and usage
|
|
930
|
+
* Distance = number of statements between declaration and usage
|
|
931
|
+
* This represents logical distance, not line distance
|
|
932
|
+
*
|
|
933
|
+
* Examples:
|
|
934
|
+
* const a = 1; // Statement 1
|
|
935
|
+
* const b = 2; // Statement 2 (distance = 1)
|
|
936
|
+
* console.log(a); // Usage
|
|
937
|
+
*
|
|
938
|
+
* const x = condition // Statement 1 (multi-line ternary = 1 statement)
|
|
939
|
+
* ? {...}
|
|
940
|
+
* : {...};
|
|
941
|
+
* const y = 2; // Statement 2 (distance = 1, not 10 lines)
|
|
942
|
+
* use(x); // Usage
|
|
943
|
+
*/
|
|
944
|
+
calculateDistance(declaration, usage, sourceFile) {
|
|
945
|
+
// Try to count statements first (more accurate)
|
|
946
|
+
const stmtDistance = this.countStatementsBetween(declaration, usage);
|
|
947
|
+
if (stmtDistance !== null) {
|
|
948
|
+
return stmtDistance;
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// Fallback to line distance for edge cases
|
|
952
|
+
const declEndLine = declaration.getEndLineNumber();
|
|
953
|
+
const usageLine = usage.getStartLineNumber();
|
|
954
|
+
return usageLine - declEndLine;
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
/**
|
|
958
|
+
* Count number of statements between declaration and usage
|
|
959
|
+
* Returns null if statements cannot be counted (e.g., different scopes)
|
|
960
|
+
*/
|
|
961
|
+
countStatementsBetween(declaration, usage) {
|
|
962
|
+
// Get the statement nodes (not just the declarations)
|
|
963
|
+
const declStatement = declaration.getFirstAncestorByKind(SyntaxKind.VariableStatement) ||
|
|
964
|
+
declaration.getFirstAncestorByKind(SyntaxKind.ExpressionStatement);
|
|
965
|
+
|
|
966
|
+
if (!declStatement) {
|
|
967
|
+
return null; // Cannot find statement
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// Find the block/body containing both declaration and usage
|
|
971
|
+
const commonBlock = this.findCommonBlock(declStatement, usage);
|
|
972
|
+
if (!commonBlock) {
|
|
973
|
+
return null; // Different scopes
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
// Get all statements in the block
|
|
977
|
+
let statements = [];
|
|
978
|
+
if (Node.isSourceFile(commonBlock)) {
|
|
979
|
+
statements = commonBlock.getStatements();
|
|
980
|
+
} else if (Node.isBlock(commonBlock)) {
|
|
981
|
+
statements = commonBlock.getStatements();
|
|
982
|
+
} else if (Node.isCaseClause(commonBlock) || Node.isDefaultClause(commonBlock)) {
|
|
983
|
+
statements = commonBlock.getStatements();
|
|
984
|
+
} else {
|
|
985
|
+
return null; // Unknown block type
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
// Find indices
|
|
989
|
+
const declIndex = statements.indexOf(declStatement);
|
|
990
|
+
if (declIndex === -1) {
|
|
991
|
+
return null;
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
// Find statement containing usage
|
|
995
|
+
const usageStatement = this.findStatementContaining(statements, usage);
|
|
996
|
+
if (!usageStatement) {
|
|
997
|
+
return null;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
const usageIndex = statements.indexOf(usageStatement);
|
|
1001
|
+
if (usageIndex === -1 || usageIndex <= declIndex) {
|
|
1002
|
+
return null;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// Count distance from declaration to usage (exclusive on both ends)
|
|
1006
|
+
// usageIndex - declIndex = number of statements from decl to usage
|
|
1007
|
+
// Example: decl at 0, usage at 11 = 11 statements distance
|
|
1008
|
+
return usageIndex - declIndex;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
/**
|
|
1012
|
+
* Find common block containing both nodes
|
|
1013
|
+
*/
|
|
1014
|
+
findCommonBlock(node1, node2) {
|
|
1015
|
+
// Get all ancestor blocks for node1
|
|
1016
|
+
const blocks1 = [];
|
|
1017
|
+
let current = node1.getParent();
|
|
1018
|
+
while (current) {
|
|
1019
|
+
if (Node.isSourceFile(current) ||
|
|
1020
|
+
Node.isBlock(current) ||
|
|
1021
|
+
Node.isCaseClause(current) ||
|
|
1022
|
+
Node.isDefaultClause(current)) {
|
|
1023
|
+
blocks1.push(current);
|
|
1024
|
+
}
|
|
1025
|
+
current = current.getParent();
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// Find first common block with node2
|
|
1029
|
+
current = node2.getParent();
|
|
1030
|
+
while (current) {
|
|
1031
|
+
if (blocks1.includes(current)) {
|
|
1032
|
+
return current;
|
|
1033
|
+
}
|
|
1034
|
+
current = current.getParent();
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
return null;
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
/**
|
|
1041
|
+
* Find the statement containing a node
|
|
1042
|
+
*/
|
|
1043
|
+
findStatementContaining(statements, node) {
|
|
1044
|
+
for (const stmt of statements) {
|
|
1045
|
+
if (this.isNodeInside(node, stmt)) {
|
|
1046
|
+
return stmt;
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
return null;
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
/**
|
|
1053
|
+
* Check if node is inside ancestor
|
|
1054
|
+
*/
|
|
1055
|
+
isNodeInside(node, ancestor) {
|
|
1056
|
+
let current = node;
|
|
1057
|
+
while (current) {
|
|
1058
|
+
if (current === ancestor) {
|
|
1059
|
+
return true;
|
|
1060
|
+
}
|
|
1061
|
+
current = current.getParent();
|
|
1062
|
+
}
|
|
1063
|
+
return false;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
module.exports = C008TsMorphAnalyzer;
|