@sun-asterisk/sunlint 1.3.30 → 1.3.31

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sun-asterisk/sunlint",
3
- "version": "1.3.30",
3
+ "version": "1.3.31",
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": {
@@ -45,11 +45,92 @@ class SmartC006Analyzer {
45
45
  // Side effect patterns indicate actions
46
46
  ACTION: [/console\./, /fetch\(/, /\.send\(/, /\.post\(/]
47
47
  };
48
+
49
+ // 📚 COMPREHENSIVE VERB LIST (from C006 rule specification)
50
+ this.acceptedVerbs = [
51
+ // Getters/Queries
52
+ 'get', 'fetch', 'retrieve', 'find', 'search', 'query', 'load',
53
+ // Setters/Modifiers
54
+ 'set', 'update', 'modify', 'change', 'edit', 'alter', 'transform',
55
+ // Creation
56
+ 'create', 'build', 'make', 'generate', 'construct', 'produce',
57
+ // Deletion
58
+ 'delete', 'remove', 'destroy', 'clean', 'clear', 'reset',
59
+ // Validation
60
+ 'validate', 'verify', 'check', 'confirm', 'ensure', 'test', 'compare',
61
+ // Computation
62
+ 'calculate', 'compute', 'parse', 'format', 'convert',
63
+ // Communication
64
+ 'send', 'receive', 'transmit', 'broadcast', 'emit', 'publish',
65
+ // Collections
66
+ 'map', 'filter', 'sort', 'group', 'merge', 'split', 'add', 'append', 'insert',
67
+ 'push', 'pop', 'shift', 'splice', 'unshift', 'slice', 'concat', 'join',
68
+ // State checks (Boolean)
69
+ 'is', 'has', 'can', 'should', 'will', 'does', 'contains', 'includes', 'exists',
70
+ // UI actions
71
+ 'show', 'hide', 'display', 'render', 'draw', 'toggle', 'enable', 'disable',
72
+ 'activate', 'deactivate', 'select', 'deselect', 'focus', 'blur',
73
+ // Lifecycle
74
+ 'connect', 'disconnect', 'open', 'close', 'start', 'stop', 'run', 'refresh',
75
+ 'initialize', 'init', 'setup', 'teardown', 'shutdown', 'restart', 'reload',
76
+ 'restore', 'resume', 'pause', 'suspend',
77
+ // Event Handling
78
+ 'on', 'trigger', 'fire', 'dispatch', 'invoke', 'call', 'emit', 'handle',
79
+ // Monitoring
80
+ 'count', 'measure', 'monitor', 'watch', 'track', 'observe', 'log', 'record',
81
+ // Navigation
82
+ 'navigate', 'redirect', 'route', 'go', 'move', 'scroll',
83
+ // Data Operations
84
+ 'save', 'store', 'persist', 'cache', 'serialize', 'deserialize',
85
+ // Detection/Analysis
86
+ 'detect', 'analyze', 'scan', 'inspect', 'evaluate', 'assess',
87
+ // Computation & Transformation
88
+ 'calculate', 'compute', 'parse', 'format', 'convert',
89
+ 'normalize', 'sanitize', 'encode', 'decode', 'encrypt', 'decrypt',
90
+ 'compress', 'decompress', 'zip', 'unzip', 'tar', 'untar', // ← Add these
91
+ 'stringify', 'objectify',
92
+ // State Management
93
+ 'apply', 'revert', 'undo', 'redo', 'commit', 'rollback',
94
+ // Async Operations
95
+ 'await', 'defer', 'debounce', 'throttle', 'delay',
96
+ // Error Handling
97
+ 'throw', 'catch', 'handle', 'recover', 'retry',
98
+ // Copying/Cloning
99
+ 'copy', 'clone', 'duplicate', 'replicate',
100
+ // Comparison
101
+ 'equals', 'match', 'differ', 'compare',
102
+ // Registration
103
+ 'register', 'unregister', 'subscribe', 'unsubscribe',
104
+ // Import/Export
105
+ 'import', 'export', 'download', 'upload',
106
+ // Expansion/Collapse
107
+ 'expand', 'collapse', 'maximize', 'minimize',
108
+ // Submission
109
+ 'submit', 'cancel', 'abort', 'complete', 'finish',
110
+ // Preparation
111
+ 'prepare', 'ready', 'preload', 'precache',
112
+ // Extraction
113
+ 'extract', 'derive', 'obtain', 'acquire',
114
+ // Replacement
115
+ 'replace', 'swap', 'substitute', 'override',
116
+ // Binding
117
+ 'bind', 'unbind', 'attach', 'detach',
118
+ // Notification
119
+ 'notify', 'alert', 'warn', 'inform',
120
+ // Execution
121
+ 'execute', 'perform', 'process', 'run'
122
+ ];
123
+
124
+ // ⚠️ VAGUE/GENERIC VERBS TO FLAG (should have more specific verbs)
125
+ this.vagueVerbs = [
126
+ 'do', 'handle', 'manage', 'process', 'execute', 'perform',
127
+ 'something', 'stuff', 'thing', 'work', 'data', 'item'
128
+ ];
48
129
  }
49
130
 
50
131
  async analyze(files, language, config) {
51
132
  const violations = [];
52
-
133
+
53
134
  if (config.verbose) {
54
135
  console.log(`🔧 [DEBUG] Starting Smart C006 Analysis on ${files.length} files...`);
55
136
  }
@@ -69,7 +150,7 @@ class SmartC006Analyzer {
69
150
  if (config.verbose) {
70
151
  console.log(`🔧 [DEBUG] Smart C006 Analysis complete: ${violations.length} violations found`);
71
152
  }
72
-
153
+
73
154
  return violations;
74
155
  }
75
156
 
@@ -80,10 +161,10 @@ class SmartC006Analyzer {
80
161
 
81
162
  const violations = [];
82
163
  const lines = content.split('\n');
83
-
164
+
84
165
  // 🏗️ TIER 1: ARCHITECTURAL CONTEXT ANALYSIS
85
166
  const architecturalContext = this.analyzeArchitecturalContext(filePath, content);
86
-
167
+
87
168
  // Parse TypeScript/JavaScript code
88
169
  const sourceFile = ts.createSourceFile(
89
170
  filePath,
@@ -102,13 +183,13 @@ class SmartC006Analyzer {
102
183
  architecturalContext,
103
184
  content
104
185
  );
105
-
186
+
106
187
  if (analysis.isViolation && analysis.confidence >= this.confidenceThresholds.LOW) {
107
188
  const namePosition = sourceFile.getLineAndCharacterOfPosition(node.name.getStart());
108
189
  const line = namePosition.line + 1;
109
190
  const column = namePosition.character + 1;
110
191
  const lineText = lines[line - 1]?.trim() || '';
111
-
192
+
112
193
  violations.push({
113
194
  ruleId: this.ruleId,
114
195
  file: filePath,
@@ -134,13 +215,13 @@ class SmartC006Analyzer {
134
215
  architecturalContext,
135
216
  content
136
217
  );
137
-
218
+
138
219
  if (analysis.isViolation && analysis.confidence >= this.confidenceThresholds.LOW) {
139
220
  const namePosition = sourceFile.getLineAndCharacterOfPosition(node.name.getStart());
140
221
  const line = namePosition.line + 1;
141
222
  const column = namePosition.character + 1;
142
223
  const lineText = lines[line - 1]?.trim() || '';
143
-
224
+
144
225
  violations.push({
145
226
  ruleId: this.ruleId,
146
227
  file: filePath,
@@ -158,7 +239,7 @@ class SmartC006Analyzer {
158
239
  }
159
240
 
160
241
  // Analyze arrow functions assigned to variables
161
- if (ts.isVariableDeclaration(node) && node.name && ts.isIdentifier(node.name) &&
242
+ if (ts.isVariableDeclaration(node) && node.name && ts.isIdentifier(node.name) &&
162
243
  node.initializer && ts.isArrowFunction(node.initializer)) {
163
244
  const analysis = this.smartAnalyzeFunctionName(
164
245
  node.name.text,
@@ -167,13 +248,13 @@ class SmartC006Analyzer {
167
248
  architecturalContext,
168
249
  content
169
250
  );
170
-
251
+
171
252
  if (analysis.isViolation && analysis.confidence >= this.confidenceThresholds.LOW) {
172
253
  const namePosition = sourceFile.getLineAndCharacterOfPosition(node.name.getStart());
173
254
  const line = namePosition.line + 1;
174
255
  const column = namePosition.character + 1;
175
256
  const lineText = lines[line - 1]?.trim() || '';
176
-
257
+
177
258
  violations.push({
178
259
  ruleId: this.ruleId,
179
260
  file: filePath,
@@ -189,7 +270,7 @@ class SmartC006Analyzer {
189
270
  });
190
271
  }
191
272
  }
192
-
273
+
193
274
  ts.forEachChild(node, visit);
194
275
  };
195
276
 
@@ -204,7 +285,7 @@ class SmartC006Analyzer {
204
285
  analyzeArchitecturalContext(filePath, content) {
205
286
  const fileName = path.basename(filePath, path.extname(filePath)).toLowerCase();
206
287
  const fileDir = path.dirname(filePath).toLowerCase();
207
-
288
+
208
289
  // Detect architectural layer
209
290
  let layer = 'UNKNOWN';
210
291
  for (const [layerName, patterns] of Object.entries(this.architecturalLayers)) {
@@ -213,12 +294,12 @@ class SmartC006Analyzer {
213
294
  break;
214
295
  }
215
296
  }
216
-
297
+
217
298
  // Analyze imports for additional context
218
299
  const imports = this.extractImports(content);
219
300
  const isReactComponent = imports.some(imp => imp.includes('react')) || content.includes('JSX.Element');
220
301
  const isTestFile = fileName.includes('test') || fileName.includes('spec');
221
-
302
+
222
303
  return {
223
304
  layer,
224
305
  isReactComponent,
@@ -234,83 +315,96 @@ class SmartC006Analyzer {
234
315
  */
235
316
  analyzeSemanticIntent(functionNode, sourceFile, content) {
236
317
  if (!functionNode.body) return 'UNKNOWN';
237
-
318
+
238
319
  const functionText = content.substring(
239
320
  functionNode.body.getStart(),
240
321
  functionNode.body.getEnd()
241
322
  );
242
-
323
+
243
324
  // Check for different semantic patterns
244
325
  for (const [intent, patterns] of Object.entries(this.semanticPatterns)) {
245
326
  if (patterns.some(pattern => pattern.test(functionText))) {
246
327
  return intent;
247
328
  }
248
329
  }
249
-
330
+
250
331
  return 'UNKNOWN';
251
332
  }
252
333
 
253
334
  /**
254
- * 🎯 TIER 3: INTELLIGENT VERB DETECTION
255
- * Uses multiple strategies to detect verbs
335
+ * 🎯 ENHANCED VERB DETECTION - FIXED VERSION
336
+ * Properly detects verbs including compound verbs
256
337
  */
257
- isVerbLikeName(functionName) {
258
- // Strategy 0: REJECT generic/vague verbs that should be flagged
259
- const genericVerbs = [
260
- 'do', 'handle', 'process', 'manage', 'execute',
261
- 'something', 'stuff', 'thing', 'work', 'data'
262
- ];
263
-
264
- const isGenericVerb = genericVerbs.some(verb => {
265
- const verbPattern = new RegExp(`^${verb}([A-Z].*|$)`, 'i');
266
- return verbPattern.test(functionName);
267
- });
268
-
269
- // Reject names starting with generic verbs
270
- if (isGenericVerb) return false;
271
-
272
- // Strategy 1: Known verb prefixes (expanded beyond static list)
273
- const verbPrefixes = [
274
- 'get', 'set', 'is', 'has', 'can', 'should', 'will', 'does',
275
- 'create', 'build', 'make', 'generate', 'construct', 'produce',
276
- 'update', 'modify', 'change', 'edit', 'alter', 'transform',
277
- 'delete', 'remove', 'destroy', 'clean', 'clear', 'reset',
278
- 'load', 'save', 'fetch', 'retrieve', 'find', 'search', 'query',
279
- 'validate', 'verify', 'check', 'confirm', 'ensure', 'test',
280
- 'calculate', 'compute', 'compare', 'parse', 'format', 'convert',
281
- 'send', 'receive', 'transmit', 'broadcast', 'emit', 'publish',
282
- 'map', 'filter', 'sort', 'group', 'merge', 'split',
283
- 'connect', 'disconnect', 'open', 'close', 'start', 'stop', 'run',
284
- 'show', 'hide', 'display', 'render', 'draw', 'paint', 'animate',
285
- 'add', 'append', 'insert', 'push', 'pop', 'shift', 'splice',
286
- 'count', 'measure', 'monitor', 'watch', 'track', 'observe',
287
- 'refresh', 'restore', 'reload', 'retry', 'resume', 'redirect',
288
- 'select', 'toggle', 'switch', 'enable', 'disable', 'activate',
289
- 'expand', 'collapse', 'scroll', 'navigate', 'submit', 'cancel',
290
- 'on', 'trigger', 'fire', 'dispatch', 'invoke', 'call'
291
- ];
292
-
293
- // Strategy 2: Check if starts with known verb
294
- const startsWithVerb = verbPrefixes.some(verb => {
295
- const verbPattern = new RegExp(`^${verb}[A-Z]?`, 'i');
296
- return verbPattern.test(functionName);
297
- });
298
-
299
- if (startsWithVerb) return true;
300
-
301
- // Strategy 3: Common verb suffixes that indicate actions
302
- const actionSuffixes = ['ize', 'ise', 'fy', 'ate', 'en'];
303
- if (actionSuffixes.some(suffix => functionName.endsWith(suffix))) {
304
- return true;
338
+ extractVerbFromName(functionName) {
339
+ // Convert to lowercase for comparison
340
+ const lowerName = functionName.toLowerCase();
341
+
342
+ // Find the longest matching verb from the accepted verbs list
343
+ let matchedVerb = null;
344
+ let maxLength = 0;
345
+
346
+ for (const verb of this.acceptedVerbs) {
347
+ // Check if function name starts with this verb
348
+ // Must be followed by uppercase letter, end of string, or be exact match
349
+ const verbPattern = new RegExp(`^${verb}([A-Z].*|$)`);
350
+
351
+ if (verbPattern.test(functionName) && verb.length > maxLength) {
352
+ matchedVerb = verb;
353
+ maxLength = verb.length;
354
+ }
305
355
  }
306
-
307
- // Strategy 4: English verb patterns (basic NLP)
308
- const verbPatterns = [
309
- /^(re|un|pre|de|dis)[A-Z]/, // prefixed verbs: revalidate, unload, preprocess
310
- /^[a-z]+ly[A-Z]/, // adverb-verb patterns: quicklyProcess
311
- ];
312
-
313
- return verbPatterns.some(pattern => pattern.test(functionName));
356
+
357
+ return matchedVerb;
358
+ }
359
+
360
+ /**
361
+ * 🎯 CHECK IF NAME FOLLOWS VERB-NOUN PATTERN
362
+ */
363
+ isVerbNounPattern(functionName) {
364
+ // Strategy 1: Extract verb from accepted verbs list
365
+ const verb = this.extractVerbFromName(functionName);
366
+
367
+ if (verb) {
368
+ // Check if it's a vague verb (should be flagged even if technically a verb)
369
+ const isVagueVerb = this.vagueVerbs.some(vague => {
370
+ const vaguePattern = new RegExp(`^${vague}([A-Z].*|$)`, 'i');
371
+ return vaguePattern.test(functionName);
372
+ });
373
+
374
+ if (isVagueVerb) {
375
+ return { isValid: false, verb: null, reason: 'vague_verb' };
376
+ }
377
+
378
+ // Valid verb found
379
+ return { isValid: true, verb, reason: null };
380
+ }
381
+
382
+ // Strategy 2: Check for verb-like patterns (morphology)
383
+ const verbSuffixes = ['ize', 'ise', 'fy', 'ate', 'en', 'ing', 'ed'];
384
+ for (const suffix of verbSuffixes) {
385
+ const suffixPattern = new RegExp(`^[a-z]+${suffix}([A-Z].*|$)`, 'i');
386
+ if (suffixPattern.test(functionName)) {
387
+ return { isValid: true, verb: 'morphological', reason: null };
388
+ }
389
+ }
390
+
391
+ // Strategy 3: Check for verb prefixes (re-, un-, pre-, de-, dis-)
392
+ const verbPrefixes = ['re', 'un', 'pre', 'de', 'dis', 'over', 'under', 'out', 'up'];
393
+ for (const prefix of verbPrefixes) {
394
+ // Must be followed by a known verb or capital letter
395
+ const prefixPattern = new RegExp(`^${prefix}[A-Z]`, 'i');
396
+ if (prefixPattern.test(functionName)) {
397
+ // Check if the part after prefix is a known verb
398
+ const remainder = functionName.substring(prefix.length);
399
+ const remainderVerb = this.extractVerbFromName(remainder);
400
+ if (remainderVerb) {
401
+ return { isValid: true, verb: `${prefix}${remainderVerb}`, reason: null };
402
+ }
403
+ }
404
+ }
405
+
406
+ // No verb pattern found
407
+ return { isValid: false, verb: null, reason: 'no_verb' };
314
408
  }
315
409
 
316
410
  /**
@@ -323,29 +417,29 @@ class SmartC006Analyzer {
323
417
  requiredPatterns: [],
324
418
  suggestions: []
325
419
  };
326
-
420
+
327
421
  // React components have different naming conventions
328
422
  if (architecturalContext.isReactComponent) {
329
423
  rules.allowedPatterns.push(/^[A-Z][a-zA-Z]*$/); // PascalCase components
330
424
  rules.allowedPatterns.push(/^use[A-Z][a-zA-Z]*$/); // React hooks
331
425
  rules.allowedPatterns.push(/^handle[A-Z][a-zA-Z]*$/); // Event handlers
332
426
  }
333
-
427
+
334
428
  // Test files have different patterns
335
429
  if (architecturalContext.isTestFile) {
336
430
  rules.allowedPatterns.push(/^(test|it|describe|should|expect)[A-Z]?/);
337
431
  }
338
-
432
+
339
433
  // Data layer functions often have CRUD patterns
340
434
  if (architecturalContext.layer === 'DATA') {
341
- rules.suggestions.push('Consider CRUD verbs: create, read, update, delete');
435
+ rules.suggestions.push('Consider CRUD verbs: create, read, update, delete, fetch, save');
342
436
  }
343
-
437
+
344
438
  // UI layer functions often have interaction verbs
345
439
  if (architecturalContext.layer === 'UI') {
346
- rules.suggestions.push('Consider UI verbs: show, hide, toggle, render, display');
440
+ rules.suggestions.push('Consider UI verbs: show, hide, toggle, render, display, handle');
347
441
  }
348
-
442
+
349
443
  return rules;
350
444
  }
351
445
 
@@ -358,88 +452,103 @@ class SmartC006Analyzer {
358
452
  if (this.isSpecialFunction(functionName, architecturalContext)) {
359
453
  return { isViolation: false };
360
454
  }
361
-
455
+
362
456
  // Get semantic intent
363
457
  const semanticIntent = this.analyzeSemanticIntent(functionNode, sourceFile, content);
364
-
458
+
365
459
  // Get context-specific rules
366
460
  const contextRules = this.getContextSpecificRules(architecturalContext, semanticIntent);
367
-
461
+
368
462
  // Check if allowed by context-specific patterns
369
463
  if (contextRules.allowedPatterns.some(pattern => pattern.test(functionName))) {
370
464
  return { isViolation: false };
371
465
  }
372
-
466
+
373
467
  // Check if name follows verb-noun pattern
374
- const isVerbLike = this.isVerbLikeName(functionName);
375
-
376
- if (isVerbLike) {
468
+ const verbCheck = this.isVerbNounPattern(functionName);
469
+
470
+ if (verbCheck.isValid) {
377
471
  return { isViolation: false };
378
472
  }
379
-
473
+
380
474
  // 🧮 CONFIDENCE CALCULATION
381
475
  let confidence = 0.5; // Base confidence
382
-
383
- // Boost confidence for clearly generic/vague patterns
384
- const vagueFunctionNames = [
385
- 'doSomething', 'handleStuff', 'processData', 'processInfo',
386
- 'executeWork', 'manageItems', 'doWork', 'handleData',
387
- 'something', 'stuff', 'thing', 'data', 'info', 'item'
388
- ];
389
-
390
- if (vagueFunctionNames.some(vague => functionName.toLowerCase().includes(vague.toLowerCase()))) {
391
- confidence += 0.4; // Strongly boost confidence for obviously vague names
392
- }
393
-
394
- // Boost confidence for clear noun-only patterns
395
- if (/^[a-z]+$/.test(functionName)) {
396
- confidence += 0.3; // Simple lowercase nouns: user, data
397
- }
398
-
399
- if (/^[a-z]+[A-Z][a-z]+$/.test(functionName)) {
400
- confidence += 0.2; // Simple camelCase nouns: userData, userInfo
476
+ let violationType = 'no_verb';
477
+
478
+ if (verbCheck.reason === 'vague_verb') {
479
+ violationType = 'vague_verb';
480
+ confidence = 0.9; // High confidence for vague verbs
481
+ } else if (verbCheck.reason === 'no_verb') {
482
+ violationType = 'no_verb';
483
+
484
+ // Check for obviously wrong patterns
485
+ const commonNounPatterns = [
486
+ /^(user|data|info|item|list|config|settings|options|params|args|props|state|value|result|response|request|error|message|event|callback|handler)([A-Z][a-z]*)*$/i,
487
+ /^[a-z]+$/, // Simple lowercase words
488
+ /^[a-z]+[A-Z][a-z]+$/ // Simple camelCase nouns
489
+ ];
490
+
491
+ if (commonNounPatterns.some(pattern => pattern.test(functionName))) {
492
+ confidence = 0.8; // High confidence for obvious nouns
493
+ }
494
+
495
+ // Boost confidence for clearly generic patterns
496
+ const vagueFunctionNames = [
497
+ 'something', 'stuff', 'thing', 'data', 'info', 'item',
498
+ 'work', 'task', 'job', 'action', 'operation'
499
+ ];
500
+
501
+ if (vagueFunctionNames.some(vague => functionName.toLowerCase().includes(vague.toLowerCase()))) {
502
+ confidence = 0.95; // Very high confidence
503
+ }
401
504
  }
402
-
403
- // Reduce confidence for complex names (might have hidden verbs)
404
- if (functionName.length > 15) {
405
- confidence -= 0.1;
505
+
506
+ // Reduce confidence for complex names (might have hidden patterns)
507
+ if (functionName.length > 20) {
508
+ confidence -= 0.15;
406
509
  }
407
-
510
+
408
511
  // Reduce confidence for utils/helpers (more flexible naming)
409
512
  if (architecturalContext.layer === 'UTILS') {
410
513
  confidence -= 0.2;
411
514
  }
412
-
515
+
413
516
  // Reduce confidence for test files
414
517
  if (architecturalContext.isTestFile) {
415
518
  confidence -= 0.3;
416
519
  }
417
-
520
+
418
521
  // Cap confidence
419
522
  confidence = Math.min(Math.max(confidence, 0.1), 1.0);
420
-
523
+
421
524
  // 💬 INTELLIGENT MESSAGING
422
525
  const context = {
423
526
  layer: architecturalContext.layer,
424
527
  intent: semanticIntent,
425
528
  isReactComponent: architecturalContext.isReactComponent,
426
- isTestFile: architecturalContext.isTestFile
529
+ isTestFile: architecturalContext.isTestFile,
530
+ violationType
427
531
  };
428
-
429
- let reason = `Function '${functionName}' should follow verb-noun naming pattern`;
430
- let suggestion = this.generateSmartSuggestion(functionName, semanticIntent, architecturalContext);
532
+
533
+ let reason = '';
534
+ let suggestion = '';
535
+
536
+ if (violationType === 'vague_verb') {
537
+ reason = `Function '${functionName}' uses vague verb. Use more specific action verbs (see C006 accepted verbs list)`;
538
+ suggestion = this.generateSmartSuggestion(functionName, semanticIntent, architecturalContext, true);
539
+ } else {
540
+ reason = `Function '${functionName}' must start with a verb (verb-noun pattern required by C006)`;
541
+ suggestion = this.generateSmartSuggestion(functionName, semanticIntent, architecturalContext, false);
542
+ }
431
543
 
432
544
  if (architecturalContext.layer !== 'UNKNOWN') {
433
545
  reason += ` (${architecturalContext.layer} layer)`;
434
546
  }
435
547
 
436
- // Add helpful note about accepted verbs
437
- reason += `. Accepted verbs: get, set, create, update, delete, validate, check, compare, etc. See: https://coding-standards.sun-asterisk.vn/rules/rule/C006/`;
438
-
439
548
  return {
440
549
  isViolation: true,
441
550
  reason,
442
- type: 'smart_naming_violation',
551
+ type: `smart_naming_violation_${violationType}`,
443
552
  confidence,
444
553
  suggestion,
445
554
  context
@@ -449,27 +558,51 @@ class SmartC006Analyzer {
449
558
  /**
450
559
  * 💡 SMART SUGGESTION GENERATOR
451
560
  */
452
- generateSmartSuggestion(functionName, semanticIntent, architecturalContext) {
453
- const baseNoun = functionName.charAt(0).toUpperCase() + functionName.slice(1);
454
-
561
+ generateSmartSuggestion(functionName, semanticIntent, architecturalContext, isVagueVerb) {
562
+ // If it's a vague verb, extract the noun part
563
+ let baseNoun = functionName;
564
+
565
+ if (isVagueVerb) {
566
+ // Try to extract the noun part after the vague verb
567
+ for (const vagueVerb of this.vagueVerbs) {
568
+ const pattern = new RegExp(`^${vagueVerb}([A-Z].*)$`, 'i');
569
+ const match = functionName.match(pattern);
570
+ if (match) {
571
+ baseNoun = match[1];
572
+ break;
573
+ }
574
+ }
575
+ }
576
+
577
+ // Ensure first letter is capitalized for camelCase
578
+ baseNoun = baseNoun.charAt(0).toUpperCase() + baseNoun.slice(1);
579
+
580
+ const suggestions = [];
581
+
455
582
  switch (semanticIntent) {
456
583
  case 'GETTER':
457
- return `get${baseNoun}()`;
584
+ suggestions.push(`get${baseNoun}()`, `fetch${baseNoun}()`, `retrieve${baseNoun}()`);
585
+ break;
458
586
  case 'SETTER':
459
- return `set${baseNoun}()`;
587
+ suggestions.push(`set${baseNoun}()`, `update${baseNoun}()`, `save${baseNoun}()`);
588
+ break;
460
589
  case 'CHECKER':
461
- return `is${baseNoun}() or has${baseNoun}()`;
590
+ suggestions.push(`is${baseNoun}()`, `has${baseNoun}()`, `can${baseNoun}()`, `should${baseNoun}()`);
591
+ break;
462
592
  case 'ACTION':
463
- return `process${baseNoun}() or handle${baseNoun}()`;
593
+ suggestions.push(`process${baseNoun}()`, `handle${baseNoun}()`, `execute${baseNoun}()`);
594
+ break;
464
595
  default:
465
596
  if (architecturalContext.layer === 'DATA') {
466
- return `fetch${baseNoun}() or create${baseNoun}()`;
467
- }
468
- if (architecturalContext.layer === 'UI') {
469
- return `render${baseNoun}() or show${baseNoun}()`;
597
+ suggestions.push(`fetch${baseNoun}()`, `create${baseNoun}()`, `save${baseNoun}()`, `load${baseNoun}()`);
598
+ } else if (architecturalContext.layer === 'UI') {
599
+ suggestions.push(`render${baseNoun}()`, `show${baseNoun}()`, `display${baseNoun}()`, `draw${baseNoun}()`);
600
+ } else {
601
+ suggestions.push(`get${baseNoun}()`, `set${baseNoun}()`, `process${baseNoun}()`, `handle${baseNoun}()`);
470
602
  }
471
- return `get${baseNoun}() or process${baseNoun}()`;
472
603
  }
604
+
605
+ return suggestions.slice(0, 3).join(' or ');
473
606
  }
474
607
 
475
608
  /**
@@ -489,22 +622,22 @@ class SmartC006Analyzer {
489
622
  if (specialFunctions.includes(name) || name.startsWith('_') || name.startsWith('$')) {
490
623
  return true;
491
624
  }
492
-
625
+
493
626
  // React component names (PascalCase)
494
627
  if (architecturalContext.isReactComponent && /^[A-Z][a-zA-Z]*$/.test(name)) {
495
628
  return true;
496
629
  }
497
-
630
+
498
631
  // React hooks
499
632
  if (name.startsWith('use') && /^use[A-Z]/.test(name)) {
500
633
  return true;
501
634
  }
502
-
635
+
503
636
  // Test function names
504
637
  if (architecturalContext.isTestFile && /^(test|it|describe|should|expect)/.test(name)) {
505
638
  return true;
506
639
  }
507
-
640
+
508
641
  return false;
509
642
  }
510
643
 
@@ -515,11 +648,11 @@ class SmartC006Analyzer {
515
648
  const importRegex = /import\s+.*?\s+from\s+['"]([^'"]+)['"]/g;
516
649
  const imports = [];
517
650
  let match;
518
-
651
+
519
652
  while ((match = importRegex.exec(content)) !== null) {
520
653
  imports.push(match[1]);
521
654
  }
522
-
655
+
523
656
  return imports;
524
657
  }
525
658
 
@@ -845,7 +845,8 @@ class C067SymbolBasedAnalyzer {
845
845
  /\.config\.(ts|js)$/,
846
846
  /\.env$/,
847
847
  /\.env\./,
848
- /constants\.(ts|js)$/,
848
+ /constants?\.(ts|js)$/, // constants.ts, constant.ts
849
+ /\.constants?\.(ts|js)$/, // app.constants.ts, common.constant.ts
849
850
  /settings\.(ts|js)$/,
850
851
  /defaults\.(ts|js)$/,
851
852
  ];
@@ -1,12 +1,10 @@
1
1
  /**
2
2
  * S031 Main Analyzer - Set Secure flag for Session Cookies
3
- * Primary: Symbol-based analysis (when available)
4
- * Fallback: Regex-based for all other cases
3
+ * Uses symbol-based analysis only (regex-based removed)
5
4
  * Command: node cli.js --rule=S031 --input=examples/rule-test-fixtures/rules/S031_secure_session_cookies --engine=heuristic
6
5
  */
7
6
 
8
7
  const S031SymbolBasedAnalyzer = require("./symbol-based-analyzer.js");
9
- const S031RegexBasedAnalyzer = require("./regex-based-analyzer.js");
10
8
 
11
9
  class S031Analyzer {
12
10
  constructor(options = {}) {
@@ -26,14 +24,7 @@ class S031Analyzer {
26
24
  this.semanticEngine = options.semanticEngine || null;
27
25
  this.verbose = options.verbose || false;
28
26
 
29
- // Configuration
30
- this.config = {
31
- useSymbolBased: true, // Primary approach
32
- fallbackToRegex: true, // Secondary approach
33
- regexBasedOnly: false, // Can be set to true for pure mode
34
- };
35
-
36
- // Initialize analyzers
27
+ // Initialize symbol analyzer only
37
28
  try {
38
29
  this.symbolAnalyzer = new S031SymbolBasedAnalyzer(this.semanticEngine);
39
30
  if (process.env.SUNLINT_DEBUG) {
@@ -43,48 +34,39 @@ class S031Analyzer {
43
34
  console.error(`🔧 [S031] Error creating symbol analyzer:`, error);
44
35
  }
45
36
 
46
- try {
47
- this.regexAnalyzer = new S031RegexBasedAnalyzer(this.semanticEngine);
48
- if (process.env.SUNLINT_DEBUG) {
49
- console.log(`🔧 [S031] Regex analyzer created successfully`);
50
- }
51
- } catch (error) {
52
- console.error(`🔧 [S031] Error creating regex analyzer:`, error);
37
+ if (process.env.SUNLINT_DEBUG) {
38
+ console.log(`🔧 [S031] Constructor completed`);
53
39
  }
54
40
  }
55
41
 
56
42
  /**
57
43
  * Initialize analyzer with semantic engine
58
-
59
44
  */
60
- async initialize(semanticEngine) {
61
- this.semanticEngine = semanticEngine;
62
-
63
- if (process.env.SUNLINT_DEBUG) {
64
- console.log(`🔧 [S031] Main analyzer initializing...`);
45
+ async initialize(semanticEngine = null) {
46
+ if (semanticEngine) {
47
+ this.semanticEngine = semanticEngine;
65
48
  }
49
+ this.verbose = semanticEngine?.verbose || false;
66
50
 
67
- // Initialize both analyzers
51
+ // Initialize symbol analyzer
68
52
  if (this.symbolAnalyzer) {
69
53
  await this.symbolAnalyzer.initialize?.(semanticEngine);
70
54
  }
71
- if (this.regexAnalyzer) {
72
- await this.regexAnalyzer.initialize?.(semanticEngine);
73
- }
74
55
 
75
- // Clean up if needed
76
- if (this.regexAnalyzer) {
77
- this.regexAnalyzer.cleanup?.();
56
+ // Ensure verbose flag is propagated
57
+ if (this.symbolAnalyzer) {
58
+ this.symbolAnalyzer.verbose = this.verbose;
78
59
  }
79
60
 
80
- if (process.env.SUNLINT_DEBUG) {
81
- console.log(`🔧 [S031] Main analyzer initialized successfully`);
61
+ if (this.verbose) {
62
+ console.log(
63
+ `🔧 [S031] Analyzer initialized - verbose: ${this.verbose}`
64
+ );
82
65
  }
83
66
  }
84
67
 
85
68
  /**
86
69
  * Single file analysis method for testing
87
-
88
70
  */
89
71
  analyzeSingle(filePath, options = {}) {
90
72
  if (process.env.SUNLINT_DEBUG) {
@@ -141,122 +123,95 @@ class S031Analyzer {
141
123
  // Create a Map to track unique violations and prevent duplicates
142
124
  const violationMap = new Map();
143
125
 
144
- // 1. Try Symbol-based analysis first (primary)
145
- if (
146
- this.config.useSymbolBased &&
147
- this.semanticEngine?.project &&
148
- this.semanticEngine?.initialized
149
- ) {
126
+ // Symbol-based analysis only
127
+ if (this.semanticEngine?.project && this.semanticEngine?.initialized) {
150
128
  try {
151
129
  if (process.env.SUNLINT_DEBUG) {
152
- console.log(`🔧 [S031] Trying symbol-based analysis...`);
130
+ console.log(`🔧 [S031] Running symbol-based analysis...`);
153
131
  }
154
132
  const sourceFile = this.semanticEngine.project.getSourceFile(filePath);
155
133
  if (sourceFile) {
156
134
  if (process.env.SUNLINT_DEBUG) {
157
- console.log(`🔧 [S031] Source file found, analyzing...`);
135
+ console.log(
136
+ `🔧 [S031] Source file found, analyzing with symbol-based...`
137
+ );
158
138
  }
159
- const symbolViolations = await this.symbolAnalyzer.analyze(
139
+
140
+ const violations = await this.symbolAnalyzer.analyze(
160
141
  sourceFile,
161
142
  filePath
162
143
  );
163
144
 
164
- // Add to violation map with deduplication
165
- symbolViolations.forEach((violation) => {
166
- // Create a cookie-specific key to allow multiple violations for same cookie at different locations
167
- const cookieName = this.extractCookieName(violation.message) || "";
168
- const key = `cookie:${cookieName}:line:${violation.line}:secure`;
145
+ // Add violations to map to deduplicate and add filePath
146
+ violations.forEach((v) => {
147
+ const key = `${v.line}:${v.column}:${v.message}`;
169
148
  if (!violationMap.has(key)) {
170
- violationMap.set(key, violation);
149
+ v.analysisStrategy = "symbol-based";
150
+ v.filePath = filePath;
151
+ v.file = filePath; // Also add 'file' for compatibility
152
+ violationMap.set(key, v);
171
153
  }
172
154
  });
173
155
 
174
156
  if (process.env.SUNLINT_DEBUG) {
175
157
  console.log(
176
- `🔧 [S031] Symbol analysis completed: ${symbolViolations.length} violations`
158
+ `✅ [S031] Symbol-based analysis: ${violations.length} violations`
177
159
  );
178
160
  }
161
+
162
+ const finalViolations = Array.from(violationMap.values());
163
+ return finalViolations; // Return deduplicated violations with filePath
179
164
  } else {
180
165
  if (process.env.SUNLINT_DEBUG) {
181
- console.log(`🔧 [S031] Source file not found, falling back...`);
166
+ console.log(`⚠️ [S031] Source file not found in project`);
182
167
  }
183
168
  }
184
169
  } catch (error) {
185
- console.warn(`⚠ [S031] Symbol analysis failed:`, error.message);
170
+ console.warn(`⚠️ [S031] Symbol analysis failed: ${error.message}`);
186
171
  }
187
- }
188
-
189
- // 2. Try Regex-based analysis (fallback or additional)
190
- if (this.config.fallbackToRegex || this.config.regexBasedOnly) {
191
- try {
192
- if (process.env.SUNLINT_DEBUG) {
193
- console.log(`🔧 [S031] Trying regex-based analysis...`);
194
- }
195
- const regexViolations = await this.regexAnalyzer.analyze(filePath);
196
-
197
- // Add to violation map with deduplication
198
- regexViolations.forEach((violation) => {
199
- // Create a cookie-specific key to allow multiple violations for same cookie at different locations
200
- const cookieName = this.extractCookieName(violation.message) || "";
201
- const key = `cookie:${cookieName}:line:${violation.line}:secure`;
202
- if (!violationMap.has(key)) {
203
- violationMap.set(key, violation);
204
- }
205
- });
206
-
207
- if (process.env.SUNLINT_DEBUG) {
208
- console.log(
209
- `🔧 [S031] Regex analysis completed: ${regexViolations.length} violations`
210
- );
211
- }
212
- } catch (error) {
213
- console.warn(`⚠ [S031] Regex analysis failed:`, error.message);
172
+ } else {
173
+ if (process.env.SUNLINT_DEBUG) {
174
+ console.log(`🔄 [S031] Symbol analysis conditions check:`);
175
+ console.log(` - semanticEngine: ${!!this.semanticEngine}`);
176
+ console.log(
177
+ ` - semanticEngine.project: ${!!this.semanticEngine?.project}`
178
+ );
179
+ console.log(
180
+ ` - semanticEngine.initialized: ${this.semanticEngine?.initialized}`
181
+ );
182
+ console.log(`🔄 [S031] Symbol analysis unavailable`);
214
183
  }
215
184
  }
216
185
 
217
- // Convert Map values to array and add filePath to each violation
218
- const finalViolations = Array.from(violationMap.values()).map(
219
- (violation) => ({
220
- ...violation,
221
- filePath: filePath,
222
- file: filePath, // Also add 'file' for compatibility
223
- })
224
- );
225
-
226
186
  if (process.env.SUNLINT_DEBUG) {
227
- console.log(
228
- `🔧 [S031] File analysis completed: ${finalViolations.length} unique violations`
229
- );
187
+ console.log(`🔧 [S031] Analysis completed: ${violationMap.size} violations`);
230
188
  }
231
-
232
- return finalViolations;
189
+ return Array.from(violationMap.values());
233
190
  }
234
191
 
235
192
  /**
236
- * Extract cookie name from violation message for better deduplication
193
+ * Methods for compatibility with different engine invocation patterns
237
194
  */
238
- extractCookieName(message) {
239
- try {
240
- const match = message.match(
241
- /Session cookie "([^"]+)"|Session cookie from "([^"]+)"/
242
- );
243
- return match ? match[1] || match[2] : "";
244
- } catch (error) {
245
- return "";
246
- }
195
+ async analyzeFileWithSymbols(filePath, options = {}) {
196
+ return this.analyzeFile(filePath, options);
247
197
  }
248
198
 
249
- /**
250
- * Clean up resources
199
+ async analyzeWithSemantics(filePath, options = {}) {
200
+ return this.analyzeFile(filePath, options);
201
+ }
251
202
 
203
+ /**
204
+ * Get analyzer metadata
252
205
  */
253
- cleanup() {
254
- if (this.symbolAnalyzer?.cleanup) {
255
- this.symbolAnalyzer.cleanup();
256
- }
257
- if (this.regexAnalyzer?.cleanup) {
258
- this.regexAnalyzer.cleanup();
259
- }
206
+ getMetadata() {
207
+ return {
208
+ rule: "S031",
209
+ name: "Set Secure flag for Session Cookies",
210
+ category: "security",
211
+ type: "symbol-based",
212
+ description:
213
+ "Uses symbol-based analysis to detect session cookies missing Secure flag",
214
+ };
260
215
  }
261
216
  }
262
217
 
@@ -1,296 +0,0 @@
1
- /**
2
- * S031 Regex-Based Analyzer - Set Secure flag for Session Cookies
3
- * Fallback analysis using regex patterns
4
-
5
- */
6
-
7
- const fs = require("fs");
8
-
9
- class S031RegexBasedAnalyzer {
10
- constructor(semanticEngine = null) {
11
- this.semanticEngine = semanticEngine;
12
- this.ruleId = "S031";
13
- this.category = "security";
14
-
15
- // Session cookie indicators
16
- this.sessionIndicators = [
17
- "session",
18
- "sessionid",
19
- "sessid",
20
- "jsessionid",
21
- "phpsessid",
22
- "asp.net_sessionid",
23
- "connect.sid",
24
- "auth",
25
- "token",
26
- "jwt",
27
- "csrf",
28
- ];
29
-
30
- // Regex patterns for cookie detection
31
- this.cookiePatterns = [
32
- // Express/Node.js patterns
33
- /res\.cookie\s*\(\s*['"`]([^'"`]+)['"`]\s*,([^)]+)\)/gi,
34
- /response\.cookie\s*\(\s*['"`]([^'"`]+)['"`]\s*,([^)]+)\)/gi,
35
- /\.setCookie\s*\(\s*['"`]([^'"`]+)['"`]\s*,([^)]+)\)/gi,
36
-
37
- // Set-Cookie header patterns
38
- /setHeader\s*\(\s*['"`]Set-Cookie['"`]\s*,\s*['"`]([^'"`]+)['"`]\s*\)/gi,
39
- /writeHead\s*\([^,]*,\s*{[^}]*['"`]Set-Cookie['"`]\s*:\s*['"`]([^'"`]+)['"`]/gi,
40
-
41
- // Document.cookie assignments
42
- /document\.cookie\s*=\s*['"`]([^'"`]+)['"`]/gi,
43
-
44
- // Session middleware patterns
45
- /session\s*\(\s*{([^}]+)}/gi,
46
- /\.use\s*\(\s*session\s*\(\s*{([^}]+)}/gi,
47
- ];
48
- }
49
-
50
- /**
51
- * Initialize analyzer
52
-
53
- */
54
- async initialize(semanticEngine) {
55
- this.semanticEngine = semanticEngine;
56
- if (process.env.SUNLINT_DEBUG) {
57
- console.log(`🔧 [S031] Regex-based analyzer initialized`);
58
- }
59
- }
60
-
61
- /**
62
- * Check if file should be skipped (test files)
63
- */
64
- shouldSkipFile(filePath) {
65
- const testPatterns = [
66
- /\.test\.(ts|tsx|js|jsx)$/,
67
- /\.spec\.(ts|tsx|js|jsx)$/,
68
- /__tests__\//,
69
- /__mocks__\//,
70
- /\/tests?\//,
71
- /\/fixtures?\//,
72
- ];
73
- return testPatterns.some((pattern) => pattern.test(filePath));
74
- }
75
-
76
- /**
77
- * Analyze file content using regex patterns
78
-
79
- */
80
- async analyze(filePath) {
81
- if (process.env.SUNLINT_DEBUG) {
82
- console.log(`🔍 [S031] Regex-based analysis for: ${filePath}`);
83
- }
84
-
85
- // Skip test files
86
- if (this.shouldSkipFile(filePath)) {
87
- if (process.env.SUNLINT_DEBUG) {
88
- console.log(`⏭ [S031] Skipping test file: ${filePath}`);
89
- }
90
- return [];
91
- }
92
-
93
- let content;
94
- try {
95
- content = fs.readFileSync(filePath, "utf8");
96
- } catch (error) {
97
- if (process.env.SUNLINT_DEBUG) {
98
- console.error(`❌ [S031] File read error:`, error);
99
- }
100
- throw error;
101
- }
102
-
103
- const violations = [];
104
- const lines = content.split("\n");
105
-
106
- // Check each pattern
107
- for (const pattern of this.cookiePatterns) {
108
- this.checkPattern(pattern, content, lines, violations, filePath);
109
- }
110
-
111
- // Check for custom cookie utilities (e.g., StorageUtils.setCookie)
112
- this.checkCustomCookieUtilities(content, lines, violations, filePath);
113
-
114
- return violations;
115
- }
116
-
117
- /**
118
- * Check specific regex pattern for violations
119
-
120
- */
121
- checkPattern(pattern, content, lines, violations, filePath) {
122
- let match;
123
- pattern.lastIndex = 0; // Reset regex state
124
-
125
- while ((match = pattern.exec(content)) !== null) {
126
- const matchText = match[0];
127
- const cookieName = match[1] || "";
128
- const cookieOptions = match[2] || match[1] || "";
129
-
130
- // Check if this is a session cookie
131
- if (!this.isSessionCookie(cookieName, matchText)) {
132
- continue;
133
- }
134
-
135
- // Check if secure flag is present
136
- if (!this.hasSecureFlag(cookieOptions, matchText)) {
137
- const lineNumber = this.getLineNumber(content, match.index);
138
-
139
- this.addViolation(
140
- matchText,
141
- lineNumber,
142
- violations,
143
- `Session cookie "${cookieName || "unknown"}" missing Secure flag`
144
- );
145
- }
146
- }
147
- }
148
-
149
- /**
150
- * Check if cookie name or context indicates session cookie
151
-
152
- */
153
- isSessionCookie(cookieName, matchText) {
154
- const textToCheck = (cookieName + " " + matchText).toLowerCase();
155
- return this.sessionIndicators.some((indicator) =>
156
- textToCheck.includes(indicator.toLowerCase())
157
- );
158
- }
159
-
160
- /**
161
- * Check if secure flag is present in cookie options
162
- */
163
- hasSecureFlag(cookieOptions, fullMatch) {
164
- const textToCheck = cookieOptions + " " + fullMatch;
165
-
166
- // Check for secure config references (likely safe)
167
- const secureConfigPatterns = [
168
- /\bcookieConfig\b/i,
169
- /\bsecureConfig\b/i,
170
- /\bsafeConfig\b/i,
171
- /\bdefaultConfig\b/i,
172
- /\.\.\..*config/i, // spread operator with config
173
- /config.*secure/i,
174
- ];
175
-
176
- // If using a secure config reference, assume it's safe
177
- if (secureConfigPatterns.some((pattern) => pattern.test(textToCheck))) {
178
- return true;
179
- }
180
-
181
- // Check for various secure flag patterns
182
- const securePatterns = [
183
- /secure\s*:\s*true/i,
184
- /secure\s*=\s*true/i,
185
- /;\s*secure\s*[;\s]/i,
186
- /;\s*secure$/i,
187
- /['"`]\s*secure\s*['"`]/i,
188
- /"secure"\s*:\s*true/i,
189
- /'secure'\s*:\s*true/i,
190
- /\bsecure\b/i, // Simple secure keyword
191
- ];
192
-
193
- return securePatterns.some((pattern) => pattern.test(textToCheck));
194
- }
195
-
196
- /**
197
- * Get line number from content position
198
-
199
- */
200
- getLineNumber(content, position) {
201
- const beforeMatch = content.substring(0, position);
202
- return beforeMatch.split("\n").length;
203
- }
204
-
205
- /**
206
- * Add violation to results
207
-
208
- */
209
- addViolation(source, lineNumber, violations, message) {
210
- violations.push({
211
- ruleId: this.ruleId,
212
- source: source.trim(),
213
- category: this.category,
214
- line: lineNumber,
215
- column: 1,
216
- message: `Insecure session cookie: ${message}`,
217
- severity: "error",
218
- });
219
- }
220
-
221
- /**
222
- * Check for custom cookie utility functions with variable names
223
- * Handles cases like: StorageUtils.setCookie(STORAGE_KEY.ACCESS_TOKEN, value)
224
- */
225
- checkCustomCookieUtilities(content, lines, violations, filePath) {
226
- // Pattern to match custom cookie utilities with variable references
227
- // Matches: Utils.setCookie(VARIABLE_NAME, value) or Utils.setCookie(VARIABLE_NAME, value, options)
228
- const customCookiePattern = /(\w+\.setCookie)\s*\(\s*([A-Z_][A-Z0-9_.]*)\s*,\s*([^,)]+)(?:\s*,\s*([^)]*))?\s*\)/gi;
229
-
230
- let match;
231
- customCookiePattern.lastIndex = 0;
232
-
233
- while ((match = customCookiePattern.exec(content)) !== null) {
234
- const methodCall = match[1]; // e.g., "StorageUtils.setCookie"
235
- const cookieNameVar = match[2]; // e.g., "STORAGE_KEY.ACCESS_TOKEN"
236
- const cookieValue = match[3]; // e.g., "response.user?.access_token || ''"
237
- const cookieOptions = match[4] || ""; // e.g., options object if present
238
- const matchText = match[0];
239
-
240
- // Check if this looks like a session cookie based on variable name or value
241
- if (!this.isSessionCookieLikely(cookieNameVar, cookieValue, matchText)) {
242
- continue;
243
- }
244
-
245
- // Check if secure flag is present in options
246
- if (!this.hasSecureFlag(cookieOptions, matchText)) {
247
- const lineNumber = this.getLineNumber(content, match.index);
248
-
249
- // Extract a friendly cookie name from the variable
250
- const friendlyCookieName = this.extractFriendlyCookieName(cookieNameVar);
251
-
252
- this.addViolation(
253
- matchText,
254
- lineNumber,
255
- violations,
256
- `Session cookie from "${friendlyCookieName}" missing Secure flag - add secure flag to options`
257
- );
258
- }
259
- }
260
- }
261
-
262
- /**
263
- * Check if cookie variable name or value suggests it's a session cookie
264
- */
265
- isSessionCookieLikely(varName, value, matchText) {
266
- const textToCheck = (varName + " " + value + " " + matchText).toLowerCase();
267
-
268
- // Check against session indicators
269
- const isSession = this.sessionIndicators.some((indicator) =>
270
- textToCheck.includes(indicator.toLowerCase())
271
- );
272
-
273
- // Also check for common patterns like ACCESS_TOKEN, REFRESH_TOKEN, etc.
274
- const tokenPatterns = [
275
- /access[_-]?token/i,
276
- /refresh[_-]?token/i,
277
- /auth[_-]?token/i,
278
- /id[_-]?token/i,
279
- /session/i,
280
- ];
281
-
282
- return isSession || tokenPatterns.some(pattern => pattern.test(textToCheck));
283
- }
284
-
285
- /**
286
- * Extract a friendly cookie name from variable reference
287
- * e.g., "STORAGE_KEY.ACCESS_TOKEN" -> "ACCESS_TOKEN"
288
- */
289
- extractFriendlyCookieName(varName) {
290
- // If it has a dot, take the last part
291
- const parts = varName.split(".");
292
- return parts[parts.length - 1] || varName;
293
- }
294
- }
295
-
296
- module.exports = S031RegexBasedAnalyzer;