@sun-asterisk/sunlint 1.3.52 → 1.3.54
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 +21 -4
- package/core/config-override-processor.js +19 -4
- package/core/summary-report-service.js +1 -2
- package/engines/heuristic-engine.js +22 -2
- package/package.json +1 -1
- package/rules/common/C024_no_scatter_hardcoded_constants/typescript/symbol-based-analyzer.js +36 -0
- package/rules/common/C041_no_sensitive_hardcode/typescript/symbol-based-analyzer.js +12 -0
- package/rules/common/C042_boolean_name_prefix/typescript/analyzer.js +27 -2
|
@@ -72,7 +72,7 @@ class CliActionHandler {
|
|
|
72
72
|
|
|
73
73
|
// Determine if we should proceed based on requested analyses
|
|
74
74
|
const hasSourceFiles = targetingResult.files.length > 0;
|
|
75
|
-
const willRunCodeQuality = rulesToRun.length > 0 && !this.isArchitectureOnly() && !this.isImpactOnly();
|
|
75
|
+
const willRunCodeQuality = rulesToRun.length > 0 && !this.isArchitectureOnly(rulesToRun) && !this.isImpactOnly(rulesToRun);
|
|
76
76
|
const willRunArchitecture = !!config.architecture?.enabled;
|
|
77
77
|
const willRunImpact = !!(this.options.impact || config.impact?.enabled);
|
|
78
78
|
|
|
@@ -93,7 +93,7 @@ class CliActionHandler {
|
|
|
93
93
|
let results = null;
|
|
94
94
|
|
|
95
95
|
// Run code quality analysis (unless --architecture or --impact is used alone)
|
|
96
|
-
if (rulesToRun.length > 0 && !this.isArchitectureOnly() && !this.isImpactOnly()) {
|
|
96
|
+
if (rulesToRun.length > 0 && !this.isArchitectureOnly(rulesToRun) && !this.isImpactOnly(rulesToRun)) {
|
|
97
97
|
results = await this.runModernAnalysis(rulesToRun, targetingResult.files, config);
|
|
98
98
|
} else {
|
|
99
99
|
results = { results: [], summary: { total: 0, errors: 0, warnings: 0 } };
|
|
@@ -530,12 +530,16 @@ class CliActionHandler {
|
|
|
530
530
|
* Check if only architecture analysis was requested (no code quality rules)
|
|
531
531
|
* Following Rule C006: Verb-noun naming
|
|
532
532
|
*/
|
|
533
|
-
isArchitectureOnly() {
|
|
533
|
+
isArchitectureOnly(rulesToRun = []) {
|
|
534
534
|
const isArchEnabled = this.options.architecture || this.loadedConfig?.architecture?.enabled;
|
|
535
535
|
const isImpactEnabled = this.options.impact || this.loadedConfig?.impact?.enabled;
|
|
536
536
|
|
|
537
|
+
// If rules were selected via config (extends/rules), this is not architecture-only
|
|
538
|
+
const hasConfigBasedRules = rulesToRun.length > 0 && this.hasConfigBasedRuleSelection();
|
|
539
|
+
|
|
537
540
|
return isArchEnabled &&
|
|
538
541
|
!isImpactEnabled &&
|
|
542
|
+
!hasConfigBasedRules &&
|
|
539
543
|
!this.options.all &&
|
|
540
544
|
!this.options.specific &&
|
|
541
545
|
!this.options.rule &&
|
|
@@ -545,16 +549,29 @@ class CliActionHandler {
|
|
|
545
549
|
!this.options.category;
|
|
546
550
|
}
|
|
547
551
|
|
|
552
|
+
/**
|
|
553
|
+
* Check if rules were explicitly configured via config file (extends or rules field)
|
|
554
|
+
* Following Rule C006: Verb-noun naming
|
|
555
|
+
*/
|
|
556
|
+
hasConfigBasedRuleSelection() {
|
|
557
|
+
const config = this.loadedConfig || {};
|
|
558
|
+
return !!(config.extends || (config.rules && Object.keys(config.rules).length > 0));
|
|
559
|
+
}
|
|
560
|
+
|
|
548
561
|
/**
|
|
549
562
|
* Check if only impact analysis was requested (no code quality rules)
|
|
550
563
|
* Following Rule C006: Verb-noun naming
|
|
551
564
|
*/
|
|
552
|
-
isImpactOnly() {
|
|
565
|
+
isImpactOnly(rulesToRun = []) {
|
|
553
566
|
const isArchEnabled = this.options.architecture || this.loadedConfig?.architecture?.enabled;
|
|
554
567
|
const isImpactEnabled = this.options.impact || this.loadedConfig?.impact?.enabled;
|
|
555
568
|
|
|
569
|
+
// If rules were selected via config (extends/rules), this is not impact-only
|
|
570
|
+
const hasConfigBasedRules = rulesToRun.length > 0 && this.hasConfigBasedRuleSelection();
|
|
571
|
+
|
|
556
572
|
return isImpactEnabled &&
|
|
557
573
|
!isArchEnabled &&
|
|
574
|
+
!hasConfigBasedRules &&
|
|
558
575
|
!this.options.all &&
|
|
559
576
|
!this.options.specific &&
|
|
560
577
|
!this.options.rule &&
|
|
@@ -7,7 +7,7 @@ class ConfigOverrideProcessor {
|
|
|
7
7
|
|
|
8
8
|
constructor() {
|
|
9
9
|
// Rule C014: Dependency injection for minimatch
|
|
10
|
-
this.minimatch = require('minimatch');
|
|
10
|
+
this.minimatch = require('minimatch').minimatch;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
/**
|
|
@@ -37,12 +37,27 @@ class ConfigOverrideProcessor {
|
|
|
37
37
|
*/
|
|
38
38
|
shouldApplyOverride(override, filePath) {
|
|
39
39
|
const { files } = override;
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
|
|
41
|
+
// No files pattern means the override applies to all files
|
|
42
|
+
if (!files) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!Array.isArray(files)) {
|
|
42
47
|
return false;
|
|
43
48
|
}
|
|
44
49
|
|
|
45
|
-
return files.some(pattern =>
|
|
50
|
+
return files.some(pattern => {
|
|
51
|
+
// Direct match (works when filePath is relative or pattern is absolute)
|
|
52
|
+
if (this.minimatch(filePath, pattern, { dot: true })) {
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
// For relative patterns, also try with **/ prefix so they match absolute paths
|
|
56
|
+
if (!pattern.startsWith('/')) {
|
|
57
|
+
return this.minimatch(filePath, '**/' + pattern, { dot: true });
|
|
58
|
+
}
|
|
59
|
+
return false;
|
|
60
|
+
});
|
|
46
61
|
}
|
|
47
62
|
|
|
48
63
|
/**
|
|
@@ -315,10 +315,9 @@ class SummaryReportService {
|
|
|
315
315
|
analysis_time_ms: archSummary.analysisTime || 0
|
|
316
316
|
};
|
|
317
317
|
|
|
318
|
-
// Add all rules
|
|
318
|
+
// Add all checked rules (passed + failed) to report
|
|
319
319
|
if (options.architecture.allRules && options.architecture.allRules.length > 0) {
|
|
320
320
|
summaryReport.architecture.rules = options.architecture.allRules
|
|
321
|
-
.filter(r => r.score > 0) // Only include rules with evidence (score > 0)
|
|
322
321
|
.map(r => ({
|
|
323
322
|
rule_code: r.ruleId,
|
|
324
323
|
rule_name: r.ruleName,
|
|
@@ -9,6 +9,7 @@ const AnalysisEngineInterface = require('../core/interfaces/analysis-engine.inte
|
|
|
9
9
|
const ASTModuleRegistry = require('../core/ast-modules/index');
|
|
10
10
|
const dependencyChecker = require('../core/dependency-checker');
|
|
11
11
|
const SunlintRuleAdapter = require('../core/adapters/sunlint-rule-adapter');
|
|
12
|
+
const ConfigOverrideProcessor = require('../core/config-override-processor');
|
|
12
13
|
const SemanticEngine = require('../core/semantic-engine');
|
|
13
14
|
const SemanticRuleBase = require('../core/semantic-rule-base');
|
|
14
15
|
const { getInstance: getUnifiedRegistry } = require('../core/unified-rule-registry');
|
|
@@ -76,6 +77,9 @@ class HeuristicEngine extends AnalysisEngineInterface {
|
|
|
76
77
|
// Unified rule registry
|
|
77
78
|
this.unifiedRegistry = getUnifiedRegistry();
|
|
78
79
|
|
|
80
|
+
// Rule C014: Dependency injection for per-file override processing
|
|
81
|
+
this.overrideProcessor = new ConfigOverrideProcessor();
|
|
82
|
+
|
|
79
83
|
// ✅ PERFORMANCE OPTIMIZATIONS (Integrated)
|
|
80
84
|
this.performanceManager = new AutoPerformanceManager();
|
|
81
85
|
this.performanceConfig = null;
|
|
@@ -996,16 +1000,32 @@ class HeuristicEngine extends AnalysisEngineInterface {
|
|
|
996
1000
|
const violationsByFile = this.groupViolationsByFile(ruleViolations);
|
|
997
1001
|
|
|
998
1002
|
for (const [filePath, violations] of violationsByFile) {
|
|
1003
|
+
|
|
1004
|
+
// Apply per-file config overrides — skip violations disabled by overrides
|
|
1005
|
+
let overriddenSeverity = null;
|
|
1006
|
+
if (options.config?.overrides?.length > 0) {
|
|
1007
|
+
const effectiveConfig = this.overrideProcessor.applyFileOverrides(options.config, filePath);
|
|
1008
|
+
const overrideValue = effectiveConfig.rules?.[rule.id];
|
|
1009
|
+
if (overrideValue === 'off') {
|
|
1010
|
+
continue;
|
|
1011
|
+
}
|
|
1012
|
+
if (overrideValue === 'error') {
|
|
1013
|
+
overriddenSeverity = 'error';
|
|
1014
|
+
} else if (overrideValue === 'warn' || overrideValue === 'warning') {
|
|
1015
|
+
overriddenSeverity = 'warning';
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
|
|
999
1019
|
// Find or create file result
|
|
1000
1020
|
let fileResult = results.results.find(r => r.file === filePath);
|
|
1001
1021
|
if (!fileResult) {
|
|
1002
1022
|
fileResult = { file: filePath, violations: [] };
|
|
1003
1023
|
results.results.push(fileResult);
|
|
1004
1024
|
}
|
|
1005
|
-
// Apply rule severity to each violation (
|
|
1025
|
+
// Apply rule severity to each violation (override takes precedence over rule default)
|
|
1006
1026
|
const violationsWithSeverity = violations.map(v => ({
|
|
1007
1027
|
...v,
|
|
1008
|
-
severity: rule.severity || v.severity || 'warning'
|
|
1028
|
+
severity: overriddenSeverity || rule.severity || v.severity || 'warning'
|
|
1009
1029
|
}));
|
|
1010
1030
|
fileResult.violations.push(...violationsWithSeverity);
|
|
1011
1031
|
}
|
package/package.json
CHANGED
package/rules/common/C024_no_scatter_hardcoded_constants/typescript/symbol-based-analyzer.js
CHANGED
|
@@ -480,6 +480,11 @@ class C024SymbolBasedAnalyzer {
|
|
|
480
480
|
return;
|
|
481
481
|
}
|
|
482
482
|
|
|
483
|
+
// Skip strings inside throw/new Error() - error messages don't need to be constants
|
|
484
|
+
if (this.isInThrowOrErrorContext(literal)) {
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
483
488
|
this.trackConstant(constantUsage, `string:${value}`, literal);
|
|
484
489
|
|
|
485
490
|
if (this.isInLogicContext(literal) || this.isInComparison(literal)) {
|
|
@@ -1122,6 +1127,37 @@ class C024SymbolBasedAnalyzer {
|
|
|
1122
1127
|
return false;
|
|
1123
1128
|
}
|
|
1124
1129
|
|
|
1130
|
+
isInThrowOrErrorContext(node) {
|
|
1131
|
+
let parent = node.getParent();
|
|
1132
|
+
let depth = 0;
|
|
1133
|
+
const maxDepth = 5;
|
|
1134
|
+
|
|
1135
|
+
while (parent && depth < maxDepth) {
|
|
1136
|
+
const kind = parent.getKind();
|
|
1137
|
+
|
|
1138
|
+
// Direct: throw new Error("message")
|
|
1139
|
+
if (kind === SyntaxKind.ThrowStatement) {
|
|
1140
|
+
return true;
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
// new Error("message") or new CustomError("message")
|
|
1144
|
+
if (kind === SyntaxKind.NewExpression) {
|
|
1145
|
+
const expression = parent.getExpression();
|
|
1146
|
+
if (expression) {
|
|
1147
|
+
const text = expression.getText();
|
|
1148
|
+
if (/Error$/.test(text) || /Exception$/.test(text)) {
|
|
1149
|
+
return true;
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
parent = parent.getParent();
|
|
1155
|
+
depth++;
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
return false;
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1125
1161
|
trackConstant(constantUsage, key, node) {
|
|
1126
1162
|
if (!constantUsage.has(key)) {
|
|
1127
1163
|
constantUsage.set(key, []);
|
|
@@ -153,6 +153,13 @@ class C041SymbolBasedAnalyzer {
|
|
|
153
153
|
/^[A-Z_]+CODE[A-Z_]*$/, // ERROR_CODE, STATUS_CODE
|
|
154
154
|
/^[A-Z_]+STATUS[A-Z_]*$/, // USER_STATUS
|
|
155
155
|
/^[A-Z_]+TYPE[A-Z_]*$/, // MESSAGE_TYPE
|
|
156
|
+
/^[A-Z_]+NAME[A-Z_]*$/, // REFRESH_TOKEN_COOKIE_NAME, SESSION_TOKEN_HEADER_NAME
|
|
157
|
+
/^[A-Z_]+KEY[A-Z_]*_NAME$/, // API_KEY_PARAM_NAME
|
|
158
|
+
/^[A-Z_]+HEADER[A-Z_]*$/, // AUTH_TOKEN_HEADER, CSRF_TOKEN_HEADER
|
|
159
|
+
/^[A-Z_]+COOKIE[A-Z_]*$/, // SESSION_TOKEN_COOKIE, REFRESH_TOKEN_COOKIE
|
|
160
|
+
/^[A-Z_]+LABEL[A-Z_]*$/, // TOKEN_LABEL, PASSWORD_LABEL
|
|
161
|
+
/^[A-Z_]+FIELD[A-Z_]*$/, // PASSWORD_FIELD, TOKEN_FIELD
|
|
162
|
+
/^[A-Z_]+PARAM[A-Z_]*$/, // API_KEY_PARAM, TOKEN_PARAM
|
|
156
163
|
];
|
|
157
164
|
|
|
158
165
|
// Parent object names that contain service/activity mappings (not secrets)
|
|
@@ -300,6 +307,11 @@ class C041SymbolBasedAnalyzer {
|
|
|
300
307
|
const isSensitiveName = this.sensitiveVariableNames.some(pattern => pattern.test(name));
|
|
301
308
|
|
|
302
309
|
if (isSensitiveName) {
|
|
310
|
+
// Skip if the variable name is a naming/labeling constant (e.g., REFRESH_TOKEN_COOKIE_NAME)
|
|
311
|
+
if (this.isErrorMessageConstant(name)) {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
|
|
303
315
|
const initText = initializer.getText();
|
|
304
316
|
|
|
305
317
|
// Skip if using env variables or config
|
|
@@ -171,12 +171,18 @@ class C042Analyzer {
|
|
|
171
171
|
|
|
172
172
|
isBooleanValue(value) {
|
|
173
173
|
const trimmedValue = value.trim();
|
|
174
|
-
|
|
174
|
+
|
|
175
175
|
// Direct boolean literals
|
|
176
176
|
if (trimmedValue === 'true' || trimmedValue === 'false') {
|
|
177
177
|
return true;
|
|
178
178
|
}
|
|
179
|
-
|
|
179
|
+
|
|
180
|
+
// Ternary expressions: the result type depends on the branches, not the condition
|
|
181
|
+
// e.g., `typeof value === "string" ? value : JSON.stringify(value)` is NOT boolean
|
|
182
|
+
if (this.isTernaryExpression(trimmedValue)) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
|
|
180
186
|
// Boolean expressions that clearly result in boolean
|
|
181
187
|
const booleanExpressions = [
|
|
182
188
|
/\w+\s*[<>!=]=/, // Comparisons
|
|
@@ -283,6 +289,25 @@ class C042Analyzer {
|
|
|
283
289
|
return false;
|
|
284
290
|
}
|
|
285
291
|
|
|
292
|
+
isTernaryExpression(value) {
|
|
293
|
+
// Detect ternary operator: condition ? trueValue : falseValue
|
|
294
|
+
// Must account for nested ternaries and template literals with colons
|
|
295
|
+
const questionIndex = value.indexOf('?');
|
|
296
|
+
if (questionIndex === -1) return false;
|
|
297
|
+
|
|
298
|
+
// Check there's a colon after the question mark (outside of template literals)
|
|
299
|
+
const afterQuestion = value.slice(questionIndex + 1);
|
|
300
|
+
// Simple heuristic: contains `:` that's not inside a template literal or object
|
|
301
|
+
let depth = 0;
|
|
302
|
+
for (let i = 0; i < afterQuestion.length; i++) {
|
|
303
|
+
const ch = afterQuestion[i];
|
|
304
|
+
if (ch === '(' || ch === '[' || ch === '{') depth++;
|
|
305
|
+
else if (ch === ')' || ch === ']' || ch === '}') depth--;
|
|
306
|
+
else if (ch === ':' && depth === 0) return true;
|
|
307
|
+
}
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
|
|
286
311
|
generateSuggestions(varName) {
|
|
287
312
|
const suggestions = [];
|
|
288
313
|
const baseName = varName.replace(/^(is|has|should|can|will|must|may|check)/i, '');
|