@sun-asterisk/sunlint 1.3.16 → 1.3.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/config/rule-analysis-strategies.js +3 -3
- package/config/rules/enhanced-rules-registry.json +40 -20
- package/core/cli-action-handler.js +2 -2
- package/core/config-merger.js +28 -6
- package/core/constants/defaults.js +1 -1
- package/core/file-targeting-service.js +72 -4
- package/core/output-service.js +21 -4
- package/engines/heuristic-engine.js +5 -0
- package/package.json +1 -1
- package/rules/common/C002_no_duplicate_code/README.md +115 -0
- package/rules/common/C002_no_duplicate_code/analyzer.js +615 -219
- package/rules/common/C002_no_duplicate_code/test-cases/api-handlers.ts +64 -0
- package/rules/common/C002_no_duplicate_code/test-cases/data-processor.ts +46 -0
- package/rules/common/C002_no_duplicate_code/test-cases/good-example.tsx +40 -0
- package/rules/common/C002_no_duplicate_code/test-cases/product-service.ts +57 -0
- package/rules/common/C002_no_duplicate_code/test-cases/user-service.ts +49 -0
- package/rules/common/C008/analyzer.js +40 -0
- package/rules/common/C008/config.json +20 -0
- package/rules/common/C008/ts-morph-analyzer.js +1067 -0
- package/rules/common/C018_no_throw_generic_error/analyzer.js +1 -1
- package/rules/common/C018_no_throw_generic_error/symbol-based-analyzer.js +27 -3
- package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +504 -162
- package/rules/common/C029_catch_block_logging/analyzer.js +499 -89
- package/rules/common/C033_separate_service_repository/README.md +131 -20
- package/rules/common/C033_separate_service_repository/analyzer.js +1 -1
- package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +417 -274
- package/rules/common/C041_no_sensitive_hardcode/analyzer.js +144 -254
- package/rules/common/C041_no_sensitive_hardcode/config.json +50 -0
- package/rules/common/C041_no_sensitive_hardcode/symbol-based-analyzer.js +575 -0
- package/rules/common/C067_no_hardcoded_config/analyzer.js +17 -16
- package/rules/common/C067_no_hardcoded_config/symbol-based-analyzer.js +3477 -659
- package/rules/docs/C002_no_duplicate_code.md +276 -11
- package/rules/index.js +5 -1
- package/rules/security/S006_no_plaintext_recovery_codes/analyzer.js +266 -88
- package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +805 -0
- package/rules/security/S010_no_insecure_encryption/README.md +78 -0
- package/rules/security/S010_no_insecure_encryption/analyzer.js +463 -398
- package/rules/security/S013_tls_enforcement/README.md +51 -0
- package/rules/security/S013_tls_enforcement/analyzer.js +99 -0
- package/rules/security/S013_tls_enforcement/config.json +41 -0
- package/rules/security/S013_tls_enforcement/symbol-based-analyzer.js +339 -0
- package/rules/security/S014_tls_version_enforcement/README.md +354 -0
- package/rules/security/S014_tls_version_enforcement/analyzer.js +118 -0
- package/rules/security/S014_tls_version_enforcement/config.json +56 -0
- package/rules/security/S014_tls_version_enforcement/symbol-based-analyzer.js +194 -0
- package/rules/security/S055_content_type_validation/analyzer.js +121 -279
- package/rules/security/S055_content_type_validation/symbol-based-analyzer.js +346 -0
- package/rules/tests/C002_no_duplicate_code.test.js +111 -22
- package/rules/common/C029_catch_block_logging/analyzer-smart-pipeline.js +0 -755
- 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
|
|
76
|
-
methods: ['
|
|
77
|
-
accuracy: {
|
|
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": "
|
|
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",
|
|
@@ -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
|
|
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) :
|
|
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
|
package/core/config-merger.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
227
|
-
|
|
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
|
-
|
|
234
|
-
|
|
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
|
-
|
|
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, //
|
|
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
|
|
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
|
-
//
|
|
533
|
-
|
|
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
|
}
|
package/core/output-service.js
CHANGED
|
@@ -232,12 +232,29 @@ class OutputService {
|
|
|
232
232
|
fileGroups[file].push(violation);
|
|
233
233
|
});
|
|
234
234
|
|
|
235
|
+
// Sort file paths alphabetically for consistent output
|
|
236
|
+
const sortedFiles = Object.keys(fileGroups).sort();
|
|
237
|
+
|
|
235
238
|
// Format each file's violations (ESLint-compatible format)
|
|
236
|
-
|
|
239
|
+
sortedFiles.forEach((file, index) => {
|
|
240
|
+
// Sort violations by line number within each file
|
|
241
|
+
const sortedViolations = fileGroups[file].sort((a, b) => {
|
|
242
|
+
const lineA = a.location?.start?.line || a.line || 1;
|
|
243
|
+
const lineB = b.location?.start?.line || b.line || 1;
|
|
244
|
+
if (lineA !== lineB) return lineA - lineB;
|
|
245
|
+
return (a.location?.start?.column || a.column || 1) - (b.location?.start?.column || b.column || 1);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
// Add blank line before file header (except for the first file)
|
|
249
|
+
if (index > 0) {
|
|
250
|
+
output += '\n';
|
|
251
|
+
}
|
|
237
252
|
output += `\n${chalk.underline(path.resolve(file))}\n`;
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
253
|
+
|
|
254
|
+
sortedViolations.forEach(violation => {
|
|
255
|
+
// Support both location.start.line (new format) and line (legacy format)
|
|
256
|
+
const line = (violation.location?.start?.line || violation.line || 1).toString();
|
|
257
|
+
const column = (violation.location?.start?.column || violation.column || 1).toString();
|
|
241
258
|
const severityText = violation.severity === 'error' ? 'error' : 'warning';
|
|
242
259
|
const severityColor = violation.severity === 'error' ? chalk.red : chalk.yellow;
|
|
243
260
|
|
|
@@ -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
|
@@ -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
|
+
```
|