@sun-asterisk/sunlint 1.1.5 โ 1.1.7
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/config/rules/rules-registry.json +1 -1
- package/engines/eslint-engine.js +938 -33
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,76 @@
|
|
|
1
|
+
# ๐ SunLint v1.1.7 Release Notes
|
|
2
|
+
|
|
3
|
+
**Release Date**: July 24, 2025
|
|
4
|
+
**Type**: Minor Release (ESLint Engine Enhancement & Smart Installation Guidance)
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## ๐ **Key Improvements**
|
|
9
|
+
|
|
10
|
+
### ๐ง **ESLint Engine Enhancement**
|
|
11
|
+
- **Enhanced**: ESLint v9+ flat config support with automatic legacy config conversion
|
|
12
|
+
- **Improved**: Dynamic plugin loading with availability detection (React, TypeScript, React Hooks)
|
|
13
|
+
- **Robust**: Better error handling and parsing error filtering for TypeScript files
|
|
14
|
+
- **Smart**: Temporary flat config generation for legacy compatibility
|
|
15
|
+
|
|
16
|
+
### ๐ฏ **Smart Installation Guidance**
|
|
17
|
+
- **Intelligent**: Project type detection (NestJS, React, Next.js, Node.js)
|
|
18
|
+
- **Targeted**: Package manager detection (npm, yarn, pnpm) from package.json
|
|
19
|
+
- **Conditional**: Smart `--legacy-peer-deps` suggestion only when dependency conflicts detected
|
|
20
|
+
- **Clear**: Descriptive project-specific installation instructions
|
|
21
|
+
|
|
22
|
+
### ๐ง **Project Type Detection**
|
|
23
|
+
- **NestJS Projects**: `pnpm install --save-dev @typescript-eslint/parser @typescript-eslint/eslint-plugin`
|
|
24
|
+
- **React Projects**: `npm install --save-dev @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-react eslint-plugin-react-hooks`
|
|
25
|
+
- **Conflict Detection**: Automatic detection of date-fns, React version conflicts, ESLint v8 issues
|
|
26
|
+
|
|
27
|
+
### ๐ฆ **Dependency Management**
|
|
28
|
+
- **Aggregated Warnings**: Consolidated messages for missing plugins instead of spam
|
|
29
|
+
- **Graceful Fallback**: Analysis continues even with missing plugins, filtering parsing errors
|
|
30
|
+
- **Cleanup**: Automatic temporary config file cleanup after analysis
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## ๐ **Technical Details**
|
|
35
|
+
|
|
36
|
+
### **ESLint Integration**
|
|
37
|
+
- **Config Detection**: Automatic detection of flat config vs legacy config
|
|
38
|
+
- **Plugin Availability**: Runtime detection of React, TypeScript, React Hooks plugins
|
|
39
|
+
- **Parser Support**: Conditional TypeScript parser loading based on availability
|
|
40
|
+
- **Rule Filtering**: Skip rules for unavailable plugins with clear messaging
|
|
41
|
+
|
|
42
|
+
### **Smart Guidance Logic**
|
|
43
|
+
- **Package Manager**: Detects preferred package manager from scripts and preinstall hooks
|
|
44
|
+
- **Conflict Detection**: Analyzes package.json for known dependency conflicts
|
|
45
|
+
- **Project Classification**: Distinguishes between frontend (React/Next.js) and backend (NestJS/Node.js) projects
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## ๐ **Usage Examples**
|
|
50
|
+
|
|
51
|
+
### **Minimal Installation (Works for basic analysis)**
|
|
52
|
+
```bash
|
|
53
|
+
npm install --save-dev @sun-asterisk/sunlint
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### **TypeScript Projects (Recommended)**
|
|
57
|
+
```bash
|
|
58
|
+
npm install --save-dev @sun-asterisk/sunlint typescript
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### **Full Installation (All project types)**
|
|
62
|
+
```bash
|
|
63
|
+
npm install --save-dev @sun-asterisk/sunlint eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-react eslint-plugin-react-hooks typescript
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## ๐ **What's Next**
|
|
69
|
+
|
|
70
|
+
SunLint v1.1.7 makes ESLint integration more robust and user-friendly with intelligent project detection and clear installation guidance. No more guessing what dependencies to install! ๐
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
1
74
|
# ๐ SunLint v1.1.0 Release Notes
|
|
2
75
|
|
|
3
76
|
**Release Date**: July 23, 2025
|
package/engines/eslint-engine.js
CHANGED
|
@@ -66,22 +66,14 @@ class ESLintEngine extends AnalysisEngineInterface {
|
|
|
66
66
|
// Check for ESLint dependencies first
|
|
67
67
|
dependencyChecker.checkAndNotify('eslint');
|
|
68
68
|
|
|
69
|
-
//
|
|
70
|
-
|
|
69
|
+
// Store config for later use in analyze()
|
|
70
|
+
this.config = config;
|
|
71
|
+
this.eslint = null; // Initialize later in analyze() with project path
|
|
71
72
|
|
|
72
|
-
// Initialize ESLint with base configuration (v9+ compatible)
|
|
73
|
-
this.eslint = new ESLint({
|
|
74
|
-
overrideConfigFile: null, // ESLint 9.x requires null instead of true
|
|
75
|
-
overrideConfig: this.createBaseConfig(),
|
|
76
|
-
fix: config?.fix || false,
|
|
77
|
-
cache: config?.cache || false,
|
|
78
|
-
cwd: process.cwd()
|
|
79
|
-
});
|
|
80
|
-
|
|
81
73
|
// Rule mapping already loaded in constructor
|
|
82
74
|
if (this.verbose) {
|
|
83
75
|
console.log(`๐ง [ESLintEngine] Initialize: Rule mapping size = ${this.ruleMapping.size}`);
|
|
84
|
-
console.log(`๐ง ESLint engine initialized`);
|
|
76
|
+
console.log(`๐ง ESLint engine initialized (ESLint instance will be created per-project)`);
|
|
85
77
|
}
|
|
86
78
|
|
|
87
79
|
this.initialized = true;
|
|
@@ -116,6 +108,429 @@ class ESLintEngine extends AnalysisEngineInterface {
|
|
|
116
108
|
}
|
|
117
109
|
}
|
|
118
110
|
|
|
111
|
+
/**
|
|
112
|
+
* Check for ESLint config files in project
|
|
113
|
+
* Following Rule C006: Verb-noun naming
|
|
114
|
+
* @param {string} projectPath - Path to the project being analyzed
|
|
115
|
+
* @returns {Object} Config file detection results
|
|
116
|
+
*/
|
|
117
|
+
detectESLintConfig(projectPath) {
|
|
118
|
+
const fs = require('fs');
|
|
119
|
+
const path = require('path');
|
|
120
|
+
|
|
121
|
+
const configFiles = {
|
|
122
|
+
flat: ['eslint.config.js', 'eslint.config.mjs'],
|
|
123
|
+
legacy: ['.eslintrc.js', '.eslintrc.json', '.eslintrc.yml', '.eslintrc.yaml', '.eslintrc'],
|
|
124
|
+
packageJson: 'package.json'
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const results = {
|
|
128
|
+
hasFlatConfig: false,
|
|
129
|
+
hasLegacyConfig: false,
|
|
130
|
+
hasPackageConfig: false,
|
|
131
|
+
foundFiles: []
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// Check for flat config files
|
|
135
|
+
for (const file of configFiles.flat) {
|
|
136
|
+
const filePath = path.join(projectPath, file);
|
|
137
|
+
if (fs.existsSync(filePath)) {
|
|
138
|
+
results.hasFlatConfig = true;
|
|
139
|
+
results.foundFiles.push(file);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Check for legacy config files
|
|
144
|
+
for (const file of configFiles.legacy) {
|
|
145
|
+
const filePath = path.join(projectPath, file);
|
|
146
|
+
if (fs.existsSync(filePath)) {
|
|
147
|
+
results.hasLegacyConfig = true;
|
|
148
|
+
results.foundFiles.push(file);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Check for package.json eslintConfig
|
|
153
|
+
const packagePath = path.join(projectPath, configFiles.packageJson);
|
|
154
|
+
if (fs.existsSync(packagePath)) {
|
|
155
|
+
try {
|
|
156
|
+
const packageJson = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
157
|
+
if (packageJson.eslintConfig) {
|
|
158
|
+
results.hasPackageConfig = true;
|
|
159
|
+
results.foundFiles.push('package.json (eslintConfig)');
|
|
160
|
+
}
|
|
161
|
+
} catch (error) {
|
|
162
|
+
// Ignore package.json parsing errors
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return results;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Create ESLint instance with proper configuration
|
|
171
|
+
* Following Rule C006: Verb-noun naming
|
|
172
|
+
* @param {string} projectPath - Path to the project being analyzed
|
|
173
|
+
* @returns {Promise<Object>} Configured ESLint instance
|
|
174
|
+
*/
|
|
175
|
+
async createESLintInstance(projectPath) {
|
|
176
|
+
try {
|
|
177
|
+
const { ESLint } = await this.loadESLint();
|
|
178
|
+
|
|
179
|
+
// Detect existing config
|
|
180
|
+
const configDetection = this.detectESLintConfig(projectPath);
|
|
181
|
+
console.log(`๐ [ESLintEngine] Config detection for ${projectPath}:`, configDetection);
|
|
182
|
+
|
|
183
|
+
let eslintOptions;
|
|
184
|
+
|
|
185
|
+
if (configDetection.hasFlatConfig) {
|
|
186
|
+
// Use flat config (ESLint v9+ preferred)
|
|
187
|
+
eslintOptions = {
|
|
188
|
+
overrideConfigFile: null, // Let ESLint find flat config automatically
|
|
189
|
+
fix: this.config?.fix || false,
|
|
190
|
+
cache: this.config?.cache || false,
|
|
191
|
+
cwd: projectPath
|
|
192
|
+
};
|
|
193
|
+
console.log(`โ
[ESLintEngine] Using flat config (modern ESLint v9+)`);
|
|
194
|
+
} else if (configDetection.hasLegacyConfig || configDetection.hasPackageConfig) {
|
|
195
|
+
// ESLint v9+ requires flat config - convert legacy config to flat config format
|
|
196
|
+
const flatConfig = await this.convertLegacyToFlatConfig(projectPath, configDetection);
|
|
197
|
+
eslintOptions = {
|
|
198
|
+
overrideConfigFile: null, // Use our generated flat config
|
|
199
|
+
overrideConfig: flatConfig,
|
|
200
|
+
fix: this.config?.fix || false,
|
|
201
|
+
cache: this.config?.cache || false,
|
|
202
|
+
cwd: projectPath
|
|
203
|
+
};
|
|
204
|
+
console.log(`โ
[ESLintEngine] Legacy config converted to flat config for ESLint v9+ compatibility`);
|
|
205
|
+
} else {
|
|
206
|
+
// No config found - use SunLint's base config only
|
|
207
|
+
eslintOptions = {
|
|
208
|
+
overrideConfig: this.createBaseConfig(),
|
|
209
|
+
fix: this.config?.fix || false,
|
|
210
|
+
cache: this.config?.cache || false,
|
|
211
|
+
cwd: projectPath
|
|
212
|
+
};
|
|
213
|
+
console.log(`โ ๏ธ [ESLintEngine] No ESLint config found, using SunLint base config only`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (this.verbose) {
|
|
217
|
+
console.log(`๐ [ESLintEngine] ESLint options:`, JSON.stringify(eslintOptions, null, 2));
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const eslint = new ESLint(eslintOptions);
|
|
221
|
+
console.log(`โ
[ESLintEngine] ESLint instance created successfully`);
|
|
222
|
+
|
|
223
|
+
return eslint;
|
|
224
|
+
} catch (error) {
|
|
225
|
+
console.error('Failed to create ESLint instance:', error.message);
|
|
226
|
+
throw error;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Extract rules array from eslint config
|
|
232
|
+
* @param {Object} eslintConfig - ESLint config object
|
|
233
|
+
* @returns {Array} Rules array for plugin detection
|
|
234
|
+
*/
|
|
235
|
+
extractRulesFromConfig(eslintConfig) {
|
|
236
|
+
// Convert rules object keys back to rule objects for plugin detection
|
|
237
|
+
const rules = [];
|
|
238
|
+
for (const ruleKey of Object.keys(eslintConfig.rules || {})) {
|
|
239
|
+
if (ruleKey.startsWith('custom/typescript_s')) {
|
|
240
|
+
rules.push({ id: ruleKey.replace('custom/typescript_', '').toUpperCase() });
|
|
241
|
+
} else if (ruleKey.startsWith('custom/')) {
|
|
242
|
+
rules.push({ id: ruleKey.replace('custom/', '').toUpperCase() });
|
|
243
|
+
} else if (ruleKey.startsWith('react/')) {
|
|
244
|
+
rules.push({ id: 'R001' }); // Mock React rule for detection
|
|
245
|
+
} else if (ruleKey.includes('@typescript-eslint/')) {
|
|
246
|
+
rules.push({ id: 'T001' }); // Mock TypeScript rule for detection
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return rules;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Create temporary flat config file for legacy compatibility
|
|
254
|
+
* Following Rule C006: Verb-noun naming
|
|
255
|
+
* @param {string} projectPath - Path to the project
|
|
256
|
+
* @param {Object} configDetection - Config detection results
|
|
257
|
+
* @param {Object} eslintConfig - Analysis config to merge
|
|
258
|
+
* @returns {Promise<string>} Path to temporary flat config file
|
|
259
|
+
*/
|
|
260
|
+
async createTemporaryFlatConfig(projectPath, configDetection, eslintConfig) {
|
|
261
|
+
const fs = require('fs');
|
|
262
|
+
const path = require('path');
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
let baseConfig;
|
|
266
|
+
|
|
267
|
+
if (configDetection.hasFlatConfig) {
|
|
268
|
+
// Load existing flat config
|
|
269
|
+
const existingConfigPath = path.join(projectPath, 'eslint.config.js');
|
|
270
|
+
if (fs.existsSync(existingConfigPath)) {
|
|
271
|
+
try {
|
|
272
|
+
// Read and parse existing flat config
|
|
273
|
+
const configContent = fs.readFileSync(existingConfigPath, 'utf8');
|
|
274
|
+
// For now, use a simple base config - parsing dynamic imports is complex
|
|
275
|
+
baseConfig = {
|
|
276
|
+
files: ['**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx'],
|
|
277
|
+
languageOptions: {
|
|
278
|
+
ecmaVersion: 'latest',
|
|
279
|
+
sourceType: 'module',
|
|
280
|
+
parserOptions: {
|
|
281
|
+
ecmaFeatures: {
|
|
282
|
+
jsx: true
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
rules: {}
|
|
287
|
+
};
|
|
288
|
+
} catch (error) {
|
|
289
|
+
console.warn(`โ ๏ธ [ESLintEngine] Failed to parse existing flat config: ${error.message}`);
|
|
290
|
+
baseConfig = this.createBaseConfig();
|
|
291
|
+
}
|
|
292
|
+
} else {
|
|
293
|
+
baseConfig = this.createBaseConfig();
|
|
294
|
+
}
|
|
295
|
+
} else {
|
|
296
|
+
// Convert legacy config
|
|
297
|
+
baseConfig = await this.convertLegacyToFlatConfig(projectPath, configDetection);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Build plugin imports based on what's needed AND what's available
|
|
301
|
+
const rules = this.extractRulesFromConfig(eslintConfig);
|
|
302
|
+
const needsReact = this.needsReactPlugins(rules);
|
|
303
|
+
const needsTypeScript = this.needsTypeScriptPlugins(rules);
|
|
304
|
+
|
|
305
|
+
// Check plugin availability in target project
|
|
306
|
+
const hasReact = needsReact && this.isReactPluginAvailable(projectPath);
|
|
307
|
+
const hasReactHooks = needsReact && this.isReactHooksPluginAvailable(projectPath);
|
|
308
|
+
const hasTypeScript = needsTypeScript && this.isTypeScriptPluginAvailable(projectPath);
|
|
309
|
+
const hasTypeScriptParser = this.isTypeScriptParserAvailable(projectPath);
|
|
310
|
+
|
|
311
|
+
let pluginImports = '';
|
|
312
|
+
let pluginDefs = '{ "custom": customPlugin';
|
|
313
|
+
|
|
314
|
+
if (hasReact) {
|
|
315
|
+
pluginImports += `\nimport reactPlugin from 'eslint-plugin-react';`;
|
|
316
|
+
pluginDefs += ', "react": reactPlugin';
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (hasReactHooks) {
|
|
320
|
+
pluginImports += `\nimport reactHooksPlugin from 'eslint-plugin-react-hooks';`;
|
|
321
|
+
pluginDefs += ', "react-hooks": reactHooksPlugin';
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (hasTypeScript) {
|
|
325
|
+
pluginImports += `\nimport typescriptPlugin from '@typescript-eslint/eslint-plugin';`;
|
|
326
|
+
pluginDefs += ', "@typescript-eslint": typescriptPlugin';
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
pluginDefs += ' }';
|
|
330
|
+
|
|
331
|
+
// Filter rules to only include those for available plugins
|
|
332
|
+
const filteredRules = {};
|
|
333
|
+
const skippedRules = { react: [], reactHooks: [], typescript: [] };
|
|
334
|
+
|
|
335
|
+
for (const [ruleKey, ruleConfig] of Object.entries(eslintConfig.rules || {})) {
|
|
336
|
+
if (ruleKey.startsWith('react/') && !hasReact) {
|
|
337
|
+
skippedRules.react.push(ruleKey);
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
if (ruleKey.startsWith('react-hooks/') && !hasReactHooks) {
|
|
341
|
+
skippedRules.reactHooks.push(ruleKey);
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
if (ruleKey.startsWith('@typescript-eslint/') && !hasTypeScript) {
|
|
345
|
+
skippedRules.typescript.push(ruleKey);
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
filteredRules[ruleKey] = ruleConfig;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// Summary of skipped rules instead of individual warnings
|
|
352
|
+
if (skippedRules.react.length > 0) {
|
|
353
|
+
console.warn(`โ ๏ธ [ESLintEngine] Skipped ${skippedRules.react.length} React rules - plugin not available`);
|
|
354
|
+
}
|
|
355
|
+
if (skippedRules.reactHooks.length > 0) {
|
|
356
|
+
console.warn(`โ ๏ธ [ESLintEngine] Skipped ${skippedRules.reactHooks.length} React Hooks rules - plugin not available`);
|
|
357
|
+
}
|
|
358
|
+
if (skippedRules.typescript.length > 0) {
|
|
359
|
+
console.warn(`โ ๏ธ [ESLintEngine] Skipped ${skippedRules.typescript.length} TypeScript ESLint rules - plugin not available`);
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Merge with analysis config using filtered rules
|
|
363
|
+
const mergedConfig = {
|
|
364
|
+
...baseConfig,
|
|
365
|
+
...eslintConfig,
|
|
366
|
+
rules: {
|
|
367
|
+
...baseConfig.rules,
|
|
368
|
+
...filteredRules
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
// Create temporary config file in project directory
|
|
373
|
+
const tempConfigPath = path.join(projectPath, '.sunlint-eslint.config.js');
|
|
374
|
+
|
|
375
|
+
// Create simple config compatible with flat config format
|
|
376
|
+
const configForExport = {
|
|
377
|
+
files: ['**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx'],
|
|
378
|
+
languageOptions: {
|
|
379
|
+
ecmaVersion: 'latest',
|
|
380
|
+
sourceType: 'module',
|
|
381
|
+
parserOptions: {
|
|
382
|
+
ecmaFeatures: {
|
|
383
|
+
jsx: true
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
},
|
|
387
|
+
rules: {
|
|
388
|
+
...baseConfig.rules,
|
|
389
|
+
...filteredRules
|
|
390
|
+
}
|
|
391
|
+
};
|
|
392
|
+
|
|
393
|
+
const configContent = `// Temporary flat config generated by SunLint
|
|
394
|
+
import customPlugin from '${path.resolve(__dirname, '../integrations/eslint/plugin/index.js')}';${pluginImports}
|
|
395
|
+
|
|
396
|
+
export default [
|
|
397
|
+
${JSON.stringify(configForExport, null, 2).replace('"rules":', `"plugins": ${pluginDefs},\n "rules":`)},
|
|
398
|
+
{
|
|
399
|
+
files: ['**/*.ts', '**/*.tsx'],
|
|
400
|
+
plugins: ${pluginDefs},
|
|
401
|
+
languageOptions: {${hasTypeScriptParser ? `
|
|
402
|
+
parser: (await import('@typescript-eslint/parser')).default,` : ''}
|
|
403
|
+
ecmaVersion: 'latest',
|
|
404
|
+
sourceType: 'module',
|
|
405
|
+
parserOptions: {
|
|
406
|
+
ecmaFeatures: {
|
|
407
|
+
jsx: true
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
},
|
|
411
|
+
rules: ${JSON.stringify({...baseConfig.rules, ...filteredRules}, null, 2)}
|
|
412
|
+
}
|
|
413
|
+
];
|
|
414
|
+
`;
|
|
415
|
+
|
|
416
|
+
fs.writeFileSync(tempConfigPath, configContent);
|
|
417
|
+
console.log(`๐ง [ESLintEngine] Created temporary flat config: ${tempConfigPath}`);
|
|
418
|
+
|
|
419
|
+
// Schedule cleanup
|
|
420
|
+
this.tempConfigPaths = this.tempConfigPaths || [];
|
|
421
|
+
this.tempConfigPaths.push(tempConfigPath);
|
|
422
|
+
|
|
423
|
+
return tempConfigPath;
|
|
424
|
+
} catch (error) {
|
|
425
|
+
console.warn(`โ ๏ธ [ESLintEngine] Failed to create temporary flat config: ${error.message}`);
|
|
426
|
+
throw error;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Convert legacy ESLint config to flat config format
|
|
432
|
+
* Following Rule C006: Verb-noun naming
|
|
433
|
+
* @param {string} projectPath - Path to the project
|
|
434
|
+
* @param {Object} configDetection - Config detection results
|
|
435
|
+
* @returns {Promise<Object>} Flat config object
|
|
436
|
+
*/
|
|
437
|
+
async convertLegacyToFlatConfig(projectPath, configDetection) {
|
|
438
|
+
const fs = require('fs');
|
|
439
|
+
const path = require('path');
|
|
440
|
+
|
|
441
|
+
let legacyConfig = {};
|
|
442
|
+
|
|
443
|
+
try {
|
|
444
|
+
// Load legacy config from .eslintrc.json
|
|
445
|
+
if (configDetection.foundFiles.includes('.eslintrc.json')) {
|
|
446
|
+
const configPath = path.join(projectPath, '.eslintrc.json');
|
|
447
|
+
const configContent = fs.readFileSync(configPath, 'utf8');
|
|
448
|
+
legacyConfig = JSON.parse(configContent);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Convert to flat config format
|
|
452
|
+
const flatConfig = {
|
|
453
|
+
files: ['**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx'],
|
|
454
|
+
languageOptions: {
|
|
455
|
+
ecmaVersion: legacyConfig.env?.es2022 ? 2022 :
|
|
456
|
+
legacyConfig.env?.es2021 ? 2021 :
|
|
457
|
+
legacyConfig.env?.es6 ? 6 : 'latest',
|
|
458
|
+
sourceType: legacyConfig.parserOptions?.sourceType || 'module',
|
|
459
|
+
globals: {}
|
|
460
|
+
},
|
|
461
|
+
plugins: {},
|
|
462
|
+
rules: legacyConfig.rules || {}
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
// Convert env to globals
|
|
466
|
+
if (legacyConfig.env) {
|
|
467
|
+
if (legacyConfig.env.browser) {
|
|
468
|
+
Object.assign(flatConfig.languageOptions.globals, {
|
|
469
|
+
window: 'readonly',
|
|
470
|
+
document: 'readonly',
|
|
471
|
+
navigator: 'readonly',
|
|
472
|
+
console: 'readonly'
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
if (legacyConfig.env.node) {
|
|
476
|
+
Object.assign(flatConfig.languageOptions.globals, {
|
|
477
|
+
process: 'readonly',
|
|
478
|
+
Buffer: 'readonly',
|
|
479
|
+
__dirname: 'readonly',
|
|
480
|
+
__filename: 'readonly',
|
|
481
|
+
module: 'readonly',
|
|
482
|
+
require: 'readonly',
|
|
483
|
+
exports: 'readonly',
|
|
484
|
+
global: 'readonly'
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
if (legacyConfig.env.es6) {
|
|
488
|
+
Object.assign(flatConfig.languageOptions.globals, {
|
|
489
|
+
Promise: 'readonly',
|
|
490
|
+
Set: 'readonly',
|
|
491
|
+
Map: 'readonly'
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Set parser if specified
|
|
497
|
+
if (legacyConfig.parser) {
|
|
498
|
+
if (legacyConfig.parser === '@typescript-eslint/parser') {
|
|
499
|
+
flatConfig.languageOptions.parser = this.loadTypeScriptParser();
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Convert parser options
|
|
504
|
+
if (legacyConfig.parserOptions) {
|
|
505
|
+
flatConfig.languageOptions.parserOptions = legacyConfig.parserOptions;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Handle extends - merge base rules
|
|
509
|
+
if (legacyConfig.extends) {
|
|
510
|
+
const extendsList = Array.isArray(legacyConfig.extends) ? legacyConfig.extends : [legacyConfig.extends];
|
|
511
|
+
|
|
512
|
+
for (const extend of extendsList) {
|
|
513
|
+
if (extend === 'eslint:recommended') {
|
|
514
|
+
// Add some basic recommended rules
|
|
515
|
+
Object.assign(flatConfig.rules, {
|
|
516
|
+
'no-unused-vars': 'warn',
|
|
517
|
+
'no-undef': 'error',
|
|
518
|
+
'no-console': 'warn'
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
console.log(`๐ [ESLintEngine] Converted legacy config to flat config`);
|
|
525
|
+
return flatConfig;
|
|
526
|
+
|
|
527
|
+
} catch (error) {
|
|
528
|
+
console.warn(`โ ๏ธ [ESLintEngine] Failed to convert legacy config: ${error.message}`);
|
|
529
|
+
// Fallback to base config
|
|
530
|
+
return this.createBaseConfig();
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
|
|
119
534
|
/**
|
|
120
535
|
* Create base ESLint configuration
|
|
121
536
|
* Following Rule C006: Verb-noun naming
|
|
@@ -191,6 +606,298 @@ class ESLintEngine extends AnalysisEngineInterface {
|
|
|
191
606
|
console.warn('โ ๏ธ Using default ESLint rule mapping');
|
|
192
607
|
}
|
|
193
608
|
|
|
609
|
+
/**
|
|
610
|
+
* Detect project type from package.json and file patterns
|
|
611
|
+
* @param {string} projectPath - Project path
|
|
612
|
+
* @param {string[]} files - Files being analyzed
|
|
613
|
+
* @returns {Object} Project type information
|
|
614
|
+
*/
|
|
615
|
+
detectProjectType(projectPath, files) {
|
|
616
|
+
const fs = require('fs');
|
|
617
|
+
const path = require('path');
|
|
618
|
+
|
|
619
|
+
const result = {
|
|
620
|
+
isReactProject: false,
|
|
621
|
+
isNextProject: false,
|
|
622
|
+
isNestProject: false,
|
|
623
|
+
isNodeProject: false,
|
|
624
|
+
hasReactFiles: false,
|
|
625
|
+
hasNestFiles: false,
|
|
626
|
+
packageManager: 'npm'
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
try {
|
|
630
|
+
// Check package.json for project type indicators
|
|
631
|
+
const packageJsonPath = path.join(projectPath, 'package.json');
|
|
632
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
633
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
634
|
+
|
|
635
|
+
// Check dependencies for project type
|
|
636
|
+
const allDeps = {
|
|
637
|
+
...packageJson.dependencies,
|
|
638
|
+
...packageJson.devDependencies,
|
|
639
|
+
...packageJson.peerDependencies
|
|
640
|
+
};
|
|
641
|
+
|
|
642
|
+
if (allDeps.react || allDeps['@types/react']) {
|
|
643
|
+
result.isReactProject = true;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
if (allDeps.next || allDeps['@types/next']) {
|
|
647
|
+
result.isNextProject = true;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
if (allDeps['@nestjs/core'] || allDeps['@nestjs/common']) {
|
|
651
|
+
result.isNestProject = true;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Check package manager from scripts
|
|
655
|
+
if (packageJson.scripts && Object.values(packageJson.scripts).some(script => script.includes('pnpm'))) {
|
|
656
|
+
result.packageManager = 'pnpm';
|
|
657
|
+
} else if (packageJson.scripts && Object.values(packageJson.scripts).some(script => script.includes('yarn'))) {
|
|
658
|
+
result.packageManager = 'yarn';
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Check for preinstall script indicating package manager preference
|
|
662
|
+
if (packageJson.scripts?.preinstall?.includes('pnpm')) {
|
|
663
|
+
result.packageManager = 'pnpm';
|
|
664
|
+
} else if (packageJson.scripts?.preinstall?.includes('yarn')) {
|
|
665
|
+
result.packageManager = 'yarn';
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// Check file patterns
|
|
670
|
+
const hasJsxTsx = files.some(file => {
|
|
671
|
+
const ext = path.extname(file).toLowerCase();
|
|
672
|
+
return ['.jsx', '.tsx'].includes(ext);
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
const hasNestFiles = files.some(file => {
|
|
676
|
+
return file.includes('controller.ts') ||
|
|
677
|
+
file.includes('service.ts') ||
|
|
678
|
+
file.includes('module.ts') ||
|
|
679
|
+
file.includes('main.ts');
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
result.hasReactFiles = hasJsxTsx && !result.isNestProject;
|
|
683
|
+
result.hasNestFiles = hasNestFiles;
|
|
684
|
+
result.isNodeProject = !result.isReactProject && !result.isNextProject;
|
|
685
|
+
|
|
686
|
+
} catch (error) {
|
|
687
|
+
console.warn(`โ ๏ธ [ESLintEngine] Failed to detect project type: ${error.message}`);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
return result;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* Check if project has dependency conflicts that require --legacy-peer-deps
|
|
695
|
+
* @param {string} projectPath - Project path
|
|
696
|
+
* @returns {boolean} True if project has known dependency conflicts
|
|
697
|
+
*/
|
|
698
|
+
hasKnownDependencyConflicts(projectPath) {
|
|
699
|
+
const fs = require('fs');
|
|
700
|
+
const path = require('path');
|
|
701
|
+
|
|
702
|
+
try {
|
|
703
|
+
const packageJsonPath = path.join(projectPath, 'package.json');
|
|
704
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
705
|
+
return false;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
709
|
+
const allDeps = {
|
|
710
|
+
...packageJson.dependencies,
|
|
711
|
+
...packageJson.devDependencies,
|
|
712
|
+
...packageJson.peerDependencies
|
|
713
|
+
};
|
|
714
|
+
|
|
715
|
+
// Check for known problematic combinations
|
|
716
|
+
const conflicts = [
|
|
717
|
+
// date-fns version conflicts
|
|
718
|
+
() => {
|
|
719
|
+
const dateFns = allDeps['date-fns'];
|
|
720
|
+
const dateFnsTz = allDeps['date-fns-tz'];
|
|
721
|
+
if (dateFns && dateFnsTz) {
|
|
722
|
+
// If date-fns is v2.x and date-fns-tz is v3.x, there's likely a conflict
|
|
723
|
+
if (dateFns.includes('2.') && dateFnsTz.includes('3.')) {
|
|
724
|
+
return true;
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
return false;
|
|
728
|
+
},
|
|
729
|
+
|
|
730
|
+
// React version conflicts
|
|
731
|
+
() => {
|
|
732
|
+
const react = allDeps['react'];
|
|
733
|
+
const reactDom = allDeps['react-dom'];
|
|
734
|
+
if (react && reactDom) {
|
|
735
|
+
// Check for major version mismatches
|
|
736
|
+
const reactMajor = react.match(/(\d+)\./)?.[1];
|
|
737
|
+
const reactDomMajor = reactDom.match(/(\d+)\./)?.[1];
|
|
738
|
+
if (reactMajor && reactDomMajor && reactMajor !== reactDomMajor) {
|
|
739
|
+
return true;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return false;
|
|
743
|
+
},
|
|
744
|
+
|
|
745
|
+
// ESLint version conflicts (common with older projects)
|
|
746
|
+
() => {
|
|
747
|
+
const eslint = allDeps['eslint'];
|
|
748
|
+
if (eslint && eslint.includes('8.')) {
|
|
749
|
+
// ESLint v8 with newer plugins often has peer dependency issues
|
|
750
|
+
return true;
|
|
751
|
+
}
|
|
752
|
+
return false;
|
|
753
|
+
}
|
|
754
|
+
];
|
|
755
|
+
|
|
756
|
+
return conflicts.some(check => check());
|
|
757
|
+
|
|
758
|
+
} catch (error) {
|
|
759
|
+
// If we can't read package.json, assume no conflicts
|
|
760
|
+
return false;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
/**
|
|
765
|
+
* Provide appropriate installation guidance based on project type
|
|
766
|
+
* @param {Object} projectType - Project type information
|
|
767
|
+
* @param {number} tsFileCount - Number of TypeScript files
|
|
768
|
+
* @param {number} reactFileCount - Number of React files
|
|
769
|
+
* @param {boolean} hasTypeScriptParser - TypeScript parser availability
|
|
770
|
+
* @param {boolean} hasReactPlugin - React plugin availability
|
|
771
|
+
* @param {boolean} hasReactHooksPlugin - React Hooks plugin availability
|
|
772
|
+
* @param {string} projectPath - Project path for conflict detection
|
|
773
|
+
*/
|
|
774
|
+
provideInstallationGuidance(projectType, tsFileCount, reactFileCount, hasTypeScriptParser, hasReactPlugin, hasReactHooksPlugin, projectPath) {
|
|
775
|
+
const missingDeps = [];
|
|
776
|
+
const projectDescription = this.getProjectDescription(projectType, tsFileCount, reactFileCount);
|
|
777
|
+
|
|
778
|
+
// TypeScript dependencies (needed for most projects with .ts files)
|
|
779
|
+
if (tsFileCount > 0 && !hasTypeScriptParser) {
|
|
780
|
+
missingDeps.push('@typescript-eslint/parser', '@typescript-eslint/eslint-plugin');
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// React dependencies (only for actual React projects, not NestJS)
|
|
784
|
+
if (projectType.hasReactFiles && !projectType.isNestProject) {
|
|
785
|
+
if (!hasReactPlugin) missingDeps.push('eslint-plugin-react');
|
|
786
|
+
if (!hasReactHooksPlugin) missingDeps.push('eslint-plugin-react-hooks');
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
if (missingDeps.length > 0) {
|
|
790
|
+
console.log(`\n๐ฆ [SunLint] To enable full analysis of your ${projectDescription}, install:`);
|
|
791
|
+
|
|
792
|
+
// Use appropriate package manager and flags
|
|
793
|
+
const packageManager = projectType.packageManager;
|
|
794
|
+
const installFlag = packageManager === 'npm' ? '--save-dev' : packageManager === 'yarn' ? '--dev' : '--save-dev';
|
|
795
|
+
|
|
796
|
+
// Only suggest --legacy-peer-deps if the project has known dependency conflicts
|
|
797
|
+
let legacyFlag = '';
|
|
798
|
+
if (packageManager === 'npm' && this.hasKnownDependencyConflicts(projectPath)) {
|
|
799
|
+
legacyFlag = ' --legacy-peer-deps';
|
|
800
|
+
console.log(` โ ๏ธ Detected dependency conflicts in your project.`);
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
console.log(` ${packageManager} install ${installFlag} ${missingDeps.join(' ')}${legacyFlag}`);
|
|
804
|
+
console.log(` Then SunLint will analyze all files with full ${this.getToolDescription(missingDeps)} support.\n`);
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Get project description for user guidance
|
|
810
|
+
* @param {Object} projectType - Project type information
|
|
811
|
+
* @param {number} tsFileCount - Number of TypeScript files
|
|
812
|
+
* @param {number} reactFileCount - Number of React files
|
|
813
|
+
* @returns {string} Project description
|
|
814
|
+
*/
|
|
815
|
+
getProjectDescription(projectType, tsFileCount, reactFileCount) {
|
|
816
|
+
if (projectType.isNestProject) {
|
|
817
|
+
return `${tsFileCount} TypeScript files (NestJS backend)`;
|
|
818
|
+
} else if (projectType.isNextProject) {
|
|
819
|
+
return `${tsFileCount} TypeScript and ${reactFileCount} React files (Next.js project)`;
|
|
820
|
+
} else if (projectType.isReactProject) {
|
|
821
|
+
return `${tsFileCount} TypeScript and ${reactFileCount} React files (React project)`;
|
|
822
|
+
} else if (tsFileCount > 0) {
|
|
823
|
+
return `${tsFileCount} TypeScript files (Node.js project)`;
|
|
824
|
+
} else {
|
|
825
|
+
return 'JavaScript files';
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* Get tool description for user guidance
|
|
831
|
+
* @param {string[]} missingDeps - Missing dependencies
|
|
832
|
+
* @returns {string} Tool description
|
|
833
|
+
*/
|
|
834
|
+
getToolDescription(missingDeps) {
|
|
835
|
+
const tools = [];
|
|
836
|
+
if (missingDeps.some(dep => dep.includes('typescript-eslint'))) {
|
|
837
|
+
tools.push('TypeScript');
|
|
838
|
+
}
|
|
839
|
+
if (missingDeps.some(dep => dep.includes('react'))) {
|
|
840
|
+
tools.push('React');
|
|
841
|
+
}
|
|
842
|
+
return tools.join(' and ');
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* Check if React plugin is available in project
|
|
847
|
+
* @param {string} projectPath - Project path to check
|
|
848
|
+
* @returns {boolean} True if React plugin is available
|
|
849
|
+
*/
|
|
850
|
+
isReactPluginAvailable(projectPath) {
|
|
851
|
+
try {
|
|
852
|
+
require.resolve('eslint-plugin-react', { paths: [projectPath] });
|
|
853
|
+
return true;
|
|
854
|
+
} catch (error) {
|
|
855
|
+
return false;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
/**
|
|
860
|
+
* Check if React Hooks plugin is available in project
|
|
861
|
+
* @param {string} projectPath - Project path to check
|
|
862
|
+
* @returns {boolean} True if React Hooks plugin is available
|
|
863
|
+
*/
|
|
864
|
+
isReactHooksPluginAvailable(projectPath) {
|
|
865
|
+
try {
|
|
866
|
+
require.resolve('eslint-plugin-react-hooks', { paths: [projectPath] });
|
|
867
|
+
return true;
|
|
868
|
+
} catch (error) {
|
|
869
|
+
return false;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
/**
|
|
874
|
+
* Check if TypeScript plugin is available in project
|
|
875
|
+
* @param {string} projectPath - Project path to check
|
|
876
|
+
* @returns {boolean} True if TypeScript plugin is available
|
|
877
|
+
*/
|
|
878
|
+
isTypeScriptPluginAvailable(projectPath) {
|
|
879
|
+
try {
|
|
880
|
+
require.resolve('@typescript-eslint/eslint-plugin', { paths: [projectPath] });
|
|
881
|
+
return true;
|
|
882
|
+
} catch (error) {
|
|
883
|
+
return false;
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
/**
|
|
888
|
+
* Check if TypeScript parser is available in project
|
|
889
|
+
* @param {string} projectPath - Project path to check
|
|
890
|
+
* @returns {boolean} True if TypeScript parser is available
|
|
891
|
+
*/
|
|
892
|
+
isTypeScriptParserAvailable(projectPath) {
|
|
893
|
+
try {
|
|
894
|
+
require.resolve('@typescript-eslint/parser', { paths: [projectPath] });
|
|
895
|
+
return true;
|
|
896
|
+
} catch (error) {
|
|
897
|
+
return false;
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
|
|
194
901
|
/**
|
|
195
902
|
* Load React ESLint plugin
|
|
196
903
|
* Following Rule C006: Verb-noun naming
|
|
@@ -321,7 +1028,7 @@ class ESLintEngine extends AnalysisEngineInterface {
|
|
|
321
1028
|
|
|
322
1029
|
try {
|
|
323
1030
|
// Filter files for JS/TS only
|
|
324
|
-
|
|
1031
|
+
let jstsFiles = files.filter(file => this.isJavaScriptTypeScriptFile(file));
|
|
325
1032
|
|
|
326
1033
|
if (jstsFiles.length === 0) {
|
|
327
1034
|
console.warn('โ ๏ธ No JavaScript/TypeScript files found for ESLint analysis');
|
|
@@ -336,22 +1043,109 @@ class ESLintEngine extends AnalysisEngineInterface {
|
|
|
336
1043
|
return results;
|
|
337
1044
|
}
|
|
338
1045
|
|
|
339
|
-
//
|
|
1046
|
+
// Find project root from input path (usually the project's working directory)
|
|
1047
|
+
const path = require('path');
|
|
1048
|
+
let projectPath;
|
|
1049
|
+
|
|
1050
|
+
if (options.input) {
|
|
1051
|
+
// If input is specified, find project root from it
|
|
1052
|
+
const inputPath = path.resolve(options.input);
|
|
1053
|
+
// Always go up to find project root, not use input directory directly
|
|
1054
|
+
projectPath = this.findProjectRoot([inputPath]);
|
|
1055
|
+
} else if (jstsFiles.length > 0) {
|
|
1056
|
+
// Find project root from all files
|
|
1057
|
+
projectPath = this.findProjectRoot(jstsFiles);
|
|
1058
|
+
} else {
|
|
1059
|
+
projectPath = process.cwd();
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
console.log(`๐ [ESLintEngine] Using project path: ${projectPath}`);
|
|
1063
|
+
|
|
1064
|
+
// Get config detection for reuse
|
|
1065
|
+
const configDetection = this.detectESLintConfig(projectPath);
|
|
1066
|
+
|
|
1067
|
+
// Check for missing dependencies and provide installation guidance
|
|
1068
|
+
const hasTypeScriptParser = this.isTypeScriptParserAvailable(projectPath);
|
|
1069
|
+
const hasReactPlugin = this.isReactPluginAvailable(projectPath);
|
|
1070
|
+
const hasReactHooksPlugin = this.isReactHooksPluginAvailable(projectPath);
|
|
1071
|
+
const hasTypeScriptPlugin = this.isTypeScriptPluginAvailable(projectPath);
|
|
1072
|
+
|
|
1073
|
+
// Detect project type from package.json and file patterns
|
|
1074
|
+
const projectType = this.detectProjectType(projectPath, jstsFiles);
|
|
1075
|
+
|
|
1076
|
+
// Count TypeScript files to determine if we need to recommend TypeScript tools
|
|
1077
|
+
const tsFileCount = jstsFiles.filter(file => {
|
|
1078
|
+
const ext = path.extname(file).toLowerCase();
|
|
1079
|
+
return ['.ts', '.tsx'].includes(ext);
|
|
1080
|
+
}).length;
|
|
1081
|
+
|
|
1082
|
+
// Count React-like files to determine if we need React tools
|
|
1083
|
+
const reactFileCount = jstsFiles.filter(file => {
|
|
1084
|
+
const ext = path.extname(file).toLowerCase();
|
|
1085
|
+
return ['.jsx', '.tsx'].includes(ext);
|
|
1086
|
+
}).length;
|
|
1087
|
+
|
|
1088
|
+
// Provide helpful installation guidance based on project type
|
|
1089
|
+
this.provideInstallationGuidance(projectType, tsFileCount, reactFileCount, hasTypeScriptParser, hasReactPlugin, hasReactHooksPlugin, projectPath);
|
|
1090
|
+
|
|
1091
|
+
// Create ESLint instance with proper config
|
|
340
1092
|
const { ESLint } = await this.loadESLint();
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
1093
|
+
let finalESLintOptions;
|
|
1094
|
+
|
|
1095
|
+
// Configure ESLint to handle files appropriately
|
|
1096
|
+
if (configDetection.hasFlatConfig) {
|
|
1097
|
+
// For flat config, always create temporary config to ensure plugin compatibility
|
|
1098
|
+
const tempFlatConfigPath = await this.createTemporaryFlatConfig(projectPath, configDetection, eslintConfig);
|
|
1099
|
+
finalESLintOptions = {
|
|
1100
|
+
overrideConfigFile: tempFlatConfigPath,
|
|
1101
|
+
cwd: projectPath
|
|
1102
|
+
};
|
|
1103
|
+
console.log(`โ
[ESLintEngine] Created temporary flat config for plugin compatibility`);
|
|
1104
|
+
} else if (configDetection.hasLegacyConfig || configDetection.hasPackageConfig) {
|
|
1105
|
+
// For legacy config, create a temporary flat config file
|
|
1106
|
+
const tempFlatConfigPath = await this.createTemporaryFlatConfig(projectPath, configDetection, eslintConfig);
|
|
1107
|
+
finalESLintOptions = {
|
|
1108
|
+
overrideConfigFile: tempFlatConfigPath,
|
|
1109
|
+
cwd: projectPath
|
|
1110
|
+
};
|
|
1111
|
+
console.log(`โ
[ESLintEngine] Created temporary flat config for legacy compatibility`);
|
|
1112
|
+
} else {
|
|
1113
|
+
// No config found - use analysis config only
|
|
1114
|
+
finalESLintOptions = {
|
|
1115
|
+
overrideConfig: eslintConfig,
|
|
1116
|
+
cwd: projectPath
|
|
1117
|
+
};
|
|
1118
|
+
console.log(`โ ๏ธ [ESLintEngine] Using analysis config only`);
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
const finalESLintInstance = new ESLint(finalESLintOptions);
|
|
349
1122
|
|
|
350
|
-
// Run ESLint analysis
|
|
351
|
-
|
|
1123
|
+
// Run ESLint analysis - let ESLint handle parsing errors gracefully
|
|
1124
|
+
console.log(`๐ [ESLintEngine] Analyzing ${jstsFiles.length} JavaScript/TypeScript files...`);
|
|
1125
|
+
const eslintResults = await finalESLintInstance.lintFiles(jstsFiles);
|
|
1126
|
+
|
|
1127
|
+
// Filter out parsing errors when TypeScript parser is not available
|
|
1128
|
+
let processedResults = eslintResults;
|
|
1129
|
+
if (!hasTypeScriptParser) {
|
|
1130
|
+
let parsingErrorCount = 0;
|
|
1131
|
+
processedResults = eslintResults.map(result => {
|
|
1132
|
+
const filteredMessages = result.messages.filter(message => {
|
|
1133
|
+
if (message.ruleId === null && message.message.includes('Parsing error')) {
|
|
1134
|
+
parsingErrorCount++;
|
|
1135
|
+
return false; // Skip parsing errors
|
|
1136
|
+
}
|
|
1137
|
+
return true; // Keep all other messages
|
|
1138
|
+
});
|
|
1139
|
+
return { ...result, messages: filteredMessages };
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
if (parsingErrorCount > 0) {
|
|
1143
|
+
console.log(`โน๏ธ [ESLintEngine] Filtered ${parsingErrorCount} TypeScript parsing errors (install @typescript-eslint/parser for full TypeScript support)`);
|
|
1144
|
+
}
|
|
1145
|
+
}
|
|
352
1146
|
|
|
353
1147
|
// Convert ESLint results to SunLint format
|
|
354
|
-
results.results = this.convertESLintResults(
|
|
1148
|
+
results.results = this.convertESLintResults(processedResults, rules);
|
|
355
1149
|
results.filesAnalyzed = jstsFiles.length;
|
|
356
1150
|
results.metadata.rulesAnalyzed = rules.map(r => r.id);
|
|
357
1151
|
results.metadata.eslintRulesUsed = Object.keys(eslintConfig.rules);
|
|
@@ -364,6 +1158,76 @@ class ESLintEngine extends AnalysisEngineInterface {
|
|
|
364
1158
|
return results;
|
|
365
1159
|
}
|
|
366
1160
|
|
|
1161
|
+
/**
|
|
1162
|
+
* Find project root from a list of files or a directory
|
|
1163
|
+
* Following Rule C006: Verb-noun naming
|
|
1164
|
+
* @param {string[]} paths - List of file paths or directories
|
|
1165
|
+
* @returns {string} Project root path
|
|
1166
|
+
*/
|
|
1167
|
+
findProjectRoot(paths) {
|
|
1168
|
+
const path = require('path');
|
|
1169
|
+
|
|
1170
|
+
if (paths.length === 0) {
|
|
1171
|
+
return process.cwd();
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
// Start from the first path (could be directory or file)
|
|
1175
|
+
let startPath = paths[0];
|
|
1176
|
+
|
|
1177
|
+
// If it's a file, get its directory
|
|
1178
|
+
if (fs.existsSync(startPath) && fs.statSync(startPath).isFile()) {
|
|
1179
|
+
startPath = path.dirname(startPath);
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// Look for project indicators going up the tree from start path
|
|
1183
|
+
let currentPath = path.resolve(startPath);
|
|
1184
|
+
while (currentPath !== path.dirname(currentPath)) { // Stop at root
|
|
1185
|
+
const packageJsonPath = path.join(currentPath, 'package.json');
|
|
1186
|
+
const eslintConfigPath = path.join(currentPath, 'eslint.config.js');
|
|
1187
|
+
const eslintrcPath = path.join(currentPath, '.eslintrc.json');
|
|
1188
|
+
const tsConfigPath = path.join(currentPath, 'tsconfig.json');
|
|
1189
|
+
|
|
1190
|
+
// Found project root indicators
|
|
1191
|
+
if (fs.existsSync(packageJsonPath) || fs.existsSync(eslintConfigPath) ||
|
|
1192
|
+
fs.existsSync(eslintrcPath) || fs.existsSync(tsConfigPath)) {
|
|
1193
|
+
return currentPath;
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// Go up one level
|
|
1197
|
+
currentPath = path.dirname(currentPath);
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
// If nothing found, return the original start path
|
|
1201
|
+
return path.resolve(startPath);
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
/**
|
|
1205
|
+
* Find common path between two paths
|
|
1206
|
+
* Following Rule C006: Verb-noun naming
|
|
1207
|
+
* @param {string} path1 - First path
|
|
1208
|
+
* @param {string} path2 - Second path
|
|
1209
|
+
* @returns {string} Common path
|
|
1210
|
+
*/
|
|
1211
|
+
findCommonPath(path1, path2) {
|
|
1212
|
+
const path = require('path');
|
|
1213
|
+
|
|
1214
|
+
const parts1 = path1.split(path.sep);
|
|
1215
|
+
const parts2 = path2.split(path.sep);
|
|
1216
|
+
|
|
1217
|
+
const commonParts = [];
|
|
1218
|
+
const minLength = Math.min(parts1.length, parts2.length);
|
|
1219
|
+
|
|
1220
|
+
for (let i = 0; i < minLength; i++) {
|
|
1221
|
+
if (parts1[i] === parts2[i]) {
|
|
1222
|
+
commonParts.push(parts1[i]);
|
|
1223
|
+
} else {
|
|
1224
|
+
break;
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
return commonParts.join(path.sep) || path.sep;
|
|
1229
|
+
}
|
|
1230
|
+
|
|
367
1231
|
/**
|
|
368
1232
|
* Check if file is JavaScript or TypeScript
|
|
369
1233
|
* Following Rule C006: Verb-noun naming
|
|
@@ -461,6 +1325,37 @@ class ESLintEngine extends AnalysisEngineInterface {
|
|
|
461
1325
|
|
|
462
1326
|
// Map SunLint rules to ESLint rules
|
|
463
1327
|
for (const rule of rules) {
|
|
1328
|
+
// For Security rules, always use custom plugin (ignore mapping file)
|
|
1329
|
+
if (rule.id.startsWith('S')) {
|
|
1330
|
+
const customRuleName = `custom/typescript_${rule.id.toLowerCase()}`;
|
|
1331
|
+
const ruleConfig = this.mapSeverity(rule.severity || 'warning');
|
|
1332
|
+
config.rules[customRuleName] = ruleConfig;
|
|
1333
|
+
continue;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// For Common rules (C series), use rule ID directly in custom plugin
|
|
1337
|
+
if (rule.id.startsWith('C')) {
|
|
1338
|
+
const customRuleName = `custom/${rule.id.toLowerCase()}`;
|
|
1339
|
+
const ruleConfig = this.mapSeverity(rule.severity || 'warning');
|
|
1340
|
+
|
|
1341
|
+
// Add rule configuration for specific rules
|
|
1342
|
+
if (rule.id === 'C010') {
|
|
1343
|
+
config.rules[customRuleName] = [ruleConfig, { maxDepth: 3 }];
|
|
1344
|
+
} else {
|
|
1345
|
+
config.rules[customRuleName] = ruleConfig;
|
|
1346
|
+
}
|
|
1347
|
+
continue;
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
// For TypeScript rules (T series), use rule ID directly in custom plugin
|
|
1351
|
+
if (rule.id.startsWith('T')) {
|
|
1352
|
+
const customRuleName = `custom/${rule.id.toLowerCase()}`;
|
|
1353
|
+
const ruleConfig = this.mapSeverity(rule.severity || 'warning');
|
|
1354
|
+
config.rules[customRuleName] = ruleConfig;
|
|
1355
|
+
continue;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
// For other rules, check mapping file
|
|
464
1359
|
const eslintRules = this.ruleMapping.get(rule.id);
|
|
465
1360
|
|
|
466
1361
|
if (eslintRules && Array.isArray(eslintRules)) {
|
|
@@ -470,16 +1365,10 @@ class ESLintEngine extends AnalysisEngineInterface {
|
|
|
470
1365
|
config.rules[eslintRule] = severity;
|
|
471
1366
|
}
|
|
472
1367
|
} else {
|
|
473
|
-
//
|
|
1368
|
+
// Fallback - try as custom rule
|
|
474
1369
|
const customRuleName = `custom/${rule.id.toLowerCase()}`;
|
|
475
1370
|
const ruleConfig = this.mapSeverity(rule.severity || 'warning');
|
|
476
|
-
|
|
477
|
-
// Add rule configuration for specific rules
|
|
478
|
-
if (rule.id === 'C010') {
|
|
479
|
-
config.rules[customRuleName] = [ruleConfig, { maxDepth: 3 }];
|
|
480
|
-
} else {
|
|
481
|
-
config.rules[customRuleName] = ruleConfig;
|
|
482
|
-
}
|
|
1371
|
+
config.rules[customRuleName] = ruleConfig;
|
|
483
1372
|
}
|
|
484
1373
|
}
|
|
485
1374
|
|
|
@@ -598,6 +1487,22 @@ class ESLintEngine extends AnalysisEngineInterface {
|
|
|
598
1487
|
* Following Rule C006: Verb-noun naming
|
|
599
1488
|
*/
|
|
600
1489
|
async cleanup() {
|
|
1490
|
+
// Clean up temporary config files
|
|
1491
|
+
if (this.tempConfigPaths && this.tempConfigPaths.length > 0) {
|
|
1492
|
+
const fs = require('fs');
|
|
1493
|
+
for (const tempPath of this.tempConfigPaths) {
|
|
1494
|
+
try {
|
|
1495
|
+
if (fs.existsSync(tempPath)) {
|
|
1496
|
+
fs.unlinkSync(tempPath);
|
|
1497
|
+
console.log(`๐งน [ESLintEngine] Cleaned up temporary config: ${tempPath}`);
|
|
1498
|
+
}
|
|
1499
|
+
} catch (error) {
|
|
1500
|
+
console.warn(`โ ๏ธ [ESLintEngine] Failed to cleanup temp config ${tempPath}: ${error.message}`);
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
this.tempConfigPaths = [];
|
|
1504
|
+
}
|
|
1505
|
+
|
|
601
1506
|
this.eslint = null;
|
|
602
1507
|
this.configFiles.clear();
|
|
603
1508
|
this.ruleMapping.clear();
|
package/package.json
CHANGED