@sun-asterisk/sunlint 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +202 -0
- package/LICENSE +21 -0
- package/README.md +490 -0
- package/cli-legacy.js +355 -0
- package/cli.js +35 -0
- package/config/default.json +22 -0
- package/config/presets/beginner.json +36 -0
- package/config/presets/ci.json +46 -0
- package/config/presets/recommended.json +24 -0
- package/config/presets/strict.json +32 -0
- package/config/rules-registry.json +681 -0
- package/config/sunlint-schema.json +166 -0
- package/config/typescript/custom-rules-new.js +0 -0
- package/config/typescript/custom-rules.js +9 -0
- package/config/typescript/eslint.config.js +110 -0
- package/config/typescript/package-lock.json +1585 -0
- package/config/typescript/package.json +13 -0
- package/config/typescript/security-rules/index.js +90 -0
- package/config/typescript/security-rules/s005-no-origin-auth.js +95 -0
- package/config/typescript/security-rules/s006-activation-recovery-secret-not-plaintext.js +69 -0
- package/config/typescript/security-rules/s008-crypto-agility.js +62 -0
- package/config/typescript/security-rules/s009-no-insecure-crypto.js +103 -0
- package/config/typescript/security-rules/s010-no-insecure-random-in-sensitive-context.js +123 -0
- package/config/typescript/security-rules/s011-no-insecure-uuid.js +66 -0
- package/config/typescript/security-rules/s012-hardcode-secret.js +71 -0
- package/config/typescript/security-rules/s014-insecure-tls-version.js +50 -0
- package/config/typescript/security-rules/s015-insecure-tls-certificate.js +43 -0
- package/config/typescript/security-rules/s016-sensitive-query-parameter.js +59 -0
- package/config/typescript/security-rules/s017-no-sql-injection.js +193 -0
- package/config/typescript/security-rules/s018-positive-input-validation.js +56 -0
- package/config/typescript/security-rules/s019-no-raw-user-input-in-email.js +113 -0
- package/config/typescript/security-rules/s020-no-eval-dynamic-execution.js +89 -0
- package/config/typescript/security-rules/s022-output-encoding.js +78 -0
- package/config/typescript/security-rules/s023-no-json-injection.js +300 -0
- package/config/typescript/security-rules/s025-server-side-input-validation.js +217 -0
- package/config/typescript/security-rules/s026-json-schema-validation.js +68 -0
- package/config/typescript/security-rules/s027-no-hardcoded-secrets.js +80 -0
- package/config/typescript/security-rules/s029-require-csrf-protection.js +79 -0
- package/config/typescript/security-rules/s030-no-directory-browsing.js +78 -0
- package/config/typescript/security-rules/s033-require-samesite-cookie.js +80 -0
- package/config/typescript/security-rules/s034-require-host-cookie-prefix.js +77 -0
- package/config/typescript/security-rules/s035-cookie-specific-path.js +74 -0
- package/config/typescript/security-rules/s036-no-unsafe-file-include.js +68 -0
- package/config/typescript/security-rules/s037-require-anti-cache-headers.js +70 -0
- package/config/typescript/security-rules/s038-no-version-disclosure.js +74 -0
- package/config/typescript/security-rules/s039-no-session-token-in-url.js +63 -0
- package/config/typescript/security-rules/s041-require-session-invalidate-on-logout.js +211 -0
- package/config/typescript/security-rules/s042-require-periodic-reauthentication.js +294 -0
- package/config/typescript/security-rules/s043-terminate-sessions-on-password-change.js +254 -0
- package/config/typescript/security-rules/s044-require-full-session-for-sensitive-operations.js +292 -0
- package/config/typescript/security-rules/s045-anti-automation-controls.js +46 -0
- package/config/typescript/security-rules/s046-secure-notification-on-auth-change.js +44 -0
- package/config/typescript/security-rules/s048-password-credential-recovery.js +54 -0
- package/config/typescript/security-rules/s050-session-token-weak-hash.js +94 -0
- package/config/typescript/security-rules/s052-secure-random-authentication-code.js +66 -0
- package/config/typescript/security-rules/s054-verification-default-account.js +109 -0
- package/config/typescript/security-rules/s057-utc-logging.js +54 -0
- package/config/typescript/security-rules/s058-no-ssrf.js +73 -0
- package/config/typescript/test-s005-working.ts +22 -0
- package/config/typescript/tsconfig.json +29 -0
- package/core/ai-analyzer.js +169 -0
- package/core/analysis-orchestrator.js +705 -0
- package/core/cli-action-handler.js +230 -0
- package/core/cli-program.js +106 -0
- package/core/config-manager.js +396 -0
- package/core/config-merger.js +136 -0
- package/core/config-override-processor.js +74 -0
- package/core/config-preset-resolver.js +65 -0
- package/core/config-source-loader.js +152 -0
- package/core/config-validator.js +126 -0
- package/core/dependency-manager.js +105 -0
- package/core/eslint-engine-service.js +312 -0
- package/core/eslint-instance-manager.js +104 -0
- package/core/eslint-integration-service.js +363 -0
- package/core/git-utils.js +170 -0
- package/core/multi-rule-runner.js +239 -0
- package/core/output-service.js +250 -0
- package/core/report-generator.js +320 -0
- package/core/rule-mapping-service.js +309 -0
- package/core/rule-selection-service.js +121 -0
- package/core/sunlint-engine-service.js +23 -0
- package/core/typescript-analyzer.js +262 -0
- package/core/typescript-engine.js +313 -0
- package/docs/AI.md +163 -0
- package/docs/ARCHITECTURE.md +78 -0
- package/docs/CI-CD-GUIDE.md +315 -0
- package/docs/COMMAND-EXAMPLES.md +256 -0
- package/docs/DEBUG.md +86 -0
- package/docs/DISTRIBUTION.md +153 -0
- package/docs/ESLINT-INTEGRATION-STRATEGY.md +392 -0
- package/docs/ESLINT_INTEGRATION.md +238 -0
- package/docs/FOLDER_STRUCTURE.md +59 -0
- package/docs/HEURISTIC_VS_AI.md +113 -0
- package/docs/README.md +32 -0
- package/docs/RELEASE_GUIDE.md +230 -0
- package/docs/RULE-RESPONSIBILITY-MATRIX.md +204 -0
- package/eslint-integration/.eslintrc.js +98 -0
- package/eslint-integration/cli.js +35 -0
- package/eslint-integration/eslint-plugin-custom/c002-no-duplicate-code.js +204 -0
- package/eslint-integration/eslint-plugin-custom/c003-no-vague-abbreviations.js +246 -0
- package/eslint-integration/eslint-plugin-custom/c006-function-name-verb-noun.js +207 -0
- package/eslint-integration/eslint-plugin-custom/c010-limit-block-nesting.js +90 -0
- package/eslint-integration/eslint-plugin-custom/c013-no-dead-code.js +43 -0
- package/eslint-integration/eslint-plugin-custom/c014-abstract-dependency-preferred.js +38 -0
- package/eslint-integration/eslint-plugin-custom/c017-limit-constructor-logic.js +39 -0
- package/eslint-integration/eslint-plugin-custom/c018-no-generic-throw.js +335 -0
- package/eslint-integration/eslint-plugin-custom/c023-no-duplicate-variable-name-in-scope.js +142 -0
- package/eslint-integration/eslint-plugin-custom/c027-limit-function-nesting.js +50 -0
- package/eslint-integration/eslint-plugin-custom/c029-catch-block-logging.js +80 -0
- package/eslint-integration/eslint-plugin-custom/c030-use-custom-error-classes.js +294 -0
- package/eslint-integration/eslint-plugin-custom/c034-no-implicit-return.js +34 -0
- package/eslint-integration/eslint-plugin-custom/c035-no-empty-catch.js +32 -0
- package/eslint-integration/eslint-plugin-custom/c041-no-config-inline.js +64 -0
- package/eslint-integration/eslint-plugin-custom/c042-boolean-name-prefix.js +406 -0
- package/eslint-integration/eslint-plugin-custom/c043-no-console-or-print.js +300 -0
- package/eslint-integration/eslint-plugin-custom/c047-no-duplicate-retry-logic.js +239 -0
- package/eslint-integration/eslint-plugin-custom/c048-no-var-declaration.js +31 -0
- package/eslint-integration/eslint-plugin-custom/c076-one-assert-per-test.js +184 -0
- package/eslint-integration/eslint-plugin-custom/index.js +155 -0
- package/eslint-integration/eslint-plugin-custom/package.json +13 -0
- package/eslint-integration/eslint-plugin-custom/package.json.bak +9 -0
- package/eslint-integration/eslint-plugin-custom/s003-no-unvalidated-redirect.js +86 -0
- package/eslint-integration/eslint-plugin-custom/s005-no-origin-auth.js +95 -0
- package/eslint-integration/eslint-plugin-custom/s006-activation-recovery-secret-not-plaintext.js +69 -0
- package/eslint-integration/eslint-plugin-custom/s008-crypto-agility.js +62 -0
- package/eslint-integration/eslint-plugin-custom/s009-no-insecure-crypto.js +103 -0
- package/eslint-integration/eslint-plugin-custom/s010-no-insecure-random-in-sensitive-context.js +123 -0
- package/eslint-integration/eslint-plugin-custom/s011-no-insecure-uuid.js +66 -0
- package/eslint-integration/eslint-plugin-custom/s012-hardcode-secret.js +71 -0
- package/eslint-integration/eslint-plugin-custom/s014-insecure-tls-version.js +50 -0
- package/eslint-integration/eslint-plugin-custom/s015-insecure-tls-certificate.js +43 -0
- package/eslint-integration/eslint-plugin-custom/s016-sensitive-query-parameter.js +59 -0
- package/eslint-integration/eslint-plugin-custom/s017-no-sql-injection.js +193 -0
- package/eslint-integration/eslint-plugin-custom/s018-positive-input-validation.js +56 -0
- package/eslint-integration/eslint-plugin-custom/s019-no-raw-user-input-in-email.js +113 -0
- package/eslint-integration/eslint-plugin-custom/s020-no-eval-dynamic-execution.js +89 -0
- package/eslint-integration/eslint-plugin-custom/s022-output-encoding.js +78 -0
- package/eslint-integration/eslint-plugin-custom/s023-no-json-injection.js +300 -0
- package/eslint-integration/eslint-plugin-custom/s025-server-side-input-validation.js +217 -0
- package/eslint-integration/eslint-plugin-custom/s026-json-schema-validation.js +68 -0
- package/eslint-integration/eslint-plugin-custom/s027-no-hardcoded-secrets.js +80 -0
- package/eslint-integration/eslint-plugin-custom/s029-require-csrf-protection.js +79 -0
- package/eslint-integration/eslint-plugin-custom/s030-no-directory-browsing.js +78 -0
- package/eslint-integration/eslint-plugin-custom/s033-require-samesite-cookie.js +80 -0
- package/eslint-integration/eslint-plugin-custom/s034-require-host-cookie-prefix.js +77 -0
- package/eslint-integration/eslint-plugin-custom/s035-cookie-specific-path.js +74 -0
- package/eslint-integration/eslint-plugin-custom/s036-no-unsafe-file-include.js +68 -0
- package/eslint-integration/eslint-plugin-custom/s037-require-anti-cache-headers.js +70 -0
- package/eslint-integration/eslint-plugin-custom/s038-no-version-disclosure.js +74 -0
- package/eslint-integration/eslint-plugin-custom/s039-no-session-token-in-url.js +63 -0
- package/eslint-integration/eslint-plugin-custom/s041-require-session-invalidate-on-logout.js +211 -0
- package/eslint-integration/eslint-plugin-custom/s042-require-periodic-reauthentication.js +294 -0
- package/eslint-integration/eslint-plugin-custom/s043-terminate-sessions-on-password-change.js +254 -0
- package/eslint-integration/eslint-plugin-custom/s044-require-full-session-for-sensitive-operations.js +292 -0
- package/eslint-integration/eslint-plugin-custom/s045-anti-automation-controls.js +46 -0
- package/eslint-integration/eslint-plugin-custom/s046-secure-notification-on-auth-change.js +44 -0
- package/eslint-integration/eslint-plugin-custom/s047-secure-random-passwords.js +108 -0
- package/eslint-integration/eslint-plugin-custom/s048-password-credential-recovery.js +54 -0
- package/eslint-integration/eslint-plugin-custom/s050-session-token-weak-hash.js +94 -0
- package/eslint-integration/eslint-plugin-custom/s052-secure-random-authentication-code.js +66 -0
- package/eslint-integration/eslint-plugin-custom/s054-verification-default-account.js +109 -0
- package/eslint-integration/eslint-plugin-custom/s055-verification-rest-check-the-incoming-content-type.js +143 -0
- package/eslint-integration/eslint-plugin-custom/s057-utc-logging.js +54 -0
- package/eslint-integration/eslint-plugin-custom/s058-no-ssrf.js +73 -0
- package/eslint-integration/eslint-plugin-custom/t002-interface-prefix-i.js +42 -0
- package/eslint-integration/eslint-plugin-custom/t003-ts-ignore-reason.js +48 -0
- package/eslint-integration/eslint-plugin-custom/t004-interface-public-only.js +160 -0
- package/eslint-integration/eslint-plugin-custom/t007-no-fn-in-constructor.js +52 -0
- package/eslint-integration/eslint-plugin-custom/t011-no-real-time-dependency.js +175 -0
- package/eslint-integration/eslint-plugin-custom/t019-no-empty-type.js +95 -0
- package/eslint-integration/eslint-plugin-custom/t025-no-nested-union-tuple.js +48 -0
- package/eslint-integration/eslint-plugin-custom/t026-limit-nested-generics.js +377 -0
- package/eslint-integration/eslint.config.js +125 -0
- package/eslint-integration/eslint.config.simple.js +24 -0
- package/eslint-integration/node_modules/eslint-plugin-custom/package.json +0 -0
- package/eslint-integration/package.json +23 -0
- package/eslint-integration/sample.ts +53 -0
- package/eslint-integration/test-s003.js +5 -0
- package/eslint-integration/tsconfig.json +27 -0
- package/examples/.github/workflows/code-quality.yml +111 -0
- package/examples/.sunlint.json +42 -0
- package/examples/README.md +47 -0
- package/examples/package.json +33 -0
- package/package.json +100 -0
- package/rules/C006_function_naming/analyzer.js +338 -0
- package/rules/C006_function_naming/config.json +86 -0
- package/rules/C019_log_level_usage/analyzer.js +359 -0
- package/rules/C019_log_level_usage/config.json +121 -0
- package/rules/C029_catch_block_logging/analyzer.js +339 -0
- package/rules/C029_catch_block_logging/config.json +59 -0
- package/rules/C031_validation_separation/README.md +72 -0
- package/rules/C031_validation_separation/analyzer.js +186 -0
|
@@ -0,0 +1,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;
|