@sun-asterisk/sunlint 1.2.2 → 1.3.1

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 (124) hide show
  1. package/CHANGELOG.md +107 -1
  2. package/CONTRIBUTING.md +1654 -66
  3. package/README.md +19 -6
  4. package/config/ci-cd.json +54 -0
  5. package/config/development.json +56 -0
  6. package/config/engines/engines-enhanced.json +86 -0
  7. package/config/engines/semantic-config.json +114 -0
  8. package/config/eslint-rule-mapping.json +50 -38
  9. package/config/large-project.json +143 -0
  10. package/config/presets/all.json +0 -1
  11. package/config/release.json +70 -0
  12. package/config/rule-analysis-strategies.js +23 -4
  13. package/config/rules/S027-categories.json +122 -0
  14. package/config/rules/enhanced-rules-registry.json +2564 -0
  15. package/config/rules/rules-registry-generated.json +785 -837
  16. package/config/rules/rules-registry.json +13 -1
  17. package/core/adapters/sunlint-rule-adapter.js +25 -30
  18. package/core/analysis-orchestrator.js +42 -2
  19. package/core/categories.js +52 -0
  20. package/core/category-constants.js +39 -0
  21. package/core/cli-action-handler.js +53 -32
  22. package/core/cli-program.js +11 -3
  23. package/core/config-manager.js +111 -0
  24. package/core/config-merger.js +88 -0
  25. package/core/constants/categories.js +168 -0
  26. package/core/constants/defaults.js +165 -0
  27. package/core/constants/engines.js +185 -0
  28. package/core/constants/index.js +30 -0
  29. package/core/constants/rules.js +215 -0
  30. package/core/enhanced-rules-registry.js +3 -3
  31. package/core/file-targeting-service.js +128 -7
  32. package/core/interfaces/rule-plugin.interface.js +207 -0
  33. package/core/plugin-manager.js +448 -0
  34. package/core/rule-selection-service.js +42 -15
  35. package/core/semantic-engine.js +658 -0
  36. package/core/semantic-rule-base.js +433 -0
  37. package/core/unified-rule-registry.js +484 -0
  38. package/docs/COMMAND-EXAMPLES.md +134 -0
  39. package/docs/CONSTANTS-ARCHITECTURE.md +288 -0
  40. package/docs/LARGE-PROJECT-GUIDE.md +324 -0
  41. package/engines/core/base-engine.js +249 -0
  42. package/engines/engine-factory.js +275 -0
  43. package/engines/eslint-engine.js +171 -19
  44. package/engines/heuristic-engine.js +569 -78
  45. package/integrations/eslint/plugin/index.js +26 -28
  46. package/origin-rules/common-en.md +8 -8
  47. package/package.json +10 -6
  48. package/rules/common/C003_no_vague_abbreviations/analyzer.js +1 -1
  49. package/rules/common/C017_constructor_logic/analyzer.js +254 -17
  50. package/rules/common/C017_constructor_logic/semantic-analyzer.js +340 -0
  51. package/rules/common/C029_catch_block_logging/analyzer.js +17 -5
  52. package/rules/common/C033_separate_service_repository/README.md +78 -0
  53. package/rules/common/C033_separate_service_repository/analyzer.js +160 -0
  54. package/rules/common/C033_separate_service_repository/config.json +50 -0
  55. package/rules/common/C033_separate_service_repository/regex-based-analyzer.js +585 -0
  56. package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +368 -0
  57. package/rules/common/C035_error_logging_context/STRATEGY.md +99 -0
  58. package/rules/common/C035_error_logging_context/analyzer.js +230 -0
  59. package/rules/common/C035_error_logging_context/config.json +54 -0
  60. package/rules/common/C035_error_logging_context/regex-based-analyzer.js +299 -0
  61. package/rules/common/C035_error_logging_context/symbol-based-analyzer.js +454 -0
  62. package/rules/common/C040_centralized_validation/analyzer.js +165 -0
  63. package/rules/common/C040_centralized_validation/config.json +46 -0
  64. package/rules/common/C040_centralized_validation/regex-based-analyzer.js +243 -0
  65. package/rules/common/C040_centralized_validation/symbol-based-analyzer.js +416 -0
  66. package/rules/common/C047_no_duplicate_retry_logic/c047-semantic-rule.js +278 -0
  67. package/rules/common/C047_no_duplicate_retry_logic/symbol-analyzer-enhanced.js +968 -0
  68. package/rules/common/C047_no_duplicate_retry_logic/symbol-config.json +71 -0
  69. package/rules/common/{C076_single_test_behavior → C072_single_test_behavior}/analyzer.js +6 -6
  70. package/rules/common/C076_explicit_function_types/README.md +30 -0
  71. package/rules/common/C076_explicit_function_types/analyzer.js +172 -0
  72. package/rules/common/C076_explicit_function_types/config.json +15 -0
  73. package/rules/common/C076_explicit_function_types/semantic-analyzer.js +341 -0
  74. package/rules/index.js +8 -0
  75. package/rules/parser/rule-parser.js +13 -2
  76. package/rules/security/S005_no_origin_auth/README.md +226 -0
  77. package/rules/security/S005_no_origin_auth/analyzer.js +184 -0
  78. package/rules/security/S005_no_origin_auth/ast-analyzer.js +406 -0
  79. package/rules/security/S005_no_origin_auth/config.json +85 -0
  80. package/rules/security/S006_no_plaintext_recovery_codes/README.md +139 -0
  81. package/rules/security/S006_no_plaintext_recovery_codes/analyzer.js +306 -0
  82. package/rules/security/S006_no_plaintext_recovery_codes/config.json +48 -0
  83. package/rules/security/S007_no_plaintext_otp/README.md +198 -0
  84. package/rules/security/S007_no_plaintext_otp/analyzer.js +406 -0
  85. package/rules/security/S007_no_plaintext_otp/config.json +79 -0
  86. package/rules/security/S007_no_plaintext_otp/semantic-analyzer.js +609 -0
  87. package/rules/security/S007_no_plaintext_otp/semantic-config.json +195 -0
  88. package/rules/security/S007_no_plaintext_otp/semantic-wrapper.js +280 -0
  89. package/rules/security/S027_no_hardcoded_secrets/analyzer.js +180 -366
  90. package/rules/security/S027_no_hardcoded_secrets/categories.json +153 -0
  91. package/rules/security/S027_no_hardcoded_secrets/categorized-analyzer.js +250 -0
  92. package/scripts/category-manager.js +150 -0
  93. package/scripts/generate-rules-registry.js +88 -0
  94. package/scripts/migrate-rule-registry.js +157 -0
  95. package/scripts/prepare-release.sh +1 -1
  96. package/scripts/validate-system.js +48 -0
  97. package/.sunlint.json +0 -35
  98. package/config/README.md +0 -88
  99. package/config/engines/eslint-rule-mapping.json +0 -74
  100. package/config/schemas/sunlint-schema.json +0 -0
  101. package/config/testing/test-s005-working.ts +0 -22
  102. package/core/multi-rule-runner.js +0 -0
  103. package/docs/ESLINT-INTEGRATION-STRATEGY.md +0 -392
  104. package/docs/FUTURE_PACKAGES.md +0 -83
  105. package/docs/HEURISTIC_VS_AI.md +0 -113
  106. package/docs/PRODUCTION_DEPLOYMENT_ANALYSIS.md +0 -112
  107. package/docs/PRODUCTION_SIZE_IMPACT.md +0 -183
  108. package/docs/RELEASE_GUIDE.md +0 -230
  109. package/docs/STANDARDIZED-CATEGORY-FILTERING.md +0 -156
  110. package/engines/tree-sitter-parser.js +0 -0
  111. package/engines/universal-ast-engine.js +0 -0
  112. package/integrations/eslint/plugin/rules/common/c076-single-behavior-per-test.js +0 -254
  113. package/rules/common/C029_catch_block_logging/analyzer-backup.js +0 -426
  114. package/rules/common/C029_catch_block_logging/analyzer-fixed.js +0 -130
  115. package/rules/common/C029_catch_block_logging/analyzer-multi-tech.js +0 -487
  116. package/rules/common/C029_catch_block_logging/analyzer-simple.js +0 -110
  117. package/rules/common/C029_catch_block_logging/ast-analyzer-backup.js +0 -441
  118. package/rules/common/C029_catch_block_logging/ast-analyzer-new.js +0 -127
  119. package/rules/common/C029_catch_block_logging/ast-analyzer.js +0 -133
  120. package/rules/common/C029_catch_block_logging/cfg-analyzer.js +0 -408
  121. package/rules/common/C029_catch_block_logging/dataflow-analyzer.js +0 -454
  122. package/rules/common/C029_catch_block_logging/multi-language-ast-engine.js +0 -700
  123. package/rules/common/C029_catch_block_logging/pattern-learning-analyzer.js +0 -568
  124. package/rules/common/C029_catch_block_logging/semantic-analyzer.js +0 -459
@@ -0,0 +1,153 @@
1
+ {
2
+ "S027": {
3
+ "categories": [
4
+ {
5
+ "name": "AWS Credentials",
6
+ "severity": "critical",
7
+ "description": "AWS access keys, secret keys, and session tokens",
8
+ "patterns": [
9
+ "AKIA[0-9A-Z]{16}",
10
+ "(aws[-_]?)?(secret[-_]?access[-_]?key|access[-_]?key[-_]?id|awssecretaccesskey|awsaccesskeyid)\\s*[=:]\\s*[\"']?[A-Za-z0-9\\/+=]{20,40}[\"']?",
11
+ "(aws[-_]?)?session[-_]?token\\s*[=:]\\s*[\"']?[A-Za-z0-9\\/+=]{100,}[\"']?"
12
+ ],
13
+ "exclude_patterns": [
14
+ "(test|mock|fake|example|demo)[-_]?aws",
15
+ "AWS_REGION|AWS_DEFAULT_REGION"
16
+ ]
17
+ },
18
+ {
19
+ "name": "JWT & Authentication Tokens",
20
+ "severity": "critical",
21
+ "description": "JWT tokens and authentication credentials",
22
+ "patterns": [
23
+ "eyJ[A-Za-z0-9\\-_=]+\\.[A-Za-z0-9\\-_=]+\\.?[A-Za-z0-9\\-_.+/=]*",
24
+ "(jwt|bearer|auth|authtoken|jwttoken)[-_]?(token|secret)?\\s*[=:]\\s*[\"']?[a-zA-Z0-9\\-_=]{20,}[\"']?",
25
+ "authorization\\s*[=:]\\s*[\"']?(bearer|basic)\\s+[a-zA-Z0-9\\-_=]{10,}[\"']?"
26
+ ]
27
+ },
28
+ {
29
+ "name": "API Keys & Secrets",
30
+ "severity": "high",
31
+ "description": "Generic API keys and secret tokens",
32
+ "patterns": [
33
+ "(api[-_]?key|apikey|secret[-_]?key|secretkey|access[-_]?token|accesstoken)\\s*[=:]\\s*[\"']?[a-zA-Z0-9\\-_=]{16,}[\"']?",
34
+ "(client[-_]?secret|clientsecret|app[-_]?secret|appsecret)\\s*[=:]\\s*[\"']?[a-zA-Z0-9\\-_=]{20,}[\"']?",
35
+ "(private[-_]?key|privatekey|encryption[-_]?key|encryptionkey)\\s*[=:]\\s*[\"']?[a-zA-Z0-9\\-_=]{20,}[\"']?",
36
+ "(password|secret)\\s*[=:]\\s*[\"'][a-zA-Z0-9\\-_=]{6,}[\"']"
37
+ ],
38
+ "exclude_patterns": [
39
+ "(display|row|sort|primary|foreign)[-_]?key",
40
+ "key(value|path|name|code|id|index)",
41
+ "^key$",
42
+ "(test|mock|demo|example).*password",
43
+ "password.*(test|mock|demo|example|123|dummy)",
44
+ "wrongpassword|correctpassword|testpassword"
45
+ ]
46
+ },
47
+ {
48
+ "name": "Database Credentials",
49
+ "severity": "high",
50
+ "description": "Database connection strings and passwords",
51
+ "patterns": [
52
+ "(mongodb|mysql|postgres|redis):\\/\\/[^\\/\\s'\"]+:[^\\/\\s'\"]+@[^\\/\\s'\"]+",
53
+ "(db|database|dbpassword|databasepassword)[-_]?(password|pass|pwd|secret)?\\s*[=:]\\s*[\"']?[a-zA-Z0-9\\-_=]{6,}[\"']?",
54
+ "connection[-_]?string\\s*[=:]\\s*[\"']?[^\"'\\s]{20,}[\"']?"
55
+ ]
56
+ },
57
+ {
58
+ "name": "Third-party Service Keys",
59
+ "severity": "high",
60
+ "description": "GitHub, Slack, Stripe and other service tokens",
61
+ "patterns": [
62
+ "gh[pousr]_[A-Za-z0-9_]{36}",
63
+ "xox[baprs]-[A-Za-z0-9-]+",
64
+ "sk_live_[A-Za-z0-9]{24,}",
65
+ "(github|slack|stripe|paypal)[-_]?(token|key|secret)[\\s:=]+[\"']?[a-zA-Z0-9\\-_=]{16,}[\"']?"
66
+ ]
67
+ },
68
+ {
69
+ "name": "Suspicious Variable Names",
70
+ "severity": "medium",
71
+ "description": "Variables with sensitive naming patterns",
72
+ "patterns": [
73
+ "(client|app|service)[-_]?(id|key|token|secret)[\"']?\\s*[:=]\\s*[\"'][A-Za-z0-9\\-_=]{12,}[\"']?",
74
+ "(oauth|openid)[-_]?(client[-_]?id|secret)[\\s:=]+[\"']?[a-zA-Z0-9\\-_=]{10,}[\"']?"
75
+ ],
76
+ "exclude_patterns": [
77
+ "(send|verify|update|register|reset).*password",
78
+ "password.*(reset|verify|update|first|time)"
79
+ ]
80
+ },
81
+ {
82
+ "name": "Base64 Encoded Secrets",
83
+ "severity": "medium",
84
+ "description": "Potentially encoded sensitive data",
85
+ "patterns": [
86
+ "(?:^|[\\s=:'\"])([A-Za-z0-9+\\/]{64,}={0,2})(?:[\\s'\";}]|$)"
87
+ ],
88
+ "exclude_patterns": [
89
+ "^[a-zA-Z0-9+\\/]*$",
90
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789",
91
+ "(test|demo|example|sample|mock)",
92
+ "^\\/[a-zA-Z0-9\\/\\-_]+$",
93
+ "^[a-zA-Z0-9\\/\\-_\\.]+$",
94
+ "[a-zA-Z]+[a-zA-Z\\/\\-_]{20,}",
95
+ "[a-zA-Z]+Slice\\/",
96
+ "[a-zA-Z]+Company\\/",
97
+ "[a-zA-Z]+Management\\/",
98
+ "[a-zA-Z]+Component",
99
+ "[a-zA-Z]+Setting",
100
+ "[a-zA-Z]+Match",
101
+ "Selector$",
102
+ "Controller$",
103
+ "Service$",
104
+ "Api$",
105
+ "slice\\/",
106
+ "Component\\/",
107
+ "management\\/",
108
+ "company\\/",
109
+ "import.*from",
110
+ "require\\("
111
+ ]
112
+ },
113
+ {
114
+ "name": "Environment Variables",
115
+ "severity": "low",
116
+ "description": "Public environment variables that might leak info",
117
+ "patterns": [
118
+ "NEXT_PUBLIC_[A-Z0-9_]+[\\s:=]+[\"'][^\"']+[\"']",
119
+ "react_app_[A-Z0-9_]+[\\s:=]+[\"'][^\"']+[\"']"
120
+ ],
121
+ "exclude_patterns": [
122
+ "NODE_ENV|ENV|ENVIRONMENT|MODE|DEBUG"
123
+ ]
124
+ },
125
+ {
126
+ "name": "File Path Leaks",
127
+ "severity": "low",
128
+ "description": "Sensitive file patterns",
129
+ "patterns": [
130
+ "^\\s*['\"]?\\.env(\\.[a-zA-Z0-9_]+)?['\"]?\\s*$",
131
+ "^\\s*['\"]?(secrets?|credentials?|private[-_]?keys?)\\.(json|ya?ml|ts|js)['\"]?\\s*$",
132
+ "^\\s*['\"]?(id_rsa|id_dsa|\\.pem|\\.p12|\\.pfx)['\"]?\\s*$"
133
+ ],
134
+ "exclude_patterns": [
135
+ "process\\.env\\.",
136
+ "import.*from",
137
+ "require\\(",
138
+ "NODE_ENV|ENVIRONMENT|MODE|DEBUG"
139
+ ]
140
+ }
141
+ ],
142
+ "global_exclude_patterns": [
143
+ "(test|mock|fake|dummy|placeholder)\\.(js|ts|jsx|tsx)$",
144
+ "\\.(test|spec|mock)\\.",
145
+ "__tests__|\\/tests?\\/|\\/spec\\/",
146
+ "process\\.env\\.",
147
+ "import.*from.*['\"]",
148
+ "require\\(['\"]"
149
+ ],
150
+ "min_length": 8,
151
+ "max_length": 1000
152
+ }
153
+ }
@@ -0,0 +1,250 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+
4
+ class S027CategorizedAnalyzer {
5
+ constructor() {
6
+ this.ruleId = 'S027';
7
+ this.ruleName = 'No Hardcoded Secrets (Categorized)';
8
+ this.description = 'PhĆ”t hiện thĆ“ng tin bįŗ£o mįŗ­t theo categories vį»›i độ ʰu tiĆŖn khĆ”c nhau';
9
+
10
+ // Load categories config
11
+ this.config = this.loadConfig();
12
+ this.categories = this.config.categories;
13
+ this.globalExcludePatterns = this.config.global_exclude_patterns.map(p => new RegExp(p, 'i'));
14
+ this.minLength = this.config.min_length || 8;
15
+ this.maxLength = this.config.max_length || 1000;
16
+
17
+ // Compile patterns for performance
18
+ this.compilePatterns();
19
+ }
20
+
21
+ loadConfig() {
22
+ const configPath = path.join(__dirname, 'categories.json');
23
+ try {
24
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
25
+ return config.S027;
26
+ } catch (error) {
27
+ console.error('Failed to load S027 categories config:', error.message);
28
+ return { categories: [], global_exclude_patterns: [] };
29
+ }
30
+ }
31
+
32
+ compilePatterns() {
33
+ this.categories.forEach(category => {
34
+ category.compiledPatterns = category.patterns.map(p => ({
35
+ regex: new RegExp(p, 'gm'),
36
+ original: p
37
+ }));
38
+
39
+ if (category.exclude_patterns) {
40
+ category.compiledExcludePatterns = category.exclude_patterns.map(p => new RegExp(p, 'i'));
41
+ }
42
+ });
43
+ }
44
+
45
+ async analyze(files, language, options = {}) {
46
+ const violations = [];
47
+ this.currentFilePath = '';
48
+
49
+ for (const filePath of files) {
50
+ // Skip build/dist/node_modules
51
+ if (this.shouldSkipFile(filePath)) {
52
+ continue;
53
+ }
54
+
55
+ this.currentFilePath = filePath;
56
+
57
+ try {
58
+ const content = fs.readFileSync(filePath, 'utf8');
59
+ const fileViolations = this.analyzeFile(content, filePath);
60
+ violations.push(...fileViolations);
61
+ } catch (error) {
62
+ if (options.verbose) {
63
+ console.error(`Error analyzing ${filePath}:`, error.message);
64
+ }
65
+ }
66
+ }
67
+
68
+ return violations;
69
+ }
70
+
71
+ shouldSkipFile(filePath) {
72
+ const skipPatterns = [
73
+ 'build/', 'dist/', 'node_modules/', '.git/',
74
+ 'coverage/', '.next/', '.cache/', 'tmp/',
75
+ '.lock', '.log', '.min.js', '.bundle.js'
76
+ ];
77
+
78
+ return skipPatterns.some(pattern => filePath.includes(pattern));
79
+ }
80
+
81
+ analyzeFile(content, filePath) {
82
+ const violations = [];
83
+ // Handle different line endings (Windows \r\n, Unix \n, Mac \r)
84
+ const lines = content.split(/\r?\n/);
85
+
86
+ // Check if this is a test file for context
87
+ const isTestFile = this.isTestFile(filePath);
88
+
89
+ lines.forEach((line, index) => {
90
+ const lineNumber = index + 1;
91
+ const trimmedLine = line.trim();
92
+
93
+ // Skip comments and imports
94
+ if (this.isCommentOrImport(trimmedLine)) {
95
+ return;
96
+ }
97
+
98
+ // Check global exclude patterns first
99
+ if (this.matchesGlobalExcludes(line)) {
100
+ return;
101
+ }
102
+
103
+ // Check each category
104
+ this.categories.forEach(category => {
105
+ const categoryViolations = this.checkCategory(
106
+ category, line, lineNumber, filePath, isTestFile
107
+ );
108
+ violations.push(...categoryViolations);
109
+ });
110
+ });
111
+
112
+ return violations;
113
+ }
114
+
115
+ isTestFile(filePath) {
116
+ const testPatterns = [
117
+ /\.(test|spec)\./i,
118
+ /__tests__/i,
119
+ /\/tests?\//i,
120
+ /\/spec\//i,
121
+ /setupTests/i,
122
+ /testSetup/i,
123
+ /test[-_]/i, // Matches test- or test_
124
+ /^.*\/test[^\/]*\.js$/i // Matches files starting with test
125
+ ];
126
+
127
+ return testPatterns.some(pattern => pattern.test(filePath));
128
+ }
129
+
130
+ isCommentOrImport(line) {
131
+ return line.startsWith('//') || line.startsWith('/*') ||
132
+ line.startsWith('import') || line.startsWith('export') ||
133
+ line.startsWith('*') || line.startsWith('<');
134
+ }
135
+
136
+ matchesGlobalExcludes(line) {
137
+ return this.globalExcludePatterns.some(pattern => pattern.test(line));
138
+ }
139
+
140
+ checkCategory(category, line, lineNumber, filePath, isTestFile) {
141
+ const violations = [];
142
+
143
+ category.compiledPatterns.forEach(({ regex, original }) => {
144
+ let match;
145
+
146
+ // Reset regex lastIndex for global patterns
147
+ regex.lastIndex = 0;
148
+
149
+ while ((match = regex.exec(line)) !== null) {
150
+ const matchedText = match[0];
151
+ const column = match.index + 1;
152
+
153
+ // Check length constraints
154
+ if (matchedText.length < this.minLength || matchedText.length > this.maxLength) {
155
+ continue;
156
+ }
157
+
158
+ // Check category-specific excludes
159
+ if (category.compiledExcludePatterns &&
160
+ category.compiledExcludePatterns.some(pattern => pattern.test(matchedText))) {
161
+ continue;
162
+ }
163
+
164
+ // Be more lenient in test files for lower severity categories
165
+ // But still report critical/high severity issues even in test files
166
+ if (isTestFile && category.severity === 'low') {
167
+ continue;
168
+ }
169
+
170
+ violations.push({
171
+ file: filePath,
172
+ line: lineNumber,
173
+ column: column,
174
+ message: `[${category.name}] Potential ${category.severity} security risk: '${matchedText}'. ${category.description}`,
175
+ severity: this.mapSeverity(category.severity),
176
+ ruleId: this.ruleId,
177
+ category: category.name,
178
+ categoryDescription: category.description,
179
+ matchedPattern: original,
180
+ matchedText: matchedText
181
+ });
182
+ }
183
+ });
184
+
185
+ return violations;
186
+ }
187
+
188
+ mapSeverity(categorySeverity) {
189
+ const severityMap = {
190
+ 'critical': 'error',
191
+ 'high': 'warning',
192
+ 'medium': 'warning',
193
+ 'low': 'info'
194
+ };
195
+
196
+ return severityMap[categorySeverity] || 'warning';
197
+ }
198
+
199
+ // Method for getting category statistics
200
+ getCategoryStats(violations) {
201
+ const stats = {};
202
+
203
+ violations.forEach(violation => {
204
+ const category = violation.category;
205
+ if (!stats[category]) {
206
+ stats[category] = {
207
+ count: 0,
208
+ severity: violation.severity,
209
+ files: new Set()
210
+ };
211
+ }
212
+ stats[category].count++;
213
+ stats[category].files.add(violation.file);
214
+ });
215
+
216
+ // Convert Set to array for JSON serialization
217
+ Object.keys(stats).forEach(category => {
218
+ stats[category].files = Array.from(stats[category].files);
219
+ stats[category].fileCount = stats[category].files.length;
220
+ });
221
+
222
+ return stats;
223
+ }
224
+
225
+ // Method for filtering by category
226
+ filterByCategory(violations, categoryNames) {
227
+ if (!categoryNames || categoryNames.length === 0) {
228
+ return violations;
229
+ }
230
+
231
+ return violations.filter(violation =>
232
+ categoryNames.includes(violation.category)
233
+ );
234
+ }
235
+
236
+ // Method for filtering by severity
237
+ filterBySeverity(violations, minSeverity = 'info') {
238
+ const severityOrder = ['info', 'warning', 'error'];
239
+ const minIndex = severityOrder.indexOf(minSeverity);
240
+
241
+ if (minIndex === -1) return violations;
242
+
243
+ return violations.filter(violation => {
244
+ const violationIndex = severityOrder.indexOf(violation.severity);
245
+ return violationIndex >= minIndex;
246
+ });
247
+ }
248
+ }
249
+
250
+ module.exports = S027CategorizedAnalyzer;
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * SunLint Category Management CLI
5
+ * Utility for managing categories and principles
6
+ *
7
+ * Usage:
8
+ * node scripts/category-manager.js list
9
+ * node scripts/category-manager.js add <category> <principle> <description>
10
+ * node scripts/category-manager.js validate
11
+ * node scripts/category-manager.js stats
12
+ */
13
+
14
+ const path = require('path');
15
+ const {
16
+ getValidCategories,
17
+ getCategoryPrinciples,
18
+ getCategoryDescription,
19
+ getCategoryStats,
20
+ isValidCategory,
21
+ addCategoryMapping
22
+ } = require('../core/constants/categories');
23
+
24
+ const command = process.argv[2];
25
+
26
+ switch (command) {
27
+ case 'list':
28
+ listCategories();
29
+ break;
30
+
31
+ case 'validate':
32
+ validateCategories();
33
+ break;
34
+
35
+ case 'stats':
36
+ showStats();
37
+ break;
38
+
39
+ case 'add':
40
+ addCategory(process.argv[3], process.argv[4], process.argv[5]);
41
+ break;
42
+
43
+ case 'check':
44
+ checkCategory(process.argv[3]);
45
+ break;
46
+
47
+ default:
48
+ showHelp();
49
+ }
50
+
51
+ function listCategories() {
52
+ console.log('šŸ“‹ SunLint Categories & Principles\n');
53
+
54
+ const categories = getValidCategories();
55
+ categories.forEach(category => {
56
+ const principles = getCategoryPrinciples(category);
57
+ const description = getCategoryDescription(category);
58
+
59
+ console.log(`šŸ·ļø ${category.toUpperCase()}`);
60
+ console.log(` Principles: ${principles.join(', ')}`);
61
+ console.log(` Description: ${description}`);
62
+ console.log('');
63
+ });
64
+ }
65
+
66
+ function validateCategories() {
67
+ console.log('šŸ” Validating Category System\n');
68
+
69
+ const stats = getCategoryStats();
70
+ console.log(`āœ… Total Categories: ${stats.totalCategories}`);
71
+ console.log(`āœ… Total Principles: ${stats.totalPrinciples}`);
72
+
73
+ // Check for missing principles
74
+ const allPrinciples = Object.values(SUNLINT_PRINCIPLES);
75
+ const mappedPrinciples = Object.values(CATEGORY_PRINCIPLE_MAP).flat();
76
+
77
+ const missingPrinciples = allPrinciples.filter(p => !mappedPrinciples.includes(p));
78
+
79
+ if (missingPrinciples.length > 0) {
80
+ console.log(`āš ļø Unmapped Principles: ${missingPrinciples.join(', ')}`);
81
+ } else {
82
+ console.log('āœ… All principles mapped to categories');
83
+ }
84
+
85
+ console.log('\nšŸ“Š Category Mapping:');
86
+ Object.entries(CATEGORY_PRINCIPLE_MAP).forEach(([category, principles]) => {
87
+ console.log(` ${category} -> ${principles.join(', ')}`);
88
+ });
89
+ }
90
+
91
+ function showStats() {
92
+ const stats = getCategoryStats();
93
+ console.log('šŸ“Š Category Statistics\n');
94
+ console.log(JSON.stringify(stats, null, 2));
95
+ }
96
+
97
+ function addCategory(category, principle, description) {
98
+ if (!category || !principle || !description) {
99
+ console.error('āŒ Usage: add <category> <principle> <description>');
100
+ return;
101
+ }
102
+
103
+ console.log(`šŸ”„ Adding category: ${category}`);
104
+ console.log(` Principle: ${principle}`);
105
+ console.log(` Description: ${description}`);
106
+ console.log('\nāš ļø This would require updating category-constants.js manually');
107
+ console.log(' Add the following to CATEGORY_PRINCIPLE_MAP:');
108
+ console.log(` '${category.toLowerCase()}': ['${principle.toUpperCase()}'],`);
109
+ }
110
+
111
+ function checkCategory(category) {
112
+ if (!category) {
113
+ console.error('āŒ Usage: check <category>');
114
+ return;
115
+ }
116
+
117
+ console.log(`šŸ” Checking category: ${category}\n`);
118
+
119
+ const isValid = isValidCategory(category);
120
+ console.log(`Valid: ${isValid ? 'āœ…' : 'āŒ'}`);
121
+
122
+ if (isValid) {
123
+ const principles = getCategoryPrinciples(category);
124
+ const description = getCategoryDescription(category);
125
+
126
+ console.log(`Principles: ${principles.join(', ')}`);
127
+ console.log(`Description: ${description}`);
128
+ } else {
129
+ console.log(`Valid categories: ${getValidCategories().join(', ')}`);
130
+ }
131
+ }
132
+
133
+ function showHelp() {
134
+ console.log(`
135
+ šŸ› ļø SunLint Category Manager
136
+
137
+ Commands:
138
+ list Show all categories and their principles
139
+ validate Validate the category system consistency
140
+ stats Show category statistics
141
+ check Check if a specific category is valid
142
+ add Add a new category (manual step required)
143
+
144
+ Examples:
145
+ node scripts/category-manager.js list
146
+ node scripts/category-manager.js check security
147
+ node scripts/category-manager.js validate
148
+ node scripts/category-manager.js add accessibility ACCESSIBILITY "Accessibility guidelines"
149
+ `);
150
+ }
@@ -0,0 +1,88 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const { SimpleRuleParser } = require('../rules/parser/rule-parser-simple');
6
+
7
+ /**
8
+ * Generate rules registry from origin-rules
9
+ * This script creates config/rules/rules-registry-generated.json
10
+ * from all *-en.md files in origin-rules/ directory
11
+ */
12
+
13
+ console.log('šŸ“‹ Generating rules registry from origin-rules...');
14
+
15
+ try {
16
+ const parser = new SimpleRuleParser();
17
+ const originRulesDir = path.join(__dirname, '..', 'origin-rules');
18
+ const targetPath = path.join(__dirname, '..', 'config', 'rules', 'rules-registry-generated.json');
19
+
20
+ console.log(`Source: ${originRulesDir}`);
21
+ console.log(`Target: ${targetPath}`);
22
+
23
+ // Parse all rules from origin-rules
24
+ const allRules = parser.parseAllRules(originRulesDir);
25
+
26
+ if (allRules.length === 0) {
27
+ console.error('āŒ No rules found in origin-rules directory');
28
+ process.exit(1);
29
+ }
30
+
31
+ // Convert to registry format
32
+ const registry = {
33
+ rules: {}
34
+ };
35
+
36
+ allRules.forEach(rule => {
37
+ if (rule.id) {
38
+ registry.rules[rule.id] = {
39
+ name: rule.title || `${rule.id} Rule`,
40
+ description: rule.description || 'No description available',
41
+ category: rule.category || 'quality',
42
+ severity: rule.severity || 'major',
43
+ languages: rule.language ? [rule.language] : ['All languages'],
44
+ version: rule.version || '1.0.0',
45
+ status: rule.status || 'draft',
46
+ tags: [rule.category || 'quality', 'readability', 'code-quality'],
47
+ tools: rule.tools || [],
48
+ framework: rule.framework || 'All',
49
+ principles: rule.principles || []
50
+ };
51
+ }
52
+ });
53
+
54
+ // Ensure target directory exists
55
+ const targetDir = path.dirname(targetPath);
56
+ if (!fs.existsSync(targetDir)) {
57
+ fs.mkdirSync(targetDir, { recursive: true });
58
+ }
59
+
60
+ // Write registry file
61
+ fs.writeFileSync(targetPath, JSON.stringify(registry, null, 2), 'utf8');
62
+
63
+ const rulesCount = Object.keys(registry.rules).length;
64
+ const fileSize = (fs.statSync(targetPath).size / 1024).toFixed(1);
65
+
66
+ console.log(`āœ… Generated registry with ${rulesCount} rules`);
67
+ console.log(`šŸ“ File: ${targetPath} (${fileSize} KB)`);
68
+ console.log('');
69
+ console.log('šŸ“Š Rules by category:');
70
+
71
+ // Stats by category
72
+ const categories = {};
73
+ Object.values(registry.rules).forEach(rule => {
74
+ const cat = rule.category || 'unknown';
75
+ categories[cat] = (categories[cat] || 0) + 1;
76
+ });
77
+
78
+ Object.entries(categories)
79
+ .sort(([,a], [,b]) => b - a)
80
+ .forEach(([category, count]) => {
81
+ console.log(` ${category}: ${count} rules`);
82
+ });
83
+
84
+ } catch (error) {
85
+ console.error('āŒ Error generating registry:', error.message);
86
+ console.error(error.stack);
87
+ process.exit(1);
88
+ }