@sun-asterisk/sunlint 1.1.6 โ 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/engines/eslint-engine.js +803 -32
- 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
|
@@ -186,25 +186,24 @@ class ESLintEngine extends AnalysisEngineInterface {
|
|
|
186
186
|
// Use flat config (ESLint v9+ preferred)
|
|
187
187
|
eslintOptions = {
|
|
188
188
|
overrideConfigFile: null, // Let ESLint find flat config automatically
|
|
189
|
-
overrideConfig: this.createBaseConfig(),
|
|
190
189
|
fix: this.config?.fix || false,
|
|
191
190
|
cache: this.config?.cache || false,
|
|
192
191
|
cwd: projectPath
|
|
193
192
|
};
|
|
194
193
|
console.log(`โ
[ESLintEngine] Using flat config (modern ESLint v9+)`);
|
|
195
194
|
} else if (configDetection.hasLegacyConfig || configDetection.hasPackageConfig) {
|
|
196
|
-
//
|
|
197
|
-
|
|
195
|
+
// ESLint v9+ requires flat config - convert legacy config to flat config format
|
|
196
|
+
const flatConfig = await this.convertLegacyToFlatConfig(projectPath, configDetection);
|
|
198
197
|
eslintOptions = {
|
|
199
|
-
overrideConfigFile: null,
|
|
200
|
-
overrideConfig:
|
|
198
|
+
overrideConfigFile: null, // Use our generated flat config
|
|
199
|
+
overrideConfig: flatConfig,
|
|
201
200
|
fix: this.config?.fix || false,
|
|
202
201
|
cache: this.config?.cache || false,
|
|
203
202
|
cwd: projectPath
|
|
204
203
|
};
|
|
205
|
-
console.log(`โ
[ESLintEngine] Legacy config
|
|
204
|
+
console.log(`โ
[ESLintEngine] Legacy config converted to flat config for ESLint v9+ compatibility`);
|
|
206
205
|
} else {
|
|
207
|
-
// No config found - use SunLint's base config only
|
|
206
|
+
// No config found - use SunLint's base config only
|
|
208
207
|
eslintOptions = {
|
|
209
208
|
overrideConfig: this.createBaseConfig(),
|
|
210
209
|
fix: this.config?.fix || false,
|
|
@@ -228,6 +227,310 @@ class ESLintEngine extends AnalysisEngineInterface {
|
|
|
228
227
|
}
|
|
229
228
|
}
|
|
230
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
|
+
|
|
231
534
|
/**
|
|
232
535
|
* Create base ESLint configuration
|
|
233
536
|
* Following Rule C006: Verb-noun naming
|
|
@@ -303,6 +606,298 @@ class ESLintEngine extends AnalysisEngineInterface {
|
|
|
303
606
|
console.warn('โ ๏ธ Using default ESLint rule mapping');
|
|
304
607
|
}
|
|
305
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
|
+
|
|
306
901
|
/**
|
|
307
902
|
* Load React ESLint plugin
|
|
308
903
|
* Following Rule C006: Verb-noun naming
|
|
@@ -433,7 +1028,7 @@ class ESLintEngine extends AnalysisEngineInterface {
|
|
|
433
1028
|
|
|
434
1029
|
try {
|
|
435
1030
|
// Filter files for JS/TS only
|
|
436
|
-
|
|
1031
|
+
let jstsFiles = files.filter(file => this.isJavaScriptTypeScriptFile(file));
|
|
437
1032
|
|
|
438
1033
|
if (jstsFiles.length === 0) {
|
|
439
1034
|
console.warn('โ ๏ธ No JavaScript/TypeScript files found for ESLint analysis');
|
|
@@ -448,44 +1043,109 @@ class ESLintEngine extends AnalysisEngineInterface {
|
|
|
448
1043
|
return results;
|
|
449
1044
|
}
|
|
450
1045
|
|
|
451
|
-
//
|
|
1046
|
+
// Find project root from input path (usually the project's working directory)
|
|
452
1047
|
const path = require('path');
|
|
453
|
-
|
|
1048
|
+
let projectPath;
|
|
454
1049
|
|
|
455
|
-
|
|
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
|
+
}
|
|
456
1061
|
|
|
457
|
-
|
|
458
|
-
const analysisConfig = {
|
|
459
|
-
...this.createBaseConfig(),
|
|
460
|
-
...eslintConfig
|
|
461
|
-
};
|
|
1062
|
+
console.log(`๐ [ESLintEngine] Using project path: ${projectPath}`);
|
|
462
1063
|
|
|
463
|
-
//
|
|
464
|
-
const { ESLint } = await this.loadESLint();
|
|
1064
|
+
// Get config detection for reuse
|
|
465
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);
|
|
466
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
|
|
1092
|
+
const { ESLint } = await this.loadESLint();
|
|
467
1093
|
let finalESLintOptions;
|
|
1094
|
+
|
|
1095
|
+
// Configure ESLint to handle files appropriately
|
|
468
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);
|
|
469
1099
|
finalESLintOptions = {
|
|
470
|
-
overrideConfigFile:
|
|
471
|
-
overrideConfig: analysisConfig,
|
|
1100
|
+
overrideConfigFile: tempFlatConfigPath,
|
|
472
1101
|
cwd: projectPath
|
|
473
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`);
|
|
474
1112
|
} else {
|
|
475
|
-
//
|
|
1113
|
+
// No config found - use analysis config only
|
|
476
1114
|
finalESLintOptions = {
|
|
477
|
-
overrideConfig:
|
|
1115
|
+
overrideConfig: eslintConfig,
|
|
478
1116
|
cwd: projectPath
|
|
479
1117
|
};
|
|
1118
|
+
console.log(`โ ๏ธ [ESLintEngine] Using analysis config only`);
|
|
480
1119
|
}
|
|
481
1120
|
|
|
482
1121
|
const finalESLintInstance = new ESLint(finalESLintOptions);
|
|
483
1122
|
|
|
484
|
-
// Run ESLint analysis
|
|
1123
|
+
// Run ESLint analysis - let ESLint handle parsing errors gracefully
|
|
1124
|
+
console.log(`๐ [ESLintEngine] Analyzing ${jstsFiles.length} JavaScript/TypeScript files...`);
|
|
485
1125
|
const eslintResults = await finalESLintInstance.lintFiles(jstsFiles);
|
|
486
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
|
+
}
|
|
1146
|
+
|
|
487
1147
|
// Convert ESLint results to SunLint format
|
|
488
|
-
results.results = this.convertESLintResults(
|
|
1148
|
+
results.results = this.convertESLintResults(processedResults, rules);
|
|
489
1149
|
results.filesAnalyzed = jstsFiles.length;
|
|
490
1150
|
results.metadata.rulesAnalyzed = rules.map(r => r.id);
|
|
491
1151
|
results.metadata.eslintRulesUsed = Object.keys(eslintConfig.rules);
|
|
@@ -498,6 +1158,76 @@ class ESLintEngine extends AnalysisEngineInterface {
|
|
|
498
1158
|
return results;
|
|
499
1159
|
}
|
|
500
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
|
+
|
|
501
1231
|
/**
|
|
502
1232
|
* Check if file is JavaScript or TypeScript
|
|
503
1233
|
* Following Rule C006: Verb-noun naming
|
|
@@ -595,6 +1325,37 @@ class ESLintEngine extends AnalysisEngineInterface {
|
|
|
595
1325
|
|
|
596
1326
|
// Map SunLint rules to ESLint rules
|
|
597
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
|
|
598
1359
|
const eslintRules = this.ruleMapping.get(rule.id);
|
|
599
1360
|
|
|
600
1361
|
if (eslintRules && Array.isArray(eslintRules)) {
|
|
@@ -604,16 +1365,10 @@ class ESLintEngine extends AnalysisEngineInterface {
|
|
|
604
1365
|
config.rules[eslintRule] = severity;
|
|
605
1366
|
}
|
|
606
1367
|
} else {
|
|
607
|
-
//
|
|
1368
|
+
// Fallback - try as custom rule
|
|
608
1369
|
const customRuleName = `custom/${rule.id.toLowerCase()}`;
|
|
609
1370
|
const ruleConfig = this.mapSeverity(rule.severity || 'warning');
|
|
610
|
-
|
|
611
|
-
// Add rule configuration for specific rules
|
|
612
|
-
if (rule.id === 'C010') {
|
|
613
|
-
config.rules[customRuleName] = [ruleConfig, { maxDepth: 3 }];
|
|
614
|
-
} else {
|
|
615
|
-
config.rules[customRuleName] = ruleConfig;
|
|
616
|
-
}
|
|
1371
|
+
config.rules[customRuleName] = ruleConfig;
|
|
617
1372
|
}
|
|
618
1373
|
}
|
|
619
1374
|
|
|
@@ -732,6 +1487,22 @@ class ESLintEngine extends AnalysisEngineInterface {
|
|
|
732
1487
|
* Following Rule C006: Verb-noun naming
|
|
733
1488
|
*/
|
|
734
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
|
+
|
|
735
1506
|
this.eslint = null;
|
|
736
1507
|
this.configFiles.clear();
|
|
737
1508
|
this.ruleMapping.clear();
|
package/package.json
CHANGED