@sun-asterisk/sunlint 1.2.1 → 1.2.2

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.
Files changed (77) hide show
  1. package/config/rule-analysis-strategies.js +18 -2
  2. package/engines/eslint-engine.js +9 -11
  3. package/engines/heuristic-engine.js +55 -31
  4. package/package.json +2 -1
  5. package/rules/README.md +252 -0
  6. package/rules/common/C002_no_duplicate_code/analyzer.js +65 -0
  7. package/rules/common/C002_no_duplicate_code/config.json +23 -0
  8. package/rules/common/C003_no_vague_abbreviations/analyzer.js +418 -0
  9. package/rules/common/C003_no_vague_abbreviations/config.json +35 -0
  10. package/rules/common/C006_function_naming/analyzer.js +504 -0
  11. package/rules/common/C006_function_naming/config.json +86 -0
  12. package/rules/common/C006_function_naming/smart-analyzer.js +503 -0
  13. package/rules/common/C010_limit_block_nesting/analyzer.js +389 -0
  14. package/rules/common/C012_command_query_separation/analyzer.js +481 -0
  15. package/rules/common/C012_command_query_separation/ast-analyzer.js +495 -0
  16. package/rules/common/C013_no_dead_code/analyzer.js +206 -0
  17. package/rules/common/C014_dependency_injection/analyzer.js +338 -0
  18. package/rules/common/C017_constructor_logic/analyzer.js +314 -0
  19. package/rules/common/C019_log_level_usage/analyzer.js +362 -0
  20. package/rules/common/C019_log_level_usage/config.json +121 -0
  21. package/rules/common/C029_catch_block_logging/analyzer-backup.js +426 -0
  22. package/rules/common/C029_catch_block_logging/analyzer-fixed.js +130 -0
  23. package/rules/common/C029_catch_block_logging/analyzer-multi-tech.js +487 -0
  24. package/rules/common/C029_catch_block_logging/analyzer-simple.js +110 -0
  25. package/rules/common/C029_catch_block_logging/analyzer-smart-pipeline.js +755 -0
  26. package/rules/common/C029_catch_block_logging/analyzer.js +129 -0
  27. package/rules/common/C029_catch_block_logging/ast-analyzer-backup.js +441 -0
  28. package/rules/common/C029_catch_block_logging/ast-analyzer-new.js +127 -0
  29. package/rules/common/C029_catch_block_logging/ast-analyzer.js +133 -0
  30. package/rules/common/C029_catch_block_logging/cfg-analyzer.js +408 -0
  31. package/rules/common/C029_catch_block_logging/config.json +59 -0
  32. package/rules/common/C029_catch_block_logging/dataflow-analyzer.js +454 -0
  33. package/rules/common/C029_catch_block_logging/multi-language-ast-engine.js +700 -0
  34. package/rules/common/C029_catch_block_logging/pattern-learning-analyzer.js +568 -0
  35. package/rules/common/C029_catch_block_logging/semantic-analyzer.js +459 -0
  36. package/rules/common/C031_validation_separation/analyzer.js +186 -0
  37. package/rules/common/C041_no_sensitive_hardcode/analyzer.js +292 -0
  38. package/rules/common/C041_no_sensitive_hardcode/ast-analyzer.js +296 -0
  39. package/rules/common/C042_boolean_name_prefix/analyzer.js +300 -0
  40. package/rules/common/C043_no_console_or_print/analyzer.js +431 -0
  41. package/rules/common/C047_no_duplicate_retry_logic/analyzer.js +590 -0
  42. package/rules/common/C075_explicit_return_types/analyzer.js +103 -0
  43. package/rules/common/C076_single_test_behavior/analyzer.js +121 -0
  44. package/rules/docs/C002_no_duplicate_code.md +57 -0
  45. package/rules/docs/C031_validation_separation.md +72 -0
  46. package/rules/index.js +155 -0
  47. package/rules/migration/converter.js +385 -0
  48. package/rules/migration/mapping.json +164 -0
  49. package/rules/parser/constants.js +31 -0
  50. package/rules/parser/file-config.js +80 -0
  51. package/rules/parser/rule-parser-simple.js +305 -0
  52. package/rules/parser/rule-parser.js +527 -0
  53. package/rules/security/S015_insecure_tls_certificate/analyzer.js +150 -0
  54. package/rules/security/S015_insecure_tls_certificate/ast-analyzer.js +237 -0
  55. package/rules/security/S023_no_json_injection/analyzer.js +278 -0
  56. package/rules/security/S023_no_json_injection/ast-analyzer.js +359 -0
  57. package/rules/security/S026_json_schema_validation/analyzer.js +251 -0
  58. package/rules/security/S026_json_schema_validation/config.json +27 -0
  59. package/rules/security/S027_no_hardcoded_secrets/analyzer.js +436 -0
  60. package/rules/security/S027_no_hardcoded_secrets/config.json +29 -0
  61. package/rules/security/S029_csrf_protection/analyzer.js +330 -0
  62. package/rules/tests/C002_no_duplicate_code.test.js +50 -0
  63. package/rules/universal/C010/generic.js +0 -0
  64. package/rules/universal/C010/tree-sitter-analyzer.js +0 -0
  65. package/rules/utils/ast-utils.js +191 -0
  66. package/rules/utils/base-analyzer.js +98 -0
  67. package/rules/utils/pattern-matchers.js +239 -0
  68. package/rules/utils/rule-helpers.js +264 -0
  69. package/rules/utils/severity-constants.js +93 -0
  70. package/scripts/generate_insights.js +188 -0
  71. package/scripts/merge-reports.js +0 -424
  72. package/scripts/test-scripts/README.md +0 -22
  73. package/scripts/test-scripts/test-c041-comparison.js +0 -114
  74. package/scripts/test-scripts/test-c041-eslint.js +0 -67
  75. package/scripts/test-scripts/test-eslint-rules.js +0 -146
  76. package/scripts/test-scripts/test-real-world.js +0 -44
  77. package/scripts/test-scripts/test-rules-on-real-projects.js +0 -86
@@ -0,0 +1,330 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ class S029Analyzer {
5
+ constructor() {
6
+ this.ruleId = 'S029';
7
+ this.ruleName = 'CSRF Protection Required';
8
+ this.description = 'Cần áp dụng cơ chế chống CSRF cho các chức năng xác thực';
9
+ }
10
+
11
+ async analyze(files, language, options = {}) {
12
+ const violations = [];
13
+
14
+ for (const filePath of files) {
15
+ if (options.verbose) {
16
+ console.log(`🔍 Running S029 analysis on ${path.basename(filePath)}`);
17
+ }
18
+
19
+ try {
20
+ const content = fs.readFileSync(filePath, 'utf8');
21
+ const fileViolations = await this.analyzeFile(filePath, content, language, options);
22
+ violations.push(...fileViolations);
23
+ } catch (error) {
24
+ console.warn(`⚠️ Failed to analyze ${filePath}: ${error.message}`);
25
+ }
26
+ }
27
+
28
+ return violations;
29
+ }
30
+
31
+ async analyzeFile(filePath, content, language, config) {
32
+ switch (language) {
33
+ case 'typescript':
34
+ case 'javascript':
35
+ return this.analyzeTypeScript(filePath, content, config);
36
+ default:
37
+ return [];
38
+ }
39
+ }
40
+
41
+ async analyzeTypeScript(filePath, content, config) {
42
+ const violations = [];
43
+ const lines = content.split('\n');
44
+
45
+ // Find lines where global CSRF protection is applied
46
+ const globalCSRFLines = this.findGlobalCSRFLines(lines);
47
+
48
+ lines.forEach((line, index) => {
49
+ const lineNumber = index + 1;
50
+ const trimmedLine = line.trim();
51
+
52
+ // Skip comments and imports
53
+ if (this.isCommentOrImport(trimmedLine)) {
54
+ return;
55
+ }
56
+
57
+ // Look for Express route handlers that need CSRF protection
58
+ const routeHandlers = this.findRouteHandlers(trimmedLine, line);
59
+
60
+ routeHandlers.forEach(handler => {
61
+ // Skip if this is a mock or test context
62
+ if (this.isMockOrTestContext(content, handler.instance)) {
63
+ return;
64
+ }
65
+
66
+ // Check if global CSRF protection was applied before this route
67
+ const hasGlobalCSRFProtection = this.hasGlobalCSRFProtectionBeforeLine(globalCSRFLines, index, handler.instance);
68
+
69
+ // Check if this specific route has CSRF protection
70
+ const hasRouteCSRFProtection = this.hasRouteSpecificCSRFProtection(lines, index, handler);
71
+
72
+ if (!hasGlobalCSRFProtection && !hasRouteCSRFProtection) {
73
+ violations.push({
74
+ ruleId: this.ruleId,
75
+ file: filePath,
76
+ line: lineNumber,
77
+ column: handler.column,
78
+ message: `CSRF protection is missing for route handler '${handler.route}'. Apply csurf() or equivalent middleware`,
79
+ severity: 'error',
80
+ code: trimmedLine,
81
+ type: 'missing_csrf_protection',
82
+ confidence: handler.confidence,
83
+ suggestion: 'Add CSRF middleware: app.use(csurf()) or use CSRF token validation'
84
+ });
85
+ }
86
+ });
87
+ });
88
+
89
+ return violations;
90
+ }
91
+
92
+ isCommentOrImport(line) {
93
+ const trimmed = line.trim();
94
+ return trimmed.startsWith('//') ||
95
+ trimmed.startsWith('/*') ||
96
+ trimmed.startsWith('*') ||
97
+ trimmed.startsWith('import ') ||
98
+ trimmed.startsWith('export ');
99
+ }
100
+
101
+ findRouteHandlers(line, originalLine) {
102
+ const handlers = [];
103
+ const foundMatches = new Set(); // Prevent duplicates
104
+
105
+ // Only detect Express.js route patterns, not HTTP client methods
106
+ const routePatterns = [
107
+ // Express method with middleware: app.post('/path', middleware, handler)
108
+ {
109
+ regex: /\b(app|router|server)\s*\.\s*(post|put|delete|patch)\s*\(\s*(['"`][^'"`]*['"`])\s*,/gi,
110
+ type: 'express_route_with_middleware',
111
+ priority: 1 // Higher priority to check first
112
+ },
113
+ // app.post(), router.put(), etc.
114
+ {
115
+ regex: /\b(app|router|server)\s*\.\s*(post|put|delete|patch)\s*\(\s*(['"`][^'"`]*['"`])/gi,
116
+ type: 'express_route',
117
+ priority: 2
118
+ }
119
+ ];
120
+
121
+ // Sort by priority to avoid duplicates
122
+ routePatterns.sort((a, b) => a.priority - b.priority);
123
+
124
+ routePatterns.forEach(pattern => {
125
+ let match;
126
+ while ((match = pattern.regex.exec(line)) !== null) {
127
+ const instance = match[1]; // app, router, server
128
+ const method = match[2]; // post, put, delete, patch
129
+ const route = match[3]; // '/path'
130
+ const matchKey = `${instance}.${method}(${route})`; // Unique key
131
+
132
+ // Skip duplicates
133
+ if (foundMatches.has(matchKey)) {
134
+ continue;
135
+ }
136
+
137
+ // Skip if it's clearly not Express.js context
138
+ if (this.isNotExpressContext(line, instance)) {
139
+ continue;
140
+ }
141
+
142
+ foundMatches.add(matchKey);
143
+ handlers.push({
144
+ type: pattern.type,
145
+ instance: instance,
146
+ method: method,
147
+ route: route.replace(/['"]/g, ''),
148
+ column: match.index + 1,
149
+ confidence: this.calculateConfidence(line, pattern.type)
150
+ });
151
+ }
152
+ });
153
+
154
+ return handlers;
155
+ }
156
+
157
+ isNotExpressContext(line, instance) {
158
+ // Skip HTTP client methods (like axios, fetch wrappers)
159
+ const clientPatterns = [
160
+ 'public ', 'private ', 'protected ', // Class methods
161
+ 'async ', 'function ', // Function definitions
162
+ 'const ', 'let ', 'var ', // Variable assignments
163
+ ': Promise<', ': BaseResponse<', // TypeScript return types
164
+ 'this.http', 'httpClient', // HTTP client instances
165
+ 'axios.', 'fetch(', // HTTP client calls
166
+ ];
167
+
168
+ const lowerLine = line.toLowerCase();
169
+
170
+ // If line contains client patterns, likely not Express route
171
+ const hasClientPattern = clientPatterns.some(pattern =>
172
+ lowerLine.includes(pattern.toLowerCase())
173
+ );
174
+
175
+ if (hasClientPattern) {
176
+ return true;
177
+ }
178
+
179
+ // If instance name suggests HTTP client, skip
180
+ const clientInstanceNames = ['httpclient', 'client', 'api', 'service'];
181
+ if (clientInstanceNames.includes(instance.toLowerCase())) {
182
+ return true;
183
+ }
184
+
185
+ return false;
186
+ }
187
+
188
+ // Check if the file content suggests this is a mock/test rather than real Express app
189
+ isMockOrTestContext(content, instance) {
190
+ const lowerContent = content.toLowerCase();
191
+
192
+ // Look for mock object definitions
193
+ const mockPatterns = [
194
+ `const ${instance.toLowerCase()} = {`,
195
+ `let ${instance.toLowerCase()} = {`,
196
+ `var ${instance.toLowerCase()} = {`,
197
+ `${instance.toLowerCase()}: {`,
198
+ ];
199
+
200
+ const hasMockDefinition = mockPatterns.some(pattern =>
201
+ lowerContent.includes(pattern)
202
+ );
203
+
204
+ if (hasMockDefinition) {
205
+ return true;
206
+ }
207
+
208
+ // Check for test file patterns
209
+ const testIndicators = ['.test.', '.spec.', '__tests__', 'test case', 'mock'];
210
+ const isTestContext = testIndicators.some(indicator =>
211
+ lowerContent.includes(indicator)
212
+ );
213
+
214
+ return isTestContext;
215
+ }
216
+
217
+ hasCSRFProtection(content) {
218
+ const csrfPatterns = [
219
+ // Middleware usage
220
+ 'csurf()',
221
+ 'csrfProtection',
222
+ 'verifyCsrfToken',
223
+ 'checkCsrf',
224
+ 'csrf-token',
225
+ '_csrf',
226
+
227
+ // Manual CSRF checks
228
+ 'req.csrfToken',
229
+ 'csrf.verify',
230
+ 'validateCSRF',
231
+
232
+ // Security headers
233
+ 'x-csrf-token',
234
+ 'x-xsrf-token',
235
+
236
+ // Framework-specific
237
+ 'protect_from_forgery', // Rails
238
+ '@csrf', // Laravel
239
+ ];
240
+
241
+ const lowerContent = content.toLowerCase();
242
+
243
+ return csrfPatterns.some(pattern =>
244
+ lowerContent.includes(pattern.toLowerCase())
245
+ );
246
+ }
247
+
248
+ hasGlobalCSRFProtection(content) {
249
+ // Check for global CSRF middleware: app.use(csurf())
250
+ const globalPatterns = [
251
+ /\b(app|router|server)\s*\.\s*use\s*\(\s*csurf\(\)/gi,
252
+ /\b(app|router|server)\s*\.\s*use\s*\(\s*csrfProtection/gi,
253
+ /\b(app|router|server)\s*\.\s*use\s*\(\s*csrf\(\)/gi,
254
+ ];
255
+
256
+ return globalPatterns.some(pattern => pattern.test(content));
257
+ }
258
+
259
+ findGlobalCSRFLines(lines) {
260
+ const csrfLines = [];
261
+
262
+ lines.forEach((line, index) => {
263
+ const globalPatterns = [
264
+ /\b(app|router|server)\s*\.\s*use\s*\(\s*csurf\(\)/gi,
265
+ /\b(app|router|server)\s*\.\s*use\s*\(\s*csrfProtection/gi,
266
+ /\b(app|router|server)\s*\.\s*use\s*\(\s*csrf\(\)/gi,
267
+ ];
268
+
269
+ globalPatterns.forEach(pattern => {
270
+ let match;
271
+ while ((match = pattern.exec(line)) !== null) {
272
+ csrfLines.push({
273
+ lineIndex: index,
274
+ instance: match[1], // app, router, server
275
+ line: line.trim()
276
+ });
277
+ }
278
+ });
279
+ });
280
+
281
+ return csrfLines;
282
+ }
283
+
284
+ hasGlobalCSRFProtectionBeforeLine(globalCSRFLines, routeLineIndex, routeInstance) {
285
+ // Check if any global CSRF protection was applied for this instance before this route
286
+ return globalCSRFLines.some(csrf =>
287
+ csrf.instance === routeInstance && csrf.lineIndex < routeLineIndex
288
+ );
289
+ }
290
+
291
+ hasRouteSpecificCSRFProtection(lines, currentIndex, handler) {
292
+ // Check if the route has CSRF middleware as parameter
293
+ // e.g. app.post('/path', csrfProtection, handler)
294
+ const currentLine = lines[currentIndex];
295
+
296
+ const csrfMiddlewarePatterns = [
297
+ 'csrfProtection',
298
+ 'csurf()',
299
+ 'verifyCsrfToken',
300
+ 'checkCsrf',
301
+ ];
302
+
303
+ return csrfMiddlewarePatterns.some(pattern =>
304
+ currentLine.includes(pattern)
305
+ );
306
+ }
307
+
308
+ calculateConfidence(line, patternType) {
309
+ let confidence = 0.8;
310
+
311
+ // Higher confidence for clear Express patterns
312
+ if (patternType === 'express_route_with_middleware') {
313
+ confidence += 0.1;
314
+ }
315
+
316
+ // Lower confidence if mixed with client-like patterns
317
+ const clientIndicators = ['public', 'class', 'Promise<', 'async'];
318
+ const hasClientIndicators = clientIndicators.some(indicator =>
319
+ line.includes(indicator)
320
+ );
321
+
322
+ if (hasClientIndicators) {
323
+ confidence -= 0.3;
324
+ }
325
+
326
+ return Math.max(0.3, Math.min(1.0, confidence));
327
+ }
328
+ }
329
+
330
+ module.exports = new S029Analyzer();
@@ -0,0 +1,50 @@
1
+ /**
2
+ * C002_no_duplicate_code - Rule Tests
3
+ * Tests for heuristic rule analyzer
4
+ */
5
+
6
+ const C002_no_duplicate_codeAnalyzer = require('./analyzer');
7
+
8
+ describe('C002_no_duplicate_code Heuristic Rule', () => {
9
+ let analyzer;
10
+
11
+ beforeEach(() => {
12
+ analyzer = new C002_no_duplicate_codeAnalyzer();
13
+ });
14
+
15
+ describe('Valid Code', () => {
16
+ test('should not report violations for valid code', () => {
17
+ const code = `
18
+ // TODO: Add valid code examples
19
+ `;
20
+
21
+ const violations = analyzer.analyze(code, 'test.js');
22
+ expect(violations).toHaveLength(0);
23
+ });
24
+ });
25
+
26
+ describe('Invalid Code', () => {
27
+ test('should report violations for invalid code', () => {
28
+ const code = `
29
+ // TODO: Add invalid code examples
30
+ `;
31
+
32
+ const violations = analyzer.analyze(code, 'test.js');
33
+ expect(violations.length).toBeGreaterThan(0);
34
+ expect(violations[0].ruleId).toBe('C002_no_duplicate_code');
35
+ });
36
+ });
37
+
38
+ describe('Edge Cases', () => {
39
+ test('should handle empty code', () => {
40
+ const violations = analyzer.analyze('', 'test.js');
41
+ expect(violations).toHaveLength(0);
42
+ });
43
+
44
+ test('should handle syntax errors gracefully', () => {
45
+ const code = 'invalid javascript syntax {{{';
46
+ const violations = analyzer.analyze(code, 'test.js');
47
+ expect(Array.isArray(violations)).toBe(true);
48
+ });
49
+ });
50
+ });
File without changes
File without changes
@@ -0,0 +1,191 @@
1
+ /**
2
+ * AST Utilities for Heuristic Rules
3
+ * Provides AST parsing and traversal utilities for rule analyzers
4
+ */
5
+
6
+ class ASTUtils {
7
+ constructor() {
8
+ this.supportedLanguages = ['javascript', 'typescript'];
9
+ }
10
+
11
+ /**
12
+ * Parse code content into AST
13
+ * @param {string} content - Code content
14
+ * @param {string} language - Language (javascript, typescript)
15
+ * @returns {Object|null} Parsed AST or null if parsing fails
16
+ */
17
+ parse(content, language = 'javascript') {
18
+ try {
19
+ // TODO: Implement proper AST parsing
20
+ // For now, return a simple representation
21
+ return {
22
+ type: 'Program',
23
+ body: [],
24
+ language,
25
+ sourceCode: content
26
+ };
27
+ } catch (error) {
28
+ console.warn('AST parsing failed:', error.message);
29
+ return null;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Find function declarations in code
35
+ * @param {string} content - Code content
36
+ * @returns {Array} Array of function matches
37
+ */
38
+ findFunctions(content) {
39
+ const functions = [];
40
+ const functionRegex = /(?:function\s+(\w+)|(\w+)\s*=\s*function|(\w+)\s*=\s*\([^)]*\)\s*=>)/g;
41
+ let match;
42
+
43
+ while ((match = functionRegex.exec(content)) !== null) {
44
+ const line = this.getLineNumber(content, match.index);
45
+ const functionName = match[1] || match[2] || match[3];
46
+
47
+ functions.push({
48
+ name: functionName,
49
+ line: line,
50
+ column: match.index - this.getLineStart(content, match.index),
51
+ match: match[0]
52
+ });
53
+ }
54
+
55
+ return functions;
56
+ }
57
+
58
+ /**
59
+ * Find variable declarations
60
+ * @param {string} content - Code content
61
+ * @returns {Array} Array of variable matches
62
+ */
63
+ findVariables(content) {
64
+ const variables = [];
65
+ const varRegex = /(?:var|let|const)\s+(\w+)/g;
66
+ let match;
67
+
68
+ while ((match = varRegex.exec(content)) !== null) {
69
+ const line = this.getLineNumber(content, match.index);
70
+
71
+ variables.push({
72
+ name: match[1],
73
+ line: line,
74
+ column: match.index - this.getLineStart(content, match.index),
75
+ type: match[0].split(' ')[0] // var, let, const
76
+ });
77
+ }
78
+
79
+ return variables;
80
+ }
81
+
82
+ /**
83
+ * Find import/require statements
84
+ * @param {string} content - Code content
85
+ * @returns {Array} Array of import matches
86
+ */
87
+ findImports(content) {
88
+ const imports = [];
89
+
90
+ // ES6 imports
91
+ const importRegex = /import\s+.*?from\s+['"]([^'"]+)['"]/g;
92
+ let match;
93
+
94
+ while ((match = importRegex.exec(content)) !== null) {
95
+ const line = this.getLineNumber(content, match.index);
96
+ imports.push({
97
+ type: 'import',
98
+ module: match[1],
99
+ line: line,
100
+ match: match[0]
101
+ });
102
+ }
103
+
104
+ // CommonJS requires
105
+ const requireRegex = /require\(['"]([^'"]+)['"]\)/g;
106
+ while ((match = requireRegex.exec(content)) !== null) {
107
+ const line = this.getLineNumber(content, match.index);
108
+ imports.push({
109
+ type: 'require',
110
+ module: match[1],
111
+ line: line,
112
+ match: match[0]
113
+ });
114
+ }
115
+
116
+ return imports;
117
+ }
118
+
119
+ /**
120
+ * Get line number for a character index
121
+ * @param {string} content - Code content
122
+ * @param {number} index - Character index
123
+ * @returns {number} Line number (1-based)
124
+ */
125
+ getLineNumber(content, index) {
126
+ return content.substring(0, index).split('\n').length;
127
+ }
128
+
129
+ /**
130
+ * Get line start position for a character index
131
+ * @param {string} content - Code content
132
+ * @param {number} index - Character index
133
+ * @returns {number} Line start index
134
+ */
135
+ getLineStart(content, index) {
136
+ const beforeIndex = content.substring(0, index);
137
+ const lastNewline = beforeIndex.lastIndexOf('\n');
138
+ return lastNewline === -1 ? 0 : lastNewline + 1;
139
+ }
140
+
141
+ /**
142
+ * Get line content for a line number
143
+ * @param {string} content - Code content
144
+ * @param {number} lineNumber - Line number (1-based)
145
+ * @returns {string} Line content
146
+ */
147
+ getLineContent(content, lineNumber) {
148
+ const lines = content.split('\n');
149
+ return lines[lineNumber - 1] || '';
150
+ }
151
+
152
+ /**
153
+ * Check if position is inside a comment
154
+ * @param {string} content - Code content
155
+ * @param {number} index - Character index
156
+ * @returns {boolean} True if inside comment
157
+ */
158
+ isInComment(content, index) {
159
+ const beforeIndex = content.substring(0, index);
160
+
161
+ // Single line comment
162
+ const lastLineStart = beforeIndex.lastIndexOf('\n');
163
+ const lineContent = beforeIndex.substring(lastLineStart + 1);
164
+ if (lineContent.includes('//')) {
165
+ return true;
166
+ }
167
+
168
+ // Block comment
169
+ const lastBlockStart = beforeIndex.lastIndexOf('/*');
170
+ const lastBlockEnd = beforeIndex.lastIndexOf('*/');
171
+
172
+ return lastBlockStart > lastBlockEnd;
173
+ }
174
+
175
+ /**
176
+ * Extract function parameters
177
+ * @param {string} functionDeclaration - Function declaration string
178
+ * @returns {Array} Array of parameter names
179
+ */
180
+ extractParameters(functionDeclaration) {
181
+ const paramMatch = functionDeclaration.match(/\(([^)]*)\)/);
182
+ if (!paramMatch || !paramMatch[1]) return [];
183
+
184
+ return paramMatch[1]
185
+ .split(',')
186
+ .map(param => param.trim().split('=')[0].trim()) // Handle default parameters
187
+ .filter(param => param.length > 0);
188
+ }
189
+ }
190
+
191
+ module.exports = { ASTUtils };
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Base Analyzer Class for SunLint Rules
3
+ * Provides common functionality and consistent severity management
4
+ */
5
+
6
+ const { getSeverity, isValidSeverity } = require('./severity-constants');
7
+
8
+ class BaseAnalyzer {
9
+ constructor(ruleId, ruleName, description, category = 'QUALITY') {
10
+ this.ruleId = ruleId;
11
+ this.ruleName = ruleName;
12
+ this.description = description;
13
+ this.category = category;
14
+
15
+ // Severity will be determined dynamically
16
+ this._severity = null;
17
+ }
18
+
19
+ /**
20
+ * Get severity for this rule, considering config overrides
21
+ * @param {Object} config - Configuration object
22
+ * @returns {string} Severity level
23
+ */
24
+ getSeverity(config = {}) {
25
+ // Check if already cached
26
+ if (this._severity) {
27
+ return this._severity;
28
+ }
29
+
30
+ // Get from config override
31
+ const configOverride = config?.rules?.[this.ruleId]?.severity ||
32
+ config?.rules?.[this.ruleId];
33
+
34
+ this._severity = getSeverity(this.ruleId, this.category, configOverride);
35
+ return this._severity;
36
+ }
37
+
38
+ /**
39
+ * Set severity (for backward compatibility or testing)
40
+ * @param {string} severity - Severity level
41
+ */
42
+ setSeverity(severity) {
43
+ if (!isValidSeverity(severity)) {
44
+ console.warn(`Invalid severity '${severity}' for rule ${this.ruleId}. Using default.`);
45
+ return;
46
+ }
47
+ this._severity = severity;
48
+ }
49
+
50
+ /**
51
+ * Create a violation object with consistent structure
52
+ * @param {Object} params - Violation parameters
53
+ * @returns {Object} Formatted violation
54
+ */
55
+ createViolation(params) {
56
+ const {
57
+ filePath,
58
+ line,
59
+ column,
60
+ message,
61
+ source,
62
+ suggestion,
63
+ additionalData = {}
64
+ } = params;
65
+
66
+ return {
67
+ ruleId: this.ruleId,
68
+ severity: this._severity || this.getSeverity(),
69
+ message: message || this.description,
70
+ filePath,
71
+ line,
72
+ column,
73
+ source,
74
+ suggestion,
75
+ category: this.category,
76
+ ...additionalData
77
+ };
78
+ }
79
+
80
+ /**
81
+ * Check if rule is enabled based on severity
82
+ * @param {Object} config - Configuration
83
+ * @returns {boolean} True if rule should run
84
+ */
85
+ isEnabled(config = {}) {
86
+ const severity = this.getSeverity(config);
87
+ return severity !== 'off';
88
+ }
89
+
90
+ /**
91
+ * Abstract analyze method - must be implemented by subclasses
92
+ */
93
+ async analyze(files, language, config) {
94
+ throw new Error(`analyze() method must be implemented by ${this.constructor.name}`);
95
+ }
96
+ }
97
+
98
+ module.exports = BaseAnalyzer;