@sun-asterisk/sunlint 1.3.16 → 1.3.18

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 (59) hide show
  1. package/config/rule-analysis-strategies.js +3 -3
  2. package/config/rules/enhanced-rules-registry.json +40 -20
  3. package/core/analysis-orchestrator.js +11 -3
  4. package/core/cli-action-handler.js +2 -2
  5. package/core/config-merger.js +28 -6
  6. package/core/constants/defaults.js +1 -1
  7. package/core/file-targeting-service.js +72 -4
  8. package/core/output-service.js +48 -13
  9. package/core/summary-report-service.js +21 -3
  10. package/engines/heuristic-engine.js +5 -0
  11. package/package.json +1 -1
  12. package/rules/common/C002_no_duplicate_code/README.md +115 -0
  13. package/rules/common/C002_no_duplicate_code/analyzer.js +615 -219
  14. package/rules/common/C002_no_duplicate_code/test-cases/api-handlers.ts +64 -0
  15. package/rules/common/C002_no_duplicate_code/test-cases/data-processor.ts +46 -0
  16. package/rules/common/C002_no_duplicate_code/test-cases/good-example.tsx +40 -0
  17. package/rules/common/C002_no_duplicate_code/test-cases/product-service.ts +57 -0
  18. package/rules/common/C002_no_duplicate_code/test-cases/user-service.ts +49 -0
  19. package/rules/common/C008/analyzer.js +40 -0
  20. package/rules/common/C008/config.json +20 -0
  21. package/rules/common/C008/ts-morph-analyzer.js +1067 -0
  22. package/rules/common/C018_no_throw_generic_error/analyzer.js +1 -1
  23. package/rules/common/C018_no_throw_generic_error/symbol-based-analyzer.js +27 -3
  24. package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +504 -162
  25. package/rules/common/C029_catch_block_logging/analyzer.js +499 -89
  26. package/rules/common/C033_separate_service_repository/README.md +131 -20
  27. package/rules/common/C033_separate_service_repository/analyzer.js +1 -1
  28. package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +417 -274
  29. package/rules/common/C041_no_sensitive_hardcode/analyzer.js +144 -254
  30. package/rules/common/C041_no_sensitive_hardcode/config.json +50 -0
  31. package/rules/common/C041_no_sensitive_hardcode/symbol-based-analyzer.js +575 -0
  32. package/rules/common/C047_no_duplicate_retry_logic/analyzer.js +96 -40
  33. package/rules/common/C047_no_duplicate_retry_logic/symbol-analyzer-enhanced.js +17 -2
  34. package/rules/common/C067_no_hardcoded_config/analyzer.js +17 -16
  35. package/rules/common/C067_no_hardcoded_config/symbol-based-analyzer.js +3477 -659
  36. package/rules/docs/C002_no_duplicate_code.md +276 -11
  37. package/rules/index.js +5 -1
  38. package/rules/security/S006_no_plaintext_recovery_codes/analyzer.js +266 -88
  39. package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +805 -0
  40. package/rules/security/S010_no_insecure_encryption/README.md +78 -0
  41. package/rules/security/S010_no_insecure_encryption/analyzer.js +463 -398
  42. package/rules/security/S013_tls_enforcement/README.md +51 -0
  43. package/rules/security/S013_tls_enforcement/analyzer.js +99 -0
  44. package/rules/security/S013_tls_enforcement/config.json +41 -0
  45. package/rules/security/S013_tls_enforcement/symbol-based-analyzer.js +339 -0
  46. package/rules/security/S014_tls_version_enforcement/README.md +354 -0
  47. package/rules/security/S014_tls_version_enforcement/analyzer.js +118 -0
  48. package/rules/security/S014_tls_version_enforcement/config.json +56 -0
  49. package/rules/security/S014_tls_version_enforcement/symbol-based-analyzer.js +194 -0
  50. package/rules/security/S055_content_type_validation/analyzer.js +121 -279
  51. package/rules/security/S055_content_type_validation/symbol-based-analyzer.js +346 -0
  52. package/rules/tests/C002_no_duplicate_code.test.js +111 -22
  53. package/docs/CONSTANTS-ARCHITECTURE.md +0 -288
  54. package/docs/DEPLOYMENT-STRATEGIES.md +0 -270
  55. package/docs/ESLINT_INTEGRATION.md +0 -238
  56. package/docs/PERFORMANCE_MIGRATION_GUIDE.md +0 -368
  57. package/docs/PERFORMANCE_OPTIMIZATION_PLAN.md +0 -255
  58. package/rules/common/C029_catch_block_logging/analyzer-smart-pipeline.js +0 -755
  59. package/rules/common/C041_no_sensitive_hardcode/ast-analyzer.js +0 -296
@@ -72,9 +72,9 @@ module.exports = {
72
72
  accuracy: { regex: 95 }
73
73
  },
74
74
  'C002': {
75
- reason: 'Duplicate code detection requires cross-file analysis',
76
- methods: ['regex'],
77
- accuracy: { regex: 85 }
75
+ reason: 'Duplicate code detection with AST-based semantic analysis',
76
+ methods: ['heuristic'],
77
+ accuracy: { heuristic: 100 }
78
78
  },
79
79
  'C043': {
80
80
  reason: 'Console/print detection via simple patterns',
@@ -69,6 +69,29 @@
69
69
  ]
70
70
  }
71
71
  },
72
+ "C008": {
73
+ "name": "Minimize Variable Scope - Declare Near Usage",
74
+ "description": "Variables should be declared as close as possible to where they are first used",
75
+ "category": "code-quality",
76
+ "severity": "warning",
77
+ "languages": ["typescript", "javascript"],
78
+ "analyzer": "rules/common/C008/analyzer.js",
79
+ "config": "rules/common/C008/config.json",
80
+ "version": "1.0.0",
81
+ "status": "active",
82
+ "tags": ["readability", "maintainability", "scope", "best-practice"],
83
+ "strategy": {
84
+ "preferred": "semantic",
85
+ "fallbacks": ["semantic", "ast"],
86
+ "accuracy": {
87
+ "semantic": 95,
88
+ "ast": 90
89
+ }
90
+ },
91
+ "engineMappings": {
92
+ "semantic": ["rules/common/C008/analyzer.js"]
93
+ }
94
+ },
72
95
  "C010": {
73
96
  "name": "Limit Block Nesting",
74
97
  "description": "Limit nested blocks (if/for/while/switch) to maximum 3 levels for readability",
@@ -357,6 +380,21 @@
357
380
  ]
358
381
  }
359
382
  },
383
+ "C041": {
384
+ "name": "Do not hardcode or push sensitive information (token, API key, secret, URL) into the repo",
385
+ "description": "Protect sensitive application data, avoid security risks, and comply with security standards. Exposing sensitive information can lead to serious security and privacy issues.",
386
+ "category": "security",
387
+ "severity": "warning",
388
+ "languages": ["typescript", "javascript", "dart", "kotlin"],
389
+ "analyzer": "./rules/common/C041_no_sensitive_hardcode/analyzer.js",
390
+ "config": "./rules/common/C041_no_sensitive_hardcode/config.json",
391
+ "version": "1.0.0",
392
+ "status": "stable",
393
+ "tags": ["naming", "domain", "readability"],
394
+ "engineMappings": {
395
+ "eslint": ["@typescript-eslint/naming-convention", "camelcase"]
396
+ }
397
+ },
360
398
  "C043": {
361
399
  "name": "No Console Or Print",
362
400
  "description": "Do not use console.log or print in production code",
@@ -1266,7 +1304,8 @@
1266
1304
  "category": "security",
1267
1305
  "severity": "error",
1268
1306
  "languages": ["typescript", "javascript"],
1269
- "analyzer": "eslint",
1307
+ "analyzer": "./rules/security/S055_content_type_validation/analyzer.js",
1308
+ "config": "./rules/security/S055_content_type_validation/config.json",
1270
1309
  "eslintRule": "custom/typescript_s055",
1271
1310
  "version": "1.0.0",
1272
1311
  "status": "stable",
@@ -1447,25 +1486,6 @@
1447
1486
  "accuracy": {}
1448
1487
  }
1449
1488
  },
1450
- "C041": {
1451
- "id": "C041",
1452
- "name": "Rule C041",
1453
- "description": "Auto-migrated rule C041 from ESLint mapping",
1454
- "category": "general",
1455
- "severity": "warning",
1456
- "languages": ["typescript", "javascript"],
1457
- "version": "1.0.0",
1458
- "status": "migrated",
1459
- "tags": ["migrated"],
1460
- "engineMappings": {
1461
- "eslint": ["custom/no-config-inline"]
1462
- },
1463
- "strategy": {
1464
- "preferred": "regex",
1465
- "fallbacks": ["regex"],
1466
- "accuracy": {}
1467
- }
1468
- },
1469
1489
  "C042": {
1470
1490
  "id": "C042",
1471
1491
  "name": "Rule C042",
@@ -547,9 +547,17 @@ class AnalysisOrchestrator {
547
547
  for (const engineResult of engineResults) {
548
548
  uniqueEngines.add(engineResult.engine);
549
549
 
550
- // Add engine-specific results
551
- if (engineResult.results) {
552
- mergedResults.results.push(...engineResult.results);
550
+ // Add engine-specific results with validation
551
+ if (engineResult.results && Array.isArray(engineResult.results)) {
552
+ // Filter out invalid entries (non-objects or config objects)
553
+ const validResults = engineResult.results.filter(result => {
554
+ if (!result || typeof result !== 'object') return false;
555
+ // Skip objects that look like metadata/config
556
+ if (result.semanticEngine || result.project || result._context) return false;
557
+ // Must have either file/filePath or be a valid result object
558
+ return result.file || result.filePath || result.violations || result.messages;
559
+ });
560
+ mergedResults.results.push(...validResults);
553
561
  }
554
562
 
555
563
  // Track engine statistics
@@ -124,14 +124,14 @@ class CliActionHandler {
124
124
  heuristicConfig: {
125
125
  ...config.heuristic || {},
126
126
  targetFiles: this.options.targetFiles, // Pass filtered files for semantic optimization
127
- maxSemanticFiles: this.options.maxSemanticFiles !== undefined ? parseInt(this.options.maxSemanticFiles) : 1000, // Configurable semantic file limit
127
+ maxSemanticFiles: this.options.maxSemanticFiles ? parseInt(this.options.maxSemanticFiles) : 1000,
128
128
  verbose: this.options.verbose // Pass verbose for debugging
129
129
  }
130
130
  });
131
131
 
132
132
  if (this.options.verbose) {
133
133
  console.log(`🔧 Debug: maxSemanticFiles option = ${this.options.maxSemanticFiles}`);
134
- console.log(`🔧 Debug: parsed maxSemanticFiles = ${this.options.maxSemanticFiles !== undefined ? parseInt(this.options.maxSemanticFiles) : 1000}`);
134
+ console.log(`🔧 Debug: parsed maxSemanticFiles = ${this.options.maxSemanticFiles !== undefined ? parseInt(this.options.maxSemanticFiles) : Infinity}`);
135
135
  }
136
136
 
137
137
  // Run analysis with new orchestrator
@@ -215,28 +215,50 @@ class ConfigMerger {
215
215
 
216
216
  try {
217
217
  const resolvedPath = path.resolve(inputPath);
218
+ const isAbsolutePath = path.isAbsolute(inputPath);
219
+
218
220
  if (fs.existsSync(resolvedPath)) {
219
221
  const stat = fs.statSync(resolvedPath);
220
222
  if (stat.isFile()) {
221
223
  // For files, add the exact path
222
224
  expandedInclude.push(inputPath);
223
- expandedInclude.push('**/' + inputPath);
225
+ // Only add **/ prefix for relative paths
226
+ if (!isAbsolutePath) {
227
+ expandedInclude.push('**/' + inputPath);
228
+ }
224
229
  } else if (stat.isDirectory()) {
225
230
  // For directories, add recursive patterns
226
- expandedInclude.push(inputPath + '/**');
227
- expandedInclude.push('**/' + inputPath + '/**');
231
+ // Special handling for current directory
232
+ if (inputPath === '.' || inputPath === './') {
233
+ // For current directory, match all files
234
+ expandedInclude.push('**/*');
235
+ } else {
236
+ expandedInclude.push(inputPath + '/**');
237
+ // Only add **/ prefix for relative paths
238
+ if (!isAbsolutePath) {
239
+ expandedInclude.push('**/' + inputPath + '/**');
240
+ }
241
+ }
228
242
  }
229
243
  } else {
230
244
  // If path doesn't exist, assume it's a pattern and add both file and directory variants
231
245
  expandedInclude.push(inputPath);
232
246
  expandedInclude.push(inputPath + '/**');
233
- expandedInclude.push('**/' + inputPath);
234
- expandedInclude.push('**/' + inputPath + '/**');
247
+ // Only add **/ prefix for relative paths
248
+ if (!isAbsolutePath) {
249
+ expandedInclude.push('**/' + inputPath);
250
+ expandedInclude.push('**/' + inputPath + '/**');
251
+ }
235
252
  }
236
253
  } catch (error) {
237
254
  // Fallback to original logic if file system check fails
255
+ const path = require('path');
256
+ const isAbsolutePath = path.isAbsolute(inputPath);
238
257
  expandedInclude.push(inputPath + '/**');
239
- expandedInclude.push('**/' + inputPath + '/**');
258
+ // Only add **/ prefix for relative paths
259
+ if (!isAbsolutePath) {
260
+ expandedInclude.push('**/' + inputPath + '/**');
261
+ }
240
262
  }
241
263
  }
242
264
  result.include = expandedInclude;
@@ -98,7 +98,7 @@ const DEFAULT_PERFORMANCE = {
98
98
  // File filtering
99
99
  ENABLE_FILE_FILTERING: true,
100
100
  MAX_FILE_SIZE: 2 * 1024 * 1024, // 2MB per file
101
- MAX_TOTAL_FILES: 1000, // Max 1000 files per analysis
101
+ MAX_TOTAL_FILES: 1000, // Limit total files analyzed
102
102
 
103
103
  // Batch processing
104
104
  ENABLE_BATCHING: true,
@@ -150,8 +150,54 @@ class FileTargetingService {
150
150
  allFiles.push(...files);
151
151
  }
152
152
 
153
+ // IMPORTANT: If inputPaths are optimized subdirectories, also collect root-level files
154
+ // This handles files like middleware.ts, auth.ts, instrumentation.ts in Next.js projects
155
+ if (inputPaths.length > 0) {
156
+ const firstPath = inputPaths[0];
157
+ const projectRoot = path.dirname(firstPath);
158
+
159
+ // Check if we're dealing with subdirectories (optimized paths)
160
+ const isOptimized = inputPaths.every(p => path.dirname(p) === projectRoot);
161
+
162
+ if (isOptimized) {
163
+ // Collect root-level source files only (not recursive)
164
+ const rootFiles = await this.collectRootLevelFiles(projectRoot);
165
+ allFiles.push(...rootFiles);
166
+ }
167
+ }
168
+
153
169
  return allFiles;
154
170
  }
171
+
172
+ /**
173
+ * Collect only root-level source files (non-recursive)
174
+ * Handles Next.js patterns like middleware.ts, auth.ts, instrumentation.ts
175
+ */
176
+ async collectRootLevelFiles(projectRoot) {
177
+ const files = [];
178
+ const targetExtensions = ['.ts', '.tsx', '.js', '.jsx'];
179
+
180
+ try {
181
+ const entries = fs.readdirSync(projectRoot);
182
+
183
+ for (const entry of entries) {
184
+ const fullPath = path.join(projectRoot, entry);
185
+ const stat = fs.statSync(fullPath);
186
+
187
+ // Only collect files at root level, skip directories
188
+ if (stat.isFile()) {
189
+ const ext = path.extname(fullPath);
190
+ if (targetExtensions.includes(ext)) {
191
+ files.push(path.resolve(fullPath));
192
+ }
193
+ }
194
+ }
195
+ } catch (error) {
196
+ // Silent fail - root scanning is optional optimization
197
+ }
198
+
199
+ return files;
200
+ }
155
201
 
156
202
  /**
157
203
  * Optimize project paths to focus on source and test directories
@@ -200,7 +246,7 @@ class FileTargetingService {
200
246
  */
201
247
  findProjectSourceDirs(projectPath, cliOptions = {}) {
202
248
  const sourceDirs = [];
203
- const candidateDirs = ['src', 'lib', 'app', 'packages'];
249
+ const candidateDirs = ['src', 'lib', 'app', 'packages', 'components', 'hooks', 'utils', 'helpers', 'services', 'worker'];
204
250
  const testDirs = ['test', 'tests', '__tests__', 'spec', 'specs'];
205
251
 
206
252
  // Always include test directories if --include-tests flag is used
@@ -404,7 +450,25 @@ class FileTargetingService {
404
450
  const result = files.filter(file => {
405
451
  return patternArray.some(pattern => {
406
452
  const normalizedFile = this.normalizePath(file);
407
- const match = minimatch(normalizedFile, pattern, { dot: true });
453
+ const isAbsolutePattern = path.isAbsolute(pattern);
454
+ const isAbsoluteFile = path.isAbsolute(file);
455
+
456
+ // For absolute patterns, match against absolute file paths
457
+ // For relative patterns, match against normalized (relative or absolute) paths
458
+ let match = false;
459
+ if (isAbsolutePattern) {
460
+ // Match absolute pattern against absolute file path
461
+ match = minimatch(file.replace(/\\/g, '/'), pattern, { dot: true });
462
+ } else {
463
+ // Match relative pattern against normalized path
464
+ match = minimatch(normalizedFile, pattern, { dot: true });
465
+
466
+ // Also try matching relative pattern against absolute path for compatibility
467
+ if (!match && isAbsoluteFile) {
468
+ match = minimatch(file.replace(/\\/g, '/'), pattern, { dot: true });
469
+ }
470
+ }
471
+
408
472
  if (debug && file.includes('.ts') && !file.includes('.test.')) {
409
473
  console.log(`🔍 [DEBUG] Testing: '${file}' -> '${normalizedFile}' vs '${pattern}' = ${match}`);
410
474
  }
@@ -529,8 +593,12 @@ class FileTargetingService {
529
593
  * Rule C006: normalizePath - verb-noun naming
530
594
  */
531
595
  normalizePath(filePath) {
532
- // Convert to relative path from current working directory for pattern matching
533
- const relativePath = path.relative(process.cwd(), filePath);
596
+ // Always convert to relative path from cwd for pattern matching
597
+ // This ensures patterns like 'src/**' work with absolute file paths
598
+ const relativePath = path.isAbsolute(filePath)
599
+ ? path.relative(process.cwd(), filePath)
600
+ : filePath;
601
+
534
602
  // Normalize path separators for cross-platform compatibility
535
603
  return relativePath.replace(/\\/g, '/');
536
604
  }
@@ -28,9 +28,9 @@ class OutputService {
28
28
  try {
29
29
  const packageJsonPath = path.join(__dirname, '..', 'package.json');
30
30
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
31
- return packageJson.version || '1.3.16';
31
+ return packageJson.version || '1.3.18';
32
32
  } catch (error) {
33
- return '1.3.16'; // Fallback version
33
+ return '1.3.18'; // Fallback version
34
34
  }
35
35
  }
36
36
 
@@ -140,6 +140,17 @@ class OutputService {
140
140
  const allViolations = [];
141
141
  let totalFiles = results.filesAnalyzed || results.summary?.totalFiles || results.totalFiles || results.fileCount || 0;
142
142
 
143
+ // Helper function to validate violation object
144
+ const isValidViolation = (violation) => {
145
+ if (!violation || typeof violation !== 'object') return false;
146
+ // Skip config/metadata objects (have nested objects like semanticEngine, project, etc.)
147
+ if (violation.semanticEngine || violation.project || violation._context) return false;
148
+ // Must have ruleId as string
149
+ const ruleId = violation.ruleId || violation.rule;
150
+ if (!ruleId || typeof ruleId !== 'string') return false;
151
+ return true;
152
+ };
153
+
143
154
  // Collect all violations - handle both file-based and rule-based results
144
155
  if (results.results) {
145
156
  results.results.forEach(result => {
@@ -147,16 +158,20 @@ class OutputService {
147
158
  // Handle rule-based format (MultiRuleRunner)
148
159
  if (result.ruleId) {
149
160
  result.violations.forEach(violation => {
150
- allViolations.push(violation); // violation already has file path
161
+ if (isValidViolation(violation)) {
162
+ allViolations.push(violation); // violation already has file path
163
+ }
151
164
  });
152
165
  }
153
166
  // Handle file-based format (legacy)
154
167
  else {
155
168
  result.violations.forEach(violation => {
156
- allViolations.push({
157
- ...violation,
158
- file: result.filePath || result.file // Use filePath first, then file
159
- });
169
+ if (isValidViolation(violation)) {
170
+ allViolations.push({
171
+ ...violation,
172
+ file: result.filePath || result.file // Use filePath first, then file
173
+ });
174
+ }
160
175
  });
161
176
  }
162
177
  }
@@ -164,7 +179,7 @@ class OutputService {
164
179
  // Handle ESLint format (messages array)
165
180
  if (result.messages) {
166
181
  result.messages.forEach(message => {
167
- allViolations.push({
182
+ const violation = {
168
183
  file: result.filePath || message.file,
169
184
  ruleId: message.ruleId,
170
185
  severity: message.severity === 2 ? 'error' : 'warning',
@@ -172,7 +187,10 @@ class OutputService {
172
187
  line: message.line,
173
188
  column: message.column,
174
189
  source: message.source || 'eslint'
175
- });
190
+ };
191
+ if (isValidViolation(violation)) {
192
+ allViolations.push(violation);
193
+ }
176
194
  });
177
195
  }
178
196
  });
@@ -232,12 +250,29 @@ class OutputService {
232
250
  fileGroups[file].push(violation);
233
251
  });
234
252
 
253
+ // Sort file paths alphabetically for consistent output
254
+ const sortedFiles = Object.keys(fileGroups).sort();
255
+
235
256
  // Format each file's violations (ESLint-compatible format)
236
- Object.keys(fileGroups).forEach(file => {
257
+ sortedFiles.forEach((file, index) => {
258
+ // Sort violations by line number within each file
259
+ const sortedViolations = fileGroups[file].sort((a, b) => {
260
+ const lineA = a.location?.start?.line || a.line || 1;
261
+ const lineB = b.location?.start?.line || b.line || 1;
262
+ if (lineA !== lineB) return lineA - lineB;
263
+ return (a.location?.start?.column || a.column || 1) - (b.location?.start?.column || b.column || 1);
264
+ });
265
+
266
+ // Add blank line before file header (except for the first file)
267
+ if (index > 0) {
268
+ output += '\n';
269
+ }
237
270
  output += `\n${chalk.underline(path.resolve(file))}\n`;
238
- fileGroups[file].forEach(violation => {
239
- const line = (violation.line || 1).toString();
240
- const column = (violation.column || 1).toString();
271
+
272
+ sortedViolations.forEach(violation => {
273
+ // Support both location.start.line (new format) and line (legacy format)
274
+ const line = (violation.location?.start?.line || violation.line || 1).toString();
275
+ const column = (violation.location?.start?.column || violation.column || 1).toString();
241
276
  const severityText = violation.severity === 'error' ? 'error' : 'warning';
242
277
  const severityColor = violation.severity === 'error' ? chalk.red : chalk.yellow;
243
278
 
@@ -23,9 +23,9 @@ class SummaryReportService {
23
23
  try {
24
24
  const packageJsonPath = path.join(__dirname, '..', 'package.json');
25
25
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
26
- return packageJson.version || '1.3.16';
26
+ return packageJson.version || '1.3.18';
27
27
  } catch (error) {
28
- return '1.3.16'; // Fallback version
28
+ return '1.3.18'; // Fallback version
29
29
  }
30
30
  }
31
31
 
@@ -198,7 +198,25 @@ class SummaryReportService {
198
198
  // Count violations by rule
199
199
  const violationsByRule = {};
200
200
  violations.forEach(violation => {
201
- const ruleId = violation.ruleId || 'unknown';
201
+ // Validate that this is actually a violation object (not metadata/config)
202
+ // A valid violation should have ruleId/rule as string and message
203
+ if (!violation || typeof violation !== 'object') {
204
+ return; // Skip non-objects
205
+ }
206
+
207
+ // Skip objects that look like metadata/config (have nested objects like semanticEngine, project, etc.)
208
+ if (violation.semanticEngine || violation.project || violation.options) {
209
+ return; // Skip config objects
210
+ }
211
+
212
+ // Get ruleId from various possible fields
213
+ const ruleId = violation.ruleId || violation.rule || 'unknown';
214
+
215
+ // Ensure ruleId is a string (not an object)
216
+ if (typeof ruleId !== 'string') {
217
+ return; // Skip invalid ruleId
218
+ }
219
+
202
220
  if (!violationsByRule[ruleId]) {
203
221
  violationsByRule[ruleId] = {
204
222
  rule_code: ruleId,
@@ -707,6 +707,11 @@ class HeuristicEngine extends AnalysisEngineInterface {
707
707
  return parseInt(options.maxFiles);
708
708
  }
709
709
 
710
+ // User explicitly disabled limit
711
+ if (options.maxFiles === -1 || options.maxFiles === '-1') {
712
+ return Infinity;
713
+ }
714
+
710
715
  // Performance config limit
711
716
  if (this.performanceConfig?.maxFiles) {
712
717
  return this.performanceConfig.maxFiles;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sun-asterisk/sunlint",
3
- "version": "1.3.16",
3
+ "version": "1.3.18",
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": {
@@ -0,0 +1,115 @@
1
+ # C002: No Duplicate Code (Monkey Coding Detection)
2
+
3
+ ## Overview
4
+
5
+ This rule detects **true monkey coding** - code that has been copy-pasted multiple times across the codebase. It enforces the DRY (Don't Repeat Yourself) principle by identifying duplicate logic that should be refactored into shared functions.
6
+
7
+ ## Rule Details
8
+
9
+ **Rule ID**: `C002`
10
+ **Category**: Code Quality
11
+ **Severity**: Warning
12
+ **Language Support**: TypeScript/JavaScript (AST-based analysis)
13
+
14
+ ## What is Monkey Coding?
15
+
16
+ Monkey coding refers to the practice of copying and pasting code blocks instead of creating reusable abstractions.
17
+
18
+ ### ✅ TRUE DUPLICATES (Will Report):
19
+ - Functions/methods with identical or near-identical logic (≥95% similarity)
20
+ - Copy-pasted validation, calculation, or error handling logic
21
+ - Duplicate code blocks across different files or within same file
22
+
23
+ ### ❌ INTENTIONAL PATTERNS (Will NOT Report):
24
+ - Simple JSX/HTML wrapper components with different names
25
+ - React component wrappers (e.g., shadcn/ui patterns)
26
+ - Boilerplate patterns with structural similarity but different business logic
27
+
28
+ ## Configuration
29
+
30
+ ```json
31
+ {
32
+ "minLines": 10,
33
+ "similarityThreshold": 0.95
34
+ }
35
+ ```
36
+
37
+ - `minLines`: Minimum lines to consider (default: 10)
38
+ - `similarityThreshold`: 0-1 (default: 0.95 = 95%)
39
+
40
+ ## Examples
41
+
42
+ See test cases in `test-cases/` directory for complete examples.
43
+
44
+ ### ❌ Incorrect (Monkey Coding)
45
+
46
+ ```typescript
47
+ // Copy-paste API error handling
48
+ async getUserById(id: number) {
49
+ try {
50
+ const response = await fetch(`/api/users/${id}`);
51
+ if (!response.ok) {
52
+ throw new Error('Failed to fetch user');
53
+ }
54
+ return await response.json();
55
+ } catch (error) {
56
+ throw new Error('Network request failed');
57
+ }
58
+ }
59
+
60
+ // Duplicate logic!
61
+ async getOrderById(id: number) {
62
+ try {
63
+ const response = await fetch(`/api/orders/${id}`);
64
+ if (!response.ok) {
65
+ throw new Error('Failed to fetch order');
66
+ }
67
+ return await response.json();
68
+ } catch (error) {
69
+ throw new Error('Network request failed');
70
+ }
71
+ }
72
+ ```
73
+
74
+ ### ✅ Correct (Refactored)
75
+
76
+ ```typescript
77
+ async fetchAPI<T>(endpoint: string, context: string): Promise<T> {
78
+ try {
79
+ const response = await fetch(endpoint);
80
+ if (!response.ok) {
81
+ throw new Error(`Failed to fetch ${context}`);
82
+ }
83
+ return await response.json();
84
+ } catch (error) {
85
+ throw new Error('Network request failed');
86
+ }
87
+ }
88
+
89
+ async getUserById(id: number) {
90
+ return this.fetchAPI(`/api/users/${id}`, 'user');
91
+ }
92
+
93
+ async getOrderById(id: number) {
94
+ return this.fetchAPI(`/api/orders/${id}`, 'order');
95
+ }
96
+ ```
97
+
98
+ ## How It Works
99
+
100
+ 1. **AST Parsing**: Parse code with `ts-morph`
101
+ 2. **Block Extraction**: Extract functions, methods, classes
102
+ 3. **Similarity Calculation**: Structure (60%) + Code (40%)
103
+ 4. **Pattern Filtering**: Remove intentional patterns
104
+
105
+ ## Performance
106
+
107
+ - **Speed**: ~14s for 100 TypeScript files
108
+ - **Accuracy**: 100% (no false positives on tests)
109
+
110
+ ## Testing
111
+
112
+ ```bash
113
+ cd test-cases
114
+ node ../test-monkey-coding.js
115
+ ```