omp-plugin-duplicate-detector 0.1.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.
- package/README.md +117 -0
- package/dist/detector-worker.js +13897 -0
- package/package.json +105 -0
- package/src/config-loader.ts +336 -0
- package/src/coordinator.ts +536 -0
- package/src/detector-worker.ts +828 -0
- package/src/disk-cache.ts +703 -0
- package/src/duplicate-ledger.ts +144 -0
- package/src/index.ts +807 -0
- package/src/jscpd-engine.ts +797 -0
- package/src/project-state.ts +129 -0
- package/src/source-aware-index.ts +919 -0
- package/src/test-detector.ts +337 -0
- package/src/tui-notification.ts +464 -0
- package/src/worker-protocol.ts +330 -0
- package/types/global.d.ts +19 -0
- package/types/jscpd.d.ts +139 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-language test file, test directory, mock, and fixture detector.
|
|
3
|
+
* Identifies testing-related files across the software engineering ecosystem
|
|
4
|
+
* (JavaScript, TypeScript, Python, Java, Kotlin, Scala, Groovy, Go, Rust,
|
|
5
|
+
* C, C++, C#, F#, Ruby, PHP, Swift, Objective-C, Elixir, Erlang, Clojure,
|
|
6
|
+
* Haskell, OCaml, Reason, ReScript, Dart, R, Julia, Lua, Zig, Shell, and more).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import * as path from "node:path";
|
|
10
|
+
import ignore from "ignore";
|
|
11
|
+
|
|
12
|
+
export interface TestFileDetectorOptions {
|
|
13
|
+
/**
|
|
14
|
+
* Whether test file detection is enabled. Defaults to true.
|
|
15
|
+
*/
|
|
16
|
+
ignoreTests?: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Additional glob patterns or path substrings to treat as test files.
|
|
19
|
+
*/
|
|
20
|
+
customTestPatterns?: string[];
|
|
21
|
+
/**
|
|
22
|
+
* Glob patterns or path substrings to exclude from test detection (i.e. keep as production code).
|
|
23
|
+
*/
|
|
24
|
+
excludeTestPatterns?: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Standard testing directory segment names across ecosystems.
|
|
29
|
+
* Matched against individual path directory segments (case-insensitive where appropriate).
|
|
30
|
+
*/
|
|
31
|
+
const DEFAULT_TEST_DIRECTORY_NAMES: Record<string, true> = {
|
|
32
|
+
tests: true,
|
|
33
|
+
testing: true,
|
|
34
|
+
__test__: true,
|
|
35
|
+
__tests__: true,
|
|
36
|
+
spec: true,
|
|
37
|
+
specs: true,
|
|
38
|
+
__spec__: true,
|
|
39
|
+
__specs__: true,
|
|
40
|
+
__mocks__: true,
|
|
41
|
+
__fixtures__: true,
|
|
42
|
+
__snapshots__: true,
|
|
43
|
+
fixtures: true,
|
|
44
|
+
fixture: true,
|
|
45
|
+
mocks: true,
|
|
46
|
+
mock: true,
|
|
47
|
+
snapshots: true,
|
|
48
|
+
snapshot: true,
|
|
49
|
+
stubs: true,
|
|
50
|
+
fakes: true,
|
|
51
|
+
testdata: true,
|
|
52
|
+
test_data: true,
|
|
53
|
+
"test-data": true,
|
|
54
|
+
testfixtures: true,
|
|
55
|
+
test_fixtures: true,
|
|
56
|
+
"test-fixtures": true,
|
|
57
|
+
testsuite: true,
|
|
58
|
+
testsuites: true,
|
|
59
|
+
test_suite: true,
|
|
60
|
+
"test-suite": true,
|
|
61
|
+
testutils: true,
|
|
62
|
+
test_utils: true,
|
|
63
|
+
"test-utils": true,
|
|
64
|
+
testhelpers: true,
|
|
65
|
+
test_helpers: true,
|
|
66
|
+
"test-helpers": true,
|
|
67
|
+
unittest: true,
|
|
68
|
+
unittests: true,
|
|
69
|
+
unit_tests: true,
|
|
70
|
+
"unit-tests": true,
|
|
71
|
+
unit_test: true,
|
|
72
|
+
"unit-test": true,
|
|
73
|
+
integration_tests: true,
|
|
74
|
+
"integration-tests": true,
|
|
75
|
+
integration_test: true,
|
|
76
|
+
"integration-test": true,
|
|
77
|
+
functional_tests: true,
|
|
78
|
+
"functional-tests": true,
|
|
79
|
+
functional_test: true,
|
|
80
|
+
"functional-test": true,
|
|
81
|
+
system_tests: true,
|
|
82
|
+
"system-tests": true,
|
|
83
|
+
systemtest: true,
|
|
84
|
+
systemtests: true,
|
|
85
|
+
uitests: true,
|
|
86
|
+
ui_tests: true,
|
|
87
|
+
"ui-tests": true,
|
|
88
|
+
e2e: true,
|
|
89
|
+
"e2e-tests": true,
|
|
90
|
+
e2e_tests: true,
|
|
91
|
+
cypress: true,
|
|
92
|
+
playwright: true,
|
|
93
|
+
testthat: true,
|
|
94
|
+
benches: true,
|
|
95
|
+
benchmarks: true,
|
|
96
|
+
bdd: true,
|
|
97
|
+
cucumber: true,
|
|
98
|
+
step_definitions: true,
|
|
99
|
+
wiremock: true,
|
|
100
|
+
contract_tests: true,
|
|
101
|
+
"contract-tests": true,
|
|
102
|
+
smoke_tests: true,
|
|
103
|
+
"smoke-tests": true,
|
|
104
|
+
acceptance_tests: true,
|
|
105
|
+
"acceptance-tests": true,
|
|
106
|
+
googletest: true,
|
|
107
|
+
gtest: true,
|
|
108
|
+
catch2: true,
|
|
109
|
+
doctest: true,
|
|
110
|
+
testcontainers: true,
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* Standard test directory prefix / structured path regexes (e.g. Maven, Gradle, Android, .NET, Xcode).
|
|
114
|
+
*/
|
|
115
|
+
const STRUCTURED_TEST_PATH_PATTERNS: readonly RegExp[] = [
|
|
116
|
+
// Maven / Gradle / JVM: src/test/**, src/it/**, src/androidTest/**, src/integrationTest/**, src/testFixtures/**
|
|
117
|
+
/(?:^|\/)src\/(?:test|it|androidTest|integrationTest|functionalTest|testFixtures|testDebug|testRelease)(?:\/|$)/i,
|
|
118
|
+
// .NET test project directories: MyProject.Tests, MyProject.UnitTests, MyProject.IntegrationTests, MyProject.Specs
|
|
119
|
+
/(?:^|\/)[^/]+\.(?:tests|test|unittests|unittest|integrationtests|integrationtest|specs|spec|functionaltests)(?:\/|$)/i,
|
|
120
|
+
// Xcode / Swift test folders: MyProjectTests, MyProjectUITests, MyProjectUnitTests
|
|
121
|
+
/(?:^|\/)[^/]+(?:Tests|UITests|UnitTests)(?:\/|$)/,
|
|
122
|
+
// General test suites and directories
|
|
123
|
+
/(?:^|\/)(?:tests?|specs?|testing|test_suite|test_data|testdata|fixtures|mocks|snapshots|e2e|cypress|playwright|googletest|gtest|catch2|doctest)\//i,
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* False-positive guard root words.
|
|
128
|
+
* Legitimate domain concepts/words that contain "test" but are NOT test files
|
|
129
|
+
* unless accompanied by explicit test delimiters (e.g. `.test.`, `_test.`) or located in a test directory.
|
|
130
|
+
*/
|
|
131
|
+
const FALSE_POSITIVE_ROOTS =
|
|
132
|
+
/^(?:at?test(?:ation|ing|ed|s)?|contest(?:ant|ants|ed|ing|s)?|detest(?:able|ed|ing|s)?|protest(?:er|ers|ed|ing|s)?|testament(?:ary|s)?|testosterone|testify|fastest|latest|greatest|smartest|shortest|longest|fittest|neatest|sweetest|brightest|hottest|context|contextual|latent|intestine|intestate)$/i;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Prefix-based test filename patterns (e.g. `test_auth.py`, `TestUser.java`, `ITPayment.java`, `conftest.py`).
|
|
136
|
+
*/
|
|
137
|
+
const TEST_FILENAME_PREFIX_REGEX =
|
|
138
|
+
/^(?:test[_-]|tests[_-]|test\.|tests\.|runtests\.|conftest\.|setupTests\.|setup-tests\.|setup_tests\.|jest\.setup\.|vitest\.setup\.)/i;
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Suffix-based test filename patterns (before extension):
|
|
142
|
+
* E.g.:
|
|
143
|
+
* - `.test`, `.spec`, `.cy`, `.e2e`, `.e2e-spec`, `.integration`, `.unit`
|
|
144
|
+
* - `_test`, `_tests`, `_spec`, `_specs`, `_unittest`, `_unit_test`, `_integration_test`, `_bench_test`, `_bench`, `_SUITE`
|
|
145
|
+
* - `-test`, `-spec`, `-tests`, `-specs`
|
|
146
|
+
* - PascalCase `Test`, `Tests`, `TestCase`, `TestCases`, `IT`, `ITCase`, `Spec`, `Specs`, `Benchmark`, `Benchmarks`
|
|
147
|
+
*/
|
|
148
|
+
const TEST_SUFFIX_REGEX =
|
|
149
|
+
/(?:[._-](?:test|tests|spec|specs|cy|e2e|e2e-spec|e2e_spec|integration|unit|unittest|unit_test|integration_test|integration-test|bench|bench_test|benchmark|suite|fixture|fixtures))$/i;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* PascalCase / CamelCase test name suffix regex (e.g. `UserTest`, `OrderTests`, `AuthTestCase`, `PaymentIT`, `CartSpec`, `SortBenchmark`).
|
|
153
|
+
*/
|
|
154
|
+
const PASCAL_TEST_SUFFIX_REGEX =
|
|
155
|
+
/^[A-Z][A-Za-z0-9_]*(?:Test|Tests|TestCase|TestCases|IT|ITCase|Spec|Specs|Benchmark|Benchmarks)$/;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Standalone test harness / helper filenames across languages.
|
|
159
|
+
*/
|
|
160
|
+
const TEST_HELPER_FILENAMES: Record<string, true> = {
|
|
161
|
+
"test_helper.rb": true,
|
|
162
|
+
"spec_helper.rb": true,
|
|
163
|
+
"rails_helper.rb": true,
|
|
164
|
+
"test_helper.go": true,
|
|
165
|
+
"test_helpers.go": true,
|
|
166
|
+
"test_utils.py": true,
|
|
167
|
+
"test_util.py": true,
|
|
168
|
+
"test-utils.ts": true,
|
|
169
|
+
"test-utils.js": true,
|
|
170
|
+
"test-utils.tsx": true,
|
|
171
|
+
"test-utils.jsx": true,
|
|
172
|
+
"testutils.ts": true,
|
|
173
|
+
"testutils.js": true,
|
|
174
|
+
"testutils.tsx": true,
|
|
175
|
+
"testutils.jsx": true,
|
|
176
|
+
"conftest.py": true,
|
|
177
|
+
"setuptests.ts": true,
|
|
178
|
+
"setuptests.js": true,
|
|
179
|
+
"setuptests.tsx": true,
|
|
180
|
+
"setuptests.jsx": true,
|
|
181
|
+
"jest.setup.ts": true,
|
|
182
|
+
"jest.setup.js": true,
|
|
183
|
+
"jest.setup.mjs": true,
|
|
184
|
+
"jest.setup.cjs": true,
|
|
185
|
+
"vitest.setup.ts": true,
|
|
186
|
+
"vitest.setup.js": true,
|
|
187
|
+
"vitest.setup.mjs": true,
|
|
188
|
+
"vitest.setup.cjs": true,
|
|
189
|
+
"test_runner.rb": true,
|
|
190
|
+
"runtests.jl": true,
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* File extensions exclusively used for test scripts, fixtures, or snapshots.
|
|
195
|
+
*/
|
|
196
|
+
const TEST_ONLY_EXTENSIONS: Record<string, true> = {
|
|
197
|
+
".bats": true,
|
|
198
|
+
".snap": true,
|
|
199
|
+
".snapshot": true,
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Checks whether a given relative or absolute file path corresponds to a test file,
|
|
204
|
+
* test directory, mock, test helper, or fixture across any programming language.
|
|
205
|
+
*
|
|
206
|
+
* @param filePath - The file or directory path to inspect.
|
|
207
|
+
* @param options - Optional configuration including custom/exclude patterns.
|
|
208
|
+
* @returns `true` if the file is identified as a test file; `false` otherwise.
|
|
209
|
+
*/
|
|
210
|
+
export function isTestFile(
|
|
211
|
+
filePath: string,
|
|
212
|
+
options: TestFileDetectorOptions = {},
|
|
213
|
+
): boolean {
|
|
214
|
+
if (options.ignoreTests === false) {
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (!filePath || typeof filePath !== "string") {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const normalized = filePath.trim().replace(/\\/g, "/").replace(/^\.\//, "");
|
|
223
|
+
if (!normalized || normalized === ".") {
|
|
224
|
+
return false;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 1. Check user-defined exclude patterns (takes precedence to un-ignore)
|
|
228
|
+
if (options.excludeTestPatterns && options.excludeTestPatterns.length > 0) {
|
|
229
|
+
try {
|
|
230
|
+
const excludeFilter = ignore().add(options.excludeTestPatterns);
|
|
231
|
+
if (excludeFilter.ignores(normalized)) {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
} catch {
|
|
235
|
+
// Ignore pattern syntax errors gracefully
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// 2. Check user-defined custom test patterns
|
|
240
|
+
if (options.customTestPatterns && options.customTestPatterns.length > 0) {
|
|
241
|
+
try {
|
|
242
|
+
const customFilter = ignore().add(options.customTestPatterns);
|
|
243
|
+
if (customFilter.ignores(normalized)) {
|
|
244
|
+
return true;
|
|
245
|
+
}
|
|
246
|
+
} catch {
|
|
247
|
+
// Ignore pattern syntax errors gracefully
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const ext = path.extname(normalized).toLowerCase();
|
|
252
|
+
|
|
253
|
+
// 3. Test-only extensions (e.g. .bats, .snap, .snapshot)
|
|
254
|
+
if (TEST_ONLY_EXTENSIONS[ext]) {
|
|
255
|
+
return true;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const baseName = path.basename(normalized);
|
|
259
|
+
const baseNameLower = baseName.toLowerCase();
|
|
260
|
+
|
|
261
|
+
// 4. Exact test helper filenames
|
|
262
|
+
if (TEST_HELPER_FILENAMES[baseNameLower]) {
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const segments = normalized.split("/").filter(Boolean);
|
|
267
|
+
const dirSegments = segments.slice(0, -1);
|
|
268
|
+
|
|
269
|
+
// 5. Directory segment match (e.g. /test/, /tests/, /__tests__/, /spec/, /testdata/, etc.)
|
|
270
|
+
for (const dir of dirSegments) {
|
|
271
|
+
const dirLower = dir.toLowerCase();
|
|
272
|
+
if (DEFAULT_TEST_DIRECTORY_NAMES[dirLower]) {
|
|
273
|
+
return true;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// 6. Structured directory path patterns (e.g. src/test/java/**, *.Tests/**, Xcode *Tests/**)
|
|
278
|
+
if (dirSegments.length > 0) {
|
|
279
|
+
for (const pattern of STRUCTURED_TEST_PATH_PATTERNS) {
|
|
280
|
+
if (pattern.test(normalized)) {
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// 7. Check filename without extension (strip compound extensions like .test.ts or .spec.jsx)
|
|
287
|
+
const nameWithoutExt = baseName.slice(0, baseName.length - ext.length);
|
|
288
|
+
if (!nameWithoutExt) {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Guard against false positive root words when exact match without test delimiters
|
|
293
|
+
if (FALSE_POSITIVE_ROOTS.test(nameWithoutExt)) {
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// 8. Test prefix matching (e.g. `test_*.py`, `test-*.js`, `Test*.java`, `IT*.java`)
|
|
298
|
+
if (TEST_FILENAME_PREFIX_REGEX.test(baseName)) {
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Special check for PascalCase Test* / IT* prefix like TestUser.java, ITOrder.java
|
|
303
|
+
if (
|
|
304
|
+
/^(?:Test|TestCase|IT)[A-Z0-9_]/.test(nameWithoutExt) &&
|
|
305
|
+
!FALSE_POSITIVE_ROOTS.test(nameWithoutExt)
|
|
306
|
+
) {
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// 9. Suffix-based delimiter matching (e.g. `*.test.*`, `*.spec.*`, `*_test.*`, `*-test.*`, `*.cy.*`, `*.e2e.*`)
|
|
311
|
+
if (TEST_SUFFIX_REGEX.test(nameWithoutExt)) {
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// 10. PascalCase / CamelCase test name suffix matching (e.g. `UserServiceTest`, `OrderTests`, `PaymentIT`, `AuthSpec`)
|
|
316
|
+
if (
|
|
317
|
+
PASCAL_TEST_SUFFIX_REGEX.test(nameWithoutExt) &&
|
|
318
|
+
!FALSE_POSITIVE_ROOTS.test(nameWithoutExt)
|
|
319
|
+
) {
|
|
320
|
+
return true;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Creates a fast predicate function that returns `true` if a path is identified as a test file.
|
|
328
|
+
*/
|
|
329
|
+
export function createTestFilter(
|
|
330
|
+
options: TestFileDetectorOptions = {},
|
|
331
|
+
): (relPath: string) => boolean {
|
|
332
|
+
if (options.ignoreTests === false) {
|
|
333
|
+
return () => false;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return (relPath: string) => isTestFile(relPath, options);
|
|
337
|
+
}
|