@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.
Files changed (192) hide show
  1. package/CHANGELOG.md +202 -0
  2. package/LICENSE +21 -0
  3. package/README.md +490 -0
  4. package/cli-legacy.js +355 -0
  5. package/cli.js +35 -0
  6. package/config/default.json +22 -0
  7. package/config/presets/beginner.json +36 -0
  8. package/config/presets/ci.json +46 -0
  9. package/config/presets/recommended.json +24 -0
  10. package/config/presets/strict.json +32 -0
  11. package/config/rules-registry.json +681 -0
  12. package/config/sunlint-schema.json +166 -0
  13. package/config/typescript/custom-rules-new.js +0 -0
  14. package/config/typescript/custom-rules.js +9 -0
  15. package/config/typescript/eslint.config.js +110 -0
  16. package/config/typescript/package-lock.json +1585 -0
  17. package/config/typescript/package.json +13 -0
  18. package/config/typescript/security-rules/index.js +90 -0
  19. package/config/typescript/security-rules/s005-no-origin-auth.js +95 -0
  20. package/config/typescript/security-rules/s006-activation-recovery-secret-not-plaintext.js +69 -0
  21. package/config/typescript/security-rules/s008-crypto-agility.js +62 -0
  22. package/config/typescript/security-rules/s009-no-insecure-crypto.js +103 -0
  23. package/config/typescript/security-rules/s010-no-insecure-random-in-sensitive-context.js +123 -0
  24. package/config/typescript/security-rules/s011-no-insecure-uuid.js +66 -0
  25. package/config/typescript/security-rules/s012-hardcode-secret.js +71 -0
  26. package/config/typescript/security-rules/s014-insecure-tls-version.js +50 -0
  27. package/config/typescript/security-rules/s015-insecure-tls-certificate.js +43 -0
  28. package/config/typescript/security-rules/s016-sensitive-query-parameter.js +59 -0
  29. package/config/typescript/security-rules/s017-no-sql-injection.js +193 -0
  30. package/config/typescript/security-rules/s018-positive-input-validation.js +56 -0
  31. package/config/typescript/security-rules/s019-no-raw-user-input-in-email.js +113 -0
  32. package/config/typescript/security-rules/s020-no-eval-dynamic-execution.js +89 -0
  33. package/config/typescript/security-rules/s022-output-encoding.js +78 -0
  34. package/config/typescript/security-rules/s023-no-json-injection.js +300 -0
  35. package/config/typescript/security-rules/s025-server-side-input-validation.js +217 -0
  36. package/config/typescript/security-rules/s026-json-schema-validation.js +68 -0
  37. package/config/typescript/security-rules/s027-no-hardcoded-secrets.js +80 -0
  38. package/config/typescript/security-rules/s029-require-csrf-protection.js +79 -0
  39. package/config/typescript/security-rules/s030-no-directory-browsing.js +78 -0
  40. package/config/typescript/security-rules/s033-require-samesite-cookie.js +80 -0
  41. package/config/typescript/security-rules/s034-require-host-cookie-prefix.js +77 -0
  42. package/config/typescript/security-rules/s035-cookie-specific-path.js +74 -0
  43. package/config/typescript/security-rules/s036-no-unsafe-file-include.js +68 -0
  44. package/config/typescript/security-rules/s037-require-anti-cache-headers.js +70 -0
  45. package/config/typescript/security-rules/s038-no-version-disclosure.js +74 -0
  46. package/config/typescript/security-rules/s039-no-session-token-in-url.js +63 -0
  47. package/config/typescript/security-rules/s041-require-session-invalidate-on-logout.js +211 -0
  48. package/config/typescript/security-rules/s042-require-periodic-reauthentication.js +294 -0
  49. package/config/typescript/security-rules/s043-terminate-sessions-on-password-change.js +254 -0
  50. package/config/typescript/security-rules/s044-require-full-session-for-sensitive-operations.js +292 -0
  51. package/config/typescript/security-rules/s045-anti-automation-controls.js +46 -0
  52. package/config/typescript/security-rules/s046-secure-notification-on-auth-change.js +44 -0
  53. package/config/typescript/security-rules/s048-password-credential-recovery.js +54 -0
  54. package/config/typescript/security-rules/s050-session-token-weak-hash.js +94 -0
  55. package/config/typescript/security-rules/s052-secure-random-authentication-code.js +66 -0
  56. package/config/typescript/security-rules/s054-verification-default-account.js +109 -0
  57. package/config/typescript/security-rules/s057-utc-logging.js +54 -0
  58. package/config/typescript/security-rules/s058-no-ssrf.js +73 -0
  59. package/config/typescript/test-s005-working.ts +22 -0
  60. package/config/typescript/tsconfig.json +29 -0
  61. package/core/ai-analyzer.js +169 -0
  62. package/core/analysis-orchestrator.js +705 -0
  63. package/core/cli-action-handler.js +230 -0
  64. package/core/cli-program.js +106 -0
  65. package/core/config-manager.js +396 -0
  66. package/core/config-merger.js +136 -0
  67. package/core/config-override-processor.js +74 -0
  68. package/core/config-preset-resolver.js +65 -0
  69. package/core/config-source-loader.js +152 -0
  70. package/core/config-validator.js +126 -0
  71. package/core/dependency-manager.js +105 -0
  72. package/core/eslint-engine-service.js +312 -0
  73. package/core/eslint-instance-manager.js +104 -0
  74. package/core/eslint-integration-service.js +363 -0
  75. package/core/git-utils.js +170 -0
  76. package/core/multi-rule-runner.js +239 -0
  77. package/core/output-service.js +250 -0
  78. package/core/report-generator.js +320 -0
  79. package/core/rule-mapping-service.js +309 -0
  80. package/core/rule-selection-service.js +121 -0
  81. package/core/sunlint-engine-service.js +23 -0
  82. package/core/typescript-analyzer.js +262 -0
  83. package/core/typescript-engine.js +313 -0
  84. package/docs/AI.md +163 -0
  85. package/docs/ARCHITECTURE.md +78 -0
  86. package/docs/CI-CD-GUIDE.md +315 -0
  87. package/docs/COMMAND-EXAMPLES.md +256 -0
  88. package/docs/DEBUG.md +86 -0
  89. package/docs/DISTRIBUTION.md +153 -0
  90. package/docs/ESLINT-INTEGRATION-STRATEGY.md +392 -0
  91. package/docs/ESLINT_INTEGRATION.md +238 -0
  92. package/docs/FOLDER_STRUCTURE.md +59 -0
  93. package/docs/HEURISTIC_VS_AI.md +113 -0
  94. package/docs/README.md +32 -0
  95. package/docs/RELEASE_GUIDE.md +230 -0
  96. package/docs/RULE-RESPONSIBILITY-MATRIX.md +204 -0
  97. package/eslint-integration/.eslintrc.js +98 -0
  98. package/eslint-integration/cli.js +35 -0
  99. package/eslint-integration/eslint-plugin-custom/c002-no-duplicate-code.js +204 -0
  100. package/eslint-integration/eslint-plugin-custom/c003-no-vague-abbreviations.js +246 -0
  101. package/eslint-integration/eslint-plugin-custom/c006-function-name-verb-noun.js +207 -0
  102. package/eslint-integration/eslint-plugin-custom/c010-limit-block-nesting.js +90 -0
  103. package/eslint-integration/eslint-plugin-custom/c013-no-dead-code.js +43 -0
  104. package/eslint-integration/eslint-plugin-custom/c014-abstract-dependency-preferred.js +38 -0
  105. package/eslint-integration/eslint-plugin-custom/c017-limit-constructor-logic.js +39 -0
  106. package/eslint-integration/eslint-plugin-custom/c018-no-generic-throw.js +335 -0
  107. package/eslint-integration/eslint-plugin-custom/c023-no-duplicate-variable-name-in-scope.js +142 -0
  108. package/eslint-integration/eslint-plugin-custom/c027-limit-function-nesting.js +50 -0
  109. package/eslint-integration/eslint-plugin-custom/c029-catch-block-logging.js +80 -0
  110. package/eslint-integration/eslint-plugin-custom/c030-use-custom-error-classes.js +294 -0
  111. package/eslint-integration/eslint-plugin-custom/c034-no-implicit-return.js +34 -0
  112. package/eslint-integration/eslint-plugin-custom/c035-no-empty-catch.js +32 -0
  113. package/eslint-integration/eslint-plugin-custom/c041-no-config-inline.js +64 -0
  114. package/eslint-integration/eslint-plugin-custom/c042-boolean-name-prefix.js +406 -0
  115. package/eslint-integration/eslint-plugin-custom/c043-no-console-or-print.js +300 -0
  116. package/eslint-integration/eslint-plugin-custom/c047-no-duplicate-retry-logic.js +239 -0
  117. package/eslint-integration/eslint-plugin-custom/c048-no-var-declaration.js +31 -0
  118. package/eslint-integration/eslint-plugin-custom/c076-one-assert-per-test.js +184 -0
  119. package/eslint-integration/eslint-plugin-custom/index.js +155 -0
  120. package/eslint-integration/eslint-plugin-custom/package.json +13 -0
  121. package/eslint-integration/eslint-plugin-custom/package.json.bak +9 -0
  122. package/eslint-integration/eslint-plugin-custom/s003-no-unvalidated-redirect.js +86 -0
  123. package/eslint-integration/eslint-plugin-custom/s005-no-origin-auth.js +95 -0
  124. package/eslint-integration/eslint-plugin-custom/s006-activation-recovery-secret-not-plaintext.js +69 -0
  125. package/eslint-integration/eslint-plugin-custom/s008-crypto-agility.js +62 -0
  126. package/eslint-integration/eslint-plugin-custom/s009-no-insecure-crypto.js +103 -0
  127. package/eslint-integration/eslint-plugin-custom/s010-no-insecure-random-in-sensitive-context.js +123 -0
  128. package/eslint-integration/eslint-plugin-custom/s011-no-insecure-uuid.js +66 -0
  129. package/eslint-integration/eslint-plugin-custom/s012-hardcode-secret.js +71 -0
  130. package/eslint-integration/eslint-plugin-custom/s014-insecure-tls-version.js +50 -0
  131. package/eslint-integration/eslint-plugin-custom/s015-insecure-tls-certificate.js +43 -0
  132. package/eslint-integration/eslint-plugin-custom/s016-sensitive-query-parameter.js +59 -0
  133. package/eslint-integration/eslint-plugin-custom/s017-no-sql-injection.js +193 -0
  134. package/eslint-integration/eslint-plugin-custom/s018-positive-input-validation.js +56 -0
  135. package/eslint-integration/eslint-plugin-custom/s019-no-raw-user-input-in-email.js +113 -0
  136. package/eslint-integration/eslint-plugin-custom/s020-no-eval-dynamic-execution.js +89 -0
  137. package/eslint-integration/eslint-plugin-custom/s022-output-encoding.js +78 -0
  138. package/eslint-integration/eslint-plugin-custom/s023-no-json-injection.js +300 -0
  139. package/eslint-integration/eslint-plugin-custom/s025-server-side-input-validation.js +217 -0
  140. package/eslint-integration/eslint-plugin-custom/s026-json-schema-validation.js +68 -0
  141. package/eslint-integration/eslint-plugin-custom/s027-no-hardcoded-secrets.js +80 -0
  142. package/eslint-integration/eslint-plugin-custom/s029-require-csrf-protection.js +79 -0
  143. package/eslint-integration/eslint-plugin-custom/s030-no-directory-browsing.js +78 -0
  144. package/eslint-integration/eslint-plugin-custom/s033-require-samesite-cookie.js +80 -0
  145. package/eslint-integration/eslint-plugin-custom/s034-require-host-cookie-prefix.js +77 -0
  146. package/eslint-integration/eslint-plugin-custom/s035-cookie-specific-path.js +74 -0
  147. package/eslint-integration/eslint-plugin-custom/s036-no-unsafe-file-include.js +68 -0
  148. package/eslint-integration/eslint-plugin-custom/s037-require-anti-cache-headers.js +70 -0
  149. package/eslint-integration/eslint-plugin-custom/s038-no-version-disclosure.js +74 -0
  150. package/eslint-integration/eslint-plugin-custom/s039-no-session-token-in-url.js +63 -0
  151. package/eslint-integration/eslint-plugin-custom/s041-require-session-invalidate-on-logout.js +211 -0
  152. package/eslint-integration/eslint-plugin-custom/s042-require-periodic-reauthentication.js +294 -0
  153. package/eslint-integration/eslint-plugin-custom/s043-terminate-sessions-on-password-change.js +254 -0
  154. package/eslint-integration/eslint-plugin-custom/s044-require-full-session-for-sensitive-operations.js +292 -0
  155. package/eslint-integration/eslint-plugin-custom/s045-anti-automation-controls.js +46 -0
  156. package/eslint-integration/eslint-plugin-custom/s046-secure-notification-on-auth-change.js +44 -0
  157. package/eslint-integration/eslint-plugin-custom/s047-secure-random-passwords.js +108 -0
  158. package/eslint-integration/eslint-plugin-custom/s048-password-credential-recovery.js +54 -0
  159. package/eslint-integration/eslint-plugin-custom/s050-session-token-weak-hash.js +94 -0
  160. package/eslint-integration/eslint-plugin-custom/s052-secure-random-authentication-code.js +66 -0
  161. package/eslint-integration/eslint-plugin-custom/s054-verification-default-account.js +109 -0
  162. package/eslint-integration/eslint-plugin-custom/s055-verification-rest-check-the-incoming-content-type.js +143 -0
  163. package/eslint-integration/eslint-plugin-custom/s057-utc-logging.js +54 -0
  164. package/eslint-integration/eslint-plugin-custom/s058-no-ssrf.js +73 -0
  165. package/eslint-integration/eslint-plugin-custom/t002-interface-prefix-i.js +42 -0
  166. package/eslint-integration/eslint-plugin-custom/t003-ts-ignore-reason.js +48 -0
  167. package/eslint-integration/eslint-plugin-custom/t004-interface-public-only.js +160 -0
  168. package/eslint-integration/eslint-plugin-custom/t007-no-fn-in-constructor.js +52 -0
  169. package/eslint-integration/eslint-plugin-custom/t011-no-real-time-dependency.js +175 -0
  170. package/eslint-integration/eslint-plugin-custom/t019-no-empty-type.js +95 -0
  171. package/eslint-integration/eslint-plugin-custom/t025-no-nested-union-tuple.js +48 -0
  172. package/eslint-integration/eslint-plugin-custom/t026-limit-nested-generics.js +377 -0
  173. package/eslint-integration/eslint.config.js +125 -0
  174. package/eslint-integration/eslint.config.simple.js +24 -0
  175. package/eslint-integration/node_modules/eslint-plugin-custom/package.json +0 -0
  176. package/eslint-integration/package.json +23 -0
  177. package/eslint-integration/sample.ts +53 -0
  178. package/eslint-integration/test-s003.js +5 -0
  179. package/eslint-integration/tsconfig.json +27 -0
  180. package/examples/.github/workflows/code-quality.yml +111 -0
  181. package/examples/.sunlint.json +42 -0
  182. package/examples/README.md +47 -0
  183. package/examples/package.json +33 -0
  184. package/package.json +100 -0
  185. package/rules/C006_function_naming/analyzer.js +338 -0
  186. package/rules/C006_function_naming/config.json +86 -0
  187. package/rules/C019_log_level_usage/analyzer.js +359 -0
  188. package/rules/C019_log_level_usage/config.json +121 -0
  189. package/rules/C029_catch_block_logging/analyzer.js +339 -0
  190. package/rules/C029_catch_block_logging/config.json +59 -0
  191. package/rules/C031_validation_separation/README.md +72 -0
  192. 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;