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