@sun-asterisk/sunlint 1.3.53 → 1.3.55
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/engines/heuristic-engine.js +126 -1
- 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 &&
|
|
@@ -999,7 +999,11 @@ class HeuristicEngine extends AnalysisEngineInterface {
|
|
|
999
999
|
// Group violations by file
|
|
1000
1000
|
const violationsByFile = this.groupViolationsByFile(ruleViolations);
|
|
1001
1001
|
|
|
1002
|
-
for (const [filePath,
|
|
1002
|
+
for (const [filePath, fileViolations] of violationsByFile) {
|
|
1003
|
+
|
|
1004
|
+
// Filter violations suppressed by inline disable comments
|
|
1005
|
+
const violations = this.filterViolationsByInlineDisable(filePath, fileViolations);
|
|
1006
|
+
if (violations.length === 0) continue;
|
|
1003
1007
|
|
|
1004
1008
|
// Apply per-file config overrides — skip violations disabled by overrides
|
|
1005
1009
|
let overriddenSeverity = null;
|
|
@@ -1343,6 +1347,127 @@ class HeuristicEngine extends AnalysisEngineInterface {
|
|
|
1343
1347
|
return groups;
|
|
1344
1348
|
}
|
|
1345
1349
|
|
|
1350
|
+
/**
|
|
1351
|
+
* Filter violations suppressed by inline disable comments
|
|
1352
|
+
* Supports: sunlint-disable-next-line <ruleId>, sunlint-disable <ruleId>,
|
|
1353
|
+
* sunlint-disable-file <ruleId>, sunlint-disable (all rules)
|
|
1354
|
+
* Following Rule C006: Verb-noun naming
|
|
1355
|
+
* @param {string} filePath - Path to the source file
|
|
1356
|
+
* @param {Object[]} violations - Violations found in the file
|
|
1357
|
+
* @returns {Object[]} Filtered violations (without suppressed ones)
|
|
1358
|
+
*/
|
|
1359
|
+
filterViolationsByInlineDisable(filePath, violations) {
|
|
1360
|
+
if (!violations || violations.length === 0) return violations;
|
|
1361
|
+
|
|
1362
|
+
let lines;
|
|
1363
|
+
try {
|
|
1364
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
1365
|
+
lines = content.split('\n');
|
|
1366
|
+
} catch {
|
|
1367
|
+
return violations;
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
// Pre-scan for file-level disables and block-level disable/enable ranges
|
|
1371
|
+
const fileDisabledRules = new Set();
|
|
1372
|
+
const disableRanges = []; // { ruleId: string|null, startLine: number, endLine: number }
|
|
1373
|
+
let activeDisables = new Map(); // ruleId|'*' → startLineNumber
|
|
1374
|
+
|
|
1375
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1376
|
+
const line = lines[i].trim();
|
|
1377
|
+
|
|
1378
|
+
// File-level disable: sunlint-disable-file or sunlint-disable-file S004
|
|
1379
|
+
const fileDisableMatch = line.match(/(?:\/\/|\/\*)\s*sunlint-disable-file(?:\s+([\w,\s]+))?\s*(?:\*\/)?/);
|
|
1380
|
+
if (fileDisableMatch) {
|
|
1381
|
+
const ruleIds = fileDisableMatch[1] ? fileDisableMatch[1].split(',').map(r => r.trim()) : [null];
|
|
1382
|
+
for (const ruleId of ruleIds) {
|
|
1383
|
+
fileDisabledRules.add(ruleId); // null means all rules
|
|
1384
|
+
}
|
|
1385
|
+
continue;
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
// Block-level disable: sunlint-disable or sunlint-disable S004
|
|
1389
|
+
const blockDisableMatch = line.match(/(?:\/\/|\/\*)\s*sunlint-disable(?!\s*-(?:next-line|file))(?:\s+([\w,\s]+))?\s*(?:\*\/)?/);
|
|
1390
|
+
if (blockDisableMatch) {
|
|
1391
|
+
const ruleIds = blockDisableMatch[1] ? blockDisableMatch[1].split(',').map(r => r.trim()) : ['*'];
|
|
1392
|
+
for (const ruleId of ruleIds) {
|
|
1393
|
+
if (!activeDisables.has(ruleId)) {
|
|
1394
|
+
activeDisables.set(ruleId, i + 1); // 1-based line number
|
|
1395
|
+
}
|
|
1396
|
+
}
|
|
1397
|
+
continue;
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
// Block-level enable: sunlint-enable or sunlint-enable S004
|
|
1401
|
+
const blockEnableMatch = line.match(/(?:\/\/|\/\*)\s*sunlint-enable(?:\s+([\w,\s]+))?\s*(?:\*\/)?/);
|
|
1402
|
+
if (blockEnableMatch) {
|
|
1403
|
+
const ruleIds = blockEnableMatch[1] ? blockEnableMatch[1].split(',').map(r => r.trim()) : ['*'];
|
|
1404
|
+
for (const ruleId of ruleIds) {
|
|
1405
|
+
if (activeDisables.has(ruleId)) {
|
|
1406
|
+
disableRanges.push({
|
|
1407
|
+
ruleId: ruleId === '*' ? null : ruleId,
|
|
1408
|
+
startLine: activeDisables.get(ruleId),
|
|
1409
|
+
endLine: i + 1
|
|
1410
|
+
});
|
|
1411
|
+
activeDisables.delete(ruleId);
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
continue;
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
// Close any unclosed disable blocks (extend to end of file)
|
|
1419
|
+
for (const [ruleId, startLine] of activeDisables) {
|
|
1420
|
+
disableRanges.push({
|
|
1421
|
+
ruleId: ruleId === '*' ? null : ruleId,
|
|
1422
|
+
startLine,
|
|
1423
|
+
endLine: lines.length
|
|
1424
|
+
});
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
return violations.filter(violation => {
|
|
1428
|
+
const ruleId = violation.ruleId;
|
|
1429
|
+
const violationLine = violation.line;
|
|
1430
|
+
|
|
1431
|
+
// Check file-level disable
|
|
1432
|
+
if (fileDisabledRules.has(null) || fileDisabledRules.has(ruleId)) {
|
|
1433
|
+
return false;
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
// Check block-level disable ranges
|
|
1437
|
+
for (const range of disableRanges) {
|
|
1438
|
+
if (violationLine >= range.startLine && violationLine <= range.endLine) {
|
|
1439
|
+
if (range.ruleId === null || range.ruleId === ruleId) {
|
|
1440
|
+
return false;
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
// Check next-line directive (comment on previous line)
|
|
1446
|
+
if (violationLine > 1) {
|
|
1447
|
+
const prevLine = lines[violationLine - 2]?.trim() || '';
|
|
1448
|
+
const nextLinePattern = /(?:\/\/|\/\*)\s*sunlint-disable-next-line(?:\s+([\w,\s]+))?\s*(?:\*\/)?/;
|
|
1449
|
+
const nextLineMatch = prevLine.match(nextLinePattern);
|
|
1450
|
+
if (nextLineMatch) {
|
|
1451
|
+
if (!nextLineMatch[1]) return false; // No rule specified = disable all
|
|
1452
|
+
const disabledRules = nextLineMatch[1].split(',').map(r => r.trim());
|
|
1453
|
+
if (disabledRules.includes(ruleId)) return false;
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
// Check same-line disable (comment on the violation line itself)
|
|
1458
|
+
const currentLine = lines[violationLine - 1] || '';
|
|
1459
|
+
const sameLinePattern = /(?:\/\/|\/\*)\s*sunlint-disable(?:-next-line)?(?:\s+([\w,\s]+))?\s*(?:\*\/)?/;
|
|
1460
|
+
const sameLineMatch = currentLine.match(sameLinePattern);
|
|
1461
|
+
if (sameLineMatch) {
|
|
1462
|
+
if (!sameLineMatch[1]) return false;
|
|
1463
|
+
const disabledRules = sameLineMatch[1].split(',').map(r => r.trim());
|
|
1464
|
+
if (disabledRules.includes(ruleId)) return false;
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
return true;
|
|
1468
|
+
});
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1346
1471
|
/**
|
|
1347
1472
|
* Get supported rules
|
|
1348
1473
|
* Following Rule C006: Verb-noun naming
|
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, '');
|