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/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
|
+
[](https://www.npmjs.com/package/clone-alert)
|
|
6
|
+
[](./LICENSE)
|
|
7
|
+
[](https://nodejs.org)
|
|
8
|
+
[](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
|
+
}[];
|
package/dist/angular.js
ADDED
|
@@ -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 };
|