@angular-modernizer/api 0.1.3 → 0.2.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/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/investigation/call-graph-builder.d.ts +95 -8
- package/dist/investigation/call-graph-builder.d.ts.map +1 -1
- package/dist/investigation/call-graph-builder.js +424 -81
- package/dist/investigation/call-graph-builder.js.map +1 -1
- package/dist/investigation/codebase-searcher.d.ts +68 -0
- package/dist/investigation/codebase-searcher.d.ts.map +1 -1
- package/dist/investigation/codebase-searcher.js +154 -10
- package/dist/investigation/codebase-searcher.js.map +1 -1
- package/dist/investigation/template-usage-finder.d.ts +64 -0
- package/dist/investigation/template-usage-finder.d.ts.map +1 -0
- package/dist/investigation/template-usage-finder.js +279 -0
- package/dist/investigation/template-usage-finder.js.map +1 -0
- package/dist/investigation/template-usage-scanner.d.ts +69 -0
- package/dist/investigation/template-usage-scanner.d.ts.map +1 -0
- package/dist/investigation/template-usage-scanner.js +375 -0
- package/dist/investigation/template-usage-scanner.js.map +1 -0
- package/dist/investigation/usage-finder.d.ts +51 -5
- package/dist/investigation/usage-finder.d.ts.map +1 -1
- package/dist/investigation/usage-finder.js +129 -36
- package/dist/investigation/usage-finder.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +16 -0
- package/src/investigation/call-graph-builder.ts +528 -103
- package/src/investigation/codebase-searcher.ts +240 -11
- package/src/investigation/template-usage-finder.ts +389 -0
- package/src/investigation/template-usage-scanner.ts +529 -0
- package/src/investigation/usage-finder.ts +194 -54
|
@@ -9,6 +9,14 @@
|
|
|
9
9
|
* Uses ts-morph `findReferences()` under the hood. The project must already have
|
|
10
10
|
* the relevant source files added before calling `findUsages`.
|
|
11
11
|
*
|
|
12
|
+
* Two additional sources close gaps that would otherwise produce a false
|
|
13
|
+
* "0 usages" all-clear:
|
|
14
|
+
* - `unresolved`: same-name identifiers whose symbol does not resolve (e.g. a
|
|
15
|
+
* class still used as a type after its import was removed, TS2304).
|
|
16
|
+
* - `template`: Angular template usages (component/directive selectors, pipe
|
|
17
|
+
* names, member reads/calls/bindings) in `templateUrl` files and inline
|
|
18
|
+
* templates, see `TemplateUsageFinder`.
|
|
19
|
+
*
|
|
12
20
|
* @example
|
|
13
21
|
* ```typescript
|
|
14
22
|
* const finder = new UsageFinder(project);
|
|
@@ -17,10 +25,21 @@
|
|
|
17
25
|
* ```
|
|
18
26
|
*/
|
|
19
27
|
|
|
20
|
-
import type
|
|
28
|
+
import { Node, type Project, type SourceFile } from 'ts-morph';
|
|
29
|
+
import {
|
|
30
|
+
TemplateUsageFinder,
|
|
31
|
+
type TemplateMatch,
|
|
32
|
+
} from './template-usage-finder.js';
|
|
33
|
+
import { type TemplateUsageKind } from './template-usage-scanner.js';
|
|
21
34
|
|
|
22
|
-
/**
|
|
23
|
-
|
|
35
|
+
/**
|
|
36
|
+
* Classification of how a symbol is used at a given location.
|
|
37
|
+
*
|
|
38
|
+
* - `read` / `write` / `call`: resolved TypeScript references
|
|
39
|
+
* - `template`: usage inside an Angular template (see `templateKind`)
|
|
40
|
+
* - `unresolved`: same-name identifier whose symbol does not resolve
|
|
41
|
+
*/
|
|
42
|
+
export type UsageType = 'read' | 'write' | 'call' | 'template' | 'unresolved';
|
|
24
43
|
|
|
25
44
|
/** A single resolved symbol usage. */
|
|
26
45
|
export interface SymbolUsage {
|
|
@@ -38,13 +57,30 @@ export interface SymbolUsage {
|
|
|
38
57
|
|
|
39
58
|
/** How the symbol is used at this location. */
|
|
40
59
|
usageType: UsageType;
|
|
60
|
+
|
|
61
|
+
/** Explanation for `unresolved` and unverified `template` usages. */
|
|
62
|
+
note?: string;
|
|
63
|
+
|
|
64
|
+
/** For `template` usages: what kind of template construct matched. */
|
|
65
|
+
templateKind?: TemplateUsageKind;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* For `template` usages: `verified` when the hit certainly refers to the
|
|
69
|
+
* searched symbol, `template-unverified` for same-name matches that could
|
|
70
|
+
* not be tied to it.
|
|
71
|
+
*/
|
|
72
|
+
templateMatch?: TemplateMatch;
|
|
73
|
+
|
|
74
|
+
/** For `template` usages: the component whose template contains the hit. */
|
|
75
|
+
component?: string;
|
|
41
76
|
}
|
|
42
77
|
|
|
43
78
|
/** Options for narrowing `findUsages` results. */
|
|
44
79
|
export interface FindUsagesOptions {
|
|
45
80
|
/**
|
|
46
|
-
* Restrict
|
|
47
|
-
*
|
|
81
|
+
* Restrict reported usages to a single file (absolute path; a `.ts` source
|
|
82
|
+
* file or an external `.html` template). Declarations are still looked up
|
|
83
|
+
* across the whole project.
|
|
48
84
|
*/
|
|
49
85
|
file?: string;
|
|
50
86
|
|
|
@@ -53,6 +89,12 @@ export interface FindUsagesOptions {
|
|
|
53
89
|
* When omitted, all usage types are returned.
|
|
54
90
|
*/
|
|
55
91
|
usageType?: UsageType;
|
|
92
|
+
|
|
93
|
+
/** Include Angular template usages. Defaults to `true`. */
|
|
94
|
+
includeTemplates?: boolean;
|
|
95
|
+
|
|
96
|
+
/** Include same-name identifiers that do not resolve to a symbol. Defaults to `true`. */
|
|
97
|
+
includeUnresolved?: boolean;
|
|
56
98
|
}
|
|
57
99
|
|
|
58
100
|
/**
|
|
@@ -72,69 +114,134 @@ export class UsageFinder {
|
|
|
72
114
|
* @returns Array of resolved usages, sorted by file path then line number.
|
|
73
115
|
*/
|
|
74
116
|
findUsages(symbol: string, options: FindUsagesOptions = {}): SymbolUsage[] {
|
|
117
|
+
const declarations = this.project
|
|
118
|
+
.getSourceFiles()
|
|
119
|
+
.flatMap((sf) => this.findSymbolNodes(sf, symbol));
|
|
120
|
+
|
|
121
|
+
const usages: SymbolUsage[] = [
|
|
122
|
+
...this.findReferenceUsages(declarations),
|
|
123
|
+
...(options.includeUnresolved === false
|
|
124
|
+
? []
|
|
125
|
+
: this.findUnresolvedUsages(symbol)),
|
|
126
|
+
...(options.includeTemplates === false
|
|
127
|
+
? []
|
|
128
|
+
: this.findTemplateUsages(symbol, declarations)),
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
const fileFilter =
|
|
132
|
+
options.file === undefined ? undefined : normalizePath(options.file);
|
|
133
|
+
|
|
134
|
+
return this.deduplicate(usages)
|
|
135
|
+
.filter(
|
|
136
|
+
(u) => fileFilter === undefined || normalizePath(u.file) === fileFilter,
|
|
137
|
+
)
|
|
138
|
+
.filter(
|
|
139
|
+
(u) => options.usageType === undefined || u.usageType === options.usageType,
|
|
140
|
+
)
|
|
141
|
+
.sort((a, b) =>
|
|
142
|
+
a.file === b.file ? a.line - b.line || a.col - b.col : a.file.localeCompare(b.file),
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private findReferenceUsages(declarations: Node[]): SymbolUsage[] {
|
|
75
147
|
const usages: SymbolUsage[] = [];
|
|
148
|
+
const languageService = this.project.getLanguageService();
|
|
149
|
+
|
|
150
|
+
for (const node of declarations) {
|
|
151
|
+
// Query by the name node: a decorated class starts at its decorator.
|
|
152
|
+
for (const refSymbol of languageService.findReferences(nameNodeOf(node))) {
|
|
153
|
+
for (const ref of refSymbol.getReferences()) {
|
|
154
|
+
const refFile = ref.getSourceFile();
|
|
155
|
+
const pos = ref.getNode().getStart();
|
|
156
|
+
const lineAndCol = refFile.getLineAndColumnAtPos(pos);
|
|
157
|
+
|
|
158
|
+
usages.push({
|
|
159
|
+
file: refFile.getFilePath(),
|
|
160
|
+
line: lineAndCol.line,
|
|
161
|
+
col: lineAndCol.column,
|
|
162
|
+
snippet: lineTextAt(refFile, lineAndCol.line),
|
|
163
|
+
usageType: this.classifyUsage(ref.getNode()),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
76
168
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
169
|
+
return usages;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Same-name identifiers whose symbol does not resolve. The language
|
|
174
|
+
* service cannot link them to any declaration, so `findReferences` never
|
|
175
|
+
* returns them; without this pass a removed import looks like "0 usages".
|
|
176
|
+
*/
|
|
177
|
+
private findUnresolvedUsages(symbol: string): SymbolUsage[] {
|
|
178
|
+
const usages: SymbolUsage[] = [];
|
|
80
179
|
|
|
81
|
-
for (const
|
|
82
|
-
if (
|
|
180
|
+
for (const sf of this.project.getSourceFiles()) {
|
|
181
|
+
if (sf.isDeclarationFile() || sf.isInNodeModules()) {
|
|
182
|
+
continue;
|
|
183
|
+
}
|
|
184
|
+
if (!sf.getFullText().includes(symbol)) {
|
|
83
185
|
continue;
|
|
84
186
|
}
|
|
85
187
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const refs = this.project.getLanguageService().findReferences(node);
|
|
90
|
-
|
|
91
|
-
for (const refSymbol of refs) {
|
|
92
|
-
for (const ref of refSymbol.getReferences()) {
|
|
93
|
-
const refFile = ref.getSourceFile();
|
|
94
|
-
const refFilePath = refFile.getFilePath();
|
|
95
|
-
|
|
96
|
-
if (options.file && refFilePath !== options.file) {
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const pos = ref.getNode().getStart();
|
|
101
|
-
const lineAndCol = refFile.getLineAndColumnAtPos(pos);
|
|
102
|
-
const lineText =
|
|
103
|
-
refFile.getFullText().split('\n')[lineAndCol.line - 1] ?? '';
|
|
104
|
-
const usageType = this.classifyUsage(ref.getNode());
|
|
105
|
-
|
|
106
|
-
if (options.usageType && usageType !== options.usageType) {
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
usages.push({
|
|
111
|
-
file: refFilePath,
|
|
112
|
-
line: lineAndCol.line,
|
|
113
|
-
col: lineAndCol.column,
|
|
114
|
-
snippet: lineText.trim(),
|
|
115
|
-
usageType,
|
|
116
|
-
});
|
|
117
|
-
}
|
|
188
|
+
sf.forEachDescendant((node) => {
|
|
189
|
+
if (!Node.isIdentifier(node) || node.getText() !== symbol) {
|
|
190
|
+
return;
|
|
118
191
|
}
|
|
119
|
-
|
|
192
|
+
const resolved = node.getSymbol();
|
|
193
|
+
const isResolved =
|
|
194
|
+
resolved !== undefined && resolved.getDeclarations().length > 0;
|
|
195
|
+
if (isResolved) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const { line, column } = sf.getLineAndColumnAtPos(node.getStart());
|
|
200
|
+
usages.push({
|
|
201
|
+
file: sf.getFilePath(),
|
|
202
|
+
line,
|
|
203
|
+
col: column,
|
|
204
|
+
snippet: lineTextAt(sf, line),
|
|
205
|
+
usageType: 'unresolved',
|
|
206
|
+
note: unresolvedNote(node, symbol),
|
|
207
|
+
});
|
|
208
|
+
});
|
|
120
209
|
}
|
|
121
210
|
|
|
122
|
-
return
|
|
123
|
-
a.file !== b.file ? a.file.localeCompare(b.file) : a.line - b.line,
|
|
124
|
-
);
|
|
211
|
+
return usages;
|
|
125
212
|
}
|
|
126
213
|
|
|
127
|
-
private
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
214
|
+
private findTemplateUsages(symbol: string, declarations: Node[]): SymbolUsage[] {
|
|
215
|
+
return new TemplateUsageFinder(this.project)
|
|
216
|
+
.findTemplateUsages(symbol, declarations)
|
|
217
|
+
.map((t) => ({
|
|
218
|
+
file: t.file,
|
|
219
|
+
line: t.line,
|
|
220
|
+
col: t.col,
|
|
221
|
+
snippet: t.snippet,
|
|
222
|
+
usageType: 'template' as const,
|
|
223
|
+
templateKind: t.templateKind,
|
|
224
|
+
templateMatch: t.templateMatch,
|
|
225
|
+
component: t.component,
|
|
226
|
+
...(t.note === undefined ? {} : { note: t.note }),
|
|
227
|
+
}));
|
|
228
|
+
}
|
|
134
229
|
|
|
230
|
+
/**
|
|
231
|
+
* Declaration-like nodes named `symbol`. Expressions that merely carry a
|
|
232
|
+
* name (`svc.getUser` property accesses) are not declarations and would
|
|
233
|
+
* make `findReferences` resolve their receiver instead.
|
|
234
|
+
*/
|
|
235
|
+
private findSymbolNodes(sourceFile: SourceFile, symbol: string): Node[] {
|
|
135
236
|
const nodes: Node[] = [];
|
|
136
237
|
|
|
137
238
|
sourceFile.forEachDescendant((node) => {
|
|
239
|
+
if (
|
|
240
|
+
Node.isPropertyAccessExpression(node) ||
|
|
241
|
+
Node.isElementAccessExpression(node)
|
|
242
|
+
) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
138
245
|
if (
|
|
139
246
|
'getName' in node &&
|
|
140
247
|
typeof (node as { getName(): string }).getName === 'function'
|
|
@@ -187,7 +294,7 @@ export class UsageFinder {
|
|
|
187
294
|
private deduplicate(usages: SymbolUsage[]): SymbolUsage[] {
|
|
188
295
|
const seen = new Set<string>();
|
|
189
296
|
return usages.filter((u) => {
|
|
190
|
-
const key = `${u.file}:${u.line}:${u.col}`;
|
|
297
|
+
const key = `${u.file}:${u.line}:${u.col}:${u.templateKind ?? ''}`;
|
|
191
298
|
if (seen.has(key)) {
|
|
192
299
|
return false;
|
|
193
300
|
}
|
|
@@ -196,3 +303,36 @@ export class UsageFinder {
|
|
|
196
303
|
});
|
|
197
304
|
}
|
|
198
305
|
}
|
|
306
|
+
|
|
307
|
+
function normalizePath(path: string): string {
|
|
308
|
+
return path.replaceAll('\\', '/');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function lineTextAt(sourceFile: SourceFile, line: number): string {
|
|
312
|
+
return (sourceFile.getFullText().split('\n')[line - 1] ?? '').trim();
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function unresolvedNote(node: Node, symbol: string): string {
|
|
316
|
+
const parent = node.getParent();
|
|
317
|
+
const isMemberName =
|
|
318
|
+
parent !== undefined &&
|
|
319
|
+
Node.isPropertyAccessExpression(parent) &&
|
|
320
|
+
parent.getNameNode() === node;
|
|
321
|
+
if (isMemberName) {
|
|
322
|
+
return (
|
|
323
|
+
`'${symbol}' is accessed on a receiver whose type is 'any' or unresolved; ` +
|
|
324
|
+
'it may or may not refer to the searched member.'
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
return (
|
|
328
|
+
`'${symbol}' does not resolve to any declaration (missing import or ` +
|
|
329
|
+
'removed symbol, typically TS2304/TS2552). The file still depends on it.'
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function nameNodeOf(node: Node): Node {
|
|
334
|
+
const named = node as { getNameNode?: () => Node | undefined };
|
|
335
|
+
return typeof named.getNameNode === 'function'
|
|
336
|
+
? (named.getNameNode() ?? node)
|
|
337
|
+
: node;
|
|
338
|
+
}
|