@sun-asterisk/sunlint 1.1.7 → 1.2.0

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 (74) hide show
  1. package/.sunlint.json +1 -1
  2. package/CHANGELOG.md +83 -0
  3. package/README.md +66 -4
  4. package/config/presets/all.json +125 -0
  5. package/config/presets/beginner.json +16 -8
  6. package/config/presets/ci.json +12 -4
  7. package/config/presets/maintainability.json +38 -0
  8. package/config/presets/performance.json +32 -0
  9. package/config/presets/quality.json +103 -0
  10. package/config/presets/recommended.json +36 -12
  11. package/config/presets/security.json +88 -0
  12. package/config/presets/strict.json +15 -5
  13. package/config/rules/rules-registry-generated.json +6312 -0
  14. package/config/rules-summary.json +1941 -0
  15. package/core/adapters/sunlint-rule-adapter.js +452 -0
  16. package/core/analysis-orchestrator.js +4 -4
  17. package/core/config-manager.js +28 -5
  18. package/core/rule-selection-service.js +52 -55
  19. package/docs/CONFIGURATION.md +111 -3
  20. package/docs/LANGUAGE-SPECIFIC-RULES.md +308 -0
  21. package/docs/README.md +3 -0
  22. package/docs/STANDARDIZED-CATEGORY-FILTERING.md +156 -0
  23. package/engines/eslint-engine.js +92 -2
  24. package/engines/heuristic-engine.js +8 -31
  25. package/origin-rules/common-en.md +1320 -0
  26. package/origin-rules/dart-en.md +289 -0
  27. package/origin-rules/java-en.md +60 -0
  28. package/origin-rules/kotlin-mobile-en.md +453 -0
  29. package/origin-rules/reactjs-en.md +102 -0
  30. package/origin-rules/security-en.md +1055 -0
  31. package/origin-rules/swift-en.md +449 -0
  32. package/origin-rules/typescript-en.md +136 -0
  33. package/package.json +6 -5
  34. package/scripts/copy-rules.js +86 -0
  35. package/rules/README.md +0 -252
  36. package/rules/common/C002_no_duplicate_code/analyzer.js +0 -65
  37. package/rules/common/C002_no_duplicate_code/config.json +0 -23
  38. package/rules/common/C003_no_vague_abbreviations/analyzer.js +0 -418
  39. package/rules/common/C003_no_vague_abbreviations/config.json +0 -35
  40. package/rules/common/C006_function_naming/analyzer.js +0 -349
  41. package/rules/common/C006_function_naming/config.json +0 -86
  42. package/rules/common/C010_limit_block_nesting/analyzer.js +0 -389
  43. package/rules/common/C013_no_dead_code/analyzer.js +0 -206
  44. package/rules/common/C014_dependency_injection/analyzer.js +0 -338
  45. package/rules/common/C017_constructor_logic/analyzer.js +0 -314
  46. package/rules/common/C019_log_level_usage/analyzer.js +0 -362
  47. package/rules/common/C019_log_level_usage/config.json +0 -121
  48. package/rules/common/C029_catch_block_logging/analyzer.js +0 -373
  49. package/rules/common/C029_catch_block_logging/config.json +0 -59
  50. package/rules/common/C031_validation_separation/analyzer.js +0 -186
  51. package/rules/common/C041_no_sensitive_hardcode/analyzer.js +0 -292
  52. package/rules/common/C042_boolean_name_prefix/analyzer.js +0 -300
  53. package/rules/common/C043_no_console_or_print/analyzer.js +0 -304
  54. package/rules/common/C047_no_duplicate_retry_logic/analyzer.js +0 -351
  55. package/rules/common/C075_explicit_return_types/analyzer.js +0 -103
  56. package/rules/common/C076_single_test_behavior/analyzer.js +0 -121
  57. package/rules/docs/C002_no_duplicate_code.md +0 -57
  58. package/rules/docs/C031_validation_separation.md +0 -72
  59. package/rules/index.js +0 -149
  60. package/rules/migration/converter.js +0 -385
  61. package/rules/migration/mapping.json +0 -164
  62. package/rules/security/S026_json_schema_validation/analyzer.js +0 -251
  63. package/rules/security/S026_json_schema_validation/config.json +0 -27
  64. package/rules/security/S027_no_hardcoded_secrets/analyzer.js +0 -263
  65. package/rules/security/S027_no_hardcoded_secrets/config.json +0 -29
  66. package/rules/security/S029_csrf_protection/analyzer.js +0 -264
  67. package/rules/tests/C002_no_duplicate_code.test.js +0 -50
  68. package/rules/universal/C010/generic.js +0 -0
  69. package/rules/universal/C010/tree-sitter-analyzer.js +0 -0
  70. package/rules/utils/ast-utils.js +0 -191
  71. package/rules/utils/base-analyzer.js +0 -98
  72. package/rules/utils/pattern-matchers.js +0 -239
  73. package/rules/utils/rule-helpers.js +0 -264
  74. package/rules/utils/severity-constants.js +0 -93
@@ -1,264 +0,0 @@
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
- // Check if file contains CSRF protection patterns
46
- const hasCSRFProtection = this.hasCSRFProtection(content);
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
- if (!hasCSRFProtection) {
67
- violations.push({
68
- ruleId: this.ruleId,
69
- file: filePath,
70
- line: lineNumber,
71
- column: handler.column,
72
- message: `CSRF protection is missing for route handler '${handler.route}'. Apply csurf() or equivalent middleware`,
73
- severity: 'error',
74
- code: trimmedLine,
75
- type: 'missing_csrf_protection',
76
- confidence: handler.confidence,
77
- suggestion: 'Add CSRF middleware: app.use(csurf()) or use CSRF token validation'
78
- });
79
- }
80
- });
81
- });
82
-
83
- return violations;
84
- }
85
-
86
- isCommentOrImport(line) {
87
- const trimmed = line.trim();
88
- return trimmed.startsWith('//') ||
89
- trimmed.startsWith('/*') ||
90
- trimmed.startsWith('*') ||
91
- trimmed.startsWith('import ') ||
92
- trimmed.startsWith('export ');
93
- }
94
-
95
- findRouteHandlers(line, originalLine) {
96
- const handlers = [];
97
- const foundMatches = new Set(); // Prevent duplicates
98
-
99
- // Only detect Express.js route patterns, not HTTP client methods
100
- const routePatterns = [
101
- // Express method with middleware: app.post('/path', middleware, handler)
102
- {
103
- regex: /\b(app|router|server)\s*\.\s*(post|put|delete|patch)\s*\(\s*(['"`][^'"`]*['"`])\s*,/gi,
104
- type: 'express_route_with_middleware',
105
- priority: 1 // Higher priority to check first
106
- },
107
- // app.post(), router.put(), etc.
108
- {
109
- regex: /\b(app|router|server)\s*\.\s*(post|put|delete|patch)\s*\(\s*(['"`][^'"`]*['"`])/gi,
110
- type: 'express_route',
111
- priority: 2
112
- }
113
- ];
114
-
115
- // Sort by priority to avoid duplicates
116
- routePatterns.sort((a, b) => a.priority - b.priority);
117
-
118
- routePatterns.forEach(pattern => {
119
- let match;
120
- while ((match = pattern.regex.exec(line)) !== null) {
121
- const instance = match[1]; // app, router, server
122
- const method = match[2]; // post, put, delete, patch
123
- const route = match[3]; // '/path'
124
- const matchKey = `${instance}.${method}(${route})`; // Unique key
125
-
126
- // Skip duplicates
127
- if (foundMatches.has(matchKey)) {
128
- continue;
129
- }
130
-
131
- // Skip if it's clearly not Express.js context
132
- if (this.isNotExpressContext(line, instance)) {
133
- continue;
134
- }
135
-
136
- foundMatches.add(matchKey);
137
- handlers.push({
138
- type: pattern.type,
139
- instance: instance,
140
- method: method,
141
- route: route.replace(/['"]/g, ''),
142
- column: match.index + 1,
143
- confidence: this.calculateConfidence(line, pattern.type)
144
- });
145
- }
146
- });
147
-
148
- return handlers;
149
- }
150
-
151
- isNotExpressContext(line, instance) {
152
- // Skip HTTP client methods (like axios, fetch wrappers)
153
- const clientPatterns = [
154
- 'public ', 'private ', 'protected ', // Class methods
155
- 'async ', 'function ', // Function definitions
156
- 'const ', 'let ', 'var ', // Variable assignments
157
- ': Promise<', ': BaseResponse<', // TypeScript return types
158
- 'this.http', 'httpClient', // HTTP client instances
159
- 'axios.', 'fetch(', // HTTP client calls
160
- ];
161
-
162
- const lowerLine = line.toLowerCase();
163
-
164
- // If line contains client patterns, likely not Express route
165
- const hasClientPattern = clientPatterns.some(pattern =>
166
- lowerLine.includes(pattern.toLowerCase())
167
- );
168
-
169
- if (hasClientPattern) {
170
- return true;
171
- }
172
-
173
- // If instance name suggests HTTP client, skip
174
- const clientInstanceNames = ['httpclient', 'client', 'api', 'service'];
175
- if (clientInstanceNames.includes(instance.toLowerCase())) {
176
- return true;
177
- }
178
-
179
- return false;
180
- }
181
-
182
- // Check if the file content suggests this is a mock/test rather than real Express app
183
- isMockOrTestContext(content, instance) {
184
- const lowerContent = content.toLowerCase();
185
-
186
- // Look for mock object definitions
187
- const mockPatterns = [
188
- `const ${instance.toLowerCase()} = {`,
189
- `let ${instance.toLowerCase()} = {`,
190
- `var ${instance.toLowerCase()} = {`,
191
- `${instance.toLowerCase()}: {`,
192
- ];
193
-
194
- const hasMockDefinition = mockPatterns.some(pattern =>
195
- lowerContent.includes(pattern)
196
- );
197
-
198
- if (hasMockDefinition) {
199
- return true;
200
- }
201
-
202
- // Check for test file patterns
203
- const testIndicators = ['.test.', '.spec.', '__tests__', 'test case', 'mock'];
204
- const isTestContext = testIndicators.some(indicator =>
205
- lowerContent.includes(indicator)
206
- );
207
-
208
- return isTestContext;
209
- }
210
-
211
- hasCSRFProtection(content) {
212
- const csrfPatterns = [
213
- // Middleware usage
214
- 'csurf()',
215
- 'csrfProtection',
216
- 'verifyCsrfToken',
217
- 'checkCsrf',
218
- 'csrf-token',
219
- '_csrf',
220
-
221
- // Manual CSRF checks
222
- 'req.csrfToken',
223
- 'csrf.verify',
224
- 'validateCSRF',
225
-
226
- // Security headers
227
- 'x-csrf-token',
228
- 'x-xsrf-token',
229
-
230
- // Framework-specific
231
- 'protect_from_forgery', // Rails
232
- '@csrf', // Laravel
233
- ];
234
-
235
- const lowerContent = content.toLowerCase();
236
-
237
- return csrfPatterns.some(pattern =>
238
- lowerContent.includes(pattern.toLowerCase())
239
- );
240
- }
241
-
242
- calculateConfidence(line, patternType) {
243
- let confidence = 0.8;
244
-
245
- // Higher confidence for clear Express patterns
246
- if (patternType === 'express_route_with_middleware') {
247
- confidence += 0.1;
248
- }
249
-
250
- // Lower confidence if mixed with client-like patterns
251
- const clientIndicators = ['public', 'class', 'Promise<', 'async'];
252
- const hasClientIndicators = clientIndicators.some(indicator =>
253
- line.includes(indicator)
254
- );
255
-
256
- if (hasClientIndicators) {
257
- confidence -= 0.3;
258
- }
259
-
260
- return Math.max(0.3, Math.min(1.0, confidence));
261
- }
262
- }
263
-
264
- module.exports = new S029Analyzer();
@@ -1,50 +0,0 @@
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
@@ -1,191 +0,0 @@
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 };
@@ -1,98 +0,0 @@
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;