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
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.DEFAULTS = void 0;
|
|
37
|
+
exports.optional = optional;
|
|
38
|
+
exports.moduleResolveDirs = moduleResolveDirs;
|
|
39
|
+
exports.remap = remap;
|
|
40
|
+
exports.tokenizeTypeScript = tokenizeTypeScript;
|
|
41
|
+
exports.scriptKindFor = scriptKindFor;
|
|
42
|
+
/**
|
|
43
|
+
* The TypeScript/JavaScript tokenizer plus the shared helpers used by every
|
|
44
|
+
* framework tokenizer extension (`optional`, `moduleResolveDirs`, `remap`).
|
|
45
|
+
*
|
|
46
|
+
* @packageDocumentation
|
|
47
|
+
*/
|
|
48
|
+
const ts = __importStar(require("typescript"));
|
|
49
|
+
const core_1 = require("./core");
|
|
50
|
+
exports.DEFAULTS = {
|
|
51
|
+
ignoreIdentifiers: false,
|
|
52
|
+
ignoreLiterals: false,
|
|
53
|
+
pmdTypescriptCompatibility: true,
|
|
54
|
+
svelteTemplates: true,
|
|
55
|
+
vueTemplates: true,
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* Load an optional compiler (`@angular/compiler`, `@vue/compiler-sfc`, `svelte`)
|
|
59
|
+
* — a peerDependency. Resolution starts FROM the analyzed file and walks up the
|
|
60
|
+
* tree (so we use the compiler version installed in the scanned project), and
|
|
61
|
+
* only then falls back to clone-alert's own node_modules. `fromPaths` is usually
|
|
62
|
+
* `[dirname(filePath)]`; Node climbs node_modules from there.
|
|
63
|
+
*/
|
|
64
|
+
function optional(name, fromPaths) {
|
|
65
|
+
try {
|
|
66
|
+
const id = require.resolve(name, fromPaths && fromPaths.length ? { paths: fromPaths } : undefined);
|
|
67
|
+
return require(id);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
if (fromPaths && fromPaths.length) {
|
|
71
|
+
try {
|
|
72
|
+
return require(name);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Starting point for resolving a peer compiler: the directory of the analyzed
|
|
83
|
+
* file. Node walks up node_modules from there to the project root.
|
|
84
|
+
*/
|
|
85
|
+
function moduleResolveDirs(filePath) {
|
|
86
|
+
const slash = Math.max(filePath.lastIndexOf('/'), filePath.lastIndexOf('\\'));
|
|
87
|
+
return slash > 0 ? [filePath.slice(0, slash)] : [];
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Remap an embedded block's positions into file coordinates. Block tokens are
|
|
91
|
+
* counted from (0,0) within the block; baseLine/baseCol (1-based) give the
|
|
92
|
+
* absolute start of the block. The column shift applies only to the block's
|
|
93
|
+
* first line.
|
|
94
|
+
*/
|
|
95
|
+
function remap(tok, baseLine, baseCol) {
|
|
96
|
+
const firstLine = tok.line === 1;
|
|
97
|
+
const endFirstLine = (tok.endLine ?? tok.line) === 1;
|
|
98
|
+
return {
|
|
99
|
+
image: tok.image,
|
|
100
|
+
line: baseLine + tok.line - 1,
|
|
101
|
+
column: firstLine ? baseCol + tok.column - 1 : tok.column,
|
|
102
|
+
endLine: baseLine + (tok.endLine ?? tok.line) - 1,
|
|
103
|
+
endColumn: endFirstLine ? baseCol + (tok.endColumn ?? tok.column) - 1 : (tok.endColumn ?? tok.column),
|
|
104
|
+
barrier: tok.barrier,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Tokenize TS / TSX / JSX via the TypeScript scanner. CPD counts the stream of
|
|
109
|
+
* lexical tokens, including keywords and punctuation.
|
|
110
|
+
*/
|
|
111
|
+
function tokenizeTypeScript(filePath, source, opts = {}, scriptKind = ts.ScriptKind.TS) {
|
|
112
|
+
const o = { ...exports.DEFAULTS, ...opts };
|
|
113
|
+
// This used to call ts.createSourceFile (a full AST parse) only to map
|
|
114
|
+
// positions. The AST is not needed — we use the same line map TS builds under
|
|
115
|
+
// the hood for getLineAndCharacterOfPosition, without parsing. See createLineMap.
|
|
116
|
+
const sf = createLineMap(source);
|
|
117
|
+
const suppressedRanges = findCpdSuppressedRanges(source);
|
|
118
|
+
const scanner = ts.createScanner(ts.ScriptTarget.Latest, true, scriptKind === ts.ScriptKind.TSX || scriptKind === ts.ScriptKind.JSX
|
|
119
|
+
? ts.LanguageVariant.JSX
|
|
120
|
+
: ts.LanguageVariant.Standard, source);
|
|
121
|
+
const out = [];
|
|
122
|
+
let previousTokenKind = null;
|
|
123
|
+
const normalize = (kind, text) => {
|
|
124
|
+
switch (kind) {
|
|
125
|
+
case ts.SyntaxKind.Identifier:
|
|
126
|
+
case ts.SyntaxKind.PrivateIdentifier:
|
|
127
|
+
return o.ignoreIdentifiers ? core_1.TS_ID : text;
|
|
128
|
+
case ts.SyntaxKind.StringLiteral:
|
|
129
|
+
case ts.SyntaxKind.NumericLiteral:
|
|
130
|
+
case ts.SyntaxKind.BigIntLiteral:
|
|
131
|
+
case ts.SyntaxKind.NoSubstitutionTemplateLiteral:
|
|
132
|
+
case ts.SyntaxKind.RegularExpressionLiteral:
|
|
133
|
+
case ts.SyntaxKind.TemplateHead:
|
|
134
|
+
case ts.SyntaxKind.TemplateMiddle:
|
|
135
|
+
case ts.SyntaxKind.TemplateTail:
|
|
136
|
+
return o.ignoreLiterals ? core_1.TS_LIT : normalizeStringContinuation(text);
|
|
137
|
+
case ts.SyntaxKind.JsxText: {
|
|
138
|
+
const t = text.trim();
|
|
139
|
+
if (!t)
|
|
140
|
+
return null; // drop empty JSX text
|
|
141
|
+
return o.ignoreLiterals ? core_1.TS_LIT : t;
|
|
142
|
+
}
|
|
143
|
+
default:
|
|
144
|
+
return text;
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
// PMD typescript compatibility is enabled only for TS files. .ts/.tsx (flag
|
|
148
|
+
// ON): templates are split into PMD-typescript atoms (backtick / ${ / } / one
|
|
149
|
+
// per text char — grammar TypeScriptLexer.g4 TemplateStringAtom: ~[`\\]) and
|
|
150
|
+
// regexp is collapsed. .js/.jsx, or flag OFF: the native scanner (a template
|
|
151
|
+
// is one token, no PMD massaging).
|
|
152
|
+
const pmdTypeScript = o.pmdTypescriptCompatibility && isTypeScriptFile(filePath, scriptKind);
|
|
153
|
+
const splitTemplates = pmdTypeScript;
|
|
154
|
+
// Brace depth inside each active ${…} interpolation. At zero, the next `}`
|
|
155
|
+
// closes the interpolation -> rescan it as TemplateMiddle/Tail.
|
|
156
|
+
const templateBraceDepth = [];
|
|
157
|
+
const bumpTemplateDepth = (kind) => {
|
|
158
|
+
if (!splitTemplates)
|
|
159
|
+
return;
|
|
160
|
+
const top = templateBraceDepth.length - 1;
|
|
161
|
+
switch (kind) {
|
|
162
|
+
case ts.SyntaxKind.TemplateHead:
|
|
163
|
+
templateBraceDepth.push(0);
|
|
164
|
+
break;
|
|
165
|
+
case ts.SyntaxKind.TemplateTail:
|
|
166
|
+
templateBraceDepth.pop();
|
|
167
|
+
break;
|
|
168
|
+
case ts.SyntaxKind.OpenBraceToken:
|
|
169
|
+
if (top >= 0)
|
|
170
|
+
templateBraceDepth[top]++;
|
|
171
|
+
break;
|
|
172
|
+
case ts.SyntaxKind.CloseBraceToken:
|
|
173
|
+
if (top >= 0)
|
|
174
|
+
templateBraceDepth[top]--;
|
|
175
|
+
break;
|
|
176
|
+
default:
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
for (;;) {
|
|
181
|
+
let kind = scanner.scan();
|
|
182
|
+
if (kind === ts.SyntaxKind.EndOfFileToken)
|
|
183
|
+
break;
|
|
184
|
+
// Closing `}` of an interpolation: rescan it as a template continuation.
|
|
185
|
+
if (splitTemplates &&
|
|
186
|
+
kind === ts.SyntaxKind.CloseBraceToken &&
|
|
187
|
+
templateBraceDepth.length > 0 &&
|
|
188
|
+
templateBraceDepth[templateBraceDepth.length - 1] === 0) {
|
|
189
|
+
kind = scanner.reScanTemplateToken(false);
|
|
190
|
+
}
|
|
191
|
+
const tokenStart = scanner.getTokenPos();
|
|
192
|
+
if (isSuppressed(tokenStart, suppressedRanges)) {
|
|
193
|
+
bumpTemplateDepth(kind); // keep the stack in sync through CPD-OFF
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
if (pmdTypeScript && kind === ts.SyntaxKind.SlashToken && canStartPmdRegexpLiteral(previousTokenKind)) {
|
|
197
|
+
kind = scanner.reScanSlashToken();
|
|
198
|
+
}
|
|
199
|
+
// typescript mode: split part of the template into PMD atoms.
|
|
200
|
+
if (splitTemplates && isTemplatePart(kind)) {
|
|
201
|
+
bumpTemplateDepth(kind);
|
|
202
|
+
for (const atom of expandTemplateSpan(source, sf, tokenStart, scanner.getTextPos(), o)) {
|
|
203
|
+
out.push(atom);
|
|
204
|
+
}
|
|
205
|
+
previousTokenKind = kind;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
// native mode: the whole template with substitutions is one token.
|
|
209
|
+
if (!splitTemplates && kind === ts.SyntaxKind.TemplateHead) {
|
|
210
|
+
const templateEnd = findTemplateLiteralEnd(source, tokenStart);
|
|
211
|
+
scanner.setTextPos(templateEnd);
|
|
212
|
+
const { line, character } = sf.getLineAndCharacterOfPosition(tokenStart);
|
|
213
|
+
const end = positionAtTokenEnd(sf, templateEnd);
|
|
214
|
+
out.push({
|
|
215
|
+
image: o.ignoreLiterals ? core_1.TS_LIT : source.slice(tokenStart, templateEnd),
|
|
216
|
+
line: line + 1,
|
|
217
|
+
column: character + 1,
|
|
218
|
+
endLine: end.line,
|
|
219
|
+
endColumn: end.column,
|
|
220
|
+
});
|
|
221
|
+
previousTokenKind = kind;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
bumpTemplateDepth(kind); // balance { } inside an interpolation
|
|
225
|
+
const image = normalize(kind, normalizeStringContinuation(scanner.getTokenText()));
|
|
226
|
+
if (image === null)
|
|
227
|
+
continue;
|
|
228
|
+
const { line, character } = sf.getLineAndCharacterOfPosition(tokenStart);
|
|
229
|
+
const end = positionAtTokenEnd(sf, scanner.getTextPos());
|
|
230
|
+
out.push({
|
|
231
|
+
image,
|
|
232
|
+
line: line + 1,
|
|
233
|
+
column: character + 1,
|
|
234
|
+
endLine: end.line,
|
|
235
|
+
endColumn: end.column,
|
|
236
|
+
});
|
|
237
|
+
previousTokenKind = kind;
|
|
238
|
+
}
|
|
239
|
+
return out;
|
|
240
|
+
}
|
|
241
|
+
function canStartPmdRegexpLiteral(previousKind) {
|
|
242
|
+
if (previousKind === null) {
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
switch (previousKind) {
|
|
246
|
+
case ts.SyntaxKind.Identifier:
|
|
247
|
+
case ts.SyntaxKind.PrivateIdentifier:
|
|
248
|
+
case ts.SyntaxKind.StringLiteral:
|
|
249
|
+
case ts.SyntaxKind.NumericLiteral:
|
|
250
|
+
case ts.SyntaxKind.BigIntLiteral:
|
|
251
|
+
case ts.SyntaxKind.NoSubstitutionTemplateLiteral:
|
|
252
|
+
case ts.SyntaxKind.RegularExpressionLiteral:
|
|
253
|
+
case ts.SyntaxKind.ThisKeyword:
|
|
254
|
+
case ts.SyntaxKind.SuperKeyword:
|
|
255
|
+
case ts.SyntaxKind.TrueKeyword:
|
|
256
|
+
case ts.SyntaxKind.FalseKeyword:
|
|
257
|
+
case ts.SyntaxKind.NullKeyword:
|
|
258
|
+
case ts.SyntaxKind.CloseParenToken:
|
|
259
|
+
case ts.SyntaxKind.CloseBracketToken:
|
|
260
|
+
case ts.SyntaxKind.CloseBraceToken:
|
|
261
|
+
case ts.SyntaxKind.PlusPlusToken:
|
|
262
|
+
case ts.SyntaxKind.MinusMinusToken:
|
|
263
|
+
return false;
|
|
264
|
+
default:
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// PMD typescript compatibility applies only to TS files; .js/.jsx go through the
|
|
269
|
+
// native scanner with no PMD massaging.
|
|
270
|
+
function isTypeScriptFile(filePath, scriptKind) {
|
|
271
|
+
const ext = pathExt(filePath);
|
|
272
|
+
if (ext === '.ts' || ext === '.tsx' || ext === '.mts' || ext === '.cts')
|
|
273
|
+
return true;
|
|
274
|
+
if (ext === '.js' || ext === '.jsx' || ext === '.mjs' || ext === '.cjs')
|
|
275
|
+
return false;
|
|
276
|
+
return scriptKind === ts.ScriptKind.TS || scriptKind === ts.ScriptKind.TSX;
|
|
277
|
+
}
|
|
278
|
+
function pathExt(filePath) {
|
|
279
|
+
const dot = filePath.lastIndexOf('.');
|
|
280
|
+
return dot === -1 ? '' : filePath.slice(dot).toLowerCase();
|
|
281
|
+
}
|
|
282
|
+
function createLineMap(source) {
|
|
283
|
+
const api = ts;
|
|
284
|
+
const compute = api.computeLineStarts;
|
|
285
|
+
const lineAndChar = api.computeLineAndCharacterOfPosition;
|
|
286
|
+
if (compute && lineAndChar) {
|
|
287
|
+
const lineStarts = compute(source);
|
|
288
|
+
return { getLineAndCharacterOfPosition: (pos) => lineAndChar(lineStarts, pos) };
|
|
289
|
+
}
|
|
290
|
+
const sf = ts.createSourceFile('_.ts', source, ts.ScriptTarget.Latest, false);
|
|
291
|
+
return { getLineAndCharacterOfPosition: (pos) => sf.getLineAndCharacterOfPosition(pos) };
|
|
292
|
+
}
|
|
293
|
+
function positionAtTokenEnd(sf, offset) {
|
|
294
|
+
const { line, character } = sf.getLineAndCharacterOfPosition(offset);
|
|
295
|
+
return { line: line + 1, column: character + 1 };
|
|
296
|
+
}
|
|
297
|
+
function normalizeStringContinuation(text) {
|
|
298
|
+
return text.replace(/\\\r?\n\s*/g, '');
|
|
299
|
+
}
|
|
300
|
+
function findTemplateLiteralEnd(source, start) {
|
|
301
|
+
let expressionDepth = 0;
|
|
302
|
+
for (let i = start + 1; i < source.length; i++) {
|
|
303
|
+
const char = source[i];
|
|
304
|
+
if (char === '\\') {
|
|
305
|
+
i++;
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
if (char === '`' && expressionDepth === 0) {
|
|
309
|
+
return i + 1;
|
|
310
|
+
}
|
|
311
|
+
if (char === '$' && source[i + 1] === '{') {
|
|
312
|
+
expressionDepth++;
|
|
313
|
+
i++;
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
if (char === '}' && expressionDepth > 0) {
|
|
317
|
+
expressionDepth--;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return source.length;
|
|
321
|
+
}
|
|
322
|
+
function isTemplatePart(kind) {
|
|
323
|
+
return (kind === ts.SyntaxKind.NoSubstitutionTemplateLiteral ||
|
|
324
|
+
kind === ts.SyntaxKind.TemplateHead ||
|
|
325
|
+
kind === ts.SyntaxKind.TemplateMiddle ||
|
|
326
|
+
kind === ts.SyntaxKind.TemplateTail);
|
|
327
|
+
}
|
|
328
|
+
// Split a chunk of a template literal into PMD-typescript atoms: backtick, `${`,
|
|
329
|
+
// `}`, and an escape `\X` are one token each; everything else (text, spaces,
|
|
330
|
+
// newlines) is one token per character (grammar: TemplateStringAtom: ~[`\\]).
|
|
331
|
+
function expandTemplateSpan(source, sf, start, end, opts) {
|
|
332
|
+
const atoms = [];
|
|
333
|
+
let i = start;
|
|
334
|
+
while (i < end) {
|
|
335
|
+
let len = 1;
|
|
336
|
+
const char = source[i];
|
|
337
|
+
if (char === '\\' && i + 1 < end) {
|
|
338
|
+
len = 2; // escape atom
|
|
339
|
+
}
|
|
340
|
+
else if (char === '$' && source[i + 1] === '{') {
|
|
341
|
+
len = 2; // start of an interpolation
|
|
342
|
+
}
|
|
343
|
+
const raw = source.slice(i, i + len);
|
|
344
|
+
const structural = raw === '`' || raw === '${' || raw === '}';
|
|
345
|
+
const s = sf.getLineAndCharacterOfPosition(i);
|
|
346
|
+
const e = sf.getLineAndCharacterOfPosition(i + len);
|
|
347
|
+
atoms.push({
|
|
348
|
+
image: !structural && opts.ignoreLiterals ? core_1.TS_LIT : raw,
|
|
349
|
+
line: s.line + 1,
|
|
350
|
+
column: s.character + 1,
|
|
351
|
+
endLine: e.line + 1,
|
|
352
|
+
endColumn: e.character + 1,
|
|
353
|
+
});
|
|
354
|
+
i += len;
|
|
355
|
+
}
|
|
356
|
+
return atoms;
|
|
357
|
+
}
|
|
358
|
+
function isSuppressed(offset, ranges) {
|
|
359
|
+
return ranges.some((range) => offset >= range.start && offset < range.end);
|
|
360
|
+
}
|
|
361
|
+
function findCpdSuppressedRanges(source) {
|
|
362
|
+
const ranges = [];
|
|
363
|
+
const comments = source.matchAll(/\/\/[^\r\n]*|\/\*[\s\S]*?\*\//g);
|
|
364
|
+
let start = null;
|
|
365
|
+
for (const comment of comments) {
|
|
366
|
+
const image = comment[0];
|
|
367
|
+
const index = comment.index;
|
|
368
|
+
if (image.includes('CPD-OFF') && start === null) {
|
|
369
|
+
start = index;
|
|
370
|
+
}
|
|
371
|
+
if (image.includes('CPD-ON') && start !== null) {
|
|
372
|
+
ranges.push({ start, end: index + image.length });
|
|
373
|
+
start = null;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (start !== null) {
|
|
377
|
+
ranges.push({ start, end: source.length });
|
|
378
|
+
}
|
|
379
|
+
return ranges;
|
|
380
|
+
}
|
|
381
|
+
const TSX_EXT = new Set(['.tsx', '.jsx']);
|
|
382
|
+
const JS_EXT = new Set(['.js', '.mjs', '.cjs']);
|
|
383
|
+
function scriptKindFor(ext) {
|
|
384
|
+
if (TSX_EXT.has(ext))
|
|
385
|
+
return ts.ScriptKind.TSX;
|
|
386
|
+
if (JS_EXT.has(ext))
|
|
387
|
+
return ts.ScriptKind.JS;
|
|
388
|
+
return ts.ScriptKind.TS;
|
|
389
|
+
}
|
|
390
|
+
// Vue (.vue) and Svelte (.svelte) live in their own extension modules src/vue.ts
|
|
391
|
+
// and src/svelte.ts, modeled on src/angular.ts — each tokenizes both <script> and
|
|
392
|
+
// markup (descriptor.template.ast / ast.fragment).
|
package/dist/vue.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type RawToken } from './core';
|
|
2
|
+
import { type TokenizeOptions } from './tokenizers';
|
|
3
|
+
/** Tokenize a `.vue` single-file component (`<script>` blocks + markup). */
|
|
4
|
+
export declare function tokenizeVue(filePath: string, source: string, options?: TokenizeOptions): RawToken[];
|
package/dist/vue.js
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.tokenizeVue = tokenizeVue;
|
|
37
|
+
/**
|
|
38
|
+
* `.vue` tokenizer: `<script>`/`<script setup>` + markup (descriptor.template.ast).
|
|
39
|
+
* Built on top of the shared layer in tokenizers.ts (optional/moduleResolveDirs/
|
|
40
|
+
* remap/tokenizeTypeScript) and the sentinel `S` from core.ts. The core knows
|
|
41
|
+
* nothing about Vue. Modeled on src/svelte.ts and src/angular.ts.
|
|
42
|
+
*
|
|
43
|
+
* Two token layers:
|
|
44
|
+
* 1. markup structure (tags, directives, attribute names/values, static text)
|
|
45
|
+
* -> images with the VUE prefix (via the sentinel S), so they do NOT match
|
|
46
|
+
* script tokens;
|
|
47
|
+
* 2. binding/interpolation expressions (`{{ … }}`, `:prop`, `v-if`, `@event`)
|
|
48
|
+
* -> the same TypeScript in the same component scope, so we slice the source
|
|
49
|
+
* and run it through the shared tokenizeTypeScript WITHOUT a prefix, so a
|
|
50
|
+
* duplicated expression across template<->script is caught.
|
|
51
|
+
*
|
|
52
|
+
* @packageDocumentation
|
|
53
|
+
*/
|
|
54
|
+
const ts = __importStar(require("typescript"));
|
|
55
|
+
const core_1 = require("./core");
|
|
56
|
+
const tokenizers_1 = require("./tokenizers");
|
|
57
|
+
const VUE = `${core_1.S}VUE:`; // structural markup marker
|
|
58
|
+
const VUE_TEXT = `${core_1.S}VUETEXT`; // non-empty static text
|
|
59
|
+
const VUE_LIT = `${core_1.S}VUELIT`; // normalized static attribute value (ignoreLiterals)
|
|
60
|
+
// Numeric NodeTypes from @vue/compiler-core; stable across all of Vue 3.
|
|
61
|
+
const N_ELEMENT = 1;
|
|
62
|
+
const N_TEXT = 2;
|
|
63
|
+
const N_COMMENT = 3;
|
|
64
|
+
const N_INTERPOLATION = 5;
|
|
65
|
+
const N_ATTRIBUTE = 6;
|
|
66
|
+
const N_DIRECTIVE = 7;
|
|
67
|
+
let warnedVue = false;
|
|
68
|
+
/** Tokenize a `.vue` single-file component (`<script>` blocks + markup). */
|
|
69
|
+
function tokenizeVue(filePath, source, options = {}) {
|
|
70
|
+
const o = { ...tokenizers_1.DEFAULTS, ...options };
|
|
71
|
+
const sfc = (0, tokenizers_1.optional)('@vue/compiler-sfc', (0, tokenizers_1.moduleResolveDirs)(filePath));
|
|
72
|
+
if (!sfc || typeof sfc.parse !== 'function') {
|
|
73
|
+
if (!warnedVue) {
|
|
74
|
+
console.warn('[cpd] .vue skipped: install @vue/compiler-sfc');
|
|
75
|
+
warnedVue = true;
|
|
76
|
+
}
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
let descriptor;
|
|
80
|
+
try {
|
|
81
|
+
({ descriptor } = sfc.parse(source, { filename: filePath }));
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
const out = [];
|
|
87
|
+
// --- <script setup> and <script>: plain TS/JS, like a regular file ---
|
|
88
|
+
for (const block of [descriptor.scriptSetup, descriptor.script]) {
|
|
89
|
+
if (!block)
|
|
90
|
+
continue;
|
|
91
|
+
const lang = block.lang ?? 'js';
|
|
92
|
+
const kind = lang === 'tsx' || lang === 'jsx' ? ts.ScriptKind.TSX : lang === 'ts' ? ts.ScriptKind.TS : ts.ScriptKind.JS;
|
|
93
|
+
const baseLine = block.loc.start.line; // 1-based
|
|
94
|
+
const baseCol = block.loc.start.column; // 1-based
|
|
95
|
+
for (const t of (0, tokenizers_1.tokenizeTypeScript)(filePath, block.content, o, kind))
|
|
96
|
+
out.push((0, tokenizers_1.remap)(t, baseLine, baseCol));
|
|
97
|
+
out.push({ image: '', line: baseLine, column: baseCol, barrier: true });
|
|
98
|
+
}
|
|
99
|
+
// --- Markup ---
|
|
100
|
+
// Tokenized only when the toggle is on (default yes). Markup and script are
|
|
101
|
+
// usually run at different --minimum-tokens thresholds.
|
|
102
|
+
const tmpl = descriptor.template;
|
|
103
|
+
if ((o.vueTemplates ?? true) && tmpl?.ast) {
|
|
104
|
+
// Vue node coordinates (loc.start.{line,column}) are already absolute to the
|
|
105
|
+
// file and 1-based — no offset->line/col map is needed (unlike svelte.ts).
|
|
106
|
+
const emitStruct = (image, node) => {
|
|
107
|
+
const loc = node?.loc?.start;
|
|
108
|
+
out.push({ image, line: loc?.line ?? 1, column: loc?.column ?? 1 });
|
|
109
|
+
};
|
|
110
|
+
// SIMPLE_EXPRESSION -> slice the source -> shared TS tokenizer -> remap.
|
|
111
|
+
// id/literal normalization is done by tokenizeTypeScript itself (it is real TS).
|
|
112
|
+
const emitExpr = (exp) => {
|
|
113
|
+
const start = exp?.loc?.start;
|
|
114
|
+
const end = exp?.loc?.end;
|
|
115
|
+
if (!start || !end || typeof start.offset !== 'number' || typeof end.offset !== 'number')
|
|
116
|
+
return;
|
|
117
|
+
const code = source.slice(start.offset, end.offset);
|
|
118
|
+
if (!code.trim())
|
|
119
|
+
return;
|
|
120
|
+
for (const t of (0, tokenizers_1.tokenizeTypeScript)(filePath, code, o, ts.ScriptKind.TS)) {
|
|
121
|
+
out.push((0, tokenizers_1.remap)(t, start.line, start.column));
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
const emitLit = (value, host) => emitStruct(o.ignoreLiterals ? VUE_LIT : `${VUE}lit:${JSON.stringify(value)}`, host);
|
|
125
|
+
// Canonical directive image: name + static arg + modifiers.
|
|
126
|
+
// :class -> VUE:bind:class ; @click.stop -> VUE:on:click.stop ; v-if -> VUE:if
|
|
127
|
+
const directiveImage = (dir) => {
|
|
128
|
+
let image = `${VUE}${dir.name}`;
|
|
129
|
+
if (dir.arg && dir.arg.isStatic !== false && typeof dir.arg.content === 'string') {
|
|
130
|
+
image += `:${dir.arg.content}`;
|
|
131
|
+
}
|
|
132
|
+
for (const m of dir.modifiers ?? []) {
|
|
133
|
+
const content = typeof m === 'string' ? m : m?.content;
|
|
134
|
+
if (content)
|
|
135
|
+
image += `.${content}`;
|
|
136
|
+
}
|
|
137
|
+
return image;
|
|
138
|
+
};
|
|
139
|
+
const walkProp = (prop) => {
|
|
140
|
+
if (!prop)
|
|
141
|
+
return;
|
|
142
|
+
if (prop.type === N_ATTRIBUTE) {
|
|
143
|
+
emitStruct(`${VUE}@${prop.name}`, prop);
|
|
144
|
+
const value = prop.value;
|
|
145
|
+
if (value && typeof value.content === 'string' && value.content.length)
|
|
146
|
+
emitLit(value.content, value);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (prop.type === N_DIRECTIVE) {
|
|
150
|
+
emitStruct(directiveImage(prop), prop);
|
|
151
|
+
// Dynamic arg (:[key]) is an expression; the static arg is already in the image.
|
|
152
|
+
if (prop.arg && prop.arg.isStatic === false)
|
|
153
|
+
emitExpr(prop.arg);
|
|
154
|
+
if (prop.exp)
|
|
155
|
+
emitExpr(prop.exp);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
const walk = (node) => {
|
|
159
|
+
if (!node)
|
|
160
|
+
return;
|
|
161
|
+
switch (node.type) {
|
|
162
|
+
case N_ELEMENT:
|
|
163
|
+
emitStruct(`${VUE}<${node.tag}`, node);
|
|
164
|
+
for (const p of node.props ?? [])
|
|
165
|
+
walkProp(p);
|
|
166
|
+
for (const c of node.children ?? [])
|
|
167
|
+
walk(c);
|
|
168
|
+
return;
|
|
169
|
+
case N_TEXT:
|
|
170
|
+
if (typeof node.content === 'string' && node.content.trim())
|
|
171
|
+
emitStruct(VUE_TEXT, node);
|
|
172
|
+
return;
|
|
173
|
+
case N_COMMENT:
|
|
174
|
+
return;
|
|
175
|
+
case N_INTERPOLATION:
|
|
176
|
+
emitExpr(node.content);
|
|
177
|
+
return;
|
|
178
|
+
default:
|
|
179
|
+
// ROOT and other containers — descend into children.
|
|
180
|
+
if (Array.isArray(node.children))
|
|
181
|
+
for (const c of node.children)
|
|
182
|
+
walk(c);
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
out.push({ image: '', line: tmpl.loc?.start?.line ?? 1, column: tmpl.loc?.start?.column ?? 1, barrier: true });
|
|
186
|
+
walk(tmpl.ast);
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
189
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "clone-alert",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "PMD CPD-compatible copy-paste detector that finds duplicate code in TypeScript, JavaScript, JSX/TSX, Vue, Svelte and Angular.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"main": "dist/index.js",
|
|
8
|
+
"types": "dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"require": "./dist/index.js",
|
|
13
|
+
"default": "./dist/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./package.json": "./package.json"
|
|
16
|
+
},
|
|
17
|
+
"bin": {
|
|
18
|
+
"clone-alert": "dist/cli.js"
|
|
19
|
+
},
|
|
20
|
+
"files": [
|
|
21
|
+
"dist/**/*",
|
|
22
|
+
"scripts/compare-pmd-cpd.mjs",
|
|
23
|
+
"README.md",
|
|
24
|
+
"LICENSE"
|
|
25
|
+
],
|
|
26
|
+
"keywords": [
|
|
27
|
+
"cpd",
|
|
28
|
+
"copy-paste",
|
|
29
|
+
"copy-paste-detector",
|
|
30
|
+
"duplicate-code",
|
|
31
|
+
"code-duplication",
|
|
32
|
+
"code-clones",
|
|
33
|
+
"clone-detection",
|
|
34
|
+
"pmd",
|
|
35
|
+
"pmd-cpd",
|
|
36
|
+
"dry",
|
|
37
|
+
"static-analysis",
|
|
38
|
+
"code-quality",
|
|
39
|
+
"typescript",
|
|
40
|
+
"javascript",
|
|
41
|
+
"jsx",
|
|
42
|
+
"tsx",
|
|
43
|
+
"vue",
|
|
44
|
+
"svelte",
|
|
45
|
+
"angular",
|
|
46
|
+
"cli"
|
|
47
|
+
],
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "git+ssh://git@github.com/BaryshevRS/clone-alert.git"
|
|
51
|
+
},
|
|
52
|
+
"bugs": {
|
|
53
|
+
"url": "https://github.com/BaryshevRS/clone-alert/issues"
|
|
54
|
+
},
|
|
55
|
+
"homepage": "https://github.com/BaryshevRS/clone-alert#readme",
|
|
56
|
+
"publishConfig": {
|
|
57
|
+
"access": "public"
|
|
58
|
+
},
|
|
59
|
+
"scripts": {
|
|
60
|
+
"build": "tsc -p tsconfig.json",
|
|
61
|
+
"check": "biome check .",
|
|
62
|
+
"check:fix": "biome check --write .",
|
|
63
|
+
"check-types": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json --noEmit",
|
|
64
|
+
"compare:pmd": "npm run build && node scripts/compare-pmd-cpd.mjs",
|
|
65
|
+
"lint": "npm run check:fix && npm run lint:knip && npm run check-types && npm run lint:cpd",
|
|
66
|
+
"lint:cpd": "npm run build && node dist/cli.js --minimum-tokens 70 --files src --extensions ts --format text --fail-on-violation",
|
|
67
|
+
"lint:knip": "knip",
|
|
68
|
+
"pack:dry-run": "npm pack --dry-run",
|
|
69
|
+
"prepack": "npm run build",
|
|
70
|
+
"prepublishOnly": "npm run check && npm run lint:knip && npm run check-types && npm run lint:cpd && npm test",
|
|
71
|
+
"publish:dry-run": "npm publish --dry-run",
|
|
72
|
+
"publish:local": "npm publish --registry http://localhost:4873",
|
|
73
|
+
"publish:local:dry-run": "npm publish --dry-run --registry http://localhost:4873",
|
|
74
|
+
"test": "npm run build && vitest run",
|
|
75
|
+
"release": "npm publish"
|
|
76
|
+
},
|
|
77
|
+
"dependencies": {
|
|
78
|
+
"typescript": "^6.0.3"
|
|
79
|
+
},
|
|
80
|
+
"peerDependencies": {
|
|
81
|
+
"@angular/compiler": ">=17.0.0",
|
|
82
|
+
"@vue/compiler-sfc": ">=3.0.0",
|
|
83
|
+
"svelte": ">=3.0.0"
|
|
84
|
+
},
|
|
85
|
+
"peerDependenciesMeta": {
|
|
86
|
+
"@angular/compiler": {
|
|
87
|
+
"optional": true
|
|
88
|
+
},
|
|
89
|
+
"@vue/compiler-sfc": {
|
|
90
|
+
"optional": true
|
|
91
|
+
},
|
|
92
|
+
"svelte": {
|
|
93
|
+
"optional": true
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
"devDependencies": {
|
|
97
|
+
"@angular/compiler": "^22.0.2",
|
|
98
|
+
"@biomejs/biome": "^2.5.0",
|
|
99
|
+
"@types/node": "^24.0.0",
|
|
100
|
+
"@vue/compiler-sfc": "^3.5.38",
|
|
101
|
+
"knip": "^6.5.0",
|
|
102
|
+
"svelte": "^5.56.3",
|
|
103
|
+
"vitest": "^4.1.6"
|
|
104
|
+
},
|
|
105
|
+
"engines": {
|
|
106
|
+
"node": ">=18"
|
|
107
|
+
}
|
|
108
|
+
}
|