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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ruslan Baryshev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,243 @@
1
+ # clone-alert
2
+
3
+ > Fast **copy‑paste detector** for **TypeScript**, **JavaScript**, **JSX/TSX**, **Vue**, **Svelte** and **Angular** — a **PMD CPD‑compatible** duplicate‑code finder you can drop into any project or CI pipeline.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/clone-alert.svg)](https://www.npmjs.com/package/clone-alert)
6
+ [![license](https://img.shields.io/npm/l/clone-alert.svg)](./LICENSE)
7
+ [![node](https://img.shields.io/node/v/clone-alert.svg)](https://nodejs.org)
8
+ [![types](https://img.shields.io/npm/types/clone-alert.svg)](https://www.npmjs.com/package/clone-alert)
9
+
10
+ **clone-alert** finds duplicated and copy‑pasted code across your codebase by comparing **token streams** — the same proven approach as [PMD CPD](https://pmd.github.io/) (Copy‑Paste Detector), but built natively for the JavaScript/TypeScript ecosystem and your frontend templates. Catch code clones, enforce **DRY**, reduce technical debt, and fail your build when duplication creeps in.
11
+
12
+ ```sh
13
+ npx clone-alert --minimum-tokens 50 --files src
14
+ ```
15
+
16
+ ---
17
+
18
+ ## Why clone-alert?
19
+
20
+ - 🎯 **PMD CPD‑compatible** — a faithful port of PMD's match algorithm and JavaScript/TypeScript tokenizers, validated against PMD's own golden fixtures.
21
+ - ⚡ **Fast on large monorepos** — a struct‑of‑arrays token core with a Karp–Rabin rolling hash and radix‑sorted buckets. In our [benchmarks](#benchmarks) it runs **10–30× faster** than PMD CPD while using **1.4–2.5× less memory**, on real codebases from Next.js to nx.
22
+ - 🧩 **Frontend templates, natively** — tokenizes **Vue** `<template>`, **Svelte** markup, and **Angular** templates, not just `<script>` blocks. Detects template‑to‑script duplication too.
23
+ - 🧪 **Zero‑config CLI** — sensible defaults, recursive directory scan, `node_modules`/`.git`/`dist` skipped automatically.
24
+ - 📦 **Tiny footprint** — a single runtime dependency (`typescript`). Framework parsers are **optional peer dependencies**, loaded only when needed.
25
+ - 🛠 **CI‑ready** — `text`, `json`, and PMD‑style `xml` reports, plus `--fail-on-violation` (exit code `4`).
26
+ - 🔇 **Inline suppression** — ignore known duplication with `CPD-OFF` / `CPD-ON` comment markers.
27
+
28
+ ## Supported languages & frameworks
29
+
30
+ | Language / framework | Extensions | Notes |
31
+ | --- | --- | --- |
32
+ | TypeScript | `.ts`, `.mts`, `.cts` | PMD `typescript` token granularity by default |
33
+ | TSX / JSX | `.tsx`, `.jsx` | React‑style components |
34
+ | JavaScript | `.js`, `.mjs`, `.cjs` | Native scanner tokenization |
35
+ | Vue | `.vue` | `<script>`, `<script setup>` and `<template>` markup |
36
+ | Svelte | `.svelte` | `<script>` and markup (**requires Svelte 5+**) |
37
+ | Angular | `.html`, `.htm`, inline templates | External and `@Component` inline templates |
38
+
39
+ ## Installation
40
+
41
+ Add it as a dev dependency:
42
+
43
+ ```sh
44
+ npm install --save-dev clone-alert
45
+ # or
46
+ pnpm add -D clone-alert
47
+ # or
48
+ yarn add -D clone-alert
49
+ ```
50
+
51
+ Or run it once, without installing:
52
+
53
+ ```sh
54
+ npx clone-alert --minimum-tokens 50 --files src
55
+ ```
56
+
57
+ Requires **Node.js 18+**.
58
+
59
+ ## Quick start
60
+
61
+ ```sh
62
+ # Scan a folder and print a human‑readable report
63
+ clone-alert --minimum-tokens 50 --files src
64
+
65
+ # Fail CI when duplication is found (exit code 4)
66
+ clone-alert --minimum-tokens 50 --files src --fail-on-violation
67
+
68
+ # Machine‑readable output for dashboards
69
+ clone-alert --format json --files src,packages > duplication.json
70
+ ```
71
+
72
+ ## Usage
73
+
74
+ ```sh
75
+ clone-alert [options] [<path>...]
76
+ ```
77
+
78
+ ### CLI options
79
+
80
+ | Option | Description |
81
+ | --- | --- |
82
+ | `--files <path[,path...]>` | Files or directories to scan. Can be repeated. |
83
+ | `--minimum-tokens <n>` | Minimum duplicated token span. Default: `50`. |
84
+ | `--minimum-tile-size <n>` | Alias for `--minimum-tokens`. |
85
+ | `--format <text\|xml\|json>` | Report format. Default: `text`. |
86
+ | `--extensions <ext[,ext...]>` | Extensions to include during recursive scans. |
87
+ | `--exclude <glob[,glob...]>` | Exclude files or directories (glob). Can be repeated. |
88
+ | `--ignore-identifiers` / `--no-ignore-identifiers` | Normalize or compare identifier names. Strict by default, like PMD. |
89
+ | `--ignore-literals` / `--no-ignore-literals` | Normalize or compare literals. Strict by default, like PMD. |
90
+ | `--pmd-typescript-compatibility` / `--no-…` | Match PMD `typescript` granularity for `.ts/.tsx` (split template literals into atoms, collapse regexp). On by default. |
91
+ | `--svelte-templates` / `--no-svelte-templates` | Tokenize `.svelte` markup, not just `<script>`. On by default. |
92
+ | `--vue-templates` / `--no-vue-templates` | Tokenize `.vue` markup, not just `<script>`. On by default. |
93
+ | `--angular-inline-templates` | Also scan Angular `@Component` inline templates. |
94
+ | `--skip-angular-inline-templates` | Do not scan inline Angular templates (explicit default). |
95
+ | `--fail-on-violation` | Exit with code `4` when duplications are found. |
96
+ | `-h, --help` | Show help. |
97
+ | `-V, --version` | Show version. |
98
+
99
+ Default extensions:
100
+
101
+ ```text
102
+ .ts .tsx .js .jsx .mts .cts .mjs .cjs .vue .svelte .html .htm
103
+ ```
104
+
105
+ ### Examples
106
+
107
+ ```sh
108
+ # Strict, PMD‑like scan of a source tree, fail the build on any clone
109
+ clone-alert --minimum-tokens 30 --files src --fail-on-violation
110
+
111
+ # PMD‑style XML report across several paths
112
+ clone-alert --minimum-tokens 50 --format xml src test
113
+
114
+ # JSON report for a monorepo, excluding generated code
115
+ clone-alert --format json --files src,packages --exclude '**/generated/**'
116
+
117
+ # Find renamed clones by normalizing identifiers and literals
118
+ clone-alert --minimum-tokens 40 --ignore-identifiers --ignore-literals --files src
119
+ ```
120
+
121
+ ## PMD CPD compatibility
122
+
123
+ clone-alert targets PMD CPD‑style duplicate detection for the JavaScript/TypeScript ecosystem: `.js`, `.mjs`, `.cjs`, `.ts`, `.tsx`, `.jsx`, plus the frontend templates typical of TS projects. Verified compatibility currently covers:
124
+
125
+ - PMD JavaScript/TypeScript CPD tokenizer fixtures (vendored, so tests need no PMD checkout).
126
+ - The token‑based duplicate search, including `--ignore-identifiers`, `--ignore-literals`, and `CPD-OFF` / `CPD-ON` suppression markers.
127
+ - JSX/TSX tokenization and clone detection for React‑style components.
128
+ - Real npm layouts: `src/**/*.ts`, `src/**/*.tsx`, monorepo `packages/**`, and excluding generated files via `--exclude`.
129
+ - `text`, `json`, and `xml` reports: occurrence order, token counts, line ranges, and paths.
130
+
131
+ PMD compatibility mode is on by default: JS operators absent from PMD's ES5 JavaCC grammar are split into the same token stream (e.g. `=>` → `=` and `>`, `...` → `.` `.` `.`), and regexp literals collapse to a single token, just like PMD.
132
+
133
+ > **Note:** `--ignore-identifiers` in clone-alert really does normalize JS identifiers. In PMD's `ecmascript` lexer the same flag barely changes the token stream, so for a strict PMD‑JS comparison, leave it off.
134
+
135
+ ## Frontend templates
136
+
137
+ For `.vue`, `.svelte`, and Angular HTML, clone-alert uses the optional peer packages `@vue/compiler-sfc`, `svelte`, and `@angular/compiler`. If a package isn't installed, matching files are skipped with a warning.
138
+
139
+ - **Vue** — binding and interpolation expressions (`{{ }}`, `:prop`, `v-if`, `@event`) are tokenized as TypeScript in the component scope, so a duplicated expression across `<template>` and `<script setup>` is caught too.
140
+ - **Svelte** — markup tokenization requires **Svelte 5+** (it relies on the modern `ast.fragment` AST). On Svelte 3/4 only `<script>` is scanned, silently and without errors.
141
+ - **Angular** — inline templates are **off by default** to keep TypeScript mode closer to PMD CPD. Enable `--angular-inline-templates` to scan them as a clone-alert extension.
142
+
143
+ Markup and code often want different thresholds (markup is noisy at a low `--minimum-tokens`), so the template layers sit behind toggles. Run two passes for the best of both:
144
+
145
+ ```sh
146
+ # Code at a low threshold, markup at a high one (two runs)
147
+ clone-alert --minimum-tokens 40 --no-svelte-templates --files src
148
+ clone-alert --minimum-tokens 150 --files src
149
+ ```
150
+
151
+ ## Suppressing duplication
152
+
153
+ Wrap intentional or generated duplication in `CPD-OFF` / `CPD-ON` comments and it won't be reported:
154
+
155
+ ```ts
156
+ // CPD-OFF
157
+ const generatedTableA = { /* ... */ };
158
+ const generatedTableB = { /* ... */ };
159
+ // CPD-ON
160
+ ```
161
+
162
+ ## Programmatic API
163
+
164
+ clone-alert ships with TypeScript types and a small Node API:
165
+
166
+ ```ts
167
+ import { Cpd } from 'clone-alert';
168
+
169
+ const cpd = new Cpd({ minTileSize: 50 });
170
+ cpd.addPath('src/a.ts');
171
+ cpd.addPath('src/b.ts');
172
+
173
+ const matches = cpd.run();
174
+ console.log(cpd.report(matches));
175
+ ```
176
+
177
+ ## How it works
178
+
179
+ 1. Each file is tokenized into a flat stream of lexical tokens (TypeScript scanner for code; framework compilers for Vue/Svelte/Angular markup).
180
+ 2. Tokens are interned into a compact struct‑of‑arrays store backed by typed arrays.
181
+ 3. A Karp–Rabin rolling hash plus a stable radix sort group candidate windows, and a PMD‑style collector reports the longest non‑overlapping matches.
182
+
183
+ Framework template tokens live in a separate namespace from script tokens, so markup never cross‑matches code by accident — while shared‑language expressions still do.
184
+
185
+ ## Benchmarks
186
+
187
+ clone-alert is a drop-in for PMD CPD that runs **10–30× faster** on **1.4–2.5× less memory** — on the same files, finding the same clones.
188
+
189
+ Measured with [`npm run compare:pmd`](#development) on five real-world TypeScript codebases. Only pure `.ts` is compared (the exact file set PMD's `typescript` lexer can parse), so all tools see byte-identical input. macOS, Node 20, `--minimum-tokens 50`, JVM start-up counted for PMD as in real CLI use:
190
+
191
+ | Repository | clone-alert | PMD CPD | Speed‑up | Peak RAM (clone vs PMD) | Agreement with PMD¹ |
192
+ | --- | --- | --- | --- | --- | --- |
193
+ | `nestjs/nest` | **1.8 s** | 52.7 s | **30×** | 203 MB vs 500 MB (2.5× less) | 100% |
194
+ | `angular/components` | **1.5 s** | 42.5 s | **28×** | 338 MB vs 577 MB (1.7× less) | 95%² |
195
+ | `microsoft/playwright` | **9.3 s** | 153 s | **16×** | 838 MB vs 1.4 GB (1.7× less) | 99.98% |
196
+ | `vercel/next.js` | **5.8 s** | 73.4 s | **13×** | 1.3 GB vs 1.9 GB (1.4× less) | 99.2% |
197
+ | `nrwl/nx` | **8.6 s** | 84.1 s | **10×** | 2.1 GB vs 3.3 GB (1.6× less) | 99.9% |
198
+
199
+ <sub>¹ Jaccard overlap of the file pairs both tools flag as duplicated. ² `angular/components` ships ~20 near‑identical table demos sharing the same 398‑token block. clone-alert and PMD cut that clone's boundary **identically** (398, 391, 390, 210… tokens, token‑for‑token); they only disagree on *which* of the interchangeable demo files get grouped into the same `<duplication>` — a symmetric clustering tie‑break, not missed or mis‑sized duplication. On this small sample (~2 000 file pairs) that grouping noise is the whole 5%.</sub>
200
+
201
+ ### Same tokens as PMD — verified, not approximated
202
+
203
+ The clone-alert TypeScript tokenizer is **identical to PMD's**, byte for byte. It is checked in CI against **PMD's own original tokenizer conformance fixtures** (vendored verbatim from the PMD repository): every token's image, line, and column must match PMD's golden output, element for element. clone-alert passes the full suite.
204
+
205
+ It earns that parity without reimplementing PMD's grammar: clone-alert lexes with the **real TypeScript compiler `Scanner`** — the same lexer `tsc` uses. PMD lexes TypeScript with a hand-maintained JavaCC grammar that trails the language. So clone-alert is **1:1 with PMD where PMD can lex, and still correct on modern syntax PMD's grammar can't** (`satisfies`, `using`, decorators, template‑literal types, newer operators).
206
+
207
+ ### Where the numbers differ from PMD, and why
208
+
209
+ Because the tokens are identical and the match engine is a faithful port of PMD's `MatchCollector`, the residual differences are **never missed or invented duplication** — they live entirely in how identical matches are *bucketed*:
210
+
211
+ - **Grouping.** The same set of pairwise matches is occasionally packed into a different number of `<duplication>` groups (e.g. 30 vs 31 occurrences). Same clones, different bucketing; it nudges raw counts by ~2%.
212
+ - **Anchor jitter.** In hyper‑repetitive monorepo code (nx), a block repeated dozens of times can be anchored one line apart by each tool. Line‑exact that looks like a gap; by *which files share duplication* it's **99.9%**, and the divergence is **symmetric** (each tool has equally many "own" matches) — so it's reporting noise, not a detection error in either direction.
213
+
214
+ ## Comparison
215
+
216
+ | | clone-alert | PMD CPD | jscpd |
217
+ | --- | --- | --- | --- |
218
+ | TS/JS/JSX/TSX | ✅ | ✅ | ✅ |
219
+ | Vue `<template>` markup | ✅ | ➖ | partial |
220
+ | Svelte markup | ✅ (Svelte 5+) | ➖ | ➖ |
221
+ | Angular templates | ✅ | ➖ | flat HTML only |
222
+ | PMD CPD algorithm parity | ✅ | — | ➖ |
223
+ | Install size | tiny (1 dep) | JVM required | npm package |
224
+
225
+ ## Development
226
+
227
+ ```sh
228
+ npm install
229
+ npm run build # compile to dist/
230
+ npm test # build + Vitest suite
231
+ npm run lint # Biome + Knip + type‑check + self‑CPD
232
+ npm run compare:pmd -- /path/to/project --minimum-tokens 50
233
+ ```
234
+
235
+ `npm run compare:pmd` runs PMD CPD, clone-alert, and jscpd on the same file tree and prints a JSON summary of time, peak RSS, duplicate counts, occurrences, and overlap. (jscpd is not a dependency; install it separately or pass `--jscpd <command>`.)
236
+
237
+ ## Keywords
238
+
239
+ copy‑paste detector · duplicate code finder · code clone detection · CPD · PMD CPD alternative · jscpd alternative · TypeScript duplicate code · JavaScript duplicate code · JSX/TSX clones · Vue / Svelte / Angular duplication · DRY · static analysis · code quality · CI lint.
240
+
241
+ ## License
242
+
243
+ [MIT](./LICENSE)
@@ -0,0 +1,23 @@
1
+ import { type RawToken } from './core';
2
+ import { type TokenizeOptions } from './tokenizers';
3
+ /**
4
+ * Tokenize an Angular HTML template (external `.html` or inline). Two layers:
5
+ * (1) structure — tags, attribute names, static text; (2) binding/interpolation/
6
+ * block expressions — an AST walk over @angular/compiler with the same id/literal
7
+ * normalization as in script. All images are S-prefixed (see NG/NG_ID/NG_LIT) so
8
+ * there are no cross-matches with script tokens.
9
+ *
10
+ * Expression sub-token positions are inherited from the enclosing (host) node:
11
+ * matching is exact, report granularity is at the binding-line level (no line map
12
+ * is built).
13
+ */
14
+ export declare function tokenizeAngularHtml(filePath: string, template: string, base?: {
15
+ line: number;
16
+ col: number;
17
+ }, options?: TokenizeOptions): RawToken[];
18
+ /** Extract inline templates from `@Component({ template: \`...\` })`. */
19
+ export declare function extractAngularInlineTemplates(filePath: string, source: string): {
20
+ code: string;
21
+ line: number;
22
+ col: number;
23
+ }[];
@@ -0,0 +1,296 @@
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.tokenizeAngularHtml = tokenizeAngularHtml;
37
+ exports.extractAngularInlineTemplates = extractAngularInlineTemplates;
38
+ /**
39
+ * Tokenizer extension for Angular templates (external `.html` and inline
40
+ * `@Component`). Built on top of the shared layer in tokenizers.ts
41
+ * (optional/moduleResolveDirs/remap) and the shared sentinel `S` from core.ts.
42
+ * The core (core.ts) knows nothing about Angular.
43
+ *
44
+ * @packageDocumentation
45
+ */
46
+ const ts = __importStar(require("typescript"));
47
+ const core_1 = require("./core");
48
+ const tokenizers_1 = require("./tokenizers");
49
+ // Token namespace for Angular template expressions. The S prefix guarantees these
50
+ // images never collide with script tokens (no .ts<->template cross-matches).
51
+ const NG = `${core_1.S}NG:`; // prefix for structural markers / expression names
52
+ const NG_ID = `${core_1.S}NGID`; // normalized template identifier (ignoreIdentifiers)
53
+ const NG_LIT = `${core_1.S}NGLIT`; // normalized template literal (ignoreLiterals)
54
+ const NG_TEXT = `${core_1.S}NGTEXT`; // static template text
55
+ let warnedNg = false;
56
+ /**
57
+ * Tokenize an Angular HTML template (external `.html` or inline). Two layers:
58
+ * (1) structure — tags, attribute names, static text; (2) binding/interpolation/
59
+ * block expressions — an AST walk over @angular/compiler with the same id/literal
60
+ * normalization as in script. All images are S-prefixed (see NG/NG_ID/NG_LIT) so
61
+ * there are no cross-matches with script tokens.
62
+ *
63
+ * Expression sub-token positions are inherited from the enclosing (host) node:
64
+ * matching is exact, report granularity is at the binding-line level (no line map
65
+ * is built).
66
+ */
67
+ function tokenizeAngularHtml(filePath, template, base = { line: 1, col: 1 }, options) {
68
+ const o = { ...tokenizers_1.DEFAULTS, ...options };
69
+ const ngc = (0, tokenizers_1.optional)('@angular/compiler', (0, tokenizers_1.moduleResolveDirs)(filePath));
70
+ if (!ngc || typeof ngc.parseTemplate !== 'function') {
71
+ if (!warnedNg) {
72
+ console.warn('[cpd] Angular template skipped: install @angular/compiler');
73
+ warnedNg = true;
74
+ }
75
+ return [];
76
+ }
77
+ let parsed;
78
+ try {
79
+ parsed = ngc.parseTemplate(template, filePath, { preserveWhitespaces: false });
80
+ }
81
+ catch {
82
+ return [];
83
+ }
84
+ if (!parsed || !Array.isArray(parsed.nodes))
85
+ return [];
86
+ const local = [];
87
+ const emit = (image, node) => {
88
+ const sp = node?.sourceSpan ?? node?.startSourceSpan;
89
+ const loc = sp?.start;
90
+ local.push({ image, line: (loc?.line ?? 0) + 1, column: (loc?.col ?? 0) + 1 });
91
+ };
92
+ const emitId = (name, host) => emit(o.ignoreIdentifiers ? NG_ID : `${NG}id:${String(name)}`, host);
93
+ const emitLit = (value, host) => emit(o.ignoreLiterals ? NG_LIT : `${NG}lit:${JSON.stringify(value)}`, host);
94
+ // Walk a binding expression. host is the structural node whose coordinates we inherit.
95
+ const walkExpr = (ast, host) => {
96
+ if (ast == null || typeof ast !== 'object')
97
+ return;
98
+ // ASTWithSource is a wrapper; unwrap it down to the actual expression.
99
+ if (ast.ast && typeof ast.ast === 'object')
100
+ return walkExpr(ast.ast, host);
101
+ switch (ast.constructor?.name) {
102
+ case 'Interpolation':
103
+ for (const e of ast.expressions ?? [])
104
+ walkExpr(e, host);
105
+ return;
106
+ case 'Binary':
107
+ emit(`${NG}op:${ast.operation}`, host);
108
+ walkExpr(ast.left, host);
109
+ walkExpr(ast.right, host);
110
+ return;
111
+ case 'Unary':
112
+ emit(`${NG}op:${ast.operator}`, host);
113
+ walkExpr(ast.expr, host);
114
+ return;
115
+ case 'PrefixNot':
116
+ emit(`${NG}op:!`, host);
117
+ walkExpr(ast.expression, host);
118
+ return;
119
+ case 'NonNullAssert':
120
+ emit(`${NG}!.`, host);
121
+ walkExpr(ast.expression, host);
122
+ return;
123
+ case 'Conditional':
124
+ emit(`${NG}?:`, host);
125
+ walkExpr(ast.condition, host);
126
+ walkExpr(ast.trueExp, host);
127
+ walkExpr(ast.falseExp, host);
128
+ return;
129
+ case 'PropertyRead':
130
+ walkExpr(ast.receiver, host);
131
+ emitId(ast.name, host);
132
+ return;
133
+ case 'SafePropertyRead':
134
+ walkExpr(ast.receiver, host);
135
+ emit(`${NG}?.`, host);
136
+ emitId(ast.name, host);
137
+ return;
138
+ case 'KeyedRead':
139
+ walkExpr(ast.receiver, host);
140
+ emit(`${NG}[]`, host);
141
+ walkExpr(ast.key, host);
142
+ return;
143
+ case 'SafeKeyedRead':
144
+ walkExpr(ast.receiver, host);
145
+ emit(`${NG}?.[]`, host);
146
+ walkExpr(ast.key, host);
147
+ return;
148
+ case 'Call':
149
+ case 'SafeCall':
150
+ walkExpr(ast.receiver, host);
151
+ emit(`${NG}()`, host);
152
+ for (const a of ast.args ?? [])
153
+ walkExpr(a, host);
154
+ return;
155
+ case 'BindingPipe':
156
+ walkExpr(ast.exp, host);
157
+ emit(`${NG}pipe`, host);
158
+ emitId(ast.name, host);
159
+ for (const a of ast.args ?? [])
160
+ walkExpr(a, host);
161
+ return;
162
+ case 'LiteralArray':
163
+ emit(`${NG}[arr]`, host);
164
+ for (const e of ast.expressions ?? [])
165
+ walkExpr(e, host);
166
+ return;
167
+ case 'LiteralMap':
168
+ emit(`${NG}{map}`, host);
169
+ for (const k of ast.keys ?? [])
170
+ emitId(k?.key, host);
171
+ for (const v of ast.values ?? [])
172
+ walkExpr(v, host);
173
+ return;
174
+ case 'LiteralPrimitive':
175
+ emitLit(ast.value, host);
176
+ return;
177
+ case 'ThisReceiver':
178
+ emit(`${NG}this`, host);
179
+ return;
180
+ case 'ParenthesizedExpression':
181
+ walkExpr(ast.expression, host);
182
+ return;
183
+ case 'ImplicitReceiver':
184
+ case 'EmptyExpr':
185
+ return; // root component context — no token needed
186
+ }
187
+ };
188
+ const walk = (node) => {
189
+ if (node == null)
190
+ return;
191
+ const walkAll = (items) => {
192
+ for (const item of items ?? [])
193
+ walk(item);
194
+ };
195
+ // All of a node's binding collections (common to Element and Template/ng-template).
196
+ const walkBindings = (n) => {
197
+ walkAll(n.attributes);
198
+ walkAll(n.inputs);
199
+ walkAll(n.outputs);
200
+ walkAll(n.directives);
201
+ walkAll(n.references);
202
+ };
203
+ // Element (has a tag name + children)
204
+ if (typeof node.name === 'string' && Array.isArray(node.children)) {
205
+ emit(`${NG}<${node.name}`, node);
206
+ walkBindings(node);
207
+ walkAll(node.children);
208
+ return;
209
+ }
210
+ // Containers: ng-template, control-flow blocks (@if/@for/@switch/@defer).
211
+ // node.expression also catches leaf blocks without children (@case (…) -> SwitchBlockCase).
212
+ if (Array.isArray(node.children) ||
213
+ Array.isArray(node.branches) ||
214
+ Array.isArray(node.cases) ||
215
+ Array.isArray(node.groups) ||
216
+ node.expression) {
217
+ if (typeof node.tagName === 'string')
218
+ emit(`${NG}<${node.tagName}`, node);
219
+ // Block control expressions (@for of …; track …; @if/@case (…)).
220
+ if (node.expression) {
221
+ emit(`${NG}@expr`, node);
222
+ walkExpr(node.expression, node);
223
+ }
224
+ if (node.trackBy) {
225
+ emit(`${NG}@track`, node);
226
+ walkExpr(node.trackBy, node);
227
+ }
228
+ walkBindings(node);
229
+ walkAll(node.templateAttrs); // *ngIf / *ngFor microsyntax
230
+ walkAll(node.variables); // let-… on ng-template
231
+ walkAll(node.item ? [node.item] : undefined);
232
+ walkAll(node.children);
233
+ walkAll(node.branches);
234
+ walkAll(node.cases);
235
+ walkAll(node.groups);
236
+ walk(node.empty);
237
+ return;
238
+ }
239
+ // Attribute / input / output / reference / variable: the name is structural,
240
+ // the value/handler is an expression (if any), static is a literal.
241
+ if (typeof node.name === 'string') {
242
+ emit(`${NG}@${node.name}`, node);
243
+ if (node.value && typeof node.value === 'object')
244
+ walkExpr(node.value, node);
245
+ else if (typeof node.value === 'string' && node.value.length)
246
+ emitLit(node.value, node);
247
+ if (node.handler && typeof node.handler === 'object')
248
+ walkExpr(node.handler, node);
249
+ return;
250
+ }
251
+ // BoundText / interpolation (value is an AST expression)
252
+ if (node.value && typeof node.value === 'object') {
253
+ walkExpr(node.value, node);
254
+ return;
255
+ }
256
+ // Static text
257
+ if (typeof node.value === 'string') {
258
+ if (node.value.trim().length)
259
+ emit(NG_TEXT, node);
260
+ return;
261
+ }
262
+ };
263
+ for (const n of parsed.nodes)
264
+ walk(n);
265
+ return local.map((t) => (0, tokenizers_1.remap)(t, base.line, base.col));
266
+ }
267
+ /** Extract inline templates from `@Component({ template: \`...\` })`. */
268
+ function extractAngularInlineTemplates(filePath, source) {
269
+ const sf = ts.createSourceFile(filePath, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
270
+ const result = [];
271
+ const visit = (node) => {
272
+ if (ts.isDecorator(node) && ts.isCallExpression(node.expression)) {
273
+ const callee = node.expression.expression;
274
+ const name = ts.isIdentifier(callee) ? callee.text : '';
275
+ const arg = node.expression.arguments[0];
276
+ if (name === 'Component' && arg && ts.isObjectLiteralExpression(arg)) {
277
+ for (const p of arg.properties) {
278
+ if (!ts.isPropertyAssignment(p) || !p.name || !ts.isIdentifier(p.name))
279
+ continue;
280
+ if (p.name.text !== 'template')
281
+ continue;
282
+ const init = p.initializer;
283
+ if (ts.isStringLiteralLike(init)) {
284
+ // position of the first content char (after the quote/backtick)
285
+ const contentStart = init.getStart(sf) + 1;
286
+ const { line, character } = sf.getLineAndCharacterOfPosition(contentStart);
287
+ result.push({ code: init.text, line: line + 1, col: character + 1 });
288
+ }
289
+ }
290
+ }
291
+ }
292
+ ts.forEachChild(node, visit);
293
+ };
294
+ visit(sf);
295
+ return result;
296
+ }
package/dist/cli.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env node
2
+ import { type CpdOptions } from './index';
3
+ type ReportFormat = 'text' | 'xml' | 'json';
4
+ interface CliOptions extends CpdOptions {
5
+ paths: string[];
6
+ extensions: Set<string>;
7
+ excludePatterns: string[];
8
+ format: ReportFormat;
9
+ failOnViolation: boolean;
10
+ }
11
+ declare function main(argv: string[]): number;
12
+ declare function parseArgs(argv: string[]): CliOptions;
13
+ declare function collectFiles(paths: string[], extensions: Set<string>, excludePatterns?: string[]): string[];
14
+ export { collectFiles, main, parseArgs };