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.
@@ -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
+ }