@sun-asterisk/sunlint 1.2.2 → 1.3.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.
Files changed (64) hide show
  1. package/CHANGELOG.md +40 -1
  2. package/CONTRIBUTING.md +533 -70
  3. package/README.md +16 -2
  4. package/config/engines/engines-enhanced.json +86 -0
  5. package/config/engines/semantic-config.json +114 -0
  6. package/config/eslint-rule-mapping.json +50 -38
  7. package/config/rules/enhanced-rules-registry.json +2503 -0
  8. package/config/rules/rules-registry-generated.json +785 -837
  9. package/core/adapters/sunlint-rule-adapter.js +25 -30
  10. package/core/analysis-orchestrator.js +42 -2
  11. package/core/categories.js +52 -0
  12. package/core/category-constants.js +39 -0
  13. package/core/cli-action-handler.js +32 -5
  14. package/core/config-manager.js +111 -0
  15. package/core/config-merger.js +61 -0
  16. package/core/constants/categories.js +168 -0
  17. package/core/constants/defaults.js +165 -0
  18. package/core/constants/engines.js +185 -0
  19. package/core/constants/index.js +30 -0
  20. package/core/constants/rules.js +215 -0
  21. package/core/file-targeting-service.js +128 -7
  22. package/core/interfaces/rule-plugin.interface.js +207 -0
  23. package/core/plugin-manager.js +448 -0
  24. package/core/rule-selection-service.js +42 -15
  25. package/core/semantic-engine.js +560 -0
  26. package/core/semantic-rule-base.js +433 -0
  27. package/core/unified-rule-registry.js +484 -0
  28. package/docs/CONSTANTS-ARCHITECTURE.md +288 -0
  29. package/engines/core/base-engine.js +249 -0
  30. package/engines/engine-factory.js +275 -0
  31. package/engines/eslint-engine.js +171 -19
  32. package/engines/heuristic-engine.js +511 -78
  33. package/integrations/eslint/plugin/index.js +27 -27
  34. package/package.json +10 -6
  35. package/rules/common/C003_no_vague_abbreviations/analyzer.js +1 -1
  36. package/rules/common/C029_catch_block_logging/analyzer.js +17 -5
  37. package/rules/common/C047_no_duplicate_retry_logic/c047-semantic-rule.js +278 -0
  38. package/rules/common/C047_no_duplicate_retry_logic/symbol-analyzer-enhanced.js +968 -0
  39. package/rules/common/C047_no_duplicate_retry_logic/symbol-config.json +71 -0
  40. package/rules/index.js +7 -0
  41. package/scripts/category-manager.js +150 -0
  42. package/scripts/generate-rules-registry.js +88 -0
  43. package/scripts/migrate-rule-registry.js +157 -0
  44. package/scripts/validate-system.js +48 -0
  45. package/.sunlint.json +0 -35
  46. package/config/README.md +0 -88
  47. package/config/engines/eslint-rule-mapping.json +0 -74
  48. package/config/schemas/sunlint-schema.json +0 -0
  49. package/config/testing/test-s005-working.ts +0 -22
  50. package/core/multi-rule-runner.js +0 -0
  51. package/engines/tree-sitter-parser.js +0 -0
  52. package/engines/universal-ast-engine.js +0 -0
  53. package/rules/common/C029_catch_block_logging/analyzer-backup.js +0 -426
  54. package/rules/common/C029_catch_block_logging/analyzer-fixed.js +0 -130
  55. package/rules/common/C029_catch_block_logging/analyzer-multi-tech.js +0 -487
  56. package/rules/common/C029_catch_block_logging/analyzer-simple.js +0 -110
  57. package/rules/common/C029_catch_block_logging/ast-analyzer-backup.js +0 -441
  58. package/rules/common/C029_catch_block_logging/ast-analyzer-new.js +0 -127
  59. package/rules/common/C029_catch_block_logging/ast-analyzer.js +0 -133
  60. package/rules/common/C029_catch_block_logging/cfg-analyzer.js +0 -408
  61. package/rules/common/C029_catch_block_logging/dataflow-analyzer.js +0 -454
  62. package/rules/common/C029_catch_block_logging/multi-language-ast-engine.js +0 -700
  63. package/rules/common/C029_catch_block_logging/pattern-learning-analyzer.js +0 -568
  64. package/rules/common/C029_catch_block_logging/semantic-analyzer.js +0 -459
@@ -0,0 +1,560 @@
1
+ /**
2
+ * SunLint Semantic Engine
3
+ * Core Symbol Table Manager using ts-morph
4
+ *
5
+ * Provides shared semantic analysis capabilities for SunLint rules
6
+ * Manages project-wide Symbol Table and AST caching
7
+ */
8
+
9
+ const path = require('path');
10
+ const fs = require('fs').promises;
11
+
12
+ class SemanticEngine {
13
+ constructor(options = {}) {
14
+ this.options = {
15
+ // Compiler options
16
+ compilerOptions: {
17
+ target: 99, // ScriptTarget.Latest
18
+ allowJs: true,
19
+ checkJs: false,
20
+ skipLibCheck: true,
21
+ skipDefaultLibCheck: true,
22
+ ...options.compilerOptions
23
+ },
24
+
25
+ // Performance options
26
+ enableCaching: options.enableCaching !== false,
27
+ maxCacheSize: options.maxCacheSize || 100, // files
28
+ memoryLimit: options.memoryLimit || 500 * 1024 * 1024, // 500MB
29
+
30
+ // Analysis options
31
+ crossFileAnalysis: options.crossFileAnalysis !== false,
32
+ enableTypeChecker: options.enableTypeChecker || false,
33
+
34
+ ...options
35
+ };
36
+
37
+ this.project = null;
38
+ this.symbolTable = new Map();
39
+ this.fileCache = new Map();
40
+ this.initialized = false;
41
+ this.stats = {
42
+ filesAnalyzed: 0,
43
+ cacheHits: 0,
44
+ cacheMisses: 0,
45
+ memoryUsage: 0
46
+ };
47
+ }
48
+
49
+ /**
50
+ * Initialize ts-morph project with optimized memory configuration
51
+ * Designed for large projects (3000+ files, 800-1000 lines each)
52
+ */
53
+ async initialize(projectPath) {
54
+ try {
55
+ // Load ts-morph conditionally
56
+ const { Project } = await import('ts-morph');
57
+
58
+ // Discover TypeScript configuration
59
+ const tsConfigPath = await this.findTsConfig(projectPath);
60
+
61
+ // Initialize project with memory-optimized settings
62
+ this.project = new Project({
63
+ tsConfigFilePath: tsConfigPath,
64
+ compilerOptions: {
65
+ ...this.options.compilerOptions,
66
+ // Memory optimization flags
67
+ skipLibCheck: true,
68
+ skipDefaultLibCheck: true,
69
+ noLib: true, // Don't load standard libraries
70
+ allowJs: true,
71
+ checkJs: false,
72
+ },
73
+ // Critical memory optimizations for large projects
74
+ skipFileDependencyResolution: true, // Don't resolve dependencies
75
+ skipLoadingLibFiles: true, // Don't load .d.ts lib files
76
+ useInMemoryFileSystem: false, // Use disk for large projects
77
+
78
+ // Performance settings for large codebases
79
+ resolutionHost: undefined, // Disable resolution host
80
+ libFolderPath: undefined, // Don't load TypeScript libs
81
+ });
82
+
83
+ // Only add target files, not entire project
84
+ const sourceFiles = await this.discoverTargetFiles(projectPath);
85
+ if (sourceFiles.length > 100) {
86
+ console.warn(`⚠️ Large project detected (${sourceFiles.length} files) - limited analysis mode`);
87
+ // For large projects, only add first 50 files to avoid memory issues
88
+ this.project.addSourceFilesAtPaths(sourceFiles.slice(0, 50));
89
+ } else {
90
+ this.project.addSourceFilesAtPaths(sourceFiles);
91
+ }
92
+
93
+ console.log(`🔧 Semantic Engine initialized (Memory Optimized):`);
94
+ console.log(` 📁 Project: ${projectPath}`);
95
+ console.log(` 📋 TS Config: ${tsConfigPath || 'default (minimal)'}`);
96
+ console.log(` 📄 Files loaded: ${this.project.getSourceFiles().length}`);
97
+ console.log(` 💾 Memory mode: Optimized for large projects`);
98
+
99
+ this.initialized = true;
100
+ return true;
101
+
102
+ } catch (error) {
103
+ console.warn(`⚠️ ts-morph not available or initialization failed:`, error.message);
104
+ return false;
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Get or create Symbol Table for a file
110
+ */
111
+ async getSymbolTable(filePath) {
112
+ if (!this.initialized) {
113
+ throw new Error('Semantic Engine not initialized');
114
+ }
115
+
116
+ const absolutePath = path.resolve(filePath);
117
+
118
+ // Check cache first
119
+ if (this.fileCache.has(absolutePath)) {
120
+ this.stats.cacheHits++;
121
+ return this.fileCache.get(absolutePath);
122
+ }
123
+
124
+ this.stats.cacheMisses++;
125
+
126
+ // Get source file
127
+ const sourceFile = this.project.getSourceFile(absolutePath);
128
+ if (!sourceFile) {
129
+ console.warn(`⚠️ File not found in project: ${filePath}`);
130
+ return null;
131
+ }
132
+
133
+ // Build symbol table
134
+ const symbolTable = await this.buildSymbolTable(sourceFile);
135
+
136
+ // Cache the result
137
+ if (this.options.enableCaching) {
138
+ this.cacheSymbolTable(absolutePath, symbolTable);
139
+ }
140
+
141
+ this.stats.filesAnalyzed++;
142
+ return symbolTable;
143
+ }
144
+
145
+ /**
146
+ * Build comprehensive symbol table for a file
147
+ */
148
+ async buildSymbolTable(sourceFile) {
149
+ const symbols = {
150
+ // File metadata
151
+ filePath: sourceFile.getFilePath(),
152
+ fileName: sourceFile.getBaseName(),
153
+
154
+ // Imports and exports
155
+ imports: this.extractImports(sourceFile),
156
+ exports: this.extractExports(sourceFile),
157
+
158
+ // Declarations
159
+ functions: this.extractFunctions(sourceFile),
160
+ classes: this.extractClasses(sourceFile),
161
+ interfaces: this.extractInterfaces(sourceFile),
162
+ variables: this.extractVariables(sourceFile),
163
+ constants: this.extractConstants(sourceFile),
164
+
165
+ // React specific (if applicable)
166
+ hooks: this.extractHooks(sourceFile),
167
+ components: this.extractComponents(sourceFile),
168
+
169
+ // Call analysis
170
+ functionCalls: this.extractFunctionCalls(sourceFile),
171
+ methodCalls: this.extractMethodCalls(sourceFile),
172
+
173
+ // Cross-file references
174
+ crossFileReferences: this.extractCrossFileReferences(sourceFile),
175
+
176
+ // Metadata
177
+ lastModified: Date.now(),
178
+ analysisTime: 0
179
+ };
180
+
181
+ // Add cross-file dependency information
182
+ if (this.options.crossFileAnalysis) {
183
+ symbols.dependencies = await this.analyzeDependencies(sourceFile);
184
+ }
185
+
186
+ return symbols;
187
+ }
188
+
189
+ /**
190
+ * Extract import statements
191
+ */
192
+ extractImports(sourceFile) {
193
+ const imports = [];
194
+
195
+ sourceFile.getImportDeclarations().forEach(importDecl => {
196
+ const moduleSpecifier = importDecl.getModuleSpecifierValue();
197
+
198
+ // Named imports
199
+ const namedImports = importDecl.getNamedImports().map(namedImport => ({
200
+ name: namedImport.getName(),
201
+ alias: namedImport.getAliasNode()?.getText(),
202
+ line: namedImport.getStartLineNumber()
203
+ }));
204
+
205
+ // Default import
206
+ const defaultImport = importDecl.getDefaultImport();
207
+
208
+ imports.push({
209
+ module: moduleSpecifier,
210
+ defaultImport: defaultImport?.getText(),
211
+ namedImports,
212
+ line: importDecl.getStartLineNumber(),
213
+ isTypeOnly: importDecl.isTypeOnly(),
214
+ resolvedPath: this.resolveModule(moduleSpecifier, sourceFile)
215
+ });
216
+ });
217
+
218
+ return imports;
219
+ }
220
+
221
+ /**
222
+ * Extract function calls (cho C047 analysis)
223
+ */
224
+ extractFunctionCalls(sourceFile) {
225
+ const calls = [];
226
+
227
+ sourceFile.getDescendantsOfKind(sourceFile.getKindName().CallExpression || 210).forEach(callExpr => {
228
+ const expression = callExpr.getExpression();
229
+
230
+ calls.push({
231
+ functionName: expression.getText(),
232
+ arguments: callExpr.getArguments().map(arg => ({
233
+ text: arg.getText(),
234
+ type: this.getExpressionType(arg),
235
+ line: arg.getStartLineNumber()
236
+ })),
237
+ line: callExpr.getStartLineNumber(),
238
+ column: callExpr.getStartColumnNumber(),
239
+
240
+ // Detailed analysis for retry patterns
241
+ isRetryPattern: this.isRetryPattern(callExpr),
242
+ isConditionalCall: this.isConditionalCall(callExpr),
243
+ parentContext: this.getParentContext(callExpr)
244
+ });
245
+ });
246
+
247
+ return calls;
248
+ }
249
+
250
+ /**
251
+ * Extract React hooks usage
252
+ */
253
+ extractHooks(sourceFile) {
254
+ const hooks = [];
255
+
256
+ sourceFile.getDescendantsOfKind(sourceFile.getKindName().CallExpression || 210).forEach(callExpr => {
257
+ const expression = callExpr.getExpression();
258
+ const functionName = expression.getText();
259
+
260
+ // Detect hook patterns
261
+ if (functionName.startsWith('use') || this.isKnownHook(functionName)) {
262
+ hooks.push({
263
+ hookName: functionName,
264
+ arguments: callExpr.getArguments().map(arg => arg.getText()),
265
+ line: callExpr.getStartLineNumber(),
266
+
267
+ // Special analysis for useQuery, useMutation, etc.
268
+ isQueryHook: this.isQueryHook(functionName),
269
+ retryConfig: this.extractRetryConfig(callExpr)
270
+ });
271
+ }
272
+ });
273
+
274
+ return hooks;
275
+ }
276
+
277
+ /**
278
+ * Analyze cross-file dependencies
279
+ */
280
+ async analyzeDependencies(sourceFile) {
281
+ const dependencies = [];
282
+
283
+ // Analyze imported symbols usage
284
+ sourceFile.getImportDeclarations().forEach(importDecl => {
285
+ const moduleSpecifier = importDecl.getModuleSpecifierValue();
286
+ const resolvedPath = this.resolveModule(moduleSpecifier, sourceFile);
287
+
288
+ if (resolvedPath && this.project.getSourceFile(resolvedPath)) {
289
+ dependencies.push({
290
+ type: 'import',
291
+ module: moduleSpecifier,
292
+ resolvedPath,
293
+ usages: this.findSymbolUsages(sourceFile, importDecl.getNamedImports())
294
+ });
295
+ }
296
+ });
297
+
298
+ return dependencies;
299
+ }
300
+
301
+ /**
302
+ * Utility methods for pattern detection
303
+ */
304
+ isRetryPattern(callExpr) {
305
+ const functionName = callExpr.getExpression().getText();
306
+
307
+ // Known retry functions
308
+ const retryFunctions = ['retry', 'retries', 'withRetry', 'retryWhen'];
309
+ if (retryFunctions.some(fn => functionName.includes(fn))) {
310
+ return true;
311
+ }
312
+
313
+ // Check for retry configuration in arguments
314
+ const args = callExpr.getArguments();
315
+ return args.some(arg => {
316
+ const argText = arg.getText();
317
+ return /retry|retries/i.test(argText);
318
+ });
319
+ }
320
+
321
+ isQueryHook(functionName) {
322
+ const queryHooks = ['useQuery', 'useMutation', 'useInfiniteQuery', 'useSuspenseQuery'];
323
+ return queryHooks.includes(functionName);
324
+ }
325
+
326
+ extractRetryConfig(callExpr) {
327
+ const args = callExpr.getArguments();
328
+
329
+ // Look for retry configuration in arguments
330
+ for (const arg of args) {
331
+ const argText = arg.getText();
332
+
333
+ // Object literal with retry config
334
+ if (arg.getKind() === 204) { // ObjectLiteralExpression
335
+ const retryProperty = arg.getProperties().find(prop =>
336
+ prop.getName && prop.getName() === 'retry'
337
+ );
338
+
339
+ if (retryProperty) {
340
+ return {
341
+ hasRetryConfig: true,
342
+ retryValue: retryProperty.getValueNode()?.getText(),
343
+ line: retryProperty.getStartLineNumber()
344
+ };
345
+ }
346
+ }
347
+ }
348
+
349
+ return { hasRetryConfig: false };
350
+ }
351
+
352
+ /**
353
+ * Resolve module path
354
+ */
355
+ resolveModule(moduleSpecifier, sourceFile) {
356
+ try {
357
+ // Use ts-morph's resolution if available
358
+ if (this.options.enableTypeChecker && sourceFile.getProject().getTypeChecker) {
359
+ const symbol = sourceFile.getProject().getTypeChecker()
360
+ .getSymbolAtLocation(sourceFile.getImportDeclarations()
361
+ .find(imp => imp.getModuleSpecifierValue() === moduleSpecifier)
362
+ ?.getModuleSpecifier());
363
+
364
+ if (symbol?.getDeclarations()?.[0]) {
365
+ return symbol.getDeclarations()[0].getSourceFile().getFilePath();
366
+ }
367
+ }
368
+
369
+ // Basic resolution
370
+ if (moduleSpecifier.startsWith('.')) {
371
+ const dir = path.dirname(sourceFile.getFilePath());
372
+ return path.resolve(dir, moduleSpecifier);
373
+ }
374
+
375
+ return null;
376
+ } catch (error) {
377
+ return null;
378
+ }
379
+ }
380
+
381
+ /**
382
+ * Memory and cache management
383
+ */
384
+ cacheSymbolTable(filePath, symbolTable) {
385
+ // Check memory limits
386
+ if (this.fileCache.size >= this.options.maxCacheSize) {
387
+ this.evictOldestCache();
388
+ }
389
+
390
+ this.fileCache.set(filePath, symbolTable);
391
+ this.updateMemoryStats();
392
+ }
393
+
394
+ evictOldestCache() {
395
+ // Simple LRU eviction
396
+ const oldest = this.fileCache.keys().next().value;
397
+ this.fileCache.delete(oldest);
398
+ }
399
+
400
+ updateMemoryStats() {
401
+ this.stats.memoryUsage = process.memoryUsage().heapUsed;
402
+ }
403
+
404
+ /**
405
+ * Configuration discovery
406
+ */
407
+ async findTsConfig(projectPath) {
408
+ const candidates = [
409
+ path.join(projectPath, 'tsconfig.json'),
410
+ path.join(projectPath, 'jsconfig.json'),
411
+ path.join(projectPath, '..', 'tsconfig.json')
412
+ ];
413
+
414
+ for (const candidate of candidates) {
415
+ try {
416
+ await fs.access(candidate);
417
+ return candidate;
418
+ } catch (error) {
419
+ continue;
420
+ }
421
+ }
422
+
423
+ return null;
424
+ }
425
+
426
+ /**
427
+ * Discover target files with intelligent filtering for large projects
428
+ * Optimized for projects with 3000+ files, 800-1000 lines each
429
+ */
430
+ async discoverTargetFiles(projectPath) {
431
+ const fs = await import('fs');
432
+ const glob = require('glob');
433
+
434
+ try {
435
+ const patterns = [
436
+ '**/*.ts',
437
+ '**/*.tsx',
438
+ '**/*.js', // Include JS files for semantic analysis
439
+ '**/*.jsx' // Include JSX files
440
+ // Both TS and JS files for comprehensive analysis
441
+ ];
442
+
443
+ // Exclude common directories and large files
444
+ const excludePatterns = [
445
+ '**/node_modules/**',
446
+ '**/dist/**',
447
+ '**/build/**',
448
+ '**/coverage/**',
449
+ '**/.git/**',
450
+ '**/.next/**',
451
+ '**/out/**',
452
+ '**/*.min.js',
453
+ '**/*.min.ts',
454
+ '**/*.d.ts', // Skip declaration files
455
+ '**/vendor/**',
456
+ '**/third-party/**'
457
+ ];
458
+
459
+ // Find all matching files
460
+ const allFiles = [];
461
+ for (const pattern of patterns) {
462
+ const globPattern = path.join(projectPath, pattern);
463
+ const files = glob.sync(globPattern, {
464
+ ignore: excludePatterns.map(exclude => path.join(projectPath, exclude))
465
+ });
466
+ allFiles.push(...files);
467
+ }
468
+
469
+ // Filter by file size for memory optimization
470
+ const targetFiles = [];
471
+ for (const filePath of allFiles) {
472
+ try {
473
+ const stats = await fs.stat(filePath);
474
+ // Skip files larger than 100KB (typically auto-generated)
475
+ if (stats.size < 100 * 1024) {
476
+ targetFiles.push(filePath);
477
+ } else {
478
+ console.debug(`⚠️ Skipping large file: ${path.basename(filePath)} (${Math.round(stats.size / 1024)}KB)`);
479
+ }
480
+ } catch (error) {
481
+ // Skip files that can't be stat'd
482
+ continue;
483
+ }
484
+ }
485
+
486
+ console.log(`📁 File discovery: ${targetFiles.length}/${allFiles.length} files selected (memory optimized)`);
487
+ return targetFiles;
488
+
489
+ } catch (error) {
490
+ console.warn(`⚠️ File discovery failed, using basic patterns:`, error.message);
491
+ return this.discoverSourceFiles(projectPath);
492
+ }
493
+ }
494
+
495
+ async discoverSourceFiles(projectPath) {
496
+ const patterns = [
497
+ '**/*.ts',
498
+ '**/*.tsx',
499
+ '**/*.js',
500
+ '**/*.jsx'
501
+ ];
502
+
503
+ // Exclude common directories
504
+ const excludePatterns = [
505
+ '**/node_modules/**',
506
+ '**/dist/**',
507
+ '**/build/**',
508
+ '**/.git/**'
509
+ ];
510
+
511
+ return patterns.map(pattern => path.join(projectPath, pattern))
512
+ .filter(filePath => !excludePatterns.some(exclude =>
513
+ filePath.includes(exclude.replace('**/', ''))
514
+ ));
515
+ }
516
+
517
+ /**
518
+ * Cleanup and statistics
519
+ */
520
+ async cleanup() {
521
+ if (this.project) {
522
+ // Clear caches
523
+ this.fileCache.clear();
524
+ this.symbolTable.clear();
525
+
526
+ console.log(`📊 Semantic Engine Stats:`);
527
+ console.log(` 📄 Files analyzed: ${this.stats.filesAnalyzed}`);
528
+ console.log(` 🎯 Cache hits: ${this.stats.cacheHits}`);
529
+ console.log(` ❌ Cache misses: ${this.stats.cacheMisses}`);
530
+ console.log(` 💾 Memory usage: ${Math.round(this.stats.memoryUsage / 1024 / 1024)}MB`);
531
+ }
532
+ }
533
+
534
+ getStats() {
535
+ return {
536
+ ...this.stats,
537
+ cacheSize: this.fileCache.size,
538
+ symbolTableSize: this.symbolTable.size,
539
+ isInitialized: this.initialized
540
+ };
541
+ }
542
+
543
+ // Stub methods for full extraction implementation
544
+ extractExports(sourceFile) { return []; }
545
+ extractFunctions(sourceFile) { return []; }
546
+ extractClasses(sourceFile) { return []; }
547
+ extractInterfaces(sourceFile) { return []; }
548
+ extractVariables(sourceFile) { return []; }
549
+ extractConstants(sourceFile) { return []; }
550
+ extractComponents(sourceFile) { return []; }
551
+ extractMethodCalls(sourceFile) { return []; }
552
+ extractCrossFileReferences(sourceFile) { return []; }
553
+ getExpressionType(expr) { return 'unknown'; }
554
+ isConditionalCall(callExpr) { return false; }
555
+ getParentContext(callExpr) { return null; }
556
+ isKnownHook(functionName) { return false; }
557
+ findSymbolUsages(sourceFile, namedImports) { return []; }
558
+ }
559
+
560
+ module.exports = SemanticEngine;