@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.
Files changed (31) hide show
  1. package/dist/index.d.ts +4 -2
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +3 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/investigation/call-graph-builder.d.ts +95 -8
  6. package/dist/investigation/call-graph-builder.d.ts.map +1 -1
  7. package/dist/investigation/call-graph-builder.js +424 -81
  8. package/dist/investigation/call-graph-builder.js.map +1 -1
  9. package/dist/investigation/codebase-searcher.d.ts +68 -0
  10. package/dist/investigation/codebase-searcher.d.ts.map +1 -1
  11. package/dist/investigation/codebase-searcher.js +154 -10
  12. package/dist/investigation/codebase-searcher.js.map +1 -1
  13. package/dist/investigation/template-usage-finder.d.ts +64 -0
  14. package/dist/investigation/template-usage-finder.d.ts.map +1 -0
  15. package/dist/investigation/template-usage-finder.js +279 -0
  16. package/dist/investigation/template-usage-finder.js.map +1 -0
  17. package/dist/investigation/template-usage-scanner.d.ts +69 -0
  18. package/dist/investigation/template-usage-scanner.d.ts.map +1 -0
  19. package/dist/investigation/template-usage-scanner.js +375 -0
  20. package/dist/investigation/template-usage-scanner.js.map +1 -0
  21. package/dist/investigation/usage-finder.d.ts +51 -5
  22. package/dist/investigation/usage-finder.d.ts.map +1 -1
  23. package/dist/investigation/usage-finder.js +129 -36
  24. package/dist/investigation/usage-finder.js.map +1 -1
  25. package/package.json +1 -1
  26. package/src/index.ts +16 -0
  27. package/src/investigation/call-graph-builder.ts +528 -103
  28. package/src/investigation/codebase-searcher.ts +240 -11
  29. package/src/investigation/template-usage-finder.ts +389 -0
  30. package/src/investigation/template-usage-scanner.ts +529 -0
  31. 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 { Project, Node } from 'ts-morph';
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
- /** Classification of how a symbol is used at a given location. */
23
- export type UsageType = 'read' | 'write' | 'call';
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 results to a single source file (absolute path).
47
- * When omitted, all files in the project are searched.
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
- const sourceFiles = options.file
78
- ? [this.project.getSourceFile(options.file)].filter(Boolean)
79
- : this.project.getSourceFiles();
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 sourceFile of sourceFiles) {
82
- if (!sourceFile) {
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
- const nodes = this.findSymbolNodes(sourceFile, symbol);
87
-
88
- for (const node of nodes) {
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 this.deduplicate(usages).sort((a, b) =>
123
- a.file !== b.file ? a.file.localeCompare(b.file) : a.line - b.line,
124
- );
211
+ return usages;
125
212
  }
126
213
 
127
- private findSymbolNodes(
128
- sourceFile: ReturnType<Project['getSourceFile']>,
129
- symbol: string,
130
- ) {
131
- if (!sourceFile) {
132
- return [];
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
+ }