@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.
@@ -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
- if (!files || !Array.isArray(files)) {
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 => this.minimatch(filePath, 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 with score > 0 (passed + failed) to report
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 (from config)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sun-asterisk/sunlint",
3
- "version": "1.3.52",
3
+ "version": "1.3.54",
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, '');