@sun-asterisk/sunlint 1.3.44 → 1.3.46
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/core/cli-action-handler.js +7 -4
- package/core/cli-program.js +7 -0
- package/core/output-service.js +2 -2
- package/core/output-service.js.debug.js +1 -0
- package/core/rule-selection-service.js +72 -0
- package/package.json +1 -1
- package/rules/common/C006_function_naming/typescript/analyzer.js +1 -2
- package/rules/common/C017_constructor_logic/typescript/symbol-based-analyzer.js +1 -1
- package/rules/common/C019_log_level_usage/typescript/system-log-analyzer.js +2 -2
- package/rules/common/C021_import_organization/typescript/ts-morph-analyzer.js +3 -3
- package/rules/common/C024_no_scatter_hardcoded_constants/typescript/symbol-based-analyzer.js +1 -1
- package/rules/common/C029_catch_block_logging/typescript/analyzer.js +1 -1
- package/rules/common/C040_centralized_validation/typescript/regex-based-analyzer.js +1 -1
- package/rules/common/C073_validate_required_config_on_startup/typescript/analyzer.js +1 -1
- package/rules/security/S042_require_re_authentication_for_long_lived/typescript/symbol-based-analyzer.js +9 -9
- package/rules/security/S043_password_changes_invalidate_all_sessions/typescript/symbol-based-analyzer.js +1 -1
- package/rules/security/S046_jwt_algorithm_allowlist/dart/analyzer.js +1 -1
- package/rules/security/S047_oauth_pkce_protection/dart/analyzer.js +1 -1
- package/rules/security/S050_reference_tokens_entropy/dart/analyzer.js +1 -1
- package/rules/security/S053_generic_error_messages/dart/analyzer.js +1 -1
- package/rules/security/S055_content_type_validation/typescript/symbol-based-analyzer.js +1 -1
- package/rules/security/S059_disable_debug_mode/dart/analyzer.js +1 -1
- package/rules/security/S060_password_minimum_length/dart/analyzer.js +1 -1
- package/rules/utils/severity-constants.js +0 -1
|
@@ -406,6 +406,7 @@ class CliActionHandler {
|
|
|
406
406
|
*/
|
|
407
407
|
getPresetName() {
|
|
408
408
|
if (this.options.all) return 'all preset';
|
|
409
|
+
if (this.options.specific) return 'language-specific preset';
|
|
409
410
|
if (this.options.quality) return 'quality preset';
|
|
410
411
|
if (this.options.security) return 'security preset';
|
|
411
412
|
if (this.options.category) return `${this.options.category} preset`;
|
|
@@ -505,12 +506,12 @@ class CliActionHandler {
|
|
|
505
506
|
handleExit(results) {
|
|
506
507
|
if (this.options.noExit) return;
|
|
507
508
|
|
|
508
|
-
//
|
|
509
|
-
const
|
|
510
|
-
result.violations
|
|
509
|
+
// Only fail on errors, not warnings (ESLint-compatible behavior)
|
|
510
|
+
const hasErrors = results.results?.some(result =>
|
|
511
|
+
result.violations?.some(v => v.severity === 'error')
|
|
511
512
|
);
|
|
512
513
|
|
|
513
|
-
if (
|
|
514
|
+
if (hasErrors && this.options.failOnViolations !== false) {
|
|
514
515
|
process.exit(1);
|
|
515
516
|
} else {
|
|
516
517
|
process.exit(0);
|
|
@@ -525,6 +526,7 @@ class CliActionHandler {
|
|
|
525
526
|
return this.options.architecture &&
|
|
526
527
|
!this.options.impact &&
|
|
527
528
|
!this.options.all &&
|
|
529
|
+
!this.options.specific &&
|
|
528
530
|
!this.options.rule &&
|
|
529
531
|
!this.options.rules &&
|
|
530
532
|
!this.options.quality &&
|
|
@@ -540,6 +542,7 @@ class CliActionHandler {
|
|
|
540
542
|
return this.options.impact &&
|
|
541
543
|
!this.options.architecture &&
|
|
542
544
|
!this.options.all &&
|
|
545
|
+
!this.options.specific &&
|
|
543
546
|
!this.options.rule &&
|
|
544
547
|
!this.options.rules &&
|
|
545
548
|
!this.options.quality &&
|
package/core/cli-program.js
CHANGED
|
@@ -22,6 +22,7 @@ function createCliProgram() {
|
|
|
22
22
|
.option('--rules <rules>', 'Run multiple rules (comma-separated)')
|
|
23
23
|
.option('-a, --all', 'Run all available rules')
|
|
24
24
|
.option('-c, --category <category>', 'Run rules by category (quality, security, logging, naming)')
|
|
25
|
+
.option('-s, --specific', 'Run language-specific rules (requires --languages)')
|
|
25
26
|
.option('--quality', 'Run all code quality rules')
|
|
26
27
|
.option('--security', 'Run all secure coding rules');
|
|
27
28
|
|
|
@@ -108,6 +109,12 @@ Examples:
|
|
|
108
109
|
sunlint --quality --input=src # Quality rules only
|
|
109
110
|
sunlint --security --input=src # Security rules only
|
|
110
111
|
|
|
112
|
+
Language-Specific:
|
|
113
|
+
sunlint --specific --languages=dart --input=lib # Dart rules only
|
|
114
|
+
sunlint --specific --languages=kotlin --input=app/src # Kotlin rules only
|
|
115
|
+
sunlint --specific --languages=dart,kotlin --input=. # Dart + Kotlin rules
|
|
116
|
+
sunlint --all --languages=dart --input=lib # All rules + Dart rules
|
|
117
|
+
|
|
111
118
|
Git Integration:
|
|
112
119
|
sunlint --all --changed-files --input=. # Changed files only
|
|
113
120
|
sunlint --all --staged-files --input=. # Staged files only
|
package/core/output-service.js
CHANGED
|
@@ -273,10 +273,10 @@ class OutputService {
|
|
|
273
273
|
if (result.ruleId) {
|
|
274
274
|
result.violations.forEach(violation => {
|
|
275
275
|
if (isValidViolation(violation)) {
|
|
276
|
-
allViolations.push(violation);
|
|
276
|
+
allViolations.push(violation);
|
|
277
277
|
}
|
|
278
278
|
});
|
|
279
|
-
}
|
|
279
|
+
}
|
|
280
280
|
// Handle file-based format (legacy)
|
|
281
281
|
else {
|
|
282
282
|
result.violations.forEach(violation => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
// temp debug
|
|
@@ -60,6 +60,48 @@ class RuleSelectionService {
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
/**
|
|
64
|
+
* Collect language-specific rules from ALL versions of released-rules.json
|
|
65
|
+
* Searches across all versions because the latest version may have empty categories
|
|
66
|
+
* @param {string[]} languages - Array of language names (e.g., ['dart', 'kotlin'])
|
|
67
|
+
* @returns {string[]} Array of rule IDs matching the specified languages
|
|
68
|
+
*/
|
|
69
|
+
collectLanguageSpecificRules(languages) {
|
|
70
|
+
const { getLanguageFromRuleId } = require('./constants/rules');
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
if (!fs.existsSync(this.releasedRulesPath)) {
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const data = JSON.parse(fs.readFileSync(this.releasedRulesPath, 'utf8'));
|
|
78
|
+
const versions = data.versions || [];
|
|
79
|
+
|
|
80
|
+
if (versions.length === 0) {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const targetLanguages = new Set(languages.map(l => l.toLowerCase()));
|
|
85
|
+
const matchingRuleIds = new Set();
|
|
86
|
+
|
|
87
|
+
for (const versionEntry of versions) {
|
|
88
|
+
const rulesByCategory = versionEntry.rulesByCategory || {};
|
|
89
|
+
for (const categoryRules of Object.values(rulesByCategory)) {
|
|
90
|
+
for (const ruleId of categoryRules) {
|
|
91
|
+
const ruleLanguage = getLanguageFromRuleId(ruleId);
|
|
92
|
+
if (ruleLanguage && targetLanguages.has(ruleLanguage)) {
|
|
93
|
+
matchingRuleIds.add(ruleId);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return Array.from(matchingRuleIds);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
return [];
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
63
105
|
async selectRules(config, options) {
|
|
64
106
|
// Ensure adapter is initialized
|
|
65
107
|
await this.initialize();
|
|
@@ -75,6 +117,22 @@ class RuleSelectionService {
|
|
|
75
117
|
selectedRules = [options.rule];
|
|
76
118
|
} else if (options.rules) {
|
|
77
119
|
selectedRules = options.rules.split(',').map(r => r.trim());
|
|
120
|
+
} else if (options.specific) {
|
|
121
|
+
// --specific requires --languages to determine which language rules to load
|
|
122
|
+
if (!options.languages) {
|
|
123
|
+
throw new Error(
|
|
124
|
+
chalk.red('--specific requires --languages to be specified\n') +
|
|
125
|
+
chalk.gray('Example: sunlint --specific --languages=dart --input=lib')
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
const targetLanguages = options.languages.split(',').map(l => l.trim());
|
|
129
|
+
selectedRules = this.collectLanguageSpecificRules(targetLanguages);
|
|
130
|
+
|
|
131
|
+
if (selectedRules.length === 0) {
|
|
132
|
+
console.warn(
|
|
133
|
+
chalk.yellow(`\u26A0\uFE0F No language-specific rules found for: ${targetLanguages.join(', ')}`)
|
|
134
|
+
);
|
|
135
|
+
}
|
|
78
136
|
} else if (options.all) {
|
|
79
137
|
// Load all rules from released-rules.json
|
|
80
138
|
if (releasedRules) {
|
|
@@ -89,6 +147,20 @@ class RuleSelectionService {
|
|
|
89
147
|
// Fallback to preset file
|
|
90
148
|
selectedRules = this.loadPresetRules('all');
|
|
91
149
|
}
|
|
150
|
+
|
|
151
|
+
// Augment with language-specific rules when --languages is specified
|
|
152
|
+
// This handles the case where the latest version has empty categories
|
|
153
|
+
if (options.languages) {
|
|
154
|
+
const targetLanguages = options.languages.split(',').map(l => l.trim());
|
|
155
|
+
const languageRules = this.collectLanguageSpecificRules(targetLanguages);
|
|
156
|
+
const existingRuleSet = new Set(selectedRules);
|
|
157
|
+
|
|
158
|
+
for (const ruleId of languageRules) {
|
|
159
|
+
if (!existingRuleSet.has(ruleId)) {
|
|
160
|
+
selectedRules.push(ruleId);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
92
164
|
} else if (options.quality) {
|
|
93
165
|
// Load Common rules from released-rules.json
|
|
94
166
|
if (releasedRules && releasedRules.Common) {
|
package/package.json
CHANGED
|
@@ -669,8 +669,7 @@ class SmartC006Analyzer {
|
|
|
669
669
|
|
|
670
670
|
getSeverityFromConfidence(confidence) {
|
|
671
671
|
if (confidence >= this.confidenceThresholds.HIGH) return 'warning';
|
|
672
|
-
|
|
673
|
-
return 'hint';
|
|
672
|
+
return 'warning';
|
|
674
673
|
}
|
|
675
674
|
}
|
|
676
675
|
|
|
@@ -299,7 +299,7 @@ class C017SymbolBasedAnalyzer {
|
|
|
299
299
|
|
|
300
300
|
violations.push({
|
|
301
301
|
ruleId: this.ruleId,
|
|
302
|
-
severity: '
|
|
302
|
+
severity: 'warning',
|
|
303
303
|
message: 'Constructor contains logging - side effects should be avoided',
|
|
304
304
|
source: this.ruleId,
|
|
305
305
|
file: filePath,
|
|
@@ -581,7 +581,7 @@ class C019SystemLogAnalyzer {
|
|
|
581
581
|
filePath: filePath,
|
|
582
582
|
line: logs[0].position.line,
|
|
583
583
|
column: logs[0].position.column,
|
|
584
|
-
severity: '
|
|
584
|
+
severity: 'warning',
|
|
585
585
|
category: 'performance',
|
|
586
586
|
confidence: this.config.overusedLogPatterns.hot_path_over_logging.confidence,
|
|
587
587
|
suggestion: this.config.overusedLogPatterns.hot_path_over_logging.suggestion,
|
|
@@ -697,7 +697,7 @@ class C019SystemLogAnalyzer {
|
|
|
697
697
|
filePath: filePath,
|
|
698
698
|
line: log2.position.line,
|
|
699
699
|
column: log2.position.column,
|
|
700
|
-
severity: '
|
|
700
|
+
severity: 'warning',
|
|
701
701
|
category: 'maintainability',
|
|
702
702
|
confidence: this.config.redundancyPatterns.duplicate_log_events.confidence,
|
|
703
703
|
suggestion: this.config.redundancyPatterns.duplicate_log_events.suggestion,
|
|
@@ -234,7 +234,7 @@ class C021TsMorphAnalyzer {
|
|
|
234
234
|
violations.push({
|
|
235
235
|
ruleId: this.ruleId,
|
|
236
236
|
message: `Import group "${misplacedGroup}" should come ${this.getOrderMessage(misplacedGroup, expectedFiltered, i)}`,
|
|
237
|
-
severity: '
|
|
237
|
+
severity: 'warning',
|
|
238
238
|
location: {
|
|
239
239
|
start: {
|
|
240
240
|
line: imports[0].line,
|
|
@@ -289,7 +289,7 @@ class C021TsMorphAnalyzer {
|
|
|
289
289
|
violations.push({
|
|
290
290
|
ruleId: this.ruleId,
|
|
291
291
|
message: `Import "${current}" should come after "${next}" (alphabetical order within ${groupName} group)`,
|
|
292
|
-
severity: '
|
|
292
|
+
severity: 'warning',
|
|
293
293
|
location: {
|
|
294
294
|
start: {
|
|
295
295
|
line: imports[i].line,
|
|
@@ -345,7 +345,7 @@ class C021TsMorphAnalyzer {
|
|
|
345
345
|
violations.push({
|
|
346
346
|
ruleId: this.ruleId,
|
|
347
347
|
message: `Missing blank line between "${nonEmptyGroups[i]}" and "${nonEmptyGroups[i + 1]}" import groups`,
|
|
348
|
-
severity: '
|
|
348
|
+
severity: 'warning',
|
|
349
349
|
location: {
|
|
350
350
|
start: {
|
|
351
351
|
line: lastLine,
|
package/rules/common/C024_no_scatter_hardcoded_constants/typescript/symbol-based-analyzer.js
CHANGED
|
@@ -388,7 +388,7 @@ class C024SymbolBasedAnalyzer {
|
|
|
388
388
|
createViolation(node, sourceFile, message, type, value) {
|
|
389
389
|
return {
|
|
390
390
|
ruleId: this.ruleId,
|
|
391
|
-
severity: '
|
|
391
|
+
severity: 'warning',
|
|
392
392
|
message: message,
|
|
393
393
|
source: this.ruleId,
|
|
394
394
|
file: sourceFile.getFilePath(),
|
|
@@ -114,7 +114,7 @@ class C040RegexBasedAnalyzer {
|
|
|
114
114
|
if (duplicatePatterns.length > 0) {
|
|
115
115
|
violations.push({
|
|
116
116
|
ruleId: this.ruleId,
|
|
117
|
-
severity: '
|
|
117
|
+
severity: 'warning',
|
|
118
118
|
message: `Found potentially duplicate validation patterns: ${duplicatePatterns.join(', ')}`,
|
|
119
119
|
file: filePath,
|
|
120
120
|
line: validationMatches[0].line,
|
|
@@ -714,7 +714,7 @@ class C073ConfigValidationAnalyzer {
|
|
|
714
714
|
if (isServiceFile && analysis.envAccess && analysis.envAccess.length > 0) {
|
|
715
715
|
violations.push({
|
|
716
716
|
ruleId: 'C073',
|
|
717
|
-
severity: '
|
|
717
|
+
severity: 'warning',
|
|
718
718
|
message: 'Service layer accessing environment variables directly. Consider using dependency injection for configuration.',
|
|
719
719
|
line: analysis.envAccess[0].line || 1,
|
|
720
720
|
column: 1,
|
|
@@ -173,7 +173,7 @@ class S042SymbolBasedAnalyzer {
|
|
|
173
173
|
violations.push({
|
|
174
174
|
ruleId: this.ruleId,
|
|
175
175
|
ruleName: this.ruleName,
|
|
176
|
-
severity: '
|
|
176
|
+
severity: 'warning',
|
|
177
177
|
message: `Session maxAge is too long (${this.formatDuration(sessionConfig.maxAge)}). Consider limiting to 24 hours or less for security.`,
|
|
178
178
|
line: startLine,
|
|
179
179
|
column: node.getStart() - node.getStartLinePos() + 1,
|
|
@@ -191,7 +191,7 @@ class S042SymbolBasedAnalyzer {
|
|
|
191
191
|
violations.push({
|
|
192
192
|
ruleId: this.ruleId,
|
|
193
193
|
ruleName: this.ruleName,
|
|
194
|
-
severity: '
|
|
194
|
+
severity: 'warning',
|
|
195
195
|
message: `Persistent session (Remember Me) enabled without re-authentication requirements for sensitive actions.`,
|
|
196
196
|
line: startLine,
|
|
197
197
|
column: node.getStart() - node.getStartLinePos() + 1,
|
|
@@ -209,7 +209,7 @@ class S042SymbolBasedAnalyzer {
|
|
|
209
209
|
violations.push({
|
|
210
210
|
ruleId: this.ruleId,
|
|
211
211
|
ruleName: this.ruleName,
|
|
212
|
-
severity: '
|
|
212
|
+
severity: 'warning',
|
|
213
213
|
message: `Session configuration missing idle timeout. Consider adding inactivity-based expiration.`,
|
|
214
214
|
line: startLine,
|
|
215
215
|
column: node.getStart() - node.getStartLinePos() + 1,
|
|
@@ -420,7 +420,7 @@ class S042SymbolBasedAnalyzer {
|
|
|
420
420
|
violations.push({
|
|
421
421
|
ruleId: this.ruleId,
|
|
422
422
|
ruleName: this.ruleName,
|
|
423
|
-
severity: '
|
|
423
|
+
severity: 'error',
|
|
424
424
|
message: `JWT token created without expiration configuration. Add expiresIn option.`,
|
|
425
425
|
line: startLine,
|
|
426
426
|
column: callExpr.getStart() - callExpr.getStartLinePos() + 1,
|
|
@@ -442,7 +442,7 @@ class S042SymbolBasedAnalyzer {
|
|
|
442
442
|
violations.push({
|
|
443
443
|
ruleId: this.ruleId,
|
|
444
444
|
ruleName: this.ruleName,
|
|
445
|
-
severity: '
|
|
445
|
+
severity: 'error',
|
|
446
446
|
message: `JWT token created without expiration time. Tokens should have short expiry (e.g., 1 hour).`,
|
|
447
447
|
line: startLine,
|
|
448
448
|
column: callExpr.getStart() - callExpr.getStartLinePos() + 1,
|
|
@@ -463,7 +463,7 @@ class S042SymbolBasedAnalyzer {
|
|
|
463
463
|
violations.push({
|
|
464
464
|
ruleId: this.ruleId,
|
|
465
465
|
ruleName: this.ruleName,
|
|
466
|
-
severity: '
|
|
466
|
+
severity: 'warning',
|
|
467
467
|
message: `JWT expiration is too long (${this.formatDuration(jwtConfig.expirationValue)}). Access tokens should expire within 1 hour.`,
|
|
468
468
|
line: startLine,
|
|
469
469
|
column: jwtConfig.expirationNode.getStart() - jwtConfig.expirationNode.getStartLinePos() + 1,
|
|
@@ -613,7 +613,7 @@ class S042SymbolBasedAnalyzer {
|
|
|
613
613
|
violations.push({
|
|
614
614
|
ruleId: this.ruleId,
|
|
615
615
|
ruleName: this.ruleName,
|
|
616
|
-
severity: '
|
|
616
|
+
severity: 'error',
|
|
617
617
|
message: `Sensitive action '${funcName}' does not appear to require re-authentication.`,
|
|
618
618
|
line: startLine,
|
|
619
619
|
column: func.getStart() - func.getStartLinePos() + 1,
|
|
@@ -657,7 +657,7 @@ class S042SymbolBasedAnalyzer {
|
|
|
657
657
|
violations.push({
|
|
658
658
|
ruleId: this.ruleId,
|
|
659
659
|
ruleName: this.ruleName,
|
|
660
|
-
severity: '
|
|
660
|
+
severity: 'warning',
|
|
661
661
|
message: `Remember Me feature implemented without apparent re-authentication requirements.`,
|
|
662
662
|
line: startLine,
|
|
663
663
|
column: node.getStart() - node.getStartLinePos() + 1,
|
|
@@ -687,7 +687,7 @@ class S042SymbolBasedAnalyzer {
|
|
|
687
687
|
violations.push({
|
|
688
688
|
ruleId: this.ruleId,
|
|
689
689
|
ruleName: this.ruleName,
|
|
690
|
-
severity: '
|
|
690
|
+
severity: 'warning',
|
|
691
691
|
message: `Session management detected but no idle timeout implementation found.`,
|
|
692
692
|
line: sessionConfigLocation.line,
|
|
693
693
|
column: sessionConfigLocation.column,
|
|
@@ -523,7 +523,7 @@ class S043SymbolBasedAnalyzer {
|
|
|
523
523
|
violations.push({
|
|
524
524
|
ruleId: this.ruleId,
|
|
525
525
|
ruleName: this.ruleName,
|
|
526
|
-
severity: '
|
|
526
|
+
severity: 'warning',
|
|
527
527
|
message: `Password change function "${functionName}" must invalidate all active sessions`,
|
|
528
528
|
line: startLine,
|
|
529
529
|
column: column,
|
|
@@ -389,7 +389,7 @@ class S055SymbolBasedAnalyzer {
|
|
|
389
389
|
violations.push({
|
|
390
390
|
ruleId: this.ruleId,
|
|
391
391
|
ruleName: this.ruleName,
|
|
392
|
-
severity: '
|
|
392
|
+
severity: 'warning',
|
|
393
393
|
message: `REST endpoint '${name}' does not validate Content-Type header. Add validation for 'application/json' or other expected types.`,
|
|
394
394
|
line: startLine,
|
|
395
395
|
column: node.getStart() - node.getStartLinePos() + 1,
|