@sun-asterisk/sunlint 1.2.1 → 1.2.2
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/config/rule-analysis-strategies.js +18 -2
- package/engines/eslint-engine.js +9 -11
- package/engines/heuristic-engine.js +55 -31
- package/package.json +2 -1
- package/rules/README.md +252 -0
- package/rules/common/C002_no_duplicate_code/analyzer.js +65 -0
- package/rules/common/C002_no_duplicate_code/config.json +23 -0
- package/rules/common/C003_no_vague_abbreviations/analyzer.js +418 -0
- package/rules/common/C003_no_vague_abbreviations/config.json +35 -0
- package/rules/common/C006_function_naming/analyzer.js +504 -0
- package/rules/common/C006_function_naming/config.json +86 -0
- package/rules/common/C006_function_naming/smart-analyzer.js +503 -0
- package/rules/common/C010_limit_block_nesting/analyzer.js +389 -0
- package/rules/common/C012_command_query_separation/analyzer.js +481 -0
- package/rules/common/C012_command_query_separation/ast-analyzer.js +495 -0
- package/rules/common/C013_no_dead_code/analyzer.js +206 -0
- package/rules/common/C014_dependency_injection/analyzer.js +338 -0
- package/rules/common/C017_constructor_logic/analyzer.js +314 -0
- package/rules/common/C019_log_level_usage/analyzer.js +362 -0
- package/rules/common/C019_log_level_usage/config.json +121 -0
- package/rules/common/C029_catch_block_logging/analyzer-backup.js +426 -0
- package/rules/common/C029_catch_block_logging/analyzer-fixed.js +130 -0
- package/rules/common/C029_catch_block_logging/analyzer-multi-tech.js +487 -0
- package/rules/common/C029_catch_block_logging/analyzer-simple.js +110 -0
- package/rules/common/C029_catch_block_logging/analyzer-smart-pipeline.js +755 -0
- package/rules/common/C029_catch_block_logging/analyzer.js +129 -0
- package/rules/common/C029_catch_block_logging/ast-analyzer-backup.js +441 -0
- package/rules/common/C029_catch_block_logging/ast-analyzer-new.js +127 -0
- package/rules/common/C029_catch_block_logging/ast-analyzer.js +133 -0
- package/rules/common/C029_catch_block_logging/cfg-analyzer.js +408 -0
- package/rules/common/C029_catch_block_logging/config.json +59 -0
- package/rules/common/C029_catch_block_logging/dataflow-analyzer.js +454 -0
- package/rules/common/C029_catch_block_logging/multi-language-ast-engine.js +700 -0
- package/rules/common/C029_catch_block_logging/pattern-learning-analyzer.js +568 -0
- package/rules/common/C029_catch_block_logging/semantic-analyzer.js +459 -0
- package/rules/common/C031_validation_separation/analyzer.js +186 -0
- package/rules/common/C041_no_sensitive_hardcode/analyzer.js +292 -0
- package/rules/common/C041_no_sensitive_hardcode/ast-analyzer.js +296 -0
- package/rules/common/C042_boolean_name_prefix/analyzer.js +300 -0
- package/rules/common/C043_no_console_or_print/analyzer.js +431 -0
- package/rules/common/C047_no_duplicate_retry_logic/analyzer.js +590 -0
- package/rules/common/C075_explicit_return_types/analyzer.js +103 -0
- package/rules/common/C076_single_test_behavior/analyzer.js +121 -0
- package/rules/docs/C002_no_duplicate_code.md +57 -0
- package/rules/docs/C031_validation_separation.md +72 -0
- package/rules/index.js +155 -0
- package/rules/migration/converter.js +385 -0
- package/rules/migration/mapping.json +164 -0
- package/rules/parser/constants.js +31 -0
- package/rules/parser/file-config.js +80 -0
- package/rules/parser/rule-parser-simple.js +305 -0
- package/rules/parser/rule-parser.js +527 -0
- package/rules/security/S015_insecure_tls_certificate/analyzer.js +150 -0
- package/rules/security/S015_insecure_tls_certificate/ast-analyzer.js +237 -0
- package/rules/security/S023_no_json_injection/analyzer.js +278 -0
- package/rules/security/S023_no_json_injection/ast-analyzer.js +359 -0
- package/rules/security/S026_json_schema_validation/analyzer.js +251 -0
- package/rules/security/S026_json_schema_validation/config.json +27 -0
- package/rules/security/S027_no_hardcoded_secrets/analyzer.js +436 -0
- package/rules/security/S027_no_hardcoded_secrets/config.json +29 -0
- package/rules/security/S029_csrf_protection/analyzer.js +330 -0
- package/rules/tests/C002_no_duplicate_code.test.js +50 -0
- package/rules/universal/C010/generic.js +0 -0
- package/rules/universal/C010/tree-sitter-analyzer.js +0 -0
- package/rules/utils/ast-utils.js +191 -0
- package/rules/utils/base-analyzer.js +98 -0
- package/rules/utils/pattern-matchers.js +239 -0
- package/rules/utils/rule-helpers.js +264 -0
- package/rules/utils/severity-constants.js +93 -0
- package/scripts/generate_insights.js +188 -0
- package/scripts/merge-reports.js +0 -424
- package/scripts/test-scripts/README.md +0 -22
- package/scripts/test-scripts/test-c041-comparison.js +0 -114
- package/scripts/test-scripts/test-c041-eslint.js +0 -67
- package/scripts/test-scripts/test-eslint-rules.js +0 -146
- package/scripts/test-scripts/test-real-world.js +0 -44
- package/scripts/test-scripts/test-rules-on-real-projects.js +0 -86
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pattern Matchers for Heuristic Rules
|
|
3
|
+
* Common pattern matching utilities for code analysis
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class PatternMatcher {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.commonPatterns = this.loadCommonPatterns();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Load common patterns used across rules
|
|
13
|
+
* @returns {Object} Common patterns object
|
|
14
|
+
*/
|
|
15
|
+
loadCommonPatterns() {
|
|
16
|
+
return {
|
|
17
|
+
// Function naming patterns
|
|
18
|
+
functionName: {
|
|
19
|
+
camelCase: /^[a-z][a-zA-Z0-9]*$/,
|
|
20
|
+
verbNoun: /^(get|set|is|has|can|should|will|create|update|delete|find|search|validate|process|handle|execute)[A-Z]/,
|
|
21
|
+
constructor: /^[A-Z][a-zA-Z0-9]*$/
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
// Variable naming patterns
|
|
25
|
+
variableName: {
|
|
26
|
+
camelCase: /^[a-z][a-zA-Z0-9]*$/,
|
|
27
|
+
constant: /^[A-Z][A-Z0-9_]*$/,
|
|
28
|
+
boolean: /^(is|has|can|should|will|did)[A-Z]/
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
// Log level patterns
|
|
32
|
+
logLevel: {
|
|
33
|
+
console: /console\.(log|info|warn|error|debug)/g,
|
|
34
|
+
logger: /(logger|log)\.(trace|debug|info|warn|error|fatal)/g,
|
|
35
|
+
customLogger: /\b(log|logger)\b.*\.(trace|debug|info|warn|error|fatal)/g
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
// Security patterns
|
|
39
|
+
security: {
|
|
40
|
+
hardcodedSecret: /(['"`])((?:password|secret|key|token|api_key|apikey)[:=]\s*[^'"`\s]+)\1/gi,
|
|
41
|
+
sqlInjection: /(query|execute)\s*\(\s*['"`][^'"`]*\+.*['"`]/g,
|
|
42
|
+
xss: /innerHTML\s*=\s*[^;]+/g
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
// TypeScript patterns
|
|
46
|
+
typescript: {
|
|
47
|
+
interface: /interface\s+([I]?[A-Z][a-zA-Z0-9]*)/g,
|
|
48
|
+
tsIgnore: /@ts-ignore(?!\s+.*:)/g,
|
|
49
|
+
emptyInterface: /interface\s+\w+\s*{\s*}/g
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Find matches for a pattern in content
|
|
56
|
+
* @param {string} content - Code content
|
|
57
|
+
* @param {RegExp} pattern - Pattern to match
|
|
58
|
+
* @returns {Array} Array of matches with line/column info
|
|
59
|
+
*/
|
|
60
|
+
findMatches(content, pattern) {
|
|
61
|
+
const matches = [];
|
|
62
|
+
let match;
|
|
63
|
+
|
|
64
|
+
// Ensure pattern is global
|
|
65
|
+
const globalPattern = new RegExp(pattern.source, pattern.flags.includes('g') ? pattern.flags : pattern.flags + 'g');
|
|
66
|
+
|
|
67
|
+
while ((match = globalPattern.exec(content)) !== null) {
|
|
68
|
+
const line = this.getLineNumber(content, match.index);
|
|
69
|
+
const column = match.index - this.getLineStart(content, match.index);
|
|
70
|
+
|
|
71
|
+
matches.push({
|
|
72
|
+
match: match[0],
|
|
73
|
+
groups: match.slice(1),
|
|
74
|
+
line: line,
|
|
75
|
+
column: column,
|
|
76
|
+
index: match.index,
|
|
77
|
+
lineContent: this.getLineContent(content, line)
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return matches;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Find function naming violations
|
|
86
|
+
* @param {string} content - Code content
|
|
87
|
+
* @returns {Array} Array of violations
|
|
88
|
+
*/
|
|
89
|
+
findFunctionNamingViolations(content) {
|
|
90
|
+
const violations = [];
|
|
91
|
+
const functionRegex = /(?:function\s+(\w+)|(\w+)\s*[:=]\s*(?:function|async\s+function|\([^)]*\)\s*=>))/g;
|
|
92
|
+
let match;
|
|
93
|
+
|
|
94
|
+
while ((match = functionRegex.exec(content)) !== null) {
|
|
95
|
+
const functionName = match[1] || match[2];
|
|
96
|
+
const line = this.getLineNumber(content, match.index);
|
|
97
|
+
|
|
98
|
+
// Skip if it's a constructor (starts with capital letter)
|
|
99
|
+
if (/^[A-Z]/.test(functionName)) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Check if it follows verb-noun pattern
|
|
104
|
+
if (!this.commonPatterns.functionName.verbNoun.test(functionName)) {
|
|
105
|
+
violations.push({
|
|
106
|
+
type: 'function-naming',
|
|
107
|
+
message: `Function name '${functionName}' should follow verb-noun pattern (e.g., getUserData, validateInput)`,
|
|
108
|
+
line: line,
|
|
109
|
+
column: match.index - this.getLineStart(content, match.index),
|
|
110
|
+
functionName: functionName
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return violations;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Find log level usage violations
|
|
120
|
+
* @param {string} content - Code content
|
|
121
|
+
* @returns {Array} Array of violations
|
|
122
|
+
*/
|
|
123
|
+
findLogLevelViolations(content) {
|
|
124
|
+
const violations = [];
|
|
125
|
+
|
|
126
|
+
// Check for console.log usage (should use appropriate log levels)
|
|
127
|
+
const consoleLogMatches = this.findMatches(content, /console\.log\s*\(/g);
|
|
128
|
+
consoleLogMatches.forEach(match => {
|
|
129
|
+
violations.push({
|
|
130
|
+
type: 'log-level',
|
|
131
|
+
message: 'Use appropriate log level instead of console.log (info, warn, error)',
|
|
132
|
+
line: match.line,
|
|
133
|
+
column: match.column,
|
|
134
|
+
suggestion: 'Replace with console.info, console.warn, or console.error'
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return violations;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Find hardcoded secrets
|
|
143
|
+
* @param {string} content - Code content
|
|
144
|
+
* @returns {Array} Array of violations
|
|
145
|
+
*/
|
|
146
|
+
findHardcodedSecrets(content) {
|
|
147
|
+
const violations = [];
|
|
148
|
+
const secretPatterns = [
|
|
149
|
+
/['"`](password|secret|key|token|api_key|apikey)['"`]\s*[:=]\s*['"`][a-zA-Z0-9+/=]{8,}['"`]/gi,
|
|
150
|
+
/(password|secret|key|token)\s*=\s*['"`][a-zA-Z0-9+/=]{8,}['"`]/gi
|
|
151
|
+
];
|
|
152
|
+
|
|
153
|
+
secretPatterns.forEach(pattern => {
|
|
154
|
+
const matches = this.findMatches(content, pattern);
|
|
155
|
+
matches.forEach(match => {
|
|
156
|
+
violations.push({
|
|
157
|
+
type: 'hardcoded-secret',
|
|
158
|
+
message: 'Hardcoded secret detected. Use environment variables or secure configuration',
|
|
159
|
+
line: match.line,
|
|
160
|
+
column: match.column,
|
|
161
|
+
severity: 'error'
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
return violations;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Find TypeScript interface violations
|
|
171
|
+
* @param {string} content - Code content
|
|
172
|
+
* @returns {Array} Array of violations
|
|
173
|
+
*/
|
|
174
|
+
findTypeScriptInterfaceViolations(content) {
|
|
175
|
+
const violations = [];
|
|
176
|
+
|
|
177
|
+
// Interface should start with 'I' prefix
|
|
178
|
+
const interfaceMatches = this.findMatches(content, /interface\s+([A-Z][a-zA-Z0-9]*)/g);
|
|
179
|
+
interfaceMatches.forEach(match => {
|
|
180
|
+
const interfaceName = match.groups[0];
|
|
181
|
+
if (!interfaceName.startsWith('I')) {
|
|
182
|
+
violations.push({
|
|
183
|
+
type: 'interface-naming',
|
|
184
|
+
message: `Interface '${interfaceName}' should start with 'I' prefix`,
|
|
185
|
+
line: match.line,
|
|
186
|
+
column: match.column,
|
|
187
|
+
suggestion: `I${interfaceName}`
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
return violations;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Get line number for character index
|
|
197
|
+
*/
|
|
198
|
+
getLineNumber(content, index) {
|
|
199
|
+
return content.substring(0, index).split('\n').length;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Get line start position
|
|
204
|
+
*/
|
|
205
|
+
getLineStart(content, index) {
|
|
206
|
+
const beforeIndex = content.substring(0, index);
|
|
207
|
+
const lastNewline = beforeIndex.lastIndexOf('\n');
|
|
208
|
+
return lastNewline === -1 ? 0 : lastNewline + 1;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Get line content
|
|
213
|
+
*/
|
|
214
|
+
getLineContent(content, lineNumber) {
|
|
215
|
+
const lines = content.split('\n');
|
|
216
|
+
return lines[lineNumber - 1] || '';
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Create a custom pattern matcher
|
|
221
|
+
* @param {string} name - Pattern name
|
|
222
|
+
* @param {RegExp} pattern - Regular expression
|
|
223
|
+
* @param {Function} validator - Optional validator function
|
|
224
|
+
* @returns {Function} Pattern matcher function
|
|
225
|
+
*/
|
|
226
|
+
createMatcher(name, pattern, validator = null) {
|
|
227
|
+
return (content) => {
|
|
228
|
+
const matches = this.findMatches(content, pattern);
|
|
229
|
+
|
|
230
|
+
if (validator) {
|
|
231
|
+
return matches.filter(match => validator(match));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return matches;
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
module.exports = { PatternMatcher };
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule Helpers for Heuristic Rules
|
|
3
|
+
* Utilities for rule configuration, violation reporting, and common operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
class RuleHelper {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.severityLevels = ['off', 'info', 'warn', 'error'];
|
|
9
|
+
this.violationTypes = ['syntax', 'style', 'security', 'performance', 'maintainability'];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Create a standard violation object
|
|
14
|
+
* @param {Object} options - Violation options
|
|
15
|
+
* @returns {Object} Standardized violation object
|
|
16
|
+
*/
|
|
17
|
+
createViolation(options) {
|
|
18
|
+
const {
|
|
19
|
+
ruleId,
|
|
20
|
+
message,
|
|
21
|
+
line = 1,
|
|
22
|
+
column = 0,
|
|
23
|
+
severity = 'error',
|
|
24
|
+
type = 'style',
|
|
25
|
+
suggestion = null,
|
|
26
|
+
fix = null
|
|
27
|
+
} = options;
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
ruleId: ruleId,
|
|
31
|
+
message: message,
|
|
32
|
+
line: line,
|
|
33
|
+
column: column,
|
|
34
|
+
severity: this.validateSeverity(severity),
|
|
35
|
+
type: type,
|
|
36
|
+
suggestion: suggestion,
|
|
37
|
+
fix: fix,
|
|
38
|
+
timestamp: new Date().toISOString()
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Validate severity level
|
|
44
|
+
* @param {string} severity - Severity level
|
|
45
|
+
* @returns {string} Valid severity level
|
|
46
|
+
*/
|
|
47
|
+
validateSeverity(severity) {
|
|
48
|
+
return this.severityLevels.includes(severity) ? severity : 'error';
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Load rule configuration with defaults
|
|
53
|
+
* @param {string} ruleId - Rule ID
|
|
54
|
+
* @param {Object} userConfig - User configuration
|
|
55
|
+
* @returns {Object} Merged configuration
|
|
56
|
+
*/
|
|
57
|
+
loadRuleConfig(ruleId, userConfig = {}) {
|
|
58
|
+
const defaultConfig = {
|
|
59
|
+
enabled: true,
|
|
60
|
+
severity: 'error',
|
|
61
|
+
options: {},
|
|
62
|
+
patterns: {
|
|
63
|
+
include: ['**/*.js', '**/*.ts'],
|
|
64
|
+
exclude: ['**/*.test.*', '**/*.spec.*', 'node_modules/**']
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
...defaultConfig,
|
|
70
|
+
...userConfig,
|
|
71
|
+
ruleId: ruleId,
|
|
72
|
+
patterns: {
|
|
73
|
+
...defaultConfig.patterns,
|
|
74
|
+
...(userConfig.patterns || {})
|
|
75
|
+
},
|
|
76
|
+
options: {
|
|
77
|
+
...defaultConfig.options,
|
|
78
|
+
...(userConfig.options || {})
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Check if file should be analyzed by rule
|
|
85
|
+
* @param {string} filePath - File path
|
|
86
|
+
* @param {Object} config - Rule configuration
|
|
87
|
+
* @returns {boolean} True if file should be analyzed
|
|
88
|
+
*/
|
|
89
|
+
shouldAnalyzeFile(filePath, config) {
|
|
90
|
+
const { patterns } = config;
|
|
91
|
+
|
|
92
|
+
// Check exclusions first
|
|
93
|
+
if (patterns.exclude && patterns.exclude.length > 0) {
|
|
94
|
+
for (const pattern of patterns.exclude) {
|
|
95
|
+
if (this.matchPattern(filePath, pattern)) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Check inclusions
|
|
102
|
+
if (patterns.include && patterns.include.length > 0) {
|
|
103
|
+
for (const pattern of patterns.include) {
|
|
104
|
+
if (this.matchPattern(filePath, pattern)) {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return false; // No include patterns matched
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return true; // No specific patterns, analyze by default
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Simple pattern matching (supports * wildcards)
|
|
116
|
+
* @param {string} filePath - File path
|
|
117
|
+
* @param {string} pattern - Pattern to match
|
|
118
|
+
* @returns {boolean} True if pattern matches
|
|
119
|
+
*/
|
|
120
|
+
matchPattern(filePath, pattern) {
|
|
121
|
+
// Convert glob pattern to regex
|
|
122
|
+
const regexPattern = pattern
|
|
123
|
+
.replace(/\./g, '\\.')
|
|
124
|
+
.replace(/\*\*/g, '.*')
|
|
125
|
+
.replace(/\*/g, '[^/]*')
|
|
126
|
+
.replace(/\?/g, '.');
|
|
127
|
+
|
|
128
|
+
const regex = new RegExp(`^${regexPattern}$`, 'i');
|
|
129
|
+
return regex.test(filePath);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Format violation message with context
|
|
134
|
+
* @param {Object} violation - Violation object
|
|
135
|
+
* @param {string} context - Additional context
|
|
136
|
+
* @returns {string} Formatted message
|
|
137
|
+
*/
|
|
138
|
+
formatViolationMessage(violation, context = '') {
|
|
139
|
+
const { ruleId, message, line, column, severity } = violation;
|
|
140
|
+
const location = `${line}:${column}`;
|
|
141
|
+
const prefix = `[${severity.toUpperCase()}] ${ruleId}`;
|
|
142
|
+
|
|
143
|
+
let formatted = `${prefix} at ${location}: ${message}`;
|
|
144
|
+
|
|
145
|
+
if (context) {
|
|
146
|
+
formatted += `\n Context: ${context}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (violation.suggestion) {
|
|
150
|
+
formatted += `\n Suggestion: ${violation.suggestion}`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return formatted;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Group violations by type/severity
|
|
158
|
+
* @param {Array} violations - Array of violations
|
|
159
|
+
* @returns {Object} Grouped violations
|
|
160
|
+
*/
|
|
161
|
+
groupViolations(violations) {
|
|
162
|
+
const grouped = {
|
|
163
|
+
bySeverity: {},
|
|
164
|
+
byType: {},
|
|
165
|
+
byRule: {}
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
violations.forEach(violation => {
|
|
169
|
+
// Group by severity
|
|
170
|
+
if (!grouped.bySeverity[violation.severity]) {
|
|
171
|
+
grouped.bySeverity[violation.severity] = [];
|
|
172
|
+
}
|
|
173
|
+
grouped.bySeverity[violation.severity].push(violation);
|
|
174
|
+
|
|
175
|
+
// Group by type
|
|
176
|
+
if (!grouped.byType[violation.type]) {
|
|
177
|
+
grouped.byType[violation.type] = [];
|
|
178
|
+
}
|
|
179
|
+
grouped.byType[violation.type].push(violation);
|
|
180
|
+
|
|
181
|
+
// Group by rule
|
|
182
|
+
if (!grouped.byRule[violation.ruleId]) {
|
|
183
|
+
grouped.byRule[violation.ruleId] = [];
|
|
184
|
+
}
|
|
185
|
+
grouped.byRule[violation.ruleId].push(violation);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
return grouped;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Generate violation statistics
|
|
193
|
+
* @param {Array} violations - Array of violations
|
|
194
|
+
* @returns {Object} Statistics object
|
|
195
|
+
*/
|
|
196
|
+
generateStats(violations) {
|
|
197
|
+
const grouped = this.groupViolations(violations);
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
total: violations.length,
|
|
201
|
+
severity: {
|
|
202
|
+
error: (grouped.bySeverity.error || []).length,
|
|
203
|
+
warn: (grouped.bySeverity.warn || []).length,
|
|
204
|
+
info: (grouped.bySeverity.info || []).length
|
|
205
|
+
},
|
|
206
|
+
types: Object.keys(grouped.byType).map(type => ({
|
|
207
|
+
type,
|
|
208
|
+
count: grouped.byType[type].length
|
|
209
|
+
})),
|
|
210
|
+
rules: Object.keys(grouped.byRule).map(ruleId => ({
|
|
211
|
+
ruleId,
|
|
212
|
+
count: grouped.byRule[ruleId].length
|
|
213
|
+
})).sort((a, b) => b.count - a.count)
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Check if rule should be skipped for file
|
|
219
|
+
* @param {string} content - File content
|
|
220
|
+
* @param {string} ruleId - Rule ID
|
|
221
|
+
* @returns {boolean} True if rule should be skipped
|
|
222
|
+
*/
|
|
223
|
+
shouldSkipRule(content, ruleId) {
|
|
224
|
+
// Check for disable comments
|
|
225
|
+
const disablePatterns = [
|
|
226
|
+
`// sunlint-disable-next-line ${ruleId}`,
|
|
227
|
+
`/* sunlint-disable-next-line ${ruleId} */`,
|
|
228
|
+
`// sunlint-disable ${ruleId}`,
|
|
229
|
+
`/* sunlint-disable ${ruleId} */`
|
|
230
|
+
];
|
|
231
|
+
|
|
232
|
+
return disablePatterns.some(pattern => content.includes(pattern));
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Extract context around a violation
|
|
237
|
+
* @param {string} content - File content
|
|
238
|
+
* @param {number} line - Line number
|
|
239
|
+
* @param {number} contextLines - Number of context lines
|
|
240
|
+
* @returns {Object} Context information
|
|
241
|
+
*/
|
|
242
|
+
extractContext(content, line, contextLines = 2) {
|
|
243
|
+
const lines = content.split('\n');
|
|
244
|
+
const startLine = Math.max(0, line - 1 - contextLines);
|
|
245
|
+
const endLine = Math.min(lines.length, line + contextLines);
|
|
246
|
+
|
|
247
|
+
const contextText = lines.slice(startLine, endLine)
|
|
248
|
+
.map((text, index) => {
|
|
249
|
+
const lineNum = startLine + index + 1;
|
|
250
|
+
const marker = lineNum === line ? '>' : ' ';
|
|
251
|
+
return `${marker} ${lineNum.toString().padStart(3)}: ${text}`;
|
|
252
|
+
})
|
|
253
|
+
.join('\n');
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
startLine: startLine + 1,
|
|
257
|
+
endLine: endLine,
|
|
258
|
+
text: contextText,
|
|
259
|
+
violationLine: lines[line - 1] || ''
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
module.exports = { RuleHelper };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Centralized Severity Constants for SunLint Rules
|
|
3
|
+
* Ensures consistency across all rule implementations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const SEVERITY = {
|
|
7
|
+
OFF: 'off',
|
|
8
|
+
INFO: 'info',
|
|
9
|
+
WARNING: 'warning',
|
|
10
|
+
ERROR: 'error'
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
// Default severities by rule category
|
|
14
|
+
const DEFAULT_SEVERITIES = {
|
|
15
|
+
// Quality rules - generally warnings (can be fixed incrementally)
|
|
16
|
+
QUALITY: SEVERITY.WARNING,
|
|
17
|
+
|
|
18
|
+
// Security rules - generally errors (must be fixed)
|
|
19
|
+
SECURITY: SEVERITY.ERROR,
|
|
20
|
+
|
|
21
|
+
// Performance rules - generally warnings
|
|
22
|
+
PERFORMANCE: SEVERITY.WARNING,
|
|
23
|
+
|
|
24
|
+
// Maintainability rules - generally warnings
|
|
25
|
+
MAINTAINABILITY: SEVERITY.WARNING,
|
|
26
|
+
|
|
27
|
+
// Best practices - generally warnings
|
|
28
|
+
BEST_PRACTICE: SEVERITY.WARNING,
|
|
29
|
+
|
|
30
|
+
// Critical security - always errors
|
|
31
|
+
CRITICAL_SECURITY: SEVERITY.ERROR
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Specific rule overrides (if needed)
|
|
35
|
+
const RULE_SEVERITY_OVERRIDES = {
|
|
36
|
+
// Security rules that should be errors
|
|
37
|
+
'S001': SEVERITY.ERROR,
|
|
38
|
+
'S002': SEVERITY.ERROR,
|
|
39
|
+
'S005': SEVERITY.ERROR,
|
|
40
|
+
'S012': SEVERITY.ERROR, // No hardcoded secrets
|
|
41
|
+
'S013': SEVERITY.ERROR, // Always use TLS
|
|
42
|
+
|
|
43
|
+
// Quality rules that might be info for gradual adoption
|
|
44
|
+
// 'C007': SEVERITY.INFO, // Comment quality - can be relaxed initially
|
|
45
|
+
|
|
46
|
+
// Rules that should be strict errors
|
|
47
|
+
'C043': SEVERITY.ERROR // No console.log in production
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get the appropriate severity for a rule
|
|
52
|
+
* @param {string} ruleId - The rule ID (e.g., 'C010', 'S005')
|
|
53
|
+
* @param {string} category - The rule category (quality, security, etc.)
|
|
54
|
+
* @param {string} [configOverride] - Override from configuration
|
|
55
|
+
* @returns {string} The severity level
|
|
56
|
+
*/
|
|
57
|
+
function getSeverity(ruleId, category, configOverride = null) {
|
|
58
|
+
// 1. Configuration override has highest priority
|
|
59
|
+
if (configOverride && Object.values(SEVERITY).includes(configOverride)) {
|
|
60
|
+
return configOverride;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 2. Rule-specific override
|
|
64
|
+
if (RULE_SEVERITY_OVERRIDES[ruleId]) {
|
|
65
|
+
return RULE_SEVERITY_OVERRIDES[ruleId];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 3. Category default
|
|
69
|
+
const categoryKey = category?.toUpperCase();
|
|
70
|
+
if (DEFAULT_SEVERITIES[categoryKey]) {
|
|
71
|
+
return DEFAULT_SEVERITIES[categoryKey];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 4. Fall back to warning
|
|
75
|
+
return SEVERITY.WARNING;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Validate severity value
|
|
80
|
+
* @param {string} severity - Severity to validate
|
|
81
|
+
* @returns {boolean} True if valid
|
|
82
|
+
*/
|
|
83
|
+
function isValidSeverity(severity) {
|
|
84
|
+
return Object.values(SEVERITY).includes(severity);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = {
|
|
88
|
+
SEVERITY,
|
|
89
|
+
DEFAULT_SEVERITIES,
|
|
90
|
+
RULE_SEVERITY_OVERRIDES,
|
|
91
|
+
getSeverity,
|
|
92
|
+
isValidSeverity
|
|
93
|
+
};
|