@sun-asterisk/sunlint 1.0.5
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 +202 -0
- package/LICENSE +21 -0
- package/README.md +490 -0
- package/cli-legacy.js +355 -0
- package/cli.js +35 -0
- package/config/default.json +22 -0
- package/config/presets/beginner.json +36 -0
- package/config/presets/ci.json +46 -0
- package/config/presets/recommended.json +24 -0
- package/config/presets/strict.json +32 -0
- package/config/rules-registry.json +681 -0
- package/config/sunlint-schema.json +166 -0
- package/config/typescript/custom-rules-new.js +0 -0
- package/config/typescript/custom-rules.js +9 -0
- package/config/typescript/eslint.config.js +110 -0
- package/config/typescript/package-lock.json +1585 -0
- package/config/typescript/package.json +13 -0
- package/config/typescript/security-rules/index.js +90 -0
- package/config/typescript/security-rules/s005-no-origin-auth.js +95 -0
- package/config/typescript/security-rules/s006-activation-recovery-secret-not-plaintext.js +69 -0
- package/config/typescript/security-rules/s008-crypto-agility.js +62 -0
- package/config/typescript/security-rules/s009-no-insecure-crypto.js +103 -0
- package/config/typescript/security-rules/s010-no-insecure-random-in-sensitive-context.js +123 -0
- package/config/typescript/security-rules/s011-no-insecure-uuid.js +66 -0
- package/config/typescript/security-rules/s012-hardcode-secret.js +71 -0
- package/config/typescript/security-rules/s014-insecure-tls-version.js +50 -0
- package/config/typescript/security-rules/s015-insecure-tls-certificate.js +43 -0
- package/config/typescript/security-rules/s016-sensitive-query-parameter.js +59 -0
- package/config/typescript/security-rules/s017-no-sql-injection.js +193 -0
- package/config/typescript/security-rules/s018-positive-input-validation.js +56 -0
- package/config/typescript/security-rules/s019-no-raw-user-input-in-email.js +113 -0
- package/config/typescript/security-rules/s020-no-eval-dynamic-execution.js +89 -0
- package/config/typescript/security-rules/s022-output-encoding.js +78 -0
- package/config/typescript/security-rules/s023-no-json-injection.js +300 -0
- package/config/typescript/security-rules/s025-server-side-input-validation.js +217 -0
- package/config/typescript/security-rules/s026-json-schema-validation.js +68 -0
- package/config/typescript/security-rules/s027-no-hardcoded-secrets.js +80 -0
- package/config/typescript/security-rules/s029-require-csrf-protection.js +79 -0
- package/config/typescript/security-rules/s030-no-directory-browsing.js +78 -0
- package/config/typescript/security-rules/s033-require-samesite-cookie.js +80 -0
- package/config/typescript/security-rules/s034-require-host-cookie-prefix.js +77 -0
- package/config/typescript/security-rules/s035-cookie-specific-path.js +74 -0
- package/config/typescript/security-rules/s036-no-unsafe-file-include.js +68 -0
- package/config/typescript/security-rules/s037-require-anti-cache-headers.js +70 -0
- package/config/typescript/security-rules/s038-no-version-disclosure.js +74 -0
- package/config/typescript/security-rules/s039-no-session-token-in-url.js +63 -0
- package/config/typescript/security-rules/s041-require-session-invalidate-on-logout.js +211 -0
- package/config/typescript/security-rules/s042-require-periodic-reauthentication.js +294 -0
- package/config/typescript/security-rules/s043-terminate-sessions-on-password-change.js +254 -0
- package/config/typescript/security-rules/s044-require-full-session-for-sensitive-operations.js +292 -0
- package/config/typescript/security-rules/s045-anti-automation-controls.js +46 -0
- package/config/typescript/security-rules/s046-secure-notification-on-auth-change.js +44 -0
- package/config/typescript/security-rules/s048-password-credential-recovery.js +54 -0
- package/config/typescript/security-rules/s050-session-token-weak-hash.js +94 -0
- package/config/typescript/security-rules/s052-secure-random-authentication-code.js +66 -0
- package/config/typescript/security-rules/s054-verification-default-account.js +109 -0
- package/config/typescript/security-rules/s057-utc-logging.js +54 -0
- package/config/typescript/security-rules/s058-no-ssrf.js +73 -0
- package/config/typescript/test-s005-working.ts +22 -0
- package/config/typescript/tsconfig.json +29 -0
- package/core/ai-analyzer.js +169 -0
- package/core/analysis-orchestrator.js +705 -0
- package/core/cli-action-handler.js +230 -0
- package/core/cli-program.js +106 -0
- package/core/config-manager.js +396 -0
- package/core/config-merger.js +136 -0
- package/core/config-override-processor.js +74 -0
- package/core/config-preset-resolver.js +65 -0
- package/core/config-source-loader.js +152 -0
- package/core/config-validator.js +126 -0
- package/core/dependency-manager.js +105 -0
- package/core/eslint-engine-service.js +312 -0
- package/core/eslint-instance-manager.js +104 -0
- package/core/eslint-integration-service.js +363 -0
- package/core/git-utils.js +170 -0
- package/core/multi-rule-runner.js +239 -0
- package/core/output-service.js +250 -0
- package/core/report-generator.js +320 -0
- package/core/rule-mapping-service.js +309 -0
- package/core/rule-selection-service.js +121 -0
- package/core/sunlint-engine-service.js +23 -0
- package/core/typescript-analyzer.js +262 -0
- package/core/typescript-engine.js +313 -0
- package/docs/AI.md +163 -0
- package/docs/ARCHITECTURE.md +78 -0
- package/docs/CI-CD-GUIDE.md +315 -0
- package/docs/COMMAND-EXAMPLES.md +256 -0
- package/docs/DEBUG.md +86 -0
- package/docs/DISTRIBUTION.md +153 -0
- package/docs/ESLINT-INTEGRATION-STRATEGY.md +392 -0
- package/docs/ESLINT_INTEGRATION.md +238 -0
- package/docs/FOLDER_STRUCTURE.md +59 -0
- package/docs/HEURISTIC_VS_AI.md +113 -0
- package/docs/README.md +32 -0
- package/docs/RELEASE_GUIDE.md +230 -0
- package/docs/RULE-RESPONSIBILITY-MATRIX.md +204 -0
- package/eslint-integration/.eslintrc.js +98 -0
- package/eslint-integration/cli.js +35 -0
- package/eslint-integration/eslint-plugin-custom/c002-no-duplicate-code.js +204 -0
- package/eslint-integration/eslint-plugin-custom/c003-no-vague-abbreviations.js +246 -0
- package/eslint-integration/eslint-plugin-custom/c006-function-name-verb-noun.js +207 -0
- package/eslint-integration/eslint-plugin-custom/c010-limit-block-nesting.js +90 -0
- package/eslint-integration/eslint-plugin-custom/c013-no-dead-code.js +43 -0
- package/eslint-integration/eslint-plugin-custom/c014-abstract-dependency-preferred.js +38 -0
- package/eslint-integration/eslint-plugin-custom/c017-limit-constructor-logic.js +39 -0
- package/eslint-integration/eslint-plugin-custom/c018-no-generic-throw.js +335 -0
- package/eslint-integration/eslint-plugin-custom/c023-no-duplicate-variable-name-in-scope.js +142 -0
- package/eslint-integration/eslint-plugin-custom/c027-limit-function-nesting.js +50 -0
- package/eslint-integration/eslint-plugin-custom/c029-catch-block-logging.js +80 -0
- package/eslint-integration/eslint-plugin-custom/c030-use-custom-error-classes.js +294 -0
- package/eslint-integration/eslint-plugin-custom/c034-no-implicit-return.js +34 -0
- package/eslint-integration/eslint-plugin-custom/c035-no-empty-catch.js +32 -0
- package/eslint-integration/eslint-plugin-custom/c041-no-config-inline.js +64 -0
- package/eslint-integration/eslint-plugin-custom/c042-boolean-name-prefix.js +406 -0
- package/eslint-integration/eslint-plugin-custom/c043-no-console-or-print.js +300 -0
- package/eslint-integration/eslint-plugin-custom/c047-no-duplicate-retry-logic.js +239 -0
- package/eslint-integration/eslint-plugin-custom/c048-no-var-declaration.js +31 -0
- package/eslint-integration/eslint-plugin-custom/c076-one-assert-per-test.js +184 -0
- package/eslint-integration/eslint-plugin-custom/index.js +155 -0
- package/eslint-integration/eslint-plugin-custom/package.json +13 -0
- package/eslint-integration/eslint-plugin-custom/package.json.bak +9 -0
- package/eslint-integration/eslint-plugin-custom/s003-no-unvalidated-redirect.js +86 -0
- package/eslint-integration/eslint-plugin-custom/s005-no-origin-auth.js +95 -0
- package/eslint-integration/eslint-plugin-custom/s006-activation-recovery-secret-not-plaintext.js +69 -0
- package/eslint-integration/eslint-plugin-custom/s008-crypto-agility.js +62 -0
- package/eslint-integration/eslint-plugin-custom/s009-no-insecure-crypto.js +103 -0
- package/eslint-integration/eslint-plugin-custom/s010-no-insecure-random-in-sensitive-context.js +123 -0
- package/eslint-integration/eslint-plugin-custom/s011-no-insecure-uuid.js +66 -0
- package/eslint-integration/eslint-plugin-custom/s012-hardcode-secret.js +71 -0
- package/eslint-integration/eslint-plugin-custom/s014-insecure-tls-version.js +50 -0
- package/eslint-integration/eslint-plugin-custom/s015-insecure-tls-certificate.js +43 -0
- package/eslint-integration/eslint-plugin-custom/s016-sensitive-query-parameter.js +59 -0
- package/eslint-integration/eslint-plugin-custom/s017-no-sql-injection.js +193 -0
- package/eslint-integration/eslint-plugin-custom/s018-positive-input-validation.js +56 -0
- package/eslint-integration/eslint-plugin-custom/s019-no-raw-user-input-in-email.js +113 -0
- package/eslint-integration/eslint-plugin-custom/s020-no-eval-dynamic-execution.js +89 -0
- package/eslint-integration/eslint-plugin-custom/s022-output-encoding.js +78 -0
- package/eslint-integration/eslint-plugin-custom/s023-no-json-injection.js +300 -0
- package/eslint-integration/eslint-plugin-custom/s025-server-side-input-validation.js +217 -0
- package/eslint-integration/eslint-plugin-custom/s026-json-schema-validation.js +68 -0
- package/eslint-integration/eslint-plugin-custom/s027-no-hardcoded-secrets.js +80 -0
- package/eslint-integration/eslint-plugin-custom/s029-require-csrf-protection.js +79 -0
- package/eslint-integration/eslint-plugin-custom/s030-no-directory-browsing.js +78 -0
- package/eslint-integration/eslint-plugin-custom/s033-require-samesite-cookie.js +80 -0
- package/eslint-integration/eslint-plugin-custom/s034-require-host-cookie-prefix.js +77 -0
- package/eslint-integration/eslint-plugin-custom/s035-cookie-specific-path.js +74 -0
- package/eslint-integration/eslint-plugin-custom/s036-no-unsafe-file-include.js +68 -0
- package/eslint-integration/eslint-plugin-custom/s037-require-anti-cache-headers.js +70 -0
- package/eslint-integration/eslint-plugin-custom/s038-no-version-disclosure.js +74 -0
- package/eslint-integration/eslint-plugin-custom/s039-no-session-token-in-url.js +63 -0
- package/eslint-integration/eslint-plugin-custom/s041-require-session-invalidate-on-logout.js +211 -0
- package/eslint-integration/eslint-plugin-custom/s042-require-periodic-reauthentication.js +294 -0
- package/eslint-integration/eslint-plugin-custom/s043-terminate-sessions-on-password-change.js +254 -0
- package/eslint-integration/eslint-plugin-custom/s044-require-full-session-for-sensitive-operations.js +292 -0
- package/eslint-integration/eslint-plugin-custom/s045-anti-automation-controls.js +46 -0
- package/eslint-integration/eslint-plugin-custom/s046-secure-notification-on-auth-change.js +44 -0
- package/eslint-integration/eslint-plugin-custom/s047-secure-random-passwords.js +108 -0
- package/eslint-integration/eslint-plugin-custom/s048-password-credential-recovery.js +54 -0
- package/eslint-integration/eslint-plugin-custom/s050-session-token-weak-hash.js +94 -0
- package/eslint-integration/eslint-plugin-custom/s052-secure-random-authentication-code.js +66 -0
- package/eslint-integration/eslint-plugin-custom/s054-verification-default-account.js +109 -0
- package/eslint-integration/eslint-plugin-custom/s055-verification-rest-check-the-incoming-content-type.js +143 -0
- package/eslint-integration/eslint-plugin-custom/s057-utc-logging.js +54 -0
- package/eslint-integration/eslint-plugin-custom/s058-no-ssrf.js +73 -0
- package/eslint-integration/eslint-plugin-custom/t002-interface-prefix-i.js +42 -0
- package/eslint-integration/eslint-plugin-custom/t003-ts-ignore-reason.js +48 -0
- package/eslint-integration/eslint-plugin-custom/t004-interface-public-only.js +160 -0
- package/eslint-integration/eslint-plugin-custom/t007-no-fn-in-constructor.js +52 -0
- package/eslint-integration/eslint-plugin-custom/t011-no-real-time-dependency.js +175 -0
- package/eslint-integration/eslint-plugin-custom/t019-no-empty-type.js +95 -0
- package/eslint-integration/eslint-plugin-custom/t025-no-nested-union-tuple.js +48 -0
- package/eslint-integration/eslint-plugin-custom/t026-limit-nested-generics.js +377 -0
- package/eslint-integration/eslint.config.js +125 -0
- package/eslint-integration/eslint.config.simple.js +24 -0
- package/eslint-integration/node_modules/eslint-plugin-custom/package.json +0 -0
- package/eslint-integration/package.json +23 -0
- package/eslint-integration/sample.ts +53 -0
- package/eslint-integration/test-s003.js +5 -0
- package/eslint-integration/tsconfig.json +27 -0
- package/examples/.github/workflows/code-quality.yml +111 -0
- package/examples/.sunlint.json +42 -0
- package/examples/README.md +47 -0
- package/examples/package.json +33 -0
- package/package.json +100 -0
- package/rules/C006_function_naming/analyzer.js +338 -0
- package/rules/C006_function_naming/config.json +86 -0
- package/rules/C019_log_level_usage/analyzer.js +359 -0
- package/rules/C019_log_level_usage/config.json +121 -0
- package/rules/C029_catch_block_logging/analyzer.js +339 -0
- package/rules/C029_catch_block_logging/config.json +59 -0
- package/rules/C031_validation_separation/README.md +72 -0
- package/rules/C031_validation_separation/analyzer.js +186 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const chalk = require('chalk');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Handles ESLint instance initialization and configuration detection
|
|
7
|
+
* Rule C005: Single responsibility - only ESLint initialization
|
|
8
|
+
* Rule C015: Domain language - EslintInstanceManager
|
|
9
|
+
*/
|
|
10
|
+
class EslintInstanceManager {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.eslintInstance = null;
|
|
13
|
+
this.isInitialized = false;
|
|
14
|
+
this.eslintModulePath = null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Rule C006: initializeEslintInstance - verb-noun naming
|
|
19
|
+
* Rule C032: No external API calls in constructor - initialization method
|
|
20
|
+
*/
|
|
21
|
+
async initializeEslintInstance(eslintModulePath = null) {
|
|
22
|
+
if (this.isInitialized) {
|
|
23
|
+
return this.eslintInstance;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
// Rule C014: Dependency injection from main package
|
|
28
|
+
const { ESLint } = require('eslint');
|
|
29
|
+
|
|
30
|
+
const configPath = this.resolveConfigPath(eslintModulePath);
|
|
31
|
+
|
|
32
|
+
this.eslintInstance = new ESLint({
|
|
33
|
+
overrideConfigFile: path.join(configPath, 'eslint.config.js'),
|
|
34
|
+
cache: false,
|
|
35
|
+
fix: false,
|
|
36
|
+
overrideConfig: {
|
|
37
|
+
linterOptions: {
|
|
38
|
+
reportUnusedDisableDirectives: 'error'
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
this.isInitialized = true;
|
|
44
|
+
this.eslintModulePath = configPath;
|
|
45
|
+
|
|
46
|
+
return this.eslintInstance;
|
|
47
|
+
} catch (error) {
|
|
48
|
+
throw new Error(`Failed to initialize ESLint: ${error.message}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Rule C006: resolveConfigPath - verb-noun naming
|
|
54
|
+
* Rule C005: Single responsibility - config path resolution
|
|
55
|
+
*/
|
|
56
|
+
resolveConfigPath(eslintModulePath) {
|
|
57
|
+
if (eslintModulePath) {
|
|
58
|
+
return eslintModulePath;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Try primary config path, fallback to eslint-integration
|
|
62
|
+
const primaryPath = path.join(__dirname, '..', 'config', 'typescript');
|
|
63
|
+
const fallbackPath = path.join(__dirname, '..', 'eslint-integration');
|
|
64
|
+
const configFile = 'eslint.config.js';
|
|
65
|
+
|
|
66
|
+
// Check if primary config exists
|
|
67
|
+
if (fs.existsSync(path.join(primaryPath, configFile))) {
|
|
68
|
+
return primaryPath;
|
|
69
|
+
} else {
|
|
70
|
+
// Fallback to eslint-integration
|
|
71
|
+
console.log(chalk.yellow('⚠️ Using fallback ESLint config from eslint-integration'));
|
|
72
|
+
return fallbackPath;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Rule C006: getEslintInstance - verb-noun naming
|
|
78
|
+
* Rule C012: Query method - returns instance without side effects
|
|
79
|
+
*/
|
|
80
|
+
getEslintInstance() {
|
|
81
|
+
if (!this.isInitialized) {
|
|
82
|
+
throw new Error('ESLint instance not initialized. Call initializeEslintInstance() first.');
|
|
83
|
+
}
|
|
84
|
+
return this.eslintInstance;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Rule C006: getConfigPath - verb-noun naming
|
|
89
|
+
* Rule C012: Query method
|
|
90
|
+
*/
|
|
91
|
+
getConfigPath() {
|
|
92
|
+
return this.eslintModulePath;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Rule C006: checkInstanceReady - verb-noun naming
|
|
97
|
+
* Rule C012: Query method
|
|
98
|
+
*/
|
|
99
|
+
checkInstanceReady() {
|
|
100
|
+
return this.isInitialized && this.eslintInstance !== null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = EslintInstanceManager;
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint Integration Service
|
|
3
|
+
* Following Rule C005: Single responsibility - handle ESLint integration
|
|
4
|
+
* Following Rule C014: Dependency injection for ESLint engine
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const chalk = require('chalk');
|
|
10
|
+
const { ESLint } = require('eslint');
|
|
11
|
+
|
|
12
|
+
class ESLintIntegrationService {
|
|
13
|
+
constructor() {
|
|
14
|
+
this.eslintInstance = null;
|
|
15
|
+
this.userESLintConfig = null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Check if project has existing ESLint configuration
|
|
20
|
+
* @param {string} projectRoot - Project root directory
|
|
21
|
+
* @returns {boolean} Whether ESLint config exists
|
|
22
|
+
*/
|
|
23
|
+
hasExistingESLintConfig(projectRoot) {
|
|
24
|
+
const configFiles = [
|
|
25
|
+
'.eslintrc.js',
|
|
26
|
+
'.eslintrc.json',
|
|
27
|
+
'.eslintrc.yaml',
|
|
28
|
+
'.eslintrc.yml',
|
|
29
|
+
'eslint.config.js',
|
|
30
|
+
'eslint.config.mjs'
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
return configFiles.some(file => {
|
|
34
|
+
const configPath = path.join(projectRoot, file);
|
|
35
|
+
return fs.existsSync(configPath);
|
|
36
|
+
}) || this.hasPackageJsonESLintConfig(projectRoot);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Check if package.json has ESLint config
|
|
41
|
+
*/
|
|
42
|
+
hasPackageJsonESLintConfig(projectRoot) {
|
|
43
|
+
const packagePath = path.join(projectRoot, 'package.json');
|
|
44
|
+
if (fs.existsSync(packagePath)) {
|
|
45
|
+
try {
|
|
46
|
+
const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8'));
|
|
47
|
+
return !!(pkg.eslintConfig || pkg.eslint);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Load existing ESLint configuration
|
|
57
|
+
* @param {string} projectRoot - Project root directory
|
|
58
|
+
* @param {Object} sunlintConfig - SunLint configuration for legacy support check
|
|
59
|
+
* @returns {Object} ESLint configuration object
|
|
60
|
+
*/
|
|
61
|
+
async loadExistingESLintConfig(projectRoot, sunlintConfig = {}) {
|
|
62
|
+
try {
|
|
63
|
+
console.log('Debug: Project root:', projectRoot);
|
|
64
|
+
console.log('Debug: Config files check:', {
|
|
65
|
+
flatConfig: fs.existsSync(path.join(projectRoot, 'eslint.config.mjs')),
|
|
66
|
+
eslintrc: fs.existsSync(path.join(projectRoot, '.eslintrc.json')),
|
|
67
|
+
eslintrcJs: fs.existsSync(path.join(projectRoot, '.eslintrc.js')),
|
|
68
|
+
packageJson: fs.existsSync(path.join(projectRoot, 'package.json'))
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// For ESLint v9, prioritize flat config files
|
|
72
|
+
const flatConfigPath = path.join(projectRoot, 'eslint.config.mjs');
|
|
73
|
+
if (fs.existsSync(flatConfigPath)) {
|
|
74
|
+
console.log('Debug: Found flat config eslint.config.mjs');
|
|
75
|
+
// For flat config, we don't need to load it directly
|
|
76
|
+
// ESLint v9 will auto-discover it
|
|
77
|
+
this.userESLintConfig = { flatConfig: true };
|
|
78
|
+
return { flatConfig: true };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Fallback to legacy .eslintrc format
|
|
82
|
+
const eslintrcPath = path.join(projectRoot, '.eslintrc.json');
|
|
83
|
+
if (fs.existsSync(eslintrcPath)) {
|
|
84
|
+
const configContent = JSON.parse(fs.readFileSync(eslintrcPath, 'utf8'));
|
|
85
|
+
console.log('Debug: Loaded .eslintrc.json successfully');
|
|
86
|
+
this.userESLintConfig = configContent;
|
|
87
|
+
return configContent;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Check for .eslintrc.js
|
|
91
|
+
const eslintrcJsPath = path.join(projectRoot, '.eslintrc.js');
|
|
92
|
+
if (fs.existsSync(eslintrcJsPath)) {
|
|
93
|
+
console.log('Debug: Found .eslintrc.js, using legacy format');
|
|
94
|
+
// We can't require() the .eslintrc.js directly due to potential conflicts,
|
|
95
|
+
// but we signal that a legacy config exists
|
|
96
|
+
this.userESLintConfig = { legacyConfig: true, configFile: eslintrcJsPath };
|
|
97
|
+
return { legacyConfig: true, configFile: eslintrcJsPath };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Last resort - try ESLint API
|
|
101
|
+
const eslint = new ESLint({
|
|
102
|
+
cwd: path.resolve(projectRoot)
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const sampleFile = path.join(projectRoot, 'src/violations.ts');
|
|
106
|
+
console.log('Debug: Sample file:', sampleFile, 'exists:', fs.existsSync(sampleFile));
|
|
107
|
+
const config = await eslint.calculateConfigForFile(sampleFile);
|
|
108
|
+
|
|
109
|
+
this.userESLintConfig = config;
|
|
110
|
+
return config;
|
|
111
|
+
} catch (error) {
|
|
112
|
+
console.warn(chalk.yellow(`⚠️ Could not load ESLint config: ${error.message}`));
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Create merged ESLint configuration (SunLint + User ESLint)
|
|
119
|
+
* @param {Object} sunlintESLintConfig - SunLint's ESLint configuration
|
|
120
|
+
* @param {Object} userESLintConfig - User's existing ESLint configuration
|
|
121
|
+
* @returns {Object} Merged configuration
|
|
122
|
+
*/
|
|
123
|
+
createMergedConfig(sunlintESLintConfig, userESLintConfig) {
|
|
124
|
+
if (!userESLintConfig) {
|
|
125
|
+
return sunlintESLintConfig;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Ensure configs are safe objects
|
|
129
|
+
const safeUserConfig = typeof userESLintConfig === 'object' ? userESLintConfig : {};
|
|
130
|
+
const safeSunlintConfig = typeof sunlintESLintConfig === 'object' ? sunlintESLintConfig : {};
|
|
131
|
+
|
|
132
|
+
const merged = {
|
|
133
|
+
...safeSunlintConfig,
|
|
134
|
+
// Merge extends arrays safely
|
|
135
|
+
extends: [
|
|
136
|
+
...(Array.isArray(safeSunlintConfig.extends) ? safeSunlintConfig.extends : []),
|
|
137
|
+
...(Array.isArray(safeUserConfig.extends) ? safeUserConfig.extends : [])
|
|
138
|
+
],
|
|
139
|
+
// Merge plugins arrays safely
|
|
140
|
+
plugins: [
|
|
141
|
+
...(Array.isArray(safeSunlintConfig.plugins) ? safeSunlintConfig.plugins : []),
|
|
142
|
+
...(Array.isArray(safeUserConfig.plugins) ? safeUserConfig.plugins : [])
|
|
143
|
+
],
|
|
144
|
+
// Merge rules (user rules override SunLint in case of conflict)
|
|
145
|
+
rules: {
|
|
146
|
+
...(safeSunlintConfig.rules || {}),
|
|
147
|
+
...(safeUserConfig.rules || {})
|
|
148
|
+
},
|
|
149
|
+
// Merge parser options
|
|
150
|
+
parserOptions: {
|
|
151
|
+
...(safeSunlintConfig.parserOptions || {}),
|
|
152
|
+
...(safeUserConfig.parserOptions || {})
|
|
153
|
+
},
|
|
154
|
+
// Merge env
|
|
155
|
+
env: {
|
|
156
|
+
...(safeSunlintConfig.env || {}),
|
|
157
|
+
...(safeUserConfig.env || {})
|
|
158
|
+
},
|
|
159
|
+
// Merge settings
|
|
160
|
+
settings: {
|
|
161
|
+
...(safeSunlintConfig.settings || {}),
|
|
162
|
+
...(safeUserConfig.settings || {})
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
return merged;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Run integrated ESLint analysis (SunLint + User rules)
|
|
171
|
+
* @param {string[]} filePaths - Files to analyze
|
|
172
|
+
* @param {Object} sunlintConfig - SunLint configuration
|
|
173
|
+
* @param {Object} options - Analysis options
|
|
174
|
+
* @returns {Object} Combined ESLint results
|
|
175
|
+
*/
|
|
176
|
+
async runIntegratedAnalysis(filePaths, sunlintConfig, options = {}) {
|
|
177
|
+
try {
|
|
178
|
+
// Resolve first file path to determine project root
|
|
179
|
+
const firstFilePath = path.resolve(filePaths[0]);
|
|
180
|
+
const projectRoot = this.findProjectRoot(firstFilePath);
|
|
181
|
+
|
|
182
|
+
// Load existing ESLint config
|
|
183
|
+
const userESLintConfig = await this.loadExistingESLintConfig(projectRoot, sunlintConfig);
|
|
184
|
+
|
|
185
|
+
// Create ESLint instance based on config type
|
|
186
|
+
let eslintOptions = {
|
|
187
|
+
cwd: path.resolve(projectRoot),
|
|
188
|
+
fix: options.fix || false,
|
|
189
|
+
ignore: false
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// If we found a legacy config file, specify it explicitly
|
|
193
|
+
if (userESLintConfig && userESLintConfig.legacyConfig) {
|
|
194
|
+
eslintOptions.overrideConfigFile = userESLintConfig.configFile;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
this.eslintInstance = new ESLint(eslintOptions);
|
|
198
|
+
|
|
199
|
+
// Run analysis with resolved file paths
|
|
200
|
+
const resolvedFilePaths = [];
|
|
201
|
+
|
|
202
|
+
for (const fp of filePaths) {
|
|
203
|
+
const resolved = path.isAbsolute(fp) ? fp : path.resolve(process.cwd(), fp);
|
|
204
|
+
|
|
205
|
+
// If it's a directory, find all JS/TS files in it
|
|
206
|
+
if (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {
|
|
207
|
+
const glob = require('glob');
|
|
208
|
+
const files = glob.sync(`${resolved}/**/*.{js,jsx,ts,tsx}`, {
|
|
209
|
+
ignore: ['**/node_modules/**', '**/.next/**']
|
|
210
|
+
});
|
|
211
|
+
resolvedFilePaths.push(...files);
|
|
212
|
+
} else {
|
|
213
|
+
resolvedFilePaths.push(resolved);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const results = await this.eslintInstance.lintFiles(resolvedFilePaths);
|
|
218
|
+
|
|
219
|
+
// Categorize results by rule source
|
|
220
|
+
const categorizedResults = this.categorizeResults(results);
|
|
221
|
+
|
|
222
|
+
if (!options.quiet) {
|
|
223
|
+
this.logIntegrationSummary(categorizedResults, userESLintConfig !== null);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
results,
|
|
228
|
+
categorized: categorizedResults,
|
|
229
|
+
totalRules: results.length,
|
|
230
|
+
sunlintRules: 0, // Will be counted in analysis orchestrator
|
|
231
|
+
userRules: results.length
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
} catch (error) {
|
|
235
|
+
throw new Error(`ESLint integration failed: ${error.message}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Get SunLint's ESLint configuration
|
|
241
|
+
*/
|
|
242
|
+
getSunLintESLintConfig(sunlintConfig) {
|
|
243
|
+
// This would be the ESLint config that SunLint normally uses
|
|
244
|
+
return {
|
|
245
|
+
extends: ['@sun/sunlint/eslint-recommended'],
|
|
246
|
+
plugins: ['@sun/sunlint'],
|
|
247
|
+
rules: {
|
|
248
|
+
// Map SunLint rules to ESLint format
|
|
249
|
+
'custom/typescript_s003': 'warn',
|
|
250
|
+
'custom/typescript_s047': 'warn',
|
|
251
|
+
'custom/typescript_s055': 'warn',
|
|
252
|
+
// ... other SunLint rules
|
|
253
|
+
},
|
|
254
|
+
parserOptions: {
|
|
255
|
+
ecmaVersion: 2022,
|
|
256
|
+
sourceType: 'module',
|
|
257
|
+
project: './tsconfig.json'
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Categorize ESLint results by rule source (SunLint vs User)
|
|
264
|
+
*/
|
|
265
|
+
categorizeResults(results) {
|
|
266
|
+
const categorized = {
|
|
267
|
+
sunlint: [],
|
|
268
|
+
user: [],
|
|
269
|
+
combined: []
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
results.forEach(fileResult => {
|
|
273
|
+
fileResult.messages.forEach(message => {
|
|
274
|
+
const ruleId = message.ruleId;
|
|
275
|
+
if (this.isSunLintRule(ruleId)) {
|
|
276
|
+
categorized.sunlint.push({ ...message, filePath: fileResult.filePath });
|
|
277
|
+
} else {
|
|
278
|
+
categorized.user.push({ ...message, filePath: fileResult.filePath });
|
|
279
|
+
}
|
|
280
|
+
categorized.combined.push({ ...message, filePath: fileResult.filePath });
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
return categorized;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Check if rule belongs to SunLint
|
|
289
|
+
*/
|
|
290
|
+
isSunLintRule(ruleId) {
|
|
291
|
+
return ruleId && (
|
|
292
|
+
ruleId.startsWith('custom/typescript_') ||
|
|
293
|
+
ruleId.startsWith('@sun/sunlint/') ||
|
|
294
|
+
ruleId.startsWith('S') ||
|
|
295
|
+
ruleId.startsWith('C')
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Count total active rules in configuration
|
|
301
|
+
*/
|
|
302
|
+
countTotalRules(config) {
|
|
303
|
+
return Object.keys(config.rules || {}).length;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
countSunLintRules(config) {
|
|
307
|
+
return Object.keys(config.rules || {})
|
|
308
|
+
.filter(ruleId => this.isSunLintRule(ruleId))
|
|
309
|
+
.length;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
countUserRules(config, userConfig) {
|
|
313
|
+
if (!userConfig) return 0;
|
|
314
|
+
return Object.keys(config.rules || {})
|
|
315
|
+
.filter(ruleId => !this.isSunLintRule(ruleId))
|
|
316
|
+
.length;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Log integration summary
|
|
321
|
+
*/
|
|
322
|
+
logIntegrationSummary(categorized, hasUserConfig) {
|
|
323
|
+
console.log(chalk.blue('🔗 ESLint Integration Summary:'));
|
|
324
|
+
console.log(chalk.green(` 📋 SunLint violations: ${categorized.sunlint.length}`));
|
|
325
|
+
|
|
326
|
+
if (hasUserConfig) {
|
|
327
|
+
console.log(chalk.cyan(` 🔧 User ESLint violations: ${categorized.user.length}`));
|
|
328
|
+
console.log(chalk.yellow(` 📊 Total combined violations: ${categorized.combined.length}`));
|
|
329
|
+
} else {
|
|
330
|
+
console.log(chalk.gray(' 🔧 No existing ESLint config found'));
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Find project root directory
|
|
336
|
+
*/
|
|
337
|
+
findProjectRoot(startPath) {
|
|
338
|
+
let currentPath = path.dirname(startPath);
|
|
339
|
+
while (currentPath !== path.dirname(currentPath)) {
|
|
340
|
+
const packagePath = path.join(currentPath, 'package.json');
|
|
341
|
+
if (fs.existsSync(packagePath)) {
|
|
342
|
+
return currentPath;
|
|
343
|
+
}
|
|
344
|
+
currentPath = path.dirname(currentPath);
|
|
345
|
+
}
|
|
346
|
+
return path.dirname(startPath);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Check if legacy ESLint support is enabled in SunLint config
|
|
351
|
+
* @param {Object} sunlintConfig - SunLint configuration
|
|
352
|
+
* @returns {boolean} Whether legacy ESLint support is enabled
|
|
353
|
+
*/
|
|
354
|
+
isLegacySupportEnabled(sunlintConfig) {
|
|
355
|
+
// Default to true for backward compatibility
|
|
356
|
+
const legacySupport = sunlintConfig?.integration?.eslint?.legacySupport !== false;
|
|
357
|
+
const eslintV8Support = sunlintConfig?.compatibility?.eslintV8 !== false;
|
|
358
|
+
|
|
359
|
+
return legacySupport && eslintV8Support;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
module.exports = ESLintIntegrationService;
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git Utils - Handle git operations for file filtering
|
|
3
|
+
* Following Rule C005: Single responsibility - only handle git operations
|
|
4
|
+
* Following Rule C014: Dependency injection for git operations
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { execSync } = require('child_process');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
|
|
11
|
+
class GitUtils {
|
|
12
|
+
/**
|
|
13
|
+
* Check if current directory is a git repository
|
|
14
|
+
*/
|
|
15
|
+
static isGitRepository(cwd = process.cwd()) {
|
|
16
|
+
try {
|
|
17
|
+
execSync('git rev-parse --git-dir', { cwd, stdio: 'ignore' });
|
|
18
|
+
return true;
|
|
19
|
+
} catch (error) {
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get list of changed files compared to base reference
|
|
26
|
+
* @param {string} baseRef - Base git reference (e.g., 'origin/main')
|
|
27
|
+
* @param {string} cwd - Working directory
|
|
28
|
+
* @returns {string[]} Array of changed file paths
|
|
29
|
+
*/
|
|
30
|
+
static getChangedFiles(baseRef = 'HEAD', cwd = process.cwd()) {
|
|
31
|
+
if (!this.isGitRepository(cwd)) {
|
|
32
|
+
throw new Error('Not a git repository');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
// Get git root directory
|
|
37
|
+
const gitRoot = execSync('git rev-parse --show-toplevel', { cwd, encoding: 'utf8' }).trim();
|
|
38
|
+
|
|
39
|
+
const command = `git diff --name-only ${baseRef}`;
|
|
40
|
+
const output = execSync(command, { cwd: gitRoot, encoding: 'utf8' });
|
|
41
|
+
|
|
42
|
+
return output
|
|
43
|
+
.split('\n')
|
|
44
|
+
.filter(file => file.trim() !== '')
|
|
45
|
+
.map(file => path.resolve(gitRoot, file))
|
|
46
|
+
.filter(file => fs.existsSync(file)); // Only existing files
|
|
47
|
+
} catch (error) {
|
|
48
|
+
throw new Error(`Failed to get changed files: ${error.message}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get list of staged files
|
|
54
|
+
* @param {string} cwd - Working directory
|
|
55
|
+
* @returns {string[]} Array of staged file paths
|
|
56
|
+
*/
|
|
57
|
+
static getStagedFiles(cwd = process.cwd()) {
|
|
58
|
+
if (!this.isGitRepository(cwd)) {
|
|
59
|
+
throw new Error('Not a git repository');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
// Get git root directory
|
|
64
|
+
const gitRoot = execSync('git rev-parse --show-toplevel', { cwd, encoding: 'utf8' }).trim();
|
|
65
|
+
|
|
66
|
+
const command = 'git diff --cached --name-only';
|
|
67
|
+
const output = execSync(command, { cwd: gitRoot, encoding: 'utf8' });
|
|
68
|
+
|
|
69
|
+
return output
|
|
70
|
+
.split('\n')
|
|
71
|
+
.filter(file => file.trim() !== '')
|
|
72
|
+
.map(file => path.resolve(gitRoot, file))
|
|
73
|
+
.filter(file => fs.existsSync(file));
|
|
74
|
+
} catch (error) {
|
|
75
|
+
throw new Error(`Failed to get staged files: ${error.message}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get files changed since specific commit
|
|
81
|
+
* @param {string} commit - Commit hash or reference
|
|
82
|
+
* @param {string} cwd - Working directory
|
|
83
|
+
* @returns {string[]} Array of changed file paths
|
|
84
|
+
*/
|
|
85
|
+
static getFilesSince(commit, cwd = process.cwd()) {
|
|
86
|
+
if (!this.isGitRepository(cwd)) {
|
|
87
|
+
throw new Error('Not a git repository');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
// Get git root directory
|
|
92
|
+
const gitRoot = execSync('git rev-parse --show-toplevel', { cwd, encoding: 'utf8' }).trim();
|
|
93
|
+
|
|
94
|
+
const command = `git diff --name-only ${commit}..HEAD`;
|
|
95
|
+
const output = execSync(command, { cwd: gitRoot, encoding: 'utf8' });
|
|
96
|
+
|
|
97
|
+
return output
|
|
98
|
+
.split('\n')
|
|
99
|
+
.filter(file => file.trim() !== '')
|
|
100
|
+
.map(file => path.resolve(gitRoot, file))
|
|
101
|
+
.filter(file => fs.existsSync(file));
|
|
102
|
+
} catch (error) {
|
|
103
|
+
throw new Error(`Failed to get files since ${commit}: ${error.message}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get current branch name
|
|
109
|
+
* @param {string} cwd - Working directory
|
|
110
|
+
* @returns {string} Current branch name
|
|
111
|
+
*/
|
|
112
|
+
static getCurrentBranch(cwd = process.cwd()) {
|
|
113
|
+
if (!this.isGitRepository(cwd)) {
|
|
114
|
+
throw new Error('Not a git repository');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const command = 'git rev-parse --abbrev-ref HEAD';
|
|
119
|
+
const output = execSync(command, { cwd, encoding: 'utf8' });
|
|
120
|
+
return output.trim();
|
|
121
|
+
} catch (error) {
|
|
122
|
+
throw new Error(`Failed to get current branch: ${error.message}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Filter TypeScript/JavaScript files from file list
|
|
128
|
+
* @param {string[]} files - Array of file paths
|
|
129
|
+
* @returns {string[]} Filtered TypeScript/JavaScript files
|
|
130
|
+
*/
|
|
131
|
+
static filterSourceFiles(files) {
|
|
132
|
+
const extensions = ['.ts', '.tsx', '.js', '.jsx'];
|
|
133
|
+
return files.filter(file => {
|
|
134
|
+
const ext = path.extname(file);
|
|
135
|
+
return extensions.includes(ext);
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Get git diff base reference for PR mode
|
|
141
|
+
* @param {string} targetBranch - Target branch (e.g., 'main', 'develop')
|
|
142
|
+
* @param {string} cwd - Working directory
|
|
143
|
+
* @returns {string} Git reference for comparison
|
|
144
|
+
*/
|
|
145
|
+
static getPRDiffBase(targetBranch = 'main', cwd = process.cwd()) {
|
|
146
|
+
if (!this.isGitRepository(cwd)) {
|
|
147
|
+
throw new Error('Not a git repository');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Try common remote references
|
|
151
|
+
const candidates = [
|
|
152
|
+
`origin/${targetBranch}`,
|
|
153
|
+
`upstream/${targetBranch}`,
|
|
154
|
+
targetBranch
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
for (const candidate of candidates) {
|
|
158
|
+
try {
|
|
159
|
+
execSync(`git rev-parse --verify ${candidate}`, { cwd, stdio: 'ignore' });
|
|
160
|
+
return candidate;
|
|
161
|
+
} catch (error) {
|
|
162
|
+
// Continue to next candidate
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
throw new Error(`No valid git reference found for target branch: ${targetBranch}`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
module.exports = GitUtils;
|