@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.
Files changed (50) hide show
  1. package/config/rule-analysis-strategies.js +3 -3
  2. package/config/rules/enhanced-rules-registry.json +40 -20
  3. package/core/cli-action-handler.js +2 -2
  4. package/core/config-merger.js +28 -6
  5. package/core/constants/defaults.js +1 -1
  6. package/core/file-targeting-service.js +72 -4
  7. package/core/output-service.js +21 -4
  8. package/engines/heuristic-engine.js +5 -0
  9. package/package.json +1 -1
  10. package/rules/common/C002_no_duplicate_code/README.md +115 -0
  11. package/rules/common/C002_no_duplicate_code/analyzer.js +615 -219
  12. package/rules/common/C002_no_duplicate_code/test-cases/api-handlers.ts +64 -0
  13. package/rules/common/C002_no_duplicate_code/test-cases/data-processor.ts +46 -0
  14. package/rules/common/C002_no_duplicate_code/test-cases/good-example.tsx +40 -0
  15. package/rules/common/C002_no_duplicate_code/test-cases/product-service.ts +57 -0
  16. package/rules/common/C002_no_duplicate_code/test-cases/user-service.ts +49 -0
  17. package/rules/common/C008/analyzer.js +40 -0
  18. package/rules/common/C008/config.json +20 -0
  19. package/rules/common/C008/ts-morph-analyzer.js +1067 -0
  20. package/rules/common/C018_no_throw_generic_error/analyzer.js +1 -1
  21. package/rules/common/C018_no_throw_generic_error/symbol-based-analyzer.js +27 -3
  22. package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +504 -162
  23. package/rules/common/C029_catch_block_logging/analyzer.js +499 -89
  24. package/rules/common/C033_separate_service_repository/README.md +131 -20
  25. package/rules/common/C033_separate_service_repository/analyzer.js +1 -1
  26. package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +417 -274
  27. package/rules/common/C041_no_sensitive_hardcode/analyzer.js +144 -254
  28. package/rules/common/C041_no_sensitive_hardcode/config.json +50 -0
  29. package/rules/common/C041_no_sensitive_hardcode/symbol-based-analyzer.js +575 -0
  30. package/rules/common/C067_no_hardcoded_config/analyzer.js +17 -16
  31. package/rules/common/C067_no_hardcoded_config/symbol-based-analyzer.js +3477 -659
  32. package/rules/docs/C002_no_duplicate_code.md +276 -11
  33. package/rules/index.js +5 -1
  34. package/rules/security/S006_no_plaintext_recovery_codes/analyzer.js +266 -88
  35. package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +805 -0
  36. package/rules/security/S010_no_insecure_encryption/README.md +78 -0
  37. package/rules/security/S010_no_insecure_encryption/analyzer.js +463 -398
  38. package/rules/security/S013_tls_enforcement/README.md +51 -0
  39. package/rules/security/S013_tls_enforcement/analyzer.js +99 -0
  40. package/rules/security/S013_tls_enforcement/config.json +41 -0
  41. package/rules/security/S013_tls_enforcement/symbol-based-analyzer.js +339 -0
  42. package/rules/security/S014_tls_version_enforcement/README.md +354 -0
  43. package/rules/security/S014_tls_version_enforcement/analyzer.js +118 -0
  44. package/rules/security/S014_tls_version_enforcement/config.json +56 -0
  45. package/rules/security/S014_tls_version_enforcement/symbol-based-analyzer.js +194 -0
  46. package/rules/security/S055_content_type_validation/analyzer.js +121 -279
  47. package/rules/security/S055_content_type_validation/symbol-based-analyzer.js +346 -0
  48. package/rules/tests/C002_no_duplicate_code.test.js +111 -22
  49. package/rules/common/C029_catch_block_logging/analyzer-smart-pipeline.js +0 -755
  50. 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;