clone-alert 0.3.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/LICENSE +21 -0
- package/README.md +243 -0
- package/dist/angular.d.ts +23 -0
- package/dist/angular.js +296 -0
- package/dist/cli.d.ts +14 -0
- package/dist/cli.js +407 -0
- package/dist/core.d.ts +98 -0
- package/dist/core.js +442 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +153 -0
- package/dist/svelte.d.ts +4 -0
- package/dist/svelte.js +287 -0
- package/dist/tokenizers.d.ts +53 -0
- package/dist/tokenizers.js +392 -0
- package/dist/vue.d.ts +4 -0
- package/dist/vue.js +189 -0
- package/package.json +108 -0
- package/scripts/compare-pmd-cpd.mjs +565 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
exports.collectFiles = collectFiles;
|
|
38
|
+
exports.main = main;
|
|
39
|
+
exports.parseArgs = parseArgs;
|
|
40
|
+
const fs = __importStar(require("node:fs"));
|
|
41
|
+
const path = __importStar(require("node:path"));
|
|
42
|
+
const index_1 = require("./index");
|
|
43
|
+
const DEFAULT_EXTENSIONS = [
|
|
44
|
+
'.ts',
|
|
45
|
+
'.tsx',
|
|
46
|
+
'.js',
|
|
47
|
+
'.jsx',
|
|
48
|
+
'.mts',
|
|
49
|
+
'.cts',
|
|
50
|
+
'.mjs',
|
|
51
|
+
'.cjs',
|
|
52
|
+
'.vue',
|
|
53
|
+
'.svelte',
|
|
54
|
+
'.html',
|
|
55
|
+
'.htm',
|
|
56
|
+
];
|
|
57
|
+
const HELP = `Usage: clone-alert [options] [<path>...]
|
|
58
|
+
|
|
59
|
+
PMD CPD-like copy-paste detector for TS/JS and common frontend templates.
|
|
60
|
+
|
|
61
|
+
Options:
|
|
62
|
+
--files <path[,path...]> Files or directories to scan. Can be repeated.
|
|
63
|
+
--minimum-tokens <n> Minimum duplicated token span. Default: 50.
|
|
64
|
+
--minimum-tile-size <n> Alias for --minimum-tokens.
|
|
65
|
+
--format <text|xml|json> Report format. Default: text.
|
|
66
|
+
--extensions <ext[,ext...]> Extensions to include. Default: ts,tsx,js,jsx,vue,svelte,html.
|
|
67
|
+
--exclude <glob[,glob...]> Exclude files or directories. Can be repeated.
|
|
68
|
+
--ignore-identifiers Normalize identifiers.
|
|
69
|
+
--no-ignore-identifiers Compare exact identifiers. Default.
|
|
70
|
+
--ignore-literals Normalize literals.
|
|
71
|
+
--no-ignore-literals Compare exact literals. Default.
|
|
72
|
+
--pmd-typescript-compatibility Match PMD typescript granularity for .ts/.tsx:
|
|
73
|
+
split template literals into per-atom tokens
|
|
74
|
+
(backtick, \${, }, one per text char) and
|
|
75
|
+
collapse regexp. .js/.jsx stay native. Default.
|
|
76
|
+
--no-pmd-typescript-compatibility
|
|
77
|
+
Tokenize .ts/.tsx with the native TypeScript
|
|
78
|
+
scanner (a template literal stays one token).
|
|
79
|
+
--svelte-templates Tokenize .svelte markup (ast.fragment), not
|
|
80
|
+
just <script>. Default.
|
|
81
|
+
--no-svelte-templates Tokenize only <script> in .svelte files.
|
|
82
|
+
Use to run markup and code at different
|
|
83
|
+
--minimum-tokens thresholds.
|
|
84
|
+
--vue-templates Tokenize .vue markup (descriptor.template.ast),
|
|
85
|
+
not just <script>. Default.
|
|
86
|
+
--no-vue-templates Skip .vue markup; scan the <script> block
|
|
87
|
+
alone (handy for a code-only threshold pass).
|
|
88
|
+
--angular-inline-templates Also scan Angular @Component inline templates.
|
|
89
|
+
--skip-angular-inline-templates Do not scan inline Angular templates. Default.
|
|
90
|
+
--fail-on-violation Exit with code 4 when duplications are found.
|
|
91
|
+
-h, --help Show this help.
|
|
92
|
+
-V, --version Show version.
|
|
93
|
+
|
|
94
|
+
Examples:
|
|
95
|
+
clone-alert --minimum-tokens 50 --files src
|
|
96
|
+
clone-alert --minimum-tokens 30 --format xml src test
|
|
97
|
+
`;
|
|
98
|
+
function main(argv) {
|
|
99
|
+
let options;
|
|
100
|
+
try {
|
|
101
|
+
options = parseArgs(argv);
|
|
102
|
+
}
|
|
103
|
+
catch (error) {
|
|
104
|
+
console.error(`clone-alert: ${error.message}`);
|
|
105
|
+
console.error("Try 'clone-alert --help' for more information.");
|
|
106
|
+
return 2;
|
|
107
|
+
}
|
|
108
|
+
if (options.paths.length === 0) {
|
|
109
|
+
console.error('clone-alert: missing files or directories to scan');
|
|
110
|
+
console.error("Try 'clone-alert --help' for more information.");
|
|
111
|
+
return 2;
|
|
112
|
+
}
|
|
113
|
+
let files;
|
|
114
|
+
try {
|
|
115
|
+
files = collectFiles(options.paths, options.extensions, options.excludePatterns);
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
console.error(`clone-alert: ${error.message}`);
|
|
119
|
+
return 2;
|
|
120
|
+
}
|
|
121
|
+
if (files.length === 0) {
|
|
122
|
+
console.error('clone-alert: no supported files found');
|
|
123
|
+
return 2;
|
|
124
|
+
}
|
|
125
|
+
const cpd = new index_1.Cpd(options);
|
|
126
|
+
for (const file of files) {
|
|
127
|
+
cpd.addPath(file);
|
|
128
|
+
}
|
|
129
|
+
const matches = cpd.run();
|
|
130
|
+
process.stdout.write(formatReport(options.format, cpd, matches));
|
|
131
|
+
return options.failOnViolation && matches.length > 0 ? 4 : 0;
|
|
132
|
+
}
|
|
133
|
+
function parseArgs(argv) {
|
|
134
|
+
const paths = [];
|
|
135
|
+
const extensions = new Set(DEFAULT_EXTENSIONS);
|
|
136
|
+
const excludePatterns = [];
|
|
137
|
+
let minTileSize = 50;
|
|
138
|
+
let ignoreIdentifiers = false;
|
|
139
|
+
let ignoreLiterals = false;
|
|
140
|
+
let pmdTypescriptCompatibility = true;
|
|
141
|
+
let svelteTemplates = true;
|
|
142
|
+
let vueTemplates = true;
|
|
143
|
+
let angularInlineTemplates = false;
|
|
144
|
+
let format = 'text';
|
|
145
|
+
let failOnViolation = false;
|
|
146
|
+
for (let i = 0; i < argv.length; i++) {
|
|
147
|
+
const arg = argv[i];
|
|
148
|
+
if (arg === '-h' || arg === '--help') {
|
|
149
|
+
console.log(HELP);
|
|
150
|
+
process.exit(0);
|
|
151
|
+
}
|
|
152
|
+
if (arg === '-V' || arg === '--version') {
|
|
153
|
+
console.log(readVersion());
|
|
154
|
+
process.exit(0);
|
|
155
|
+
}
|
|
156
|
+
if (arg === '--files') {
|
|
157
|
+
paths.push(...splitList(requireValue(argv, ++i, arg)));
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (arg.startsWith('--files=')) {
|
|
161
|
+
paths.push(...splitList(arg.slice('--files='.length)));
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (arg === '--minimum-tokens' || arg === '--minimum-tile-size') {
|
|
165
|
+
minTileSize = parsePositiveInteger(requireValue(argv, ++i, arg), arg);
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (arg.startsWith('--minimum-tokens=')) {
|
|
169
|
+
minTileSize = parsePositiveInteger(arg.slice('--minimum-tokens='.length), '--minimum-tokens');
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
if (arg.startsWith('--minimum-tile-size=')) {
|
|
173
|
+
minTileSize = parsePositiveInteger(arg.slice('--minimum-tile-size='.length), '--minimum-tile-size');
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
if (arg === '--format') {
|
|
177
|
+
format = parseFormat(requireValue(argv, ++i, arg));
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
if (arg.startsWith('--format=')) {
|
|
181
|
+
format = parseFormat(arg.slice('--format='.length));
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (arg === '--extensions') {
|
|
185
|
+
replaceExtensions(extensions, requireValue(argv, ++i, arg));
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (arg.startsWith('--extensions=')) {
|
|
189
|
+
replaceExtensions(extensions, arg.slice('--extensions='.length));
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (arg === '--exclude') {
|
|
193
|
+
excludePatterns.push(...splitList(requireValue(argv, ++i, arg)));
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (arg.startsWith('--exclude=')) {
|
|
197
|
+
excludePatterns.push(...splitList(arg.slice('--exclude='.length)));
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (arg === '--ignore-identifiers') {
|
|
201
|
+
ignoreIdentifiers = true;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (arg === '--no-ignore-identifiers') {
|
|
205
|
+
ignoreIdentifiers = false;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (arg === '--ignore-literals') {
|
|
209
|
+
ignoreLiterals = true;
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (arg === '--no-ignore-literals') {
|
|
213
|
+
ignoreLiterals = false;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (arg === '--pmd-typescript-compatibility') {
|
|
217
|
+
pmdTypescriptCompatibility = true;
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
if (arg === '--no-pmd-typescript-compatibility') {
|
|
221
|
+
pmdTypescriptCompatibility = false;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (arg === '--svelte-templates') {
|
|
225
|
+
svelteTemplates = true;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (arg === '--no-svelte-templates') {
|
|
229
|
+
svelteTemplates = false;
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (arg === '--vue-templates') {
|
|
233
|
+
vueTemplates = true;
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (arg === '--no-vue-templates') {
|
|
237
|
+
vueTemplates = false;
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (arg === '--angular-inline-templates') {
|
|
241
|
+
angularInlineTemplates = true;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (arg === '--skip-angular-inline-templates') {
|
|
245
|
+
angularInlineTemplates = false;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (arg === '--fail-on-violation') {
|
|
249
|
+
failOnViolation = true;
|
|
250
|
+
continue;
|
|
251
|
+
}
|
|
252
|
+
if (arg.startsWith('-')) {
|
|
253
|
+
throw new Error(`unknown option: ${arg}`);
|
|
254
|
+
}
|
|
255
|
+
paths.push(arg);
|
|
256
|
+
}
|
|
257
|
+
return {
|
|
258
|
+
paths,
|
|
259
|
+
extensions,
|
|
260
|
+
excludePatterns,
|
|
261
|
+
minTileSize,
|
|
262
|
+
ignoreIdentifiers,
|
|
263
|
+
ignoreLiterals,
|
|
264
|
+
pmdTypescriptCompatibility,
|
|
265
|
+
svelteTemplates,
|
|
266
|
+
vueTemplates,
|
|
267
|
+
angularInlineTemplates,
|
|
268
|
+
format,
|
|
269
|
+
failOnViolation,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
function requireValue(argv, index, option) {
|
|
273
|
+
const value = argv[index];
|
|
274
|
+
if (!value || value.startsWith('-')) {
|
|
275
|
+
throw new Error(`${option} requires a value`);
|
|
276
|
+
}
|
|
277
|
+
return value;
|
|
278
|
+
}
|
|
279
|
+
function splitList(value) {
|
|
280
|
+
return value
|
|
281
|
+
.split(',')
|
|
282
|
+
.map((item) => item.trim())
|
|
283
|
+
.filter(Boolean);
|
|
284
|
+
}
|
|
285
|
+
function parsePositiveInteger(value, option) {
|
|
286
|
+
const parsed = Number(value);
|
|
287
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
288
|
+
throw new Error(`${option} must be a positive integer`);
|
|
289
|
+
}
|
|
290
|
+
return parsed;
|
|
291
|
+
}
|
|
292
|
+
function parseFormat(value) {
|
|
293
|
+
if (value === 'text' || value === 'xml' || value === 'json') {
|
|
294
|
+
return value;
|
|
295
|
+
}
|
|
296
|
+
throw new Error('--format must be one of: text, xml, json');
|
|
297
|
+
}
|
|
298
|
+
function replaceExtensions(target, value) {
|
|
299
|
+
target.clear();
|
|
300
|
+
for (const ext of splitList(value)) {
|
|
301
|
+
target.add(ext.startsWith('.') ? ext.toLowerCase() : `.${ext.toLowerCase()}`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
function collectFiles(paths, extensions, excludePatterns = []) {
|
|
305
|
+
const files = [];
|
|
306
|
+
const seen = new Set();
|
|
307
|
+
const excludeMatchers = excludePatterns.map((pattern) => globToRegExp(toPosix(pattern)));
|
|
308
|
+
const visit = (entry) => {
|
|
309
|
+
const full = path.resolve(entry);
|
|
310
|
+
if (!fs.existsSync(full)) {
|
|
311
|
+
throw new Error(`path does not exist: ${entry}`);
|
|
312
|
+
}
|
|
313
|
+
const stat = fs.statSync(full);
|
|
314
|
+
if (stat.isDirectory()) {
|
|
315
|
+
if (isExcluded(`${full}${path.sep}`, excludeMatchers))
|
|
316
|
+
return;
|
|
317
|
+
for (const child of fs.readdirSync(full).sort()) {
|
|
318
|
+
if (child === 'node_modules' || child === '.git' || child === 'dist')
|
|
319
|
+
continue;
|
|
320
|
+
visit(path.join(full, child));
|
|
321
|
+
}
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (!stat.isFile())
|
|
325
|
+
return;
|
|
326
|
+
if (isExcluded(full, excludeMatchers))
|
|
327
|
+
return;
|
|
328
|
+
if (!extensions.has(path.extname(full).toLowerCase()))
|
|
329
|
+
return;
|
|
330
|
+
if (seen.has(full))
|
|
331
|
+
return;
|
|
332
|
+
seen.add(full);
|
|
333
|
+
files.push(full);
|
|
334
|
+
};
|
|
335
|
+
for (const entry of paths)
|
|
336
|
+
visit(entry);
|
|
337
|
+
return files;
|
|
338
|
+
}
|
|
339
|
+
function formatReport(format, cpd, matches) {
|
|
340
|
+
if (format === 'json') {
|
|
341
|
+
return `${JSON.stringify({ duplicates: matches.map((match) => matchToJson(match, cpd)) }, null, 2)}\n`;
|
|
342
|
+
}
|
|
343
|
+
if (format === 'xml') {
|
|
344
|
+
return formatXml(matches, cpd);
|
|
345
|
+
}
|
|
346
|
+
return cpd.report(matches);
|
|
347
|
+
}
|
|
348
|
+
function matchToJson(match, cpd) {
|
|
349
|
+
const files = match.marks.map((mark) => cpd.locationForMark(mark, match.tokenCount));
|
|
350
|
+
return {
|
|
351
|
+
lines: Math.max(0, ...files.map((file) => file.endLine - file.startLine + 1)),
|
|
352
|
+
tokens: match.tokenCount,
|
|
353
|
+
files,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
function formatXml(matches, cpd) {
|
|
357
|
+
const lines = ['<?xml version="1.0" encoding="UTF-8"?>', '<pmd-cpd>'];
|
|
358
|
+
for (const match of matches) {
|
|
359
|
+
const duplicate = matchToJson(match, cpd);
|
|
360
|
+
lines.push(` <duplication lines="${duplicate.lines}" tokens="${match.tokenCount}" occurrences="${match.markCount}">`);
|
|
361
|
+
for (const mark of match.marks) {
|
|
362
|
+
const location = cpd.locationForMark(mark, match.tokenCount);
|
|
363
|
+
lines.push(` <file path="${escapeXml(location.path)}" line="${location.startLine}" endline="${location.endLine}" column="${location.startColumn}" endcolumn="${location.endColumn}" />`);
|
|
364
|
+
}
|
|
365
|
+
lines.push(' </duplication>');
|
|
366
|
+
}
|
|
367
|
+
lines.push('</pmd-cpd>');
|
|
368
|
+
return `${lines.join('\n')}\n`;
|
|
369
|
+
}
|
|
370
|
+
function isExcluded(filePath, matchers) {
|
|
371
|
+
const normalized = toPosix(filePath);
|
|
372
|
+
return matchers.some((matcher) => matcher.test(normalized));
|
|
373
|
+
}
|
|
374
|
+
function toPosix(value) {
|
|
375
|
+
return value.split(path.sep).join('/');
|
|
376
|
+
}
|
|
377
|
+
function globToRegExp(pattern) {
|
|
378
|
+
let source = '';
|
|
379
|
+
for (let index = 0; index < pattern.length; index++) {
|
|
380
|
+
const char = pattern[index];
|
|
381
|
+
if (char === '*') {
|
|
382
|
+
if (pattern[index + 1] === '*') {
|
|
383
|
+
source += '.*';
|
|
384
|
+
index++;
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
source += '[^/]*';
|
|
388
|
+
}
|
|
389
|
+
continue;
|
|
390
|
+
}
|
|
391
|
+
source += escapeRegExp(char);
|
|
392
|
+
}
|
|
393
|
+
return new RegExp(`^${source}$`);
|
|
394
|
+
}
|
|
395
|
+
function escapeRegExp(char) {
|
|
396
|
+
return /[\\^$+?.()|[\]{}]/.test(char) ? `\\${char}` : char;
|
|
397
|
+
}
|
|
398
|
+
function escapeXml(value) {
|
|
399
|
+
return value.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
400
|
+
}
|
|
401
|
+
function readVersion() {
|
|
402
|
+
const pkg = JSON.parse(fs.readFileSync(path.resolve(__dirname, '..', 'package.json'), 'utf-8'));
|
|
403
|
+
return pkg.version ?? '0.0.0';
|
|
404
|
+
}
|
|
405
|
+
if (require.main === module) {
|
|
406
|
+
process.exitCode = main(process.argv.slice(2));
|
|
407
|
+
}
|
package/dist/core.d.ts
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Language-agnostic CPD core. Operates on a flat stream of tokens supplied by
|
|
3
|
+
* the tokenizers (see tokenizers.ts) as `RawToken[]`.
|
|
4
|
+
*
|
|
5
|
+
* Token storage is struct-of-arrays over typed arrays (`Int32Array`): instead of
|
|
6
|
+
* ~N `TokenEntry` objects we keep parallel numeric columns. Full `TokenEntry`
|
|
7
|
+
* objects are materialized lazily, only for the marks that land in a match.
|
|
8
|
+
*
|
|
9
|
+
* @packageDocumentation
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Normalization sentinel prefix, taken from the Unicode private-use area so it
|
|
13
|
+
* is guaranteed never to collide with real source token images. Framework token
|
|
14
|
+
* namespaces (Angular/Vue/Svelte) live in their own extension modules
|
|
15
|
+
* (src/angular.ts, etc.) and are built on top of this shared sentinel.
|
|
16
|
+
*/
|
|
17
|
+
export declare const S = "\uE000";
|
|
18
|
+
/** Normalized identifier (TS). */
|
|
19
|
+
export declare const TS_ID = "\uE000ID";
|
|
20
|
+
/** Normalized literal (TS). */
|
|
21
|
+
export declare const TS_LIT = "\uE000LIT";
|
|
22
|
+
/** A raw token as emitted by a tokenizer, before it is interned into the core. */
|
|
23
|
+
export interface RawToken {
|
|
24
|
+
image: string;
|
|
25
|
+
/** 1-based. */
|
|
26
|
+
line: number;
|
|
27
|
+
/** 1-based. */
|
|
28
|
+
column: number;
|
|
29
|
+
/** 1-based, PMD-style token end position. */
|
|
30
|
+
endLine?: number;
|
|
31
|
+
/** 1-based, PMD-style exclusive end column. */
|
|
32
|
+
endColumn?: number;
|
|
33
|
+
/** Forced break; inserts an EOF token (id 0) so matches cannot span it. */
|
|
34
|
+
barrier?: boolean;
|
|
35
|
+
}
|
|
36
|
+
/** A fully materialized token with its image, interned id, and source location. */
|
|
37
|
+
export declare class TokenEntry {
|
|
38
|
+
image: string;
|
|
39
|
+
identifier: number;
|
|
40
|
+
index: number;
|
|
41
|
+
file: string;
|
|
42
|
+
beginLine: number;
|
|
43
|
+
beginColumn: number;
|
|
44
|
+
endLine: number;
|
|
45
|
+
endColumn: number;
|
|
46
|
+
constructor(image: string, identifier: number, index: number, file: string, beginLine: number, beginColumn: number, endLine?: number, endColumn?: number);
|
|
47
|
+
}
|
|
48
|
+
/** A single occurrence of a duplicated span, anchored at its starting token. */
|
|
49
|
+
export declare class Mark {
|
|
50
|
+
token: TokenEntry;
|
|
51
|
+
constructor(token: TokenEntry);
|
|
52
|
+
}
|
|
53
|
+
/** A set of marks that share an identical duplicated token span. */
|
|
54
|
+
export declare class Match {
|
|
55
|
+
tokenCount: number;
|
|
56
|
+
/** Dedupe by token index (PMD uses a TreeSet keyed by index, not by reference). */
|
|
57
|
+
private markMap;
|
|
58
|
+
/**
|
|
59
|
+
* Cache of sorted marks. The `marks` getter is hit millions of times in the
|
|
60
|
+
* hot reportMatch path; without the cache every call did Array.from + sort.
|
|
61
|
+
* Invalidated only in addMark, i.e. when the mark set actually changes.
|
|
62
|
+
*/
|
|
63
|
+
private marksSorted;
|
|
64
|
+
constructor(tokenCount: number, first: Mark, second: Mark);
|
|
65
|
+
addMark(entry: TokenEntry): void;
|
|
66
|
+
get markCount(): number;
|
|
67
|
+
get marks(): Mark[];
|
|
68
|
+
}
|
|
69
|
+
/** The duplicate-detection engine: ingests token streams and reports matches. */
|
|
70
|
+
export declare class CpdCore {
|
|
71
|
+
private minTileSize;
|
|
72
|
+
private ids;
|
|
73
|
+
private fileIds;
|
|
74
|
+
private beginLines;
|
|
75
|
+
private beginColumns;
|
|
76
|
+
private endLines;
|
|
77
|
+
private endColumns;
|
|
78
|
+
private size;
|
|
79
|
+
private capacity;
|
|
80
|
+
private imageToId;
|
|
81
|
+
private idImages;
|
|
82
|
+
private fileToId;
|
|
83
|
+
private fileNames;
|
|
84
|
+
constructor(minTileSize?: number);
|
|
85
|
+
private intern;
|
|
86
|
+
private fileId;
|
|
87
|
+
private ensureCapacity;
|
|
88
|
+
private pushToken;
|
|
89
|
+
/** Add one file's token stream. An EOF barrier is always appended at the end. */
|
|
90
|
+
addFile(file: string, raw: RawToken[]): void;
|
|
91
|
+
get tokenCount(): number;
|
|
92
|
+
/** Raw access to the id column for the collector's hot loops (module-internal). */
|
|
93
|
+
get idColumn(): Int32Array;
|
|
94
|
+
/** Materialize a TokenEntry by absolute index. Returns undefined when out of range. */
|
|
95
|
+
entryAt(index: number): TokenEntry | undefined;
|
|
96
|
+
analyze(): Match[];
|
|
97
|
+
private hash;
|
|
98
|
+
}
|