@sun-asterisk/sunlint 1.3.2 → 1.3.4
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 +73 -0
- package/README.md +5 -3
- package/config/rules/enhanced-rules-registry.json +144 -33
- package/core/analysis-orchestrator.js +173 -42
- package/core/auto-performance-manager.js +243 -0
- package/core/cli-action-handler.js +24 -2
- package/core/cli-program.js +19 -5
- package/core/constants/defaults.js +56 -0
- package/core/performance-optimizer.js +271 -0
- package/docs/FILE_LIMITS_COMPLETION_REPORT.md +151 -0
- package/docs/FILE_LIMITS_EXPLANATION.md +190 -0
- package/docs/PERFORMANCE.md +311 -0
- package/docs/PERFORMANCE_MIGRATION_GUIDE.md +368 -0
- package/docs/PERFORMANCE_OPTIMIZATION_PLAN.md +255 -0
- package/docs/QUICK_FILE_LIMITS.md +64 -0
- package/docs/SIMPLIFIED_USAGE_GUIDE.md +208 -0
- package/engines/engine-factory.js +7 -0
- package/engines/heuristic-engine.js +182 -5
- package/package.json +2 -1
- package/rules/common/C048_no_bypass_architectural_layers/analyzer.js +180 -0
- package/rules/common/C048_no_bypass_architectural_layers/config.json +50 -0
- package/rules/common/C048_no_bypass_architectural_layers/symbol-based-analyzer.js +235 -0
- package/rules/common/C052_parsing_or_data_transformation/analyzer.js +180 -0
- package/rules/common/C052_parsing_or_data_transformation/config.json +50 -0
- package/rules/common/C052_parsing_or_data_transformation/symbol-based-analyzer.js +132 -0
- package/rules/index.js +2 -0
- package/rules/security/S017_use_parameterized_queries/README.md +128 -0
- package/rules/security/S017_use_parameterized_queries/analyzer.js +286 -0
- package/rules/security/S017_use_parameterized_queries/config.json +109 -0
- package/rules/security/S017_use_parameterized_queries/regex-based-analyzer.js +541 -0
- package/rules/security/S017_use_parameterized_queries/symbol-based-analyzer.js +777 -0
- package/rules/security/S031_secure_session_cookies/README.md +127 -0
- package/rules/security/S031_secure_session_cookies/analyzer.js +245 -0
- package/rules/security/S031_secure_session_cookies/config.json +86 -0
- package/rules/security/S031_secure_session_cookies/regex-based-analyzer.js +196 -0
- package/rules/security/S031_secure_session_cookies/symbol-based-analyzer.js +1084 -0
- package/rules/security/S032_httponly_session_cookies/FRAMEWORK_SUPPORT.md +209 -0
- package/rules/security/S032_httponly_session_cookies/README.md +184 -0
- package/rules/security/S032_httponly_session_cookies/analyzer.js +282 -0
- package/rules/security/S032_httponly_session_cookies/config.json +96 -0
- package/rules/security/S032_httponly_session_cookies/regex-based-analyzer.js +715 -0
- package/rules/security/S032_httponly_session_cookies/symbol-based-analyzer.js +1348 -0
- package/rules/security/S033_samesite_session_cookies/README.md +227 -0
- package/rules/security/S033_samesite_session_cookies/analyzer.js +242 -0
- package/rules/security/S033_samesite_session_cookies/config.json +87 -0
- package/rules/security/S033_samesite_session_cookies/regex-based-analyzer.js +703 -0
- package/rules/security/S033_samesite_session_cookies/symbol-based-analyzer.js +732 -0
- package/rules/security/S034_host_prefix_session_cookies/README.md +204 -0
- package/rules/security/S034_host_prefix_session_cookies/analyzer.js +290 -0
- package/rules/security/S034_host_prefix_session_cookies/config.json +62 -0
- package/rules/security/S034_host_prefix_session_cookies/regex-based-analyzer.js +478 -0
- package/rules/security/S034_host_prefix_session_cookies/symbol-based-analyzer.js +277 -0
- package/rules/security/S035_path_session_cookies/README.md +257 -0
- package/rules/security/S035_path_session_cookies/analyzer.js +316 -0
- package/rules/security/S035_path_session_cookies/config.json +99 -0
- package/rules/security/S035_path_session_cookies/regex-based-analyzer.js +724 -0
- package/rules/security/S035_path_session_cookies/symbol-based-analyzer.js +373 -0
- package/scripts/batch-processing-demo.js +334 -0
- package/scripts/performance-test.js +541 -0
- package/scripts/quick-performance-test.js +108 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 🚀 Auto Performance Manager for SunLint
|
|
3
|
+
* Automatically detects optimal performance settings based on project characteristics
|
|
4
|
+
* GOAL: Simplify CLI by reducing user choices while maintaining performance
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
class AutoPerformanceManager {
|
|
11
|
+
constructor() {
|
|
12
|
+
// Smart defaults based on project analysis
|
|
13
|
+
this.performanceProfiles = {
|
|
14
|
+
auto: {
|
|
15
|
+
name: 'Auto-Detect',
|
|
16
|
+
detect: true,
|
|
17
|
+
description: 'Automatically choose best settings based on project size'
|
|
18
|
+
},
|
|
19
|
+
fast: {
|
|
20
|
+
name: 'Fast',
|
|
21
|
+
timeout: 30000, // 30s
|
|
22
|
+
batchSize: 20,
|
|
23
|
+
maxFiles: 500,
|
|
24
|
+
description: 'Quick analysis for small projects (<100 files)'
|
|
25
|
+
},
|
|
26
|
+
careful: {
|
|
27
|
+
name: 'Careful',
|
|
28
|
+
timeout: 120000, // 2 minutes
|
|
29
|
+
batchSize: 10,
|
|
30
|
+
maxFiles: 1500,
|
|
31
|
+
progressiveResults: true,
|
|
32
|
+
description: 'Thorough analysis for large projects (>500 files)'
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 🎯 Get optimal performance settings with minimal user input
|
|
39
|
+
* Clarifies the difference between max-files and max-semantic-files:
|
|
40
|
+
* - max-files: Total files to analyze (performance limit)
|
|
41
|
+
* - max-semantic-files: Files to load into TypeScript symbol table (memory limit)
|
|
42
|
+
*/
|
|
43
|
+
getOptimalSettings(options, targetFiles = []) {
|
|
44
|
+
const mode = options.performance || 'auto';
|
|
45
|
+
|
|
46
|
+
if (mode === 'auto') {
|
|
47
|
+
return this.autoDetectSettings(options, targetFiles);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return this.getProfileSettings(mode, options);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 🤖 Auto-detect optimal settings based on project characteristics
|
|
55
|
+
*/
|
|
56
|
+
autoDetectSettings(options, targetFiles) {
|
|
57
|
+
const projectAnalysis = this.analyzeProject(options, targetFiles);
|
|
58
|
+
const profile = this.selectOptimalProfile(projectAnalysis);
|
|
59
|
+
|
|
60
|
+
if (options.verbose) {
|
|
61
|
+
console.log(`🤖 Auto-detected performance profile: ${profile.name}`);
|
|
62
|
+
console.log(` 📊 Project: ${projectAnalysis.fileCount} files, ${projectAnalysis.size} size`);
|
|
63
|
+
console.log(` ⚡ Settings: ${profile.timeout/1000}s timeout, ${profile.batchSize} rules/batch`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
...profile,
|
|
68
|
+
autoDetected: true,
|
|
69
|
+
projectAnalysis
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* 📊 Analyze project to determine optimal settings
|
|
75
|
+
*/
|
|
76
|
+
analyzeProject(options, targetFiles) {
|
|
77
|
+
const fileCount = targetFiles.length;
|
|
78
|
+
const inputPath = options.input || process.cwd();
|
|
79
|
+
|
|
80
|
+
// Estimate project complexity
|
|
81
|
+
const hasNodeModules = fs.existsSync(path.join(inputPath, 'node_modules'));
|
|
82
|
+
const hasPackageJson = fs.existsSync(path.join(inputPath, 'package.json'));
|
|
83
|
+
const hasTsConfig = fs.existsSync(path.join(inputPath, 'tsconfig.json'));
|
|
84
|
+
const hasGitIgnore = fs.existsSync(path.join(inputPath, '.gitignore'));
|
|
85
|
+
|
|
86
|
+
// Simple heuristics for project size
|
|
87
|
+
let size = 'small';
|
|
88
|
+
let complexity = 'simple';
|
|
89
|
+
|
|
90
|
+
if (fileCount > 1000) {
|
|
91
|
+
size = 'enterprise';
|
|
92
|
+
complexity = 'complex';
|
|
93
|
+
} else if (fileCount > 500) {
|
|
94
|
+
size = 'large';
|
|
95
|
+
complexity = hasNodeModules && hasTsConfig ? 'complex' : 'medium';
|
|
96
|
+
} else if (fileCount > 100) {
|
|
97
|
+
size = 'medium';
|
|
98
|
+
complexity = hasTsConfig ? 'medium' : 'simple';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
fileCount,
|
|
103
|
+
size,
|
|
104
|
+
complexity,
|
|
105
|
+
hasNodeModules,
|
|
106
|
+
hasPackageJson,
|
|
107
|
+
hasTsConfig,
|
|
108
|
+
hasGitIgnore,
|
|
109
|
+
inputPath
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 🎯 Select optimal profile based on project analysis
|
|
115
|
+
*/
|
|
116
|
+
selectOptimalProfile(analysis) {
|
|
117
|
+
if (analysis.fileCount <= 100) {
|
|
118
|
+
return {
|
|
119
|
+
name: 'Auto-Fast',
|
|
120
|
+
timeout: 30000,
|
|
121
|
+
batchSize: 20,
|
|
122
|
+
maxFiles: 200, // Analysis limit
|
|
123
|
+
maxSemanticFiles: 100, // Symbol table limit (smaller for memory)
|
|
124
|
+
description: `Small project (${analysis.fileCount} files) - fast analysis`
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (analysis.fileCount <= 500) {
|
|
129
|
+
return {
|
|
130
|
+
name: 'Auto-Balanced',
|
|
131
|
+
timeout: 60000,
|
|
132
|
+
batchSize: 15,
|
|
133
|
+
maxFiles: 600, // Analysis limit
|
|
134
|
+
maxSemanticFiles: 300, // Symbol table limit
|
|
135
|
+
progressiveResults: true,
|
|
136
|
+
description: `Medium project (${analysis.fileCount} files) - balanced analysis`
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (analysis.fileCount <= 1000) {
|
|
141
|
+
return {
|
|
142
|
+
name: 'Auto-Careful',
|
|
143
|
+
timeout: 120000,
|
|
144
|
+
batchSize: 10,
|
|
145
|
+
maxFiles: 1200, // Analysis limit
|
|
146
|
+
maxSemanticFiles: 500, // Symbol table limit
|
|
147
|
+
progressiveResults: true,
|
|
148
|
+
streamingAnalysis: analysis.complexity === 'complex',
|
|
149
|
+
description: `Large project (${analysis.fileCount} files) - careful analysis`
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Enterprise projects
|
|
154
|
+
return {
|
|
155
|
+
name: 'Auto-Enterprise',
|
|
156
|
+
timeout: 300000,
|
|
157
|
+
batchSize: 5,
|
|
158
|
+
maxFiles: 1500, // Analysis limit
|
|
159
|
+
maxSemanticFiles: 300, // Conservative symbol table limit
|
|
160
|
+
progressiveResults: true,
|
|
161
|
+
streamingAnalysis: true,
|
|
162
|
+
smartSampling: true,
|
|
163
|
+
description: `Enterprise project (${analysis.fileCount} files) - conservative analysis`
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* ⚙️ Get predefined profile settings
|
|
169
|
+
*/
|
|
170
|
+
getProfileSettings(mode, options) {
|
|
171
|
+
const profile = this.performanceProfiles[mode];
|
|
172
|
+
|
|
173
|
+
if (!profile) {
|
|
174
|
+
console.warn(`⚠️ Unknown performance mode: ${mode}, using auto-detect`);
|
|
175
|
+
return this.autoDetectSettings(options, []);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Override with user-specified options
|
|
179
|
+
const settings = { ...profile };
|
|
180
|
+
|
|
181
|
+
if (options.timeout && options.timeout !== '0') {
|
|
182
|
+
settings.timeout = parseInt(options.timeout);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (options.maxFiles && options.maxFiles !== '1000') {
|
|
186
|
+
settings.maxFiles = parseInt(options.maxFiles);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return settings;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* 📋 Get user-friendly performance recommendations
|
|
194
|
+
*/
|
|
195
|
+
getPerformanceRecommendations(options, targetFiles) {
|
|
196
|
+
const analysis = this.analyzeProject(options, targetFiles);
|
|
197
|
+
const profile = this.selectOptimalProfile(analysis);
|
|
198
|
+
|
|
199
|
+
const recommendations = [
|
|
200
|
+
`🎯 Recommended: sunlint --all --input=${options.input || 'src'} --performance=auto`
|
|
201
|
+
];
|
|
202
|
+
|
|
203
|
+
if (analysis.fileCount > 500) {
|
|
204
|
+
recommendations.push(`💡 For faster results: sunlint --all --input=${options.input || 'src'} --performance=fast --max-files=300`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (analysis.complexity === 'complex') {
|
|
208
|
+
recommendations.push(`⚡ For thorough analysis: sunlint --all --input=${options.input || 'src'} --performance=careful --verbose`);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return {
|
|
212
|
+
analysis,
|
|
213
|
+
profile,
|
|
214
|
+
recommendations
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* 🎛️ Show simplified CLI usage for common scenarios
|
|
220
|
+
*/
|
|
221
|
+
static getSimplifiedUsageExamples() {
|
|
222
|
+
return {
|
|
223
|
+
quickStart: [
|
|
224
|
+
'sunlint --all --input=src', // Auto-detect everything
|
|
225
|
+
'sunlint --rules=C019,C041,S027 --input=src', // Specific rules
|
|
226
|
+
'sunlint --quality --input=src' // Quality rules only
|
|
227
|
+
],
|
|
228
|
+
performance: [
|
|
229
|
+
'sunlint --all --input=src --performance=auto', // Auto-detect (default)
|
|
230
|
+
'sunlint --all --input=src --performance=fast', // Quick scan
|
|
231
|
+
'sunlint --all --input=src --performance=careful', // Thorough analysis
|
|
232
|
+
'sunlint --all --input=src --timeout=60000' // Custom timeout
|
|
233
|
+
],
|
|
234
|
+
advanced: [
|
|
235
|
+
'sunlint --all --input=src --verbose', // See detailed progress
|
|
236
|
+
'sunlint --all --input=src --dry-run', // Preview analysis
|
|
237
|
+
'sunlint --all --input=src --format=json' // JSON output
|
|
238
|
+
]
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
module.exports = AutoPerformanceManager;
|
|
@@ -136,12 +136,20 @@ class CliActionHandler {
|
|
|
136
136
|
// Run analysis with new orchestrator
|
|
137
137
|
const results = await this.orchestrator.analyze(files, rulesToRun, {
|
|
138
138
|
...this.options,
|
|
139
|
+
timeout: parseInt(this.options.timeout) || 30000,
|
|
139
140
|
config: {
|
|
140
141
|
...config,
|
|
141
142
|
verbose: this.options.verbose,
|
|
142
143
|
quiet: this.options.quiet,
|
|
143
144
|
// Pass requested engine to enable strict engine mode (no fallback)
|
|
144
|
-
requestedEngine: this.options.engine
|
|
145
|
+
requestedEngine: this.options.engine,
|
|
146
|
+
// Performance optimization settings
|
|
147
|
+
performanceMode: this.options.performanceMode,
|
|
148
|
+
ruleBatchSize: parseInt(this.options.ruleBatchSize) || 10,
|
|
149
|
+
fileBatchSize: parseInt(this.options.fileBatchSize) || 50,
|
|
150
|
+
maxFiles: parseInt(this.options.maxFiles) || 1000,
|
|
151
|
+
enableFileFiltering: !this.options.noFileFiltering,
|
|
152
|
+
enableBatching: !this.options.noBatching
|
|
145
153
|
}
|
|
146
154
|
});
|
|
147
155
|
return results;
|
|
@@ -156,6 +164,20 @@ class CliActionHandler {
|
|
|
156
164
|
determineEnabledEngines(config) {
|
|
157
165
|
// If specific engine is requested via --engine option, use only that engine
|
|
158
166
|
if (this.options.engine) {
|
|
167
|
+
// Handle "auto" engine selection
|
|
168
|
+
if (this.options.engine === 'auto') {
|
|
169
|
+
// Auto-select best engines: default to heuristic for compatibility
|
|
170
|
+
const autoEngines = ['heuristic'];
|
|
171
|
+
|
|
172
|
+
// Add ESLint for JS/TS files if available
|
|
173
|
+
if (this.hasJavaScriptTypeScriptFiles() || config.eslint?.enabled !== false) {
|
|
174
|
+
autoEngines.push('eslint');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return autoEngines;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Return specific engine as requested
|
|
159
181
|
return [this.options.engine];
|
|
160
182
|
}
|
|
161
183
|
|
|
@@ -264,7 +286,7 @@ class CliActionHandler {
|
|
|
264
286
|
validateInput(config) {
|
|
265
287
|
// Validate engine option if specified (check this first, always)
|
|
266
288
|
if (this.options.engine) {
|
|
267
|
-
const validEngines = ['eslint', 'heuristic', 'openai'];
|
|
289
|
+
const validEngines = ['auto', 'eslint', 'heuristic', 'openai'];
|
|
268
290
|
if (!validEngines.includes(this.options.engine)) {
|
|
269
291
|
throw new Error(
|
|
270
292
|
chalk.red(`❌ Invalid engine: ${this.options.engine}\n`) +
|
package/core/cli-program.js
CHANGED
|
@@ -56,18 +56,22 @@ function createCliProgram() {
|
|
|
56
56
|
.option('--save-baseline <file>', 'Save current results as baseline')
|
|
57
57
|
.option('--fail-on-new-violations', 'Exit with error only on new violations (not existing)');
|
|
58
58
|
|
|
59
|
+
// Performance options (SIMPLIFIED)
|
|
60
|
+
program
|
|
61
|
+
.option('--timeout <milliseconds>', 'Analysis timeout in milliseconds (default: auto)', '0')
|
|
62
|
+
.option('--max-files <count>', 'Maximum files to analyze (default: auto-detect)', '0')
|
|
63
|
+
.option('--performance <mode>', 'Performance mode: auto, fast, careful (default: auto)', 'auto');
|
|
64
|
+
|
|
59
65
|
// Advanced options
|
|
60
66
|
program
|
|
61
|
-
.option('--engine <engine>', '
|
|
67
|
+
.option('--engine <engine>', 'Analysis engine (eslint,heuristic,auto)', 'auto')
|
|
62
68
|
.option('--dry-run', 'Show what would be analyzed without running')
|
|
63
69
|
.option('--verbose', 'Enable verbose logging')
|
|
64
70
|
.option('--quiet', 'Suppress non-error output')
|
|
65
71
|
.option('--debug', 'Enable debug mode')
|
|
66
72
|
.option('--ai', 'Enable AI-powered analysis')
|
|
67
|
-
.option('--no-ai', 'Force disable AI analysis
|
|
68
|
-
.option('--
|
|
69
|
-
.option('--modern', 'Use modern plugin-based architecture (default)')
|
|
70
|
-
.option('--max-semantic-files <number>', 'Control semantic analysis scope: 0=disable, -1=unlimited, >0=limit (default: 1000)', '1000')
|
|
73
|
+
.option('--no-ai', 'Force disable AI analysis')
|
|
74
|
+
.option('--max-semantic-files <number>', 'Symbol table file limit for TypeScript analysis (default: auto)', '0')
|
|
71
75
|
.option('--list-engines', 'List available analysis engines');
|
|
72
76
|
|
|
73
77
|
// ESLint Integration options
|
|
@@ -91,6 +95,16 @@ Examples:
|
|
|
91
95
|
|
|
92
96
|
File Targeting:
|
|
93
97
|
$ sunlint --all --include="src/**/*.ts" --exclude="**/*.test.*" --input=.
|
|
98
|
+
|
|
99
|
+
Performance (SIMPLIFIED):
|
|
100
|
+
$ sunlint --all --input=src --performance=auto # Auto-detect best settings
|
|
101
|
+
$ sunlint --all --input=src --performance=fast # Quick scan
|
|
102
|
+
$ sunlint --all --input=src --performance=careful # Thorough analysis
|
|
103
|
+
$ sunlint --all --input=src --timeout=60000 # Custom timeout (60s)
|
|
104
|
+
|
|
105
|
+
File Limits (when needed):
|
|
106
|
+
$ sunlint --all --input=src --max-files=500 # Limit total files analyzed
|
|
107
|
+
$ sunlint --all --input=src --max-semantic-files=200 # Limit TypeScript symbol table
|
|
94
108
|
$ sunlint --all --languages=typescript,dart --input=src
|
|
95
109
|
$ sunlint --typescript --exclude-tests --input=src
|
|
96
110
|
$ sunlint --all --only-source --include="src/**,lib/**" --input=.
|
|
@@ -91,6 +91,61 @@ const DEFAULT_LIMITS = {
|
|
|
91
91
|
MAX_OUTPUT_LINES: 1000 // Limit output to 1000 lines
|
|
92
92
|
};
|
|
93
93
|
|
|
94
|
+
/**
|
|
95
|
+
* Performance optimization defaults
|
|
96
|
+
*/
|
|
97
|
+
const DEFAULT_PERFORMANCE = {
|
|
98
|
+
// File filtering
|
|
99
|
+
ENABLE_FILE_FILTERING: true,
|
|
100
|
+
MAX_FILE_SIZE: 2 * 1024 * 1024, // 2MB per file
|
|
101
|
+
MAX_TOTAL_FILES: 1000, // Max 1000 files per analysis
|
|
102
|
+
|
|
103
|
+
// Batch processing
|
|
104
|
+
ENABLE_BATCHING: true,
|
|
105
|
+
RULE_BATCH_SIZE: 10, // Process 10 rules per batch
|
|
106
|
+
FILE_BATCH_SIZE: 50, // Process 50 files per batch
|
|
107
|
+
|
|
108
|
+
// Concurrency
|
|
109
|
+
MAX_CONCURRENT_BATCHES: 3, // Max 3 batches running simultaneously
|
|
110
|
+
|
|
111
|
+
// Memory management
|
|
112
|
+
ENABLE_MEMORY_MONITORING: true,
|
|
113
|
+
MAX_HEAP_SIZE_MB: 512, // 512MB heap limit
|
|
114
|
+
GC_THRESHOLD_MB: 256, // Trigger GC at 256MB
|
|
115
|
+
|
|
116
|
+
// Timeouts (adaptive)
|
|
117
|
+
BASE_TIMEOUT_MS: 30000, // 30s base timeout
|
|
118
|
+
TIMEOUT_PER_FILE_MS: 100, // +100ms per file
|
|
119
|
+
TIMEOUT_PER_RULE_MS: 1000, // +1s per rule
|
|
120
|
+
MAX_TIMEOUT_MS: 120000, // 2 minutes max timeout
|
|
121
|
+
|
|
122
|
+
// Error recovery
|
|
123
|
+
ENABLE_ERROR_RECOVERY: true,
|
|
124
|
+
MAX_RETRIES: 2, // Retry failed batches up to 2 times
|
|
125
|
+
RETRY_DELAY_MS: 1000, // 1s delay between retries
|
|
126
|
+
|
|
127
|
+
// Exclusion patterns for performance
|
|
128
|
+
HIGH_PERFORMANCE_EXCLUDES: [
|
|
129
|
+
'**/node_modules/**',
|
|
130
|
+
'**/.next/**',
|
|
131
|
+
'**/dist/**',
|
|
132
|
+
'**/build/**',
|
|
133
|
+
'**/coverage/**',
|
|
134
|
+
'**/.git/**',
|
|
135
|
+
'**/target/**',
|
|
136
|
+
'**/out/**',
|
|
137
|
+
'**/*.min.js',
|
|
138
|
+
'**/*.bundle.js',
|
|
139
|
+
'**/vendor/**',
|
|
140
|
+
'**/lib/**',
|
|
141
|
+
'**/libs/**',
|
|
142
|
+
'**/.vscode/**',
|
|
143
|
+
'**/.idea/**',
|
|
144
|
+
'**/tmp/**',
|
|
145
|
+
'**/temp/**'
|
|
146
|
+
]
|
|
147
|
+
};
|
|
148
|
+
|
|
94
149
|
/**
|
|
95
150
|
* Default language extensions mapping
|
|
96
151
|
*/
|
|
@@ -155,6 +210,7 @@ module.exports = {
|
|
|
155
210
|
DEFAULT_SEVERITIES,
|
|
156
211
|
DEFAULT_TIMEOUTS,
|
|
157
212
|
DEFAULT_LIMITS,
|
|
213
|
+
DEFAULT_PERFORMANCE,
|
|
158
214
|
DEFAULT_LANGUAGE_EXTENSIONS,
|
|
159
215
|
|
|
160
216
|
// Utility functions
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Performance Optimization Module for SunLint v1.3.2
|
|
3
|
+
* Comprehensive optimizations to handle large projects efficiently
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const fs = require('fs');
|
|
7
|
+
const path = require('path');
|
|
8
|
+
const { DEFAULT_PERFORMANCE } = require('./constants/defaults');
|
|
9
|
+
|
|
10
|
+
class PerformanceOptimizer {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.excludePatterns = DEFAULT_PERFORMANCE.HIGH_PERFORMANCE_EXCLUDES;
|
|
13
|
+
this.fileSizeLimits = {
|
|
14
|
+
maxFileSize: DEFAULT_PERFORMANCE.MAX_FILE_SIZE,
|
|
15
|
+
maxTotalFiles: DEFAULT_PERFORMANCE.MAX_TOTAL_FILES
|
|
16
|
+
};
|
|
17
|
+
this.config = {};
|
|
18
|
+
this.initialized = false;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Initialize performance optimizer with configuration
|
|
23
|
+
*/
|
|
24
|
+
async initialize(config = {}) {
|
|
25
|
+
this.config = {
|
|
26
|
+
...DEFAULT_PERFORMANCE,
|
|
27
|
+
...config
|
|
28
|
+
};
|
|
29
|
+
this.initialized = true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Main optimization method called by analysis orchestrator
|
|
34
|
+
*/
|
|
35
|
+
async optimizeAnalysis(files, rules, config) {
|
|
36
|
+
const startTime = Date.now();
|
|
37
|
+
|
|
38
|
+
// Filter files for performance
|
|
39
|
+
const optimizedFiles = this.config.enableFileFiltering !== false
|
|
40
|
+
? await this.smartFileFilter(files)
|
|
41
|
+
: files;
|
|
42
|
+
|
|
43
|
+
// Apply rule batching if enabled
|
|
44
|
+
const optimizedRules = this.config.enableBatching !== false
|
|
45
|
+
? rules
|
|
46
|
+
: rules;
|
|
47
|
+
|
|
48
|
+
const performanceMetrics = {
|
|
49
|
+
originalFiles: files.length,
|
|
50
|
+
optimizedFiles: optimizedFiles.length,
|
|
51
|
+
filteredFiles: files.length - optimizedFiles.length,
|
|
52
|
+
originalRules: rules.length,
|
|
53
|
+
optimizedRules: optimizedRules.length,
|
|
54
|
+
ruleBatches: this.calculateBatchCount(rules, config),
|
|
55
|
+
optimizationTime: Date.now() - startTime
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
if (config.verbose) {
|
|
59
|
+
console.log(`⚡ Performance optimization: ${performanceMetrics.filteredFiles} files filtered, ${performanceMetrics.ruleBatches} batches`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return {
|
|
63
|
+
optimizedFiles,
|
|
64
|
+
optimizedRules,
|
|
65
|
+
performanceMetrics
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Calculate number of batches for rules
|
|
71
|
+
*/
|
|
72
|
+
calculateBatchCount(rules, config) {
|
|
73
|
+
const batchSize = config.ruleBatchSize || this.config.RULE_BATCH_SIZE;
|
|
74
|
+
return Math.ceil(rules.length / batchSize);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Smart file filtering to exclude performance-heavy directories
|
|
79
|
+
*/
|
|
80
|
+
async smartFileFilter(files) {
|
|
81
|
+
const filtered = [];
|
|
82
|
+
let totalSize = 0;
|
|
83
|
+
|
|
84
|
+
for (const file of files) {
|
|
85
|
+
// Skip if matches exclude patterns
|
|
86
|
+
if (this.shouldExcludeFile(file)) {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
const stats = await fs.promises.stat(file);
|
|
92
|
+
|
|
93
|
+
// Skip large files
|
|
94
|
+
if (stats.size > this.fileSizeLimits.maxFileSize) {
|
|
95
|
+
if (this.config.verbose) {
|
|
96
|
+
console.log(`⚠️ Skipping large file: ${path.basename(file)} (${(stats.size / 1024 / 1024).toFixed(1)}MB)`);
|
|
97
|
+
}
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Check total size limit
|
|
102
|
+
if (totalSize + stats.size > this.fileSizeLimits.maxTotalSize) {
|
|
103
|
+
if (this.config.verbose) {
|
|
104
|
+
console.log(`⚠️ Reached total size limit, stopping at ${filtered.length} files`);
|
|
105
|
+
}
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Check file count limit
|
|
110
|
+
if (filtered.length >= this.fileSizeLimits.maxTotalFiles) {
|
|
111
|
+
if (this.config.verbose) {
|
|
112
|
+
console.log(`⚠️ Reached file count limit: ${this.fileSizeLimits.maxTotalFiles} files`);
|
|
113
|
+
}
|
|
114
|
+
break;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
filtered.push(file);
|
|
118
|
+
totalSize += stats.size;
|
|
119
|
+
|
|
120
|
+
} catch (error) {
|
|
121
|
+
// Skip files we can't read
|
|
122
|
+
if (this.config.verbose) {
|
|
123
|
+
console.warn(`⚠️ Cannot read file ${file}: ${error.message}`);
|
|
124
|
+
}
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (this.config.verbose) {
|
|
130
|
+
console.log(`📊 Performance filter: ${filtered.length}/${files.length} files (${(totalSize / 1024 / 1024).toFixed(1)}MB)`);
|
|
131
|
+
}
|
|
132
|
+
return filtered;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
shouldExcludeFile(filePath) {
|
|
136
|
+
const normalizedPath = filePath.replace(/\\/g, '/');
|
|
137
|
+
|
|
138
|
+
return this.excludePatterns.some(pattern => {
|
|
139
|
+
const regex = this.globToRegex(pattern);
|
|
140
|
+
const match = regex.test(normalizedPath);
|
|
141
|
+
return match;
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
globToRegex(glob) {
|
|
146
|
+
// Simple but effective glob to regex conversion
|
|
147
|
+
let regex = glob
|
|
148
|
+
.replace(/\./g, '\\.') // Escape dots
|
|
149
|
+
.replace(/\*\*/g, '___DOUBLE_STAR___') // Temp placeholder
|
|
150
|
+
.replace(/\*/g, '[^/]*') // Single * matches within path segment
|
|
151
|
+
.replace(/___DOUBLE_STAR___/g, '.*') // ** matches across path segments
|
|
152
|
+
.replace(/\?/g, '[^/]'); // ? matches single character
|
|
153
|
+
|
|
154
|
+
// Ensure pattern matches anywhere in the path
|
|
155
|
+
if (!regex.startsWith('.*')) {
|
|
156
|
+
regex = '.*' + regex;
|
|
157
|
+
}
|
|
158
|
+
if (!regex.endsWith('.*')) {
|
|
159
|
+
regex = regex + '.*';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return new RegExp(regex, 'i');
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Adaptive timeout based on file count and rules
|
|
167
|
+
*/
|
|
168
|
+
calculateAdaptiveTimeout(fileCount, ruleCount, baseTimeout = 30000) {
|
|
169
|
+
const perFileMs = this.config.TIMEOUT_PER_FILE_MS || 100;
|
|
170
|
+
const perRuleMs = this.config.TIMEOUT_PER_RULE_MS || 1000;
|
|
171
|
+
const maxTimeout = this.config.MAX_TIMEOUT_MS || 120000;
|
|
172
|
+
|
|
173
|
+
const adaptiveTimeout = Math.min(
|
|
174
|
+
baseTimeout + (fileCount * perFileMs) + (ruleCount * perRuleMs),
|
|
175
|
+
maxTimeout
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
if (this.config.verbose) {
|
|
179
|
+
console.log(`⏱️ Adaptive timeout: ${(adaptiveTimeout / 1000).toFixed(1)}s for ${fileCount} files, ${ruleCount} rules`);
|
|
180
|
+
}
|
|
181
|
+
return adaptiveTimeout;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Memory-aware rule batching
|
|
186
|
+
*/
|
|
187
|
+
createRuleBatches(rules, config = {}) {
|
|
188
|
+
const fileCount = config.fileCount || 100;
|
|
189
|
+
const batchSize = config.ruleBatchSize || (fileCount > 100 ? 5 : 10);
|
|
190
|
+
const batches = [];
|
|
191
|
+
|
|
192
|
+
for (let i = 0; i < rules.length; i += batchSize) {
|
|
193
|
+
batches.push(rules.slice(i, i + batchSize));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (this.config.verbose) {
|
|
197
|
+
console.log(`📦 Created ${batches.length} rule batches (${batchSize} rules each)`);
|
|
198
|
+
}
|
|
199
|
+
return batches;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Enhanced error recovery with context
|
|
204
|
+
*/
|
|
205
|
+
handleAnalysisError(error, context = {}) {
|
|
206
|
+
const errorInfo = {
|
|
207
|
+
message: error.message,
|
|
208
|
+
shouldRetry: false,
|
|
209
|
+
retryWithReducedBatch: false,
|
|
210
|
+
context
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
// Determine if error is recoverable
|
|
214
|
+
if (error.message.includes('timeout') ||
|
|
215
|
+
error.message.includes('timed out') ||
|
|
216
|
+
error.message.includes('Maximum call stack size exceeded')) {
|
|
217
|
+
errorInfo.shouldRetry = true;
|
|
218
|
+
errorInfo.retryWithReducedBatch = true;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (error.message.includes('ENOMEM') ||
|
|
222
|
+
error.message.includes('memory')) {
|
|
223
|
+
errorInfo.shouldRetry = true;
|
|
224
|
+
errorInfo.retryWithReducedBatch = true;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
return errorInfo;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Execute operation with error recovery
|
|
232
|
+
*/
|
|
233
|
+
async executeWithRecovery(operation, context = {}) {
|
|
234
|
+
const maxRetries = this.config.MAX_RETRIES || 2;
|
|
235
|
+
const retryDelay = this.config.RETRY_DELAY_MS || 1000;
|
|
236
|
+
|
|
237
|
+
for (let attempt = 1; attempt <= maxRetries + 1; attempt++) {
|
|
238
|
+
try {
|
|
239
|
+
return await operation();
|
|
240
|
+
} catch (error) {
|
|
241
|
+
if (attempt > maxRetries) {
|
|
242
|
+
throw error; // Final attempt failed
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const errorInfo = this.handleAnalysisError(error, context);
|
|
246
|
+
|
|
247
|
+
if (!errorInfo.shouldRetry) {
|
|
248
|
+
throw error; // Not recoverable
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (this.config.verbose) {
|
|
252
|
+
console.warn(`⚠️ Attempt ${attempt} failed, retrying in ${retryDelay}ms...`);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Wait before retry
|
|
256
|
+
await new Promise(resolve => setTimeout(resolve, retryDelay));
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Cleanup resources
|
|
263
|
+
*/
|
|
264
|
+
async cleanup() {
|
|
265
|
+
// Perform any necessary cleanup
|
|
266
|
+
this.initialized = false;
|
|
267
|
+
this.config = {};
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
module.exports = PerformanceOptimizer;
|