@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,239 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+ const glob = require('glob');
4
+ const chalk = require('chalk');
5
+
6
+ class MultiRuleRunner {
7
+ constructor(config, options) {
8
+ this.config = config;
9
+ this.options = options;
10
+ this.ruleCache = new Map();
11
+ }
12
+
13
+ async runRules(rules, inputPath, options) {
14
+ console.log(chalk.blue(`šŸ”„ Analyzing ${rules.length} rules...`));
15
+
16
+ // Get files to analyze
17
+ const files = await this.getFilesToAnalyze(inputPath, options);
18
+
19
+ if (files.length === 0) {
20
+ console.log(chalk.yellow('āš ļø No files found to analyze'));
21
+ return [];
22
+ }
23
+
24
+ console.log(chalk.gray(`šŸ“ Found ${files.length} files to analyze`));
25
+
26
+ // Group files by language
27
+ const filesByLanguage = this.groupFilesByLanguage(files);
28
+
29
+ // Run rules in parallel (with concurrency limit)
30
+ const maxConcurrent = parseInt(options.maxConcurrent) || 5;
31
+ const timeout = parseInt(options.timeout) || 30000;
32
+
33
+ const results = [];
34
+
35
+ // Process rules in batches
36
+ for (let i = 0; i < rules.length; i += maxConcurrent) {
37
+ const batch = rules.slice(i, i + maxConcurrent);
38
+
39
+ const batchPromises = batch.map(async (rule) => {
40
+ try {
41
+ return await this.runSingleRule(rule, filesByLanguage, options, timeout);
42
+ } catch (error) {
43
+ console.error(chalk.red(`āŒ Rule ${rule.id} failed:`), error.message);
44
+ return {
45
+ ruleId: rule.id,
46
+ ruleName: rule.name,
47
+ violations: [],
48
+ error: error.message,
49
+ status: 'failed'
50
+ };
51
+ }
52
+ });
53
+
54
+ const batchResults = await Promise.all(batchPromises);
55
+ results.push(...batchResults);
56
+
57
+ if (options.verbose) {
58
+ console.log(chalk.gray(`āœ… Completed batch ${Math.floor(i/maxConcurrent) + 1}/${Math.ceil(rules.length/maxConcurrent)}`));
59
+ }
60
+ }
61
+
62
+ return this.consolidateResults(results, files.length);
63
+ }
64
+
65
+ async runSingleRule(rule, filesByLanguage, options, timeout) {
66
+ if (options.verbose) {
67
+ console.log(chalk.gray(`šŸ” Running rule ${rule.id}: ${rule.name}`));
68
+ }
69
+
70
+ const startTime = Date.now();
71
+
72
+ // Load rule analyzer
73
+ const analyzer = await this.loadRuleAnalyzer(rule);
74
+
75
+ if (!analyzer) {
76
+ return {
77
+ ruleId: rule.id,
78
+ ruleName: rule.name,
79
+ violations: [],
80
+ error: 'Analyzer not found',
81
+ status: 'skipped'
82
+ };
83
+ }
84
+
85
+ const violations = [];
86
+
87
+ // Run analyzer for each supported language
88
+ for (const language of rule.languages) {
89
+ const languageFiles = filesByLanguage[language] || [];
90
+
91
+ if (languageFiles.length === 0) {
92
+ continue;
93
+ }
94
+
95
+ try {
96
+ // Run with timeout
97
+ const languageViolations = await Promise.race([
98
+ analyzer.analyze(languageFiles, language, this.config),
99
+ new Promise((_, reject) =>
100
+ setTimeout(() => reject(new Error('Timeout')), timeout)
101
+ )
102
+ ]);
103
+
104
+ violations.push(...languageViolations);
105
+ } catch (error) {
106
+ console.error(chalk.yellow(`āš ļø Rule ${rule.id} failed for ${language}:`), error.message);
107
+ }
108
+ }
109
+
110
+ const duration = Date.now() - startTime;
111
+
112
+ return {
113
+ ruleId: rule.id,
114
+ ruleName: rule.name,
115
+ category: rule.category,
116
+ severity: rule.severity,
117
+ violations,
118
+ filesAnalyzed: Object.values(filesByLanguage).flat().length,
119
+ duration,
120
+ status: 'completed'
121
+ };
122
+ }
123
+
124
+ async loadRuleAnalyzer(rule) {
125
+ // Check cache first
126
+ if (this.ruleCache.has(rule.id)) {
127
+ return this.ruleCache.get(rule.id);
128
+ }
129
+
130
+ try {
131
+ const analyzerPath = path.resolve(__dirname, '..', rule.analyzer);
132
+
133
+ if (!fs.existsSync(analyzerPath)) {
134
+ console.error(chalk.yellow(`āš ļø Analyzer not found for rule ${rule.id}: ${analyzerPath}`));
135
+ return null;
136
+ }
137
+
138
+ const analyzer = require(analyzerPath);
139
+ this.ruleCache.set(rule.id, analyzer);
140
+ return analyzer;
141
+ } catch (error) {
142
+ console.error(chalk.red(`āŒ Failed to load analyzer for rule ${rule.id}:`), error.message);
143
+ return null;
144
+ }
145
+ }
146
+
147
+ async getFilesToAnalyze(inputPath, options) {
148
+ const isFile = fs.statSync(inputPath).isFile();
149
+
150
+ if (isFile) {
151
+ return [path.resolve(inputPath)];
152
+ }
153
+
154
+ // Get include/exclude patterns
155
+ const includePatterns = this.config.include || ['**/*.ts', '**/*.tsx', '**/*.dart', '**/*.kt'];
156
+ const excludePatterns = this.config.exclude || ['**/node_modules/**', '**/build/**'];
157
+
158
+ // Find files using glob patterns
159
+ const files = [];
160
+
161
+ for (const pattern of includePatterns) {
162
+ const fullPattern = path.join(inputPath, pattern);
163
+ const matchedFiles = glob.sync(fullPattern, {
164
+ ignore: excludePatterns.map(p => path.join(inputPath, p)),
165
+ absolute: true
166
+ });
167
+ files.push(...matchedFiles);
168
+ }
169
+
170
+ // Remove duplicates
171
+ return [...new Set(files)];
172
+ }
173
+
174
+ groupFilesByLanguage(files) {
175
+ const rulesRegistry = require('../config/rules-registry.json');
176
+ const languageConfig = rulesRegistry.languages;
177
+
178
+ const filesByLanguage = {};
179
+
180
+ files.forEach(file => {
181
+ const ext = path.extname(file);
182
+
183
+ for (const [language, config] of Object.entries(languageConfig)) {
184
+ if (config.extensions.includes(ext)) {
185
+ if (!filesByLanguage[language]) {
186
+ filesByLanguage[language] = [];
187
+ }
188
+ filesByLanguage[language].push(file);
189
+ break; // File belongs to first matching language
190
+ }
191
+ }
192
+ });
193
+
194
+ return filesByLanguage;
195
+ }
196
+
197
+ consolidateResults(results, totalFiles) {
198
+ const consolidatedResults = {
199
+ filesAnalyzed: totalFiles,
200
+ rulesRun: results.length,
201
+ totalViolations: 0,
202
+ violationsBySeverity: { error: 0, warning: 0, info: 0 },
203
+ violationsByRule: {},
204
+ violationsByFile: {},
205
+ results: []
206
+ };
207
+
208
+ results.forEach(result => {
209
+ consolidatedResults.results.push(result);
210
+
211
+ if (result.violations) {
212
+ consolidatedResults.totalViolations += result.violations.length;
213
+
214
+ // Count by severity
215
+ result.violations.forEach(violation => {
216
+ const severity = violation.severity || result.severity || 'warning';
217
+ consolidatedResults.violationsBySeverity[severity] =
218
+ (consolidatedResults.violationsBySeverity[severity] || 0) + 1;
219
+ });
220
+
221
+ // Count by rule
222
+ consolidatedResults.violationsByRule[result.ruleId] = result.violations.length;
223
+
224
+ // Count by file
225
+ result.violations.forEach(violation => {
226
+ const file = violation.file || violation.filePath;
227
+ if (file) {
228
+ consolidatedResults.violationsByFile[file] =
229
+ (consolidatedResults.violationsByFile[file] || 0) + 1;
230
+ }
231
+ });
232
+ }
233
+ });
234
+
235
+ return consolidatedResults;
236
+ }
237
+ }
238
+
239
+ module.exports = MultiRuleRunner;
@@ -0,0 +1,250 @@
1
+ /**
2
+ * Output Service
3
+ * Following Rule C005: Single responsibility - handle output operations
4
+ */
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const chalk = require('chalk');
9
+
10
+ class OutputService {
11
+ constructor() {}
12
+
13
+ async outputResults(results, options, metadata = {}) {
14
+ // Generate report based on format
15
+ const report = this.generateReport(results, metadata, options);
16
+
17
+ // Console output
18
+ if (!options.quiet) {
19
+ console.log(report.formatted);
20
+ }
21
+
22
+ // File output
23
+ if (options.output) {
24
+ const outputData = options.format === 'json' ? report.raw : report.formatted;
25
+ const content = typeof outputData === 'string' ? outputData : JSON.stringify(outputData, null, 2);
26
+ fs.writeFileSync(options.output, content);
27
+ console.log(chalk.green(`šŸ“„ Report saved to: ${options.output}`));
28
+ }
29
+
30
+ // Summary (skip for JSON format)
31
+ if (!options.quiet && options.format !== 'json') {
32
+ console.log(report.summary);
33
+ }
34
+ }
35
+
36
+ generateReport(results, metadata, options = {}) {
37
+ const allViolations = [];
38
+ let totalFiles = results.filesAnalyzed || results.totalFiles || results.fileCount || 0;
39
+
40
+ // Collect all violations - handle both file-based and rule-based results
41
+ if (results.results) {
42
+ results.results.forEach(result => {
43
+ if (result.violations) {
44
+ // Handle rule-based format (MultiRuleRunner)
45
+ if (result.ruleId) {
46
+ result.violations.forEach(violation => {
47
+ allViolations.push(violation); // violation already has file path
48
+ });
49
+ }
50
+ // Handle file-based format (legacy)
51
+ else {
52
+ result.violations.forEach(violation => {
53
+ allViolations.push({
54
+ ...violation,
55
+ file: result.filePath || result.file // Use filePath first, then file
56
+ });
57
+ });
58
+ }
59
+ }
60
+
61
+ // Handle ESLint format (messages array)
62
+ if (result.messages) {
63
+ result.messages.forEach(message => {
64
+ allViolations.push({
65
+ file: result.filePath || message.file,
66
+ ruleId: message.ruleId,
67
+ severity: message.severity === 2 ? 'error' : 'warning',
68
+ message: message.message,
69
+ line: message.line,
70
+ column: message.column,
71
+ source: message.source || 'eslint'
72
+ });
73
+ });
74
+ }
75
+ });
76
+ }
77
+
78
+ // Generate output based on format
79
+ let formatted;
80
+ let raw;
81
+
82
+ if (options.format === 'json') {
83
+ // ESLint-compatible JSON format
84
+ raw = this.generateJsonFormat(results, allViolations, options);
85
+ formatted = JSON.stringify(raw, null, 2);
86
+ } else {
87
+ // Default text format
88
+ formatted = this.formatViolations(allViolations);
89
+ raw = {
90
+ violations: allViolations,
91
+ filesAnalyzed: totalFiles,
92
+ metadata
93
+ };
94
+ }
95
+
96
+ const summary = this.generateSummary(allViolations, totalFiles, metadata);
97
+
98
+ return {
99
+ formatted,
100
+ summary,
101
+ raw
102
+ };
103
+ }
104
+
105
+ formatViolations(violations) {
106
+ if (violations.length === 0) {
107
+ return chalk.green('āœ… No violations found!');
108
+ }
109
+
110
+ let output = '';
111
+ const fileGroups = {};
112
+
113
+ // Group violations by file
114
+ violations.forEach(violation => {
115
+ let file = violation.file || violation.filePath || 'unknown';
116
+
117
+ // Convert absolute path to relative path for better display
118
+ if (file !== 'unknown' && path.isAbsolute(file)) {
119
+ const cwd = process.cwd();
120
+ if (file.startsWith(cwd)) {
121
+ file = path.relative(cwd, file);
122
+ }
123
+ }
124
+
125
+ if (!fileGroups[file]) {
126
+ fileGroups[file] = [];
127
+ }
128
+ fileGroups[file].push(violation);
129
+ });
130
+
131
+ // Format each file's violations (ESLint-compatible format)
132
+ Object.keys(fileGroups).forEach(file => {
133
+ output += `\n${file}\n`;
134
+ fileGroups[file].forEach(violation => {
135
+ const line = (violation.line || 1).toString().padStart(3);
136
+ const column = (violation.column || 1).toString().padStart(2);
137
+ const severityText = violation.severity === 'error' ? 'error' : 'warning';
138
+ const severityColor = violation.severity === 'error' ? chalk.red : chalk.yellow;
139
+
140
+ // ESLint-style formatting: " line:col severity message ruleId"
141
+ output += ` ${line}:${column} ${severityColor(severityText)} ${violation.message} ${chalk.gray(violation.ruleId)}\n`;
142
+ });
143
+ });
144
+
145
+ // Add violation count (ESLint-compatible)
146
+ const errorCount = violations.filter(v => v.severity === 'error').length;
147
+ const warningCount = violations.filter(v => v.severity === 'warning').length;
148
+
149
+ output += `\n${chalk.red('āœ–')} ${violations.length} problems `;
150
+ output += `(${errorCount} errors, ${warningCount} warnings)\n`;
151
+
152
+ return output;
153
+ }
154
+
155
+ generateSummary(violations, filesAnalyzed, metadata) {
156
+ const duration = metadata.duration || 0;
157
+ const errorCount = violations.filter(v => v.severity === 'error').length;
158
+ const warningCount = violations.filter(v => v.severity === 'warning').length;
159
+
160
+ let summary = chalk.blue('\nšŸ“Š Sun Lint Summary:\n');
161
+ summary += `Analysis completed in ${duration}ms\n`;
162
+ summary += `Files: ${filesAnalyzed} | Total: ${violations.length}\n`;
163
+
164
+ if (errorCount > 0) {
165
+ summary += chalk.red(`Errors: ${errorCount} `);
166
+ }
167
+ if (warningCount > 0) {
168
+ summary += chalk.yellow(`Warnings: ${warningCount} `);
169
+ }
170
+ if (violations.length === 0) {
171
+ summary += chalk.green('All checks passed! āœ…');
172
+ }
173
+
174
+ return summary;
175
+ }
176
+
177
+ generateJsonFormat(results, allViolations, options = {}) {
178
+ // ESLint-compatible JSON format
179
+ const jsonResults = [];
180
+ const fileGroups = {};
181
+
182
+ // Group violations by file
183
+ allViolations.forEach(violation => {
184
+ let file = violation.file || violation.filePath || 'unknown';
185
+
186
+ // Convert absolute path to relative path for better display
187
+ if (file !== 'unknown' && path.isAbsolute(file)) {
188
+ const cwd = process.cwd();
189
+ if (file.startsWith(cwd)) {
190
+ file = path.relative(cwd, file);
191
+ }
192
+ }
193
+
194
+ if (!fileGroups[file]) {
195
+ fileGroups[file] = [];
196
+ }
197
+ fileGroups[file].push(violation);
198
+ });
199
+
200
+ // Add files with violations
201
+ Object.keys(fileGroups).forEach(filePath => {
202
+ const messages = fileGroups[filePath].map(violation => ({
203
+ ruleId: violation.ruleId,
204
+ severity: violation.severity === 'error' ? 2 : 1, // ESLint: 1=warning, 2=error
205
+ message: violation.message,
206
+ line: violation.line || 1,
207
+ column: violation.column || 1,
208
+ nodeType: violation.nodeType || null,
209
+ messageId: violation.messageId || null,
210
+ endLine: violation.endLine || null,
211
+ endColumn: violation.endColumn || null
212
+ }));
213
+
214
+ jsonResults.push({
215
+ filePath: filePath,
216
+ messages: messages,
217
+ suppressedMessages: [],
218
+ errorCount: messages.filter(m => m.severity === 2).length,
219
+ warningCount: messages.filter(m => m.severity === 1).length,
220
+ fatalErrorCount: 0,
221
+ fixableErrorCount: 0,
222
+ fixableWarningCount: 0,
223
+ source: null
224
+ });
225
+ });
226
+
227
+ // Add files without violations (if any were analyzed)
228
+ if (results.results) {
229
+ results.results.forEach(fileResult => {
230
+ if (!fileGroups[fileResult.file] && fileResult.violations.length === 0) {
231
+ jsonResults.push({
232
+ filePath: fileResult.file,
233
+ messages: [],
234
+ suppressedMessages: [],
235
+ errorCount: 0,
236
+ warningCount: 0,
237
+ fatalErrorCount: 0,
238
+ fixableErrorCount: 0,
239
+ fixableWarningCount: 0,
240
+ source: null
241
+ });
242
+ }
243
+ });
244
+ }
245
+
246
+ return jsonResults;
247
+ }
248
+ }
249
+
250
+ module.exports = OutputService;