@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,705 @@
1
+ /**
2
+ * Analysis Orchestrator
3
+ * Following Rule C005: Single responsibility - orchestrate analysis
4
+ * Following Rule C014: Dependency Injection - inject engines
5
+ */
6
+
7
+ const chalk = require('chalk');
8
+ const MultiRuleRunner = require('./multi-rule-runner');
9
+ const TypeScriptEngine = require('./typescript-engine');
10
+ const RuleMappingService = require('./rule-mapping-service');
11
+ const ESLintIntegrationService = require('./eslint-integration-service');
12
+
13
+ class AnalysisOrchestrator {
14
+ constructor() {
15
+ this.multiRuleRunner = null;
16
+ this.typeScriptEngine = null;
17
+ this.ruleMappingService = new RuleMappingService();
18
+ this.eslintIntegrationService = new ESLintIntegrationService();
19
+ }
20
+
21
+ async runAnalysis(rulesToRun, options, config = {}) {
22
+ try {
23
+ // Check for ESLint integration mode
24
+ if (this.shouldUseESLintIntegration(options, config)) {
25
+ return await this.runESLintIntegratedAnalysis(rulesToRun, options, config);
26
+ }
27
+
28
+ // Check AI configuration and provide feedback
29
+ if (options.ai === true) {
30
+ this.checkAIConfiguration(rulesToRun, config);
31
+ }
32
+
33
+ // Check if user explicitly requests TypeScript engine
34
+ if (options.typescript || options.typescriptEngine === 'eslint') {
35
+ // Force all rules through TypeScript engine when --typescript flag is used
36
+ return await this.runTypeScriptAnalysis(rulesToRun, options, config);
37
+ }
38
+
39
+ // Auto-detect based on rule analyzer
40
+ const eslintRules = rulesToRun.filter(rule =>
41
+ rule.analyzer === 'eslint' ||
42
+ rule.analyzer === 'typescript' // Legacy typescript analyzer routes to eslint
43
+ );
44
+ const customRules = rulesToRun.filter(rule =>
45
+ rule.analyzer &&
46
+ rule.analyzer !== 'eslint' &&
47
+ rule.analyzer !== 'typescript'
48
+ );
49
+
50
+ // Debug logging for rule routing
51
+ if (!options.quiet && options.format !== 'json') {
52
+ console.log(chalk.cyan(`🔍 Rule routing: ${eslintRules.length} ESLint rules, ${customRules.length} custom rules`));
53
+ if (eslintRules.length > 0) {
54
+ console.log(chalk.cyan(` ESLint rules: ${eslintRules.map(r => r.id).join(', ')}`));
55
+ }
56
+ }
57
+
58
+ // If we have ESLint rules, run TypeScript analysis
59
+ if (eslintRules.length > 0) {
60
+ return await this.runTypeScriptAnalysis(eslintRules, options, config);
61
+ }
62
+
63
+ // For custom analyzer rules, use multi-rule runner
64
+ if (customRules.length > 0) {
65
+ // Initialize multi-rule runner if available
66
+ if (!this.multiRuleRunner) {
67
+ try {
68
+ this.multiRuleRunner = new MultiRuleRunner(config, options);
69
+ } catch (error) {
70
+ console.log(chalk.yellow('⚠️ Multi-rule runner not available, using basic analysis'));
71
+ return this.runBasicAnalysis(rulesToRun, options, config);
72
+ }
73
+ }
74
+
75
+ // Run analysis using existing multi-rule runner
76
+ return await this.multiRuleRunner.runRules(customRules, options.input, options);
77
+ }
78
+
79
+ // Fallback to basic analysis
80
+ return this.runBasicAnalysis(rulesToRun, options, config);
81
+
82
+ } catch (error) {
83
+ console.log(chalk.yellow('⚠️ Falling back to basic analysis'));
84
+ return this.runBasicAnalysis(rulesToRun, options, config);
85
+ }
86
+ }
87
+
88
+ async runBasicAnalysis(rulesToRun, options, config = {}) {
89
+ console.log(chalk.blue(`🔄 Analyzing ${rulesToRun.length} rules...`));
90
+
91
+ // Basic mock analysis for now
92
+ const results = {
93
+ results: [],
94
+ filesAnalyzed: 1,
95
+ engine: 'sunlint-basic'
96
+ };
97
+
98
+ // For demonstration, create some mock violations for C006
99
+ if (rulesToRun.some(rule => rule.id === 'C006')) {
100
+ results.results.push({
101
+ file: options.input,
102
+ violations: [
103
+ {
104
+ ruleId: 'C006',
105
+ severity: 'warning',
106
+ message: 'Function name should follow verb-noun pattern',
107
+ line: 1,
108
+ column: 1
109
+ }
110
+ ]
111
+ });
112
+ }
113
+
114
+ return results;
115
+ }
116
+
117
+ /**
118
+ * Run TypeScript analysis using ESLint
119
+ * Following Rule C005: Single responsibility
120
+ */
121
+ async runTypeScriptAnalysis(rulesToRun, options, config = {}) {
122
+ try {
123
+ // Initialize TypeScript engine
124
+ if (!this.typeScriptEngine) {
125
+ this.typeScriptEngine = new TypeScriptEngine();
126
+ }
127
+
128
+ // Check and install dependencies if needed
129
+ if (options.ensureDeps) {
130
+ await this.ensureTypeScriptDependencies();
131
+ }
132
+
133
+ // Configure rules dynamically (important for security rules)
134
+ const selectedRuleIds = rulesToRun.map(rule => rule.id);
135
+ await this.typeScriptEngine.configureRules(selectedRuleIds, options);
136
+
137
+ // Filter rules that can be handled by ESLint
138
+ const eslintSupportedRules = rulesToRun.filter(rule =>
139
+ this.ruleMappingService.isSunLintRuleSupportedByEslint(rule.id)
140
+ );
141
+
142
+ if (eslintSupportedRules.length === 0) {
143
+ if (!options.quiet && options.format !== 'json') {
144
+ console.log(chalk.yellow('⚠️ No ESLint-supported rules found, falling back to basic analysis'));
145
+ }
146
+ return this.runBasicAnalysis(rulesToRun, options, config);
147
+ }
148
+
149
+ if (!options.quiet && options.format !== 'json') {
150
+ console.log(chalk.blue(`🔄 Running TypeScript analysis with ${eslintSupportedRules.length} rules...`));
151
+ }
152
+
153
+ // Run ESLint analysis
154
+ const inputPaths = options.isFileList ? options.input : [options.input];
155
+ const eslintResults = await this.typeScriptEngine.runAnalysis(inputPaths, options);
156
+
157
+ // Convert results to SunLint format
158
+ const sunlintResults = this.typeScriptEngine.convertToSunLintFormat(
159
+ eslintResults,
160
+ this.ruleMappingService.eslintToSunlintMapping || {},
161
+ options
162
+ );
163
+
164
+ // Filter results to only include requested rules
165
+ const filteredResults = this.filterResultsByRules(sunlintResults, eslintSupportedRules);
166
+
167
+ return filteredResults;
168
+
169
+ } catch (error) {
170
+ // Following Rule C035: Log full error information
171
+ console.error(chalk.red('❌ TypeScript analysis failed:'), {
172
+ message: error.message,
173
+ rules: rulesToRun.map(r => r.id),
174
+ options,
175
+ });
176
+
177
+ console.log(chalk.yellow('⚠️ Falling back to basic analysis'));
178
+ return this.runBasicAnalysis(rulesToRun, options, config);
179
+ }
180
+ }
181
+
182
+ /**
183
+ * Ensure TypeScript dependencies are installed
184
+ * Following Rule C006: Verb-noun naming
185
+ */
186
+ async ensureTypeScriptDependencies() {
187
+ const deps = await this.typeScriptEngine.checkDependencies();
188
+
189
+ if (!deps.nodeModulesExists) {
190
+ console.log(chalk.blue('📦 Installing TypeScript dependencies...'));
191
+ await this.typeScriptEngine.installDependencies();
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Filter results by requested rules
197
+ * Following Rule C012: Query - pure function
198
+ */
199
+ filterResultsByRules(results, requestedRules) {
200
+ const requestedRuleIds = new Set(requestedRules.map(r => r.id));
201
+
202
+ const filteredResults = {
203
+ ...results,
204
+ results: results.results.map(fileResult => ({
205
+ ...fileResult,
206
+ violations: fileResult.violations.filter(violation =>
207
+ requestedRuleIds.has(violation.ruleId)
208
+ )
209
+ })).filter(fileResult => fileResult.violations.length > 0)
210
+ };
211
+
212
+ // Recalculate total violations
213
+ filteredResults.totalViolations = filteredResults.results.reduce(
214
+ (total, fileResult) => total + fileResult.violations.length,
215
+ 0
216
+ );
217
+
218
+ return filteredResults;
219
+ }
220
+
221
+ /**
222
+ * Check AI configuration and provide user feedback
223
+ * Following Rule C006: Verb-noun naming - checkAIConfiguration
224
+ */
225
+ checkAIConfiguration(rulesToRun, config) {
226
+ const chalk = require('chalk');
227
+ const hasApiKey = process.env.OPENAI_API_KEY || (config.ai && config.ai.apiKey);
228
+
229
+ // Check which rules support AI
230
+ const aiSupportedRules = ['C019', 'C029']; // Rules that have AI analyzer support
231
+ const aiCapableRules = rulesToRun.filter(rule => aiSupportedRules.includes(rule.id));
232
+ const nonAiRules = rulesToRun.filter(rule => !aiSupportedRules.includes(rule.id));
233
+
234
+ if (nonAiRules.length > 0) {
235
+ console.log(chalk.yellow(`⚠️ AI analysis not supported for: ${nonAiRules.map(r => r.id).join(', ')}`));
236
+ console.log(chalk.gray(` These rules will use pattern-based analysis`));
237
+ }
238
+
239
+ if (aiCapableRules.length > 0) {
240
+ if (!hasApiKey) {
241
+ console.log(chalk.yellow(`⚠️ OPENAI_API_KEY not set for AI-capable rules: ${aiCapableRules.map(r => r.id).join(', ')}`));
242
+ console.log(chalk.gray(` Falling back to pattern-based analysis`));
243
+ console.log(chalk.gray(` To enable AI analysis: export OPENAI_API_KEY="your-api-key"`));
244
+ } else {
245
+ console.log(chalk.green(`🤖 AI analysis enabled for: ${aiCapableRules.map(r => r.id).join(', ')}`));
246
+ }
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Check if ESLint integration should be used
252
+ */
253
+ shouldUseESLintIntegration(options, config) {
254
+ return (
255
+ options.eslintIntegration ||
256
+ config.eslintIntegration?.enabled ||
257
+ options.typescript // Auto-enable for TypeScript analysis
258
+ );
259
+ }
260
+
261
+ /**
262
+ * Run analysis with ESLint integration (merged config)
263
+ */
264
+ async runESLintIntegratedAnalysis(rulesToRun, options, config) {
265
+ try {
266
+ if (!options.quiet) {
267
+ console.log(chalk.blue('🔗 Running integrated analysis (SunLint + ESLint)'));
268
+ }
269
+
270
+ // Determine file list
271
+ const filePaths = this.getFileList(options);
272
+
273
+ // First, run SunLint custom rules
274
+ const sunlintResults = await this.runStandardAnalysis(rulesToRun, options, config);
275
+
276
+ // Then, run integrated ESLint analysis
277
+ const eslintResults = await this.eslintIntegrationService.runIntegratedAnalysis(
278
+ filePaths,
279
+ config,
280
+ {
281
+ fix: options.fix,
282
+ quiet: true, // Suppress ESLint service logging
283
+ verbose: options.verbose
284
+ }
285
+ );
286
+
287
+ // Merge SunLint and ESLint results
288
+ if (!options.quiet) {
289
+ console.log('Debug: SunLint results structure:', JSON.stringify(sunlintResults, null, 2).substring(0, 300));
290
+ console.log('Debug: ESLint results structure:', JSON.stringify(eslintResults, null, 2).substring(0, 300));
291
+ }
292
+ const mergedResults = this.mergeAnalysisResults(sunlintResults, eslintResults, options);
293
+
294
+ // Log integration summary with merged results
295
+ if (!options.quiet) {
296
+ this.eslintIntegrationService.logIntegrationSummary(
297
+ eslintResults.categorized || { sunlint: [], user: [], combined: [] },
298
+ true,
299
+ mergedResults
300
+ );
301
+ }
302
+
303
+ // Add metadata
304
+ mergedResults.engine = 'sunlint-eslint-integrated';
305
+ mergedResults.integration = {
306
+ totalRules: eslintResults.totalRules,
307
+ sunlintRules: mergedResults.sources.sunlint,
308
+ userRules: mergedResults.sources.eslint,
309
+ categorized: eslintResults.categorized
310
+ };
311
+
312
+ // Convert merged results back to ESLint-compatible format for output
313
+ const finalResults = this.convertMergedResultsToESLintFormat(mergedResults);
314
+
315
+ // Update metadata for proper display
316
+ finalResults.metadata = {
317
+ ...finalResults.metadata,
318
+ totalViolations: finalResults.violationCount,
319
+ totalFiles: finalResults.totalFiles
320
+ };
321
+
322
+ return finalResults;
323
+
324
+ } catch (error) {
325
+ if (!options.quiet) {
326
+ console.log(chalk.yellow(`⚠️ ESLint integration failed: ${error.message}`));
327
+ console.log(chalk.yellow(' Falling back to standard SunLint analysis'));
328
+ }
329
+ return this.runStandardAnalysis(rulesToRun, options, config);
330
+ }
331
+ }
332
+
333
+ /**
334
+ * Get file list based on options
335
+ */
336
+ getFileList(options) {
337
+ if (options.isFileList && Array.isArray(options.input)) {
338
+ return options.input;
339
+ } else if (typeof options.input === 'string') {
340
+ // Convert single path to array
341
+ return [options.input];
342
+ } else {
343
+ return ['src/**/*.{ts,tsx,js,jsx}'];
344
+ }
345
+ }
346
+
347
+ /**
348
+ * Convert ESLint results to SunLint format
349
+ */
350
+ convertESLintResultsToSunLintFormat(eslintResults, options) {
351
+ const results = {
352
+ results: [],
353
+ filesAnalyzed: eslintResults.results.length,
354
+ engine: 'eslint-integrated'
355
+ };
356
+
357
+ eslintResults.results.forEach(fileResult => {
358
+ if (fileResult.messages && fileResult.messages.length > 0) {
359
+ const violations = fileResult.messages.map(message => ({
360
+ ruleId: message.ruleId,
361
+ severity: this.mapESLintSeverityToSunLint(message.severity),
362
+ message: message.message,
363
+ line: message.line,
364
+ column: message.column,
365
+ source: this.eslintIntegrationService.isSunLintRule(message.ruleId) ? 'sunlint' : 'user-eslint'
366
+ }));
367
+
368
+ results.results.push({
369
+ filePath: fileResult.filePath,
370
+ violations: violations
371
+ });
372
+ }
373
+ });
374
+
375
+ return results;
376
+ }
377
+
378
+ /**
379
+ * Map ESLint severity to SunLint severity
380
+ */
381
+ mapESLintSeverityToSunLint(eslintSeverity) {
382
+ switch (eslintSeverity) {
383
+ case 1: return 'warning';
384
+ case 2: return 'error';
385
+ default: return 'info';
386
+ }
387
+ }
388
+
389
+ /**
390
+ * Merge SunLint custom rules results with ESLint results with conflict resolution
391
+ */
392
+ mergeAnalysisResults(sunlintResults, eslintResults, options) {
393
+ // Convert ESLint results to SunLint format
394
+ const eslintFormatted = this.convertESLintResultsToSunLintFormat(eslintResults, options);
395
+
396
+ // Create violation maps for conflict detection
397
+ const sunlintViolations = new Map();
398
+ const eslintViolations = new Map();
399
+ const mergedViolations = [];
400
+
401
+ // Extract SunLint violations from results format (ESLint-compatible)
402
+ const sunlintResults_violations = this.extractViolationsFromResults(sunlintResults);
403
+
404
+ // Process SunLint violations first (higher priority)
405
+ if (sunlintResults_violations && sunlintResults_violations.length > 0) {
406
+ sunlintResults_violations.forEach(violation => {
407
+ const key = `${violation.file}:${violation.line}:${violation.column}`;
408
+ sunlintViolations.set(key, {
409
+ ...violation,
410
+ source: 'sunlint',
411
+ priority: 1
412
+ });
413
+ mergedViolations.push({
414
+ ...violation,
415
+ source: 'sunlint'
416
+ });
417
+ });
418
+ }
419
+
420
+ // Extract ESLint violations
421
+ const eslintResults_violations = this.extractViolationsFromResults(eslintFormatted);
422
+
423
+ // Process ESLint violations with conflict resolution
424
+ if (eslintResults_violations && eslintResults_violations.length > 0) {
425
+ eslintResults_violations.forEach(violation => {
426
+ const key = `${violation.file}:${violation.line}:${violation.column}`;
427
+
428
+ // Check for conflicts
429
+ if (sunlintViolations.has(key)) {
430
+ const sunlintViolation = sunlintViolations.get(key);
431
+
432
+ // Check if it's the same rule concept (e.g., both about console.log)
433
+ const isSameRule = this.isConflictingRule(sunlintViolation.ruleId, violation.ruleId);
434
+
435
+ if (isSameRule) {
436
+ // SunLint takes priority, add ESLint as additional info
437
+ const mergedViolation = mergedViolations.find(v =>
438
+ v.file === violation.file && v.line === violation.line && v.column === violation.column
439
+ );
440
+ if (mergedViolation) {
441
+ mergedViolation.additionalInfo = {
442
+ eslintRule: violation.ruleId,
443
+ eslintMessage: violation.message
444
+ };
445
+ }
446
+ } else {
447
+ // Different rules on same line, keep both
448
+ mergedViolations.push({
449
+ ...violation,
450
+ source: 'eslint'
451
+ });
452
+ }
453
+ } else {
454
+ // No conflict, add ESLint violation
455
+ eslintViolations.set(key, violation);
456
+ mergedViolations.push({
457
+ ...violation,
458
+ source: 'eslint'
459
+ });
460
+ }
461
+ });
462
+ }
463
+
464
+ // Create combined results in ESLint format
465
+ const mergedResults = {
466
+ ...eslintFormatted,
467
+ violations: mergedViolations,
468
+ violationCount: mergedViolations.length,
469
+ totalFiles: Math.max(sunlintResults.totalFiles || 0, eslintFormatted.totalFiles || 0),
470
+ analysisTime: (sunlintResults.analysisTime || 0) + (eslintFormatted.analysisTime || 0),
471
+ sources: {
472
+ sunlint: sunlintViolations.size,
473
+ eslint: eslintViolations.size,
474
+ conflicts: mergedViolations.filter(v => v.additionalInfo).length
475
+ }
476
+ };
477
+
478
+ return mergedResults;
479
+ }
480
+
481
+ /**
482
+ * Extract violations from results in consistent format
483
+ */
484
+ extractViolationsFromResults(results) {
485
+ const violations = [];
486
+
487
+ if (results && results.results) {
488
+ // Handle different result structures
489
+ results.results.forEach(result => {
490
+ // SunLint format: result.violations[]
491
+ if (result.violations && Array.isArray(result.violations)) {
492
+ result.violations.forEach(violation => {
493
+ violations.push({
494
+ file: violation.file,
495
+ line: violation.line,
496
+ column: violation.column || 1,
497
+ ruleId: violation.ruleId,
498
+ severity: violation.severity || 'warning',
499
+ message: violation.message
500
+ });
501
+ });
502
+ }
503
+ // ESLint format: result.messages[]
504
+ else if (result.messages && Array.isArray(result.messages)) {
505
+ result.messages.forEach(message => {
506
+ violations.push({
507
+ file: result.filePath,
508
+ line: message.line,
509
+ column: message.column,
510
+ ruleId: message.ruleId,
511
+ severity: this.mapESLintSeverityToString(message.severity),
512
+ message: message.message
513
+ });
514
+ });
515
+ }
516
+ });
517
+ } else if (Array.isArray(results)) {
518
+ // Direct array format
519
+ results.forEach(fileResult => {
520
+ if (fileResult.messages && fileResult.messages.length > 0) {
521
+ fileResult.messages.forEach(message => {
522
+ violations.push({
523
+ file: fileResult.filePath,
524
+ line: message.line,
525
+ column: message.column,
526
+ ruleId: message.ruleId,
527
+ severity: this.mapESLintSeverityToString(message.severity),
528
+ message: message.message
529
+ });
530
+ });
531
+ }
532
+ });
533
+ }
534
+
535
+ return violations;
536
+ }
537
+
538
+ /**
539
+ * Map ESLint severity numbers to strings
540
+ */
541
+ mapESLintSeverityToString(severity) {
542
+ switch (severity) {
543
+ case 1: return 'warning';
544
+ case 2: return 'error';
545
+ default: return 'info';
546
+ }
547
+ }
548
+
549
+ /**
550
+ * Check if two rules are conflicting (same concept)
551
+ */
552
+ isConflictingRule(sunlintRule, eslintRule) {
553
+ const ruleMapping = {
554
+ 'C019': ['no-console'], // Console logging rules
555
+ 'C045': ['no-console'], // Console logging rules
556
+ 'C006': [], // Function naming (SunLint specific)
557
+ 'C032': [] // Logging info (SunLint specific)
558
+ };
559
+
560
+ return ruleMapping[sunlintRule] && ruleMapping[sunlintRule].includes(eslintRule);
561
+ }
562
+
563
+ /**
564
+ * Run standard SunLint analysis (fallback)
565
+ */
566
+ async runStandardAnalysis(rulesToRun, options, config) {
567
+ // Check AI configuration and provide feedback
568
+ if (options.ai === true) {
569
+ this.checkAIConfiguration(rulesToRun, config);
570
+ }
571
+
572
+ // Check if user explicitly requests TypeScript engine
573
+ if (options.typescript || options.typescriptEngine === 'eslint') {
574
+ // Force all rules through TypeScript engine when --typescript flag is used
575
+ return await this.runTypeScriptAnalysis(rulesToRun, options, config);
576
+ }
577
+
578
+ // Auto-detect based on rule analyzer
579
+ const eslintRules = rulesToRun.filter(rule =>
580
+ rule.analyzer === 'eslint' ||
581
+ rule.analyzer === 'typescript' // Legacy typescript analyzer routes to eslint
582
+ );
583
+ const customRules = rulesToRun.filter(rule =>
584
+ rule.analyzer &&
585
+ rule.analyzer !== 'eslint' &&
586
+ rule.analyzer !== 'typescript'
587
+ );
588
+
589
+ // Debug logging for rule routing
590
+ if (!options.quiet && options.format !== 'json') {
591
+ console.log(chalk.cyan(`🔍 Rule routing: ${eslintRules.length} ESLint rules, ${customRules.length} custom rules`));
592
+ if (eslintRules.length > 0) {
593
+ console.log(chalk.cyan(` ESLint rules: ${eslintRules.map(r => r.id).join(', ')}`));
594
+ }
595
+ }
596
+
597
+ // Run ESLint rules if any
598
+ let allResults = { results: [], filesAnalyzed: 0, engine: 'combined' };
599
+
600
+ if (eslintRules.length > 0) {
601
+ const eslintResults = await this.runTypeScriptAnalysis(eslintRules, options, config);
602
+ allResults.results.push(...eslintResults.results);
603
+ allResults.filesAnalyzed = Math.max(allResults.filesAnalyzed, eslintResults.filesAnalyzed);
604
+ }
605
+
606
+ // Run custom rules if any
607
+ if (customRules.length > 0) {
608
+ try {
609
+ if (!this.multiRuleRunner) {
610
+ const MultiRuleRunner = require('./multi-rule-runner');
611
+ if (MultiRuleRunner) {
612
+ this.multiRuleRunner = new MultiRuleRunner();
613
+ } else {
614
+ console.log(chalk.yellow('⚠️ Multi-rule runner not available, using basic analysis'));
615
+ return this.runBasicAnalysis(rulesToRun, options, config);
616
+ }
617
+ }
618
+
619
+ // Run analysis using existing multi-rule runner
620
+ const customResults = await this.multiRuleRunner.runRules(customRules, options.input, options);
621
+ allResults.results.push(...customResults.results);
622
+ allResults.filesAnalyzed = Math.max(allResults.filesAnalyzed, customResults.filesAnalyzed);
623
+ } catch (error) {
624
+ console.log(chalk.yellow('⚠️ Custom rule analysis failed, continuing with ESLint results only'));
625
+ }
626
+ }
627
+
628
+ return allResults.results.length > 0 ? allResults : this.runBasicAnalysis(rulesToRun, options, config);
629
+ }
630
+
631
+ /**
632
+ * Rule C006: Name method with verb-noun - createMockSummary
633
+ */
634
+ createMockSummary() {
635
+ return {
636
+ totalViolations: 1,
637
+ filesAnalyzed: 1,
638
+ rulesSummary: {
639
+ 'C006': 1
640
+ }
641
+ };
642
+ }
643
+
644
+ /**
645
+ * Convert merged results back to ESLint-compatible format for output
646
+ */
647
+ convertMergedResultsToESLintFormat(mergedResults) {
648
+ const fileMap = new Map();
649
+
650
+ // Group violations by file
651
+ if (mergedResults.violations) {
652
+ mergedResults.violations.forEach(violation => {
653
+ if (!fileMap.has(violation.file)) {
654
+ fileMap.set(violation.file, {
655
+ filePath: violation.file,
656
+ messages: [],
657
+ suppressedMessages: [],
658
+ errorCount: 0,
659
+ warningCount: 0,
660
+ fatalErrorCount: 0,
661
+ fixableErrorCount: 0,
662
+ fixableWarningCount: 0
663
+ });
664
+ }
665
+
666
+ const fileResult = fileMap.get(violation.file);
667
+ const message = {
668
+ ruleId: violation.ruleId,
669
+ severity: violation.severity === 'error' ? 2 : 1,
670
+ message: violation.message,
671
+ line: violation.line || 1,
672
+ column: violation.column || 1,
673
+ nodeType: null,
674
+ messageId: null,
675
+ endLine: null,
676
+ endColumn: null,
677
+ source: violation.source || 'unknown'
678
+ };
679
+
680
+ // Add additional info for conflicts
681
+ if (violation.additionalInfo) {
682
+ message.message += ` (ESLint: ${violation.additionalInfo.eslintRule})`;
683
+ }
684
+
685
+ fileResult.messages.push(message);
686
+
687
+ // Update counts
688
+ if (violation.severity === 'error') {
689
+ fileResult.errorCount++;
690
+ } else {
691
+ fileResult.warningCount++;
692
+ }
693
+ });
694
+ }
695
+
696
+ return {
697
+ ...mergedResults,
698
+ results: Array.from(fileMap.values()),
699
+ violationCount: mergedResults.violations ? mergedResults.violations.length : 0,
700
+ totalFiles: fileMap.size
701
+ };
702
+ }
703
+ }
704
+
705
+ module.exports = AnalysisOrchestrator;