@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.
@@ -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, violations] of violationsByFile) {
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sun-asterisk/sunlint",
3
- "version": "1.3.53",
3
+ "version": "1.3.55",
4
4
  "description": "☀️ SunLint - Multi-language static analysis tool for code quality and security | Sun* Engineering Standards",
5
5
  "main": "cli.js",
6
6
  "bin": {
@@ -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, '');