dslop 1.5.2 → 1.6.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.
- package/CHANGELOG.md +25 -0
- package/README.md +11 -13
- package/dist/index.cjs +83 -33
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v1.6.1
|
|
4
|
+
|
|
5
|
+
[compare changes](https://github.com/turf-sports/dslop/compare/v1.6.0...v1.6.1)
|
|
6
|
+
|
|
7
|
+
### 🩹 Fixes
|
|
8
|
+
|
|
9
|
+
- Suppress all logs when using --json output ([59e30f1](https://github.com/turf-sports/dslop/commit/59e30f1))
|
|
10
|
+
- Compact JSON output (90K→4K lines) ([3e93895](https://github.com/turf-sports/dslop/commit/3e93895))
|
|
11
|
+
|
|
12
|
+
### ❤️ Contributors
|
|
13
|
+
|
|
14
|
+
- Siddharth Sharma <sharmasiddharthcs@gmail.com>
|
|
15
|
+
|
|
16
|
+
## v1.6.0
|
|
17
|
+
|
|
18
|
+
[compare changes](https://github.com/turf-sports/dslop/compare/v1.5.2...v1.6.0)
|
|
19
|
+
|
|
20
|
+
### 🚀 Enhancements
|
|
21
|
+
|
|
22
|
+
- Smarter defaults - auto-switch to full scan when no changes ([fb918a8](https://github.com/turf-sports/dslop/commit/fb918a8))
|
|
23
|
+
|
|
24
|
+
### ❤️ Contributors
|
|
25
|
+
|
|
26
|
+
- Siddharth Sharma <sharmasiddharthcs@gmail.com>
|
|
27
|
+
|
|
3
28
|
## v1.5.2
|
|
4
29
|
|
|
5
30
|
[compare changes](https://github.com/turf-sports/dslop/compare/v1.5.1...v1.5.2)
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Find duplicate code in your codebase.
|
|
|
6
6
|
npx dslop
|
|
7
7
|
```
|
|
8
8
|
|
|
9
|
-
By default, checks your branch changes
|
|
9
|
+
By default, checks your branch changes against the codebase. If no changes found, automatically does a full scan.
|
|
10
10
|
|
|
11
11
|
## Install
|
|
12
12
|
|
|
@@ -17,17 +17,18 @@ npm i -g dslop
|
|
|
17
17
|
## Usage
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
dslop # check
|
|
21
|
-
dslop
|
|
22
|
-
dslop
|
|
23
|
-
dslop --
|
|
20
|
+
dslop # check PR changes (or full scan if none)
|
|
21
|
+
dslop ./apps/web # scan apps/web (full if no changes there)
|
|
22
|
+
dslop -c # changes only, exit if none found
|
|
23
|
+
dslop --cross-package # cross-package dupes (monorepos)
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
## Options
|
|
27
27
|
|
|
28
28
|
| Flag | Description |
|
|
29
29
|
|------|-------------|
|
|
30
|
-
| `-a, --all` |
|
|
30
|
+
| `-a, --all` | force full codebase scan |
|
|
31
|
+
| `-c, --changes` | force changes-only mode (exit if no changes) |
|
|
31
32
|
| `-m, --min-lines` | min lines per block (default: 4) |
|
|
32
33
|
| `-s, --similarity` | similarity threshold 0-100 (default: 70) |
|
|
33
34
|
| `-e, --extensions` | file extensions (default: ts,tsx,js,jsx) |
|
|
@@ -59,20 +60,17 @@ Sliding window over source files extracts overlapping blocks at sizes 4, 6, 9, 1
|
|
|
59
60
|
|
|
60
61
|
Exact hash matches = exact duplicates. For similar (non-exact) matches, uses character-level similarity.
|
|
61
62
|
|
|
62
|
-
###
|
|
63
|
+
### Smart defaults
|
|
63
64
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
Regex-based extraction of types, interfaces, enums. Compares by name similarity (Levenshtein + word overlap) and content similarity.
|
|
65
|
+
1. If you have branch changes → checks those against the codebase
|
|
66
|
+
2. If no changes found → automatically scans the entire target path
|
|
67
|
+
3. Use `-c` to force changes-only mode (useful in CI)
|
|
69
68
|
|
|
70
69
|
## Limitations
|
|
71
70
|
|
|
72
71
|
- **TypeScript/JavaScript only for AST:** AST parsing uses Babel with TS/JSX plugins. Other languages fall back to text-based only.
|
|
73
72
|
- **No cross-language:** Won't detect a Python function duplicated in TypeScript.
|
|
74
73
|
- **Comments affect text matching:** Intentional tradeoff. Copy-pasted code often includes comments.
|
|
75
|
-
- **Declaration detection is regex:** Can miss edge cases like multi-line generics or decorators.
|
|
76
74
|
- **Minimum 4 lines:** Shorter duplicates ignored to reduce noise. Use `-m 2` for stricter.
|
|
77
75
|
- **Memory:** Loads all blocks in memory. Very large codebases (>1M lines) may be slow.
|
|
78
76
|
|
package/dist/index.cjs
CHANGED
|
@@ -51724,7 +51724,7 @@ async function scanDirectory(targetPath, options, enableAST = true) {
|
|
|
51724
51724
|
}
|
|
51725
51725
|
|
|
51726
51726
|
// index.ts
|
|
51727
|
-
var VERSION = process.env.npm_package_version || "1.
|
|
51727
|
+
var VERSION = process.env.npm_package_version || "1.6.1";
|
|
51728
51728
|
function parseDiffOutput(diff, cwd) {
|
|
51729
51729
|
const changes = /* @__PURE__ */ new Map();
|
|
51730
51730
|
let currentFile = null;
|
|
@@ -51812,13 +51812,15 @@ dslop - Detect Similar/Duplicate Lines Of Programming
|
|
|
51812
51812
|
Usage:
|
|
51813
51813
|
dslop [path] [options]
|
|
51814
51814
|
|
|
51815
|
-
By default, checks your branch changes
|
|
51815
|
+
By default, checks your branch changes against the codebase.
|
|
51816
|
+
If no changes are found, automatically scans the entire path.
|
|
51816
51817
|
|
|
51817
51818
|
Arguments:
|
|
51818
51819
|
path Directory to scan (default: current directory)
|
|
51819
51820
|
|
|
51820
51821
|
Options:
|
|
51821
|
-
-a, --all
|
|
51822
|
+
-a, --all Force full codebase scan
|
|
51823
|
+
-c, --changes Force changes-only mode (exit if no changes found)
|
|
51822
51824
|
-m, --min-lines <n> Minimum block size in lines (default: ${DEFAULT_MIN_LINES})
|
|
51823
51825
|
-s, --similarity <n> Minimum similarity threshold 0-100 (default: ${Math.round(DEFAULT_SIMILARITY * 100)})
|
|
51824
51826
|
-e, --extensions <s> File extensions to scan (default: ${DEFAULT_EXTENSIONS.join(",")})
|
|
@@ -51830,10 +51832,10 @@ Options:
|
|
|
51830
51832
|
-v, --version Show version
|
|
51831
51833
|
|
|
51832
51834
|
Examples:
|
|
51833
|
-
dslop Check your PR
|
|
51834
|
-
dslop
|
|
51835
|
-
dslop
|
|
51836
|
-
dslop --
|
|
51835
|
+
dslop Check your PR changes (or full scan if none)
|
|
51836
|
+
dslop ./apps/web Scan apps/web (full scan if no changes there)
|
|
51837
|
+
dslop -c Check PR changes only, exit if none
|
|
51838
|
+
dslop --cross-package Cross-package duplicates
|
|
51837
51839
|
`);
|
|
51838
51840
|
}
|
|
51839
51841
|
function showVersion() {
|
|
@@ -51850,6 +51852,7 @@ async function main() {
|
|
|
51850
51852
|
normalize: { type: "boolean", default: true },
|
|
51851
51853
|
"no-normalize": { type: "boolean", default: false },
|
|
51852
51854
|
all: { type: "boolean", short: "a", default: false },
|
|
51855
|
+
changes: { type: "boolean", short: "c", default: false },
|
|
51853
51856
|
"cross-package": { type: "boolean", default: false },
|
|
51854
51857
|
json: { type: "boolean", default: false },
|
|
51855
51858
|
help: { type: "boolean", short: "h", default: false },
|
|
@@ -51871,9 +51874,11 @@ async function main() {
|
|
|
51871
51874
|
const extensions = values.extensions.split(",").map((e) => e.trim());
|
|
51872
51875
|
const ignorePatterns = values.ignore.split(",").map((p) => p.trim());
|
|
51873
51876
|
const normalize2 = !values["no-normalize"];
|
|
51874
|
-
const
|
|
51877
|
+
const forceAll = values.all;
|
|
51878
|
+
const forceChanges = values.changes;
|
|
51875
51879
|
const crossPackage = values["cross-package"];
|
|
51876
51880
|
const jsonOutput = values.json;
|
|
51881
|
+
const hasExplicitPath = positionals.length > 0;
|
|
51877
51882
|
if (minLines < 2) {
|
|
51878
51883
|
console.error("Error: --min-lines must be at least 2");
|
|
51879
51884
|
process.exit(1);
|
|
@@ -51888,32 +51893,47 @@ async function main() {
|
|
|
51888
51893
|
minLines,
|
|
51889
51894
|
normalize: normalize2
|
|
51890
51895
|
};
|
|
51891
|
-
|
|
51892
|
-
|
|
51893
|
-
|
|
51894
|
-
|
|
51896
|
+
let scanAll = forceAll;
|
|
51897
|
+
let changedLines = null;
|
|
51898
|
+
if (!forceAll) {
|
|
51899
|
+
changedLines = getChangedLines(targetPath);
|
|
51900
|
+
if (changedLines.size === 0) {
|
|
51901
|
+
if (forceChanges) {
|
|
51902
|
+
console.log("\nNo changes found on current branch.");
|
|
51903
|
+
process.exit(0);
|
|
51904
|
+
}
|
|
51905
|
+
scanAll = true;
|
|
51906
|
+
if (!jsonOutput) {
|
|
51907
|
+
console.log(`
|
|
51908
|
+
No changes detected${hasExplicitPath ? ` in ${targetPath}` : ""}, defaulting to full scan...`);
|
|
51909
|
+
}
|
|
51910
|
+
}
|
|
51895
51911
|
}
|
|
51896
|
-
|
|
51912
|
+
if (!jsonOutput) {
|
|
51913
|
+
console.log(`
|
|
51897
51914
|
Scanning ${targetPath}...`);
|
|
51898
|
-
|
|
51899
|
-
|
|
51900
|
-
|
|
51901
|
-
|
|
51902
|
-
|
|
51903
|
-
|
|
51904
|
-
|
|
51905
|
-
|
|
51906
|
-
|
|
51907
|
-
|
|
51915
|
+
if (!scanAll && changedLines && changedLines.size > 0) {
|
|
51916
|
+
console.log(` Mode: checking changed lines in ${changedLines.size} files`);
|
|
51917
|
+
} else {
|
|
51918
|
+
console.log(` Mode: full codebase scan`);
|
|
51919
|
+
}
|
|
51920
|
+
console.log(` Extensions: ${extensions.join(", ")}`);
|
|
51921
|
+
console.log(` Min block size: ${minLines} lines`);
|
|
51922
|
+
console.log(` Similarity threshold: ${Math.round(similarity * 100)}%`);
|
|
51923
|
+
if (crossPackage) {
|
|
51924
|
+
console.log(` Cross-package: enabled`);
|
|
51925
|
+
}
|
|
51926
|
+
console.log();
|
|
51908
51927
|
}
|
|
51909
|
-
console.log();
|
|
51910
51928
|
try {
|
|
51911
51929
|
const startTime = performance.now();
|
|
51912
51930
|
const { blocks, declarations, astBlocks, fileCount, totalLines } = await scanDirectory(targetPath, scanOptions, true);
|
|
51913
51931
|
const scanTime = performance.now() - startTime;
|
|
51914
|
-
|
|
51915
|
-
|
|
51932
|
+
if (!jsonOutput) {
|
|
51933
|
+
console.log(`Scanned ${fileCount} files (${totalLines.toLocaleString()} lines) in ${Math.round(scanTime)}ms`);
|
|
51934
|
+
console.log(`Extracted ${blocks.length.toLocaleString()} blocks, ${astBlocks.length.toLocaleString()} functions/classes
|
|
51916
51935
|
`);
|
|
51936
|
+
}
|
|
51917
51937
|
if (blocks.length === 0 && astBlocks.length === 0) {
|
|
51918
51938
|
console.log("No code found to analyze.");
|
|
51919
51939
|
process.exit(0);
|
|
@@ -51974,15 +51994,19 @@ Scanning ${targetPath}...`);
|
|
|
51974
51994
|
declDuplicates = filterCrossPackage(declDuplicates);
|
|
51975
51995
|
}
|
|
51976
51996
|
const totalGroups = duplicates.length + astDuplicates.length + declDuplicates.length;
|
|
51977
|
-
|
|
51978
|
-
|
|
51979
|
-
|
|
51997
|
+
if (!jsonOutput) {
|
|
51998
|
+
console.log(`Found ${totalGroups} duplicate groups in ${Math.round(detectTime)}ms`);
|
|
51999
|
+
if (astDuplicates.length > 0 || declDuplicates.length > 0) {
|
|
52000
|
+
console.log(` (${duplicates.length} blocks, ${astDuplicates.length} AST, ${declDuplicates.length} declarations)
|
|
51980
52001
|
`);
|
|
51981
|
-
|
|
51982
|
-
|
|
52002
|
+
} else {
|
|
52003
|
+
console.log();
|
|
52004
|
+
}
|
|
51983
52005
|
}
|
|
51984
52006
|
if (totalGroups === 0) {
|
|
51985
|
-
if (
|
|
52007
|
+
if (jsonOutput) {
|
|
52008
|
+
console.log(JSON.stringify({ duplicates: [], ast: [], declarations: [] }, null, 2));
|
|
52009
|
+
} else if (!scanAll) {
|
|
51986
52010
|
console.log("No duplicates in your changes. You're good!");
|
|
51987
52011
|
} else if (crossPackage) {
|
|
51988
52012
|
console.log("No cross-package duplicates found!");
|
|
@@ -51992,7 +52016,33 @@ Scanning ${targetPath}...`);
|
|
|
51992
52016
|
process.exit(0);
|
|
51993
52017
|
}
|
|
51994
52018
|
if (jsonOutput) {
|
|
51995
|
-
|
|
52019
|
+
const compactDuplicates = duplicates.slice(0, 100).map((d) => ({
|
|
52020
|
+
lines: d.lineCount,
|
|
52021
|
+
occurrences: d.occurrences,
|
|
52022
|
+
similarity: Math.round(d.similarity * 100),
|
|
52023
|
+
preview: d.matches[0]?.content.split("\n").slice(0, 3).join("\n") || "",
|
|
52024
|
+
locations: d.matches.slice(0, 10).map((m) => `${m.filePath.replace(process.cwd() + "/", "")}:${m.startLine}`)
|
|
52025
|
+
}));
|
|
52026
|
+
const compactAST = astDuplicates.slice(0, 50).map((a) => ({
|
|
52027
|
+
type: a.type,
|
|
52028
|
+
name: a.matches[0]?.name || "unknown",
|
|
52029
|
+
occurrences: a.matches.length,
|
|
52030
|
+
locations: a.matches.map((m) => ({
|
|
52031
|
+
name: m.name,
|
|
52032
|
+
file: m.filePath.replace(process.cwd() + "/", ""),
|
|
52033
|
+
line: m.startLine,
|
|
52034
|
+
exported: m.exported
|
|
52035
|
+
}))
|
|
52036
|
+
}));
|
|
52037
|
+
console.log(JSON.stringify({
|
|
52038
|
+
summary: {
|
|
52039
|
+
duplicateGroups: duplicates.length,
|
|
52040
|
+
astGroups: astDuplicates.length,
|
|
52041
|
+
declarationGroups: declDuplicates.length
|
|
52042
|
+
},
|
|
52043
|
+
duplicates: compactDuplicates,
|
|
52044
|
+
ast: compactAST
|
|
52045
|
+
}, null, 2));
|
|
51996
52046
|
} else {
|
|
51997
52047
|
if (astDuplicates.length > 0) {
|
|
51998
52048
|
console.log(formatASTDuplicates(astDuplicates, targetPath));
|