@angular-modernizer/api 0.1.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 +507 -0
- package/dist/analysis/complexity-metrics-calculator.d.ts +99 -0
- package/dist/analysis/complexity-metrics-calculator.d.ts.map +1 -0
- package/dist/analysis/complexity-metrics-calculator.js +195 -0
- package/dist/analysis/complexity-metrics-calculator.js.map +1 -0
- package/dist/analysis/dependency-analyzer.d.ts +227 -0
- package/dist/analysis/dependency-analyzer.d.ts.map +1 -0
- package/dist/analysis/dependency-analyzer.js +451 -0
- package/dist/analysis/dependency-analyzer.js.map +1 -0
- package/dist/analysis/external-template-loader.d.ts +44 -0
- package/dist/analysis/external-template-loader.d.ts.map +1 -0
- package/dist/analysis/external-template-loader.js +69 -0
- package/dist/analysis/external-template-loader.js.map +1 -0
- package/dist/analysis/selector-mapper.d.ts +77 -0
- package/dist/analysis/selector-mapper.d.ts.map +1 -0
- package/dist/analysis/selector-mapper.js +146 -0
- package/dist/analysis/selector-mapper.js.map +1 -0
- package/dist/analysis/service-detector.d.ts +164 -0
- package/dist/analysis/service-detector.d.ts.map +1 -0
- package/dist/analysis/service-detector.js +231 -0
- package/dist/analysis/service-detector.js.map +1 -0
- package/dist/analysis/service-pattern-recognizer.d.ts +247 -0
- package/dist/analysis/service-pattern-recognizer.d.ts.map +1 -0
- package/dist/analysis/service-pattern-recognizer.js +444 -0
- package/dist/analysis/service-pattern-recognizer.js.map +1 -0
- package/dist/analysis/symbol-locator.d.ts +81 -0
- package/dist/analysis/symbol-locator.d.ts.map +1 -0
- package/dist/analysis/symbol-locator.js +128 -0
- package/dist/analysis/symbol-locator.js.map +1 -0
- package/dist/analysis/template-analyzer-usage.d.ts +144 -0
- package/dist/analysis/template-analyzer-usage.d.ts.map +1 -0
- package/dist/analysis/template-analyzer-usage.js +474 -0
- package/dist/analysis/template-analyzer-usage.js.map +1 -0
- package/dist/analysis/template-analyzer.d.ts +80 -0
- package/dist/analysis/template-analyzer.d.ts.map +1 -0
- package/dist/analysis/template-analyzer.js +146 -0
- package/dist/analysis/template-analyzer.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -0
- package/dist/investigation/angular-symbol-finder.d.ts +76 -0
- package/dist/investigation/angular-symbol-finder.d.ts.map +1 -0
- package/dist/investigation/angular-symbol-finder.js +199 -0
- package/dist/investigation/angular-symbol-finder.js.map +1 -0
- package/dist/investigation/call-graph-builder.d.ts +85 -0
- package/dist/investigation/call-graph-builder.d.ts.map +1 -0
- package/dist/investigation/call-graph-builder.js +196 -0
- package/dist/investigation/call-graph-builder.js.map +1 -0
- package/dist/investigation/codebase-searcher.d.ts +99 -0
- package/dist/investigation/codebase-searcher.d.ts.map +1 -0
- package/dist/investigation/codebase-searcher.js +201 -0
- package/dist/investigation/codebase-searcher.js.map +1 -0
- package/dist/investigation/stack-trace-parser.d.ts +70 -0
- package/dist/investigation/stack-trace-parser.d.ts.map +1 -0
- package/dist/investigation/stack-trace-parser.js +117 -0
- package/dist/investigation/stack-trace-parser.js.map +1 -0
- package/dist/investigation/type-resolver.d.ts +59 -0
- package/dist/investigation/type-resolver.d.ts.map +1 -0
- package/dist/investigation/type-resolver.js +111 -0
- package/dist/investigation/type-resolver.js.map +1 -0
- package/dist/investigation/usage-finder.d.ts +69 -0
- package/dist/investigation/usage-finder.d.ts.map +1 -0
- package/dist/investigation/usage-finder.js +132 -0
- package/dist/investigation/usage-finder.js.map +1 -0
- package/dist/metrics/complexity-metrics.d.ts +196 -0
- package/dist/metrics/complexity-metrics.d.ts.map +1 -0
- package/dist/metrics/complexity-metrics.js +445 -0
- package/dist/metrics/complexity-metrics.js.map +1 -0
- package/dist/public-api.d.ts +77 -0
- package/dist/public-api.d.ts.map +1 -0
- package/dist/public-api.js +50 -0
- package/dist/public-api.js.map +1 -0
- package/dist/transformation/import-manager.d.ts +276 -0
- package/dist/transformation/import-manager.d.ts.map +1 -0
- package/dist/transformation/import-manager.js +488 -0
- package/dist/transformation/import-manager.js.map +1 -0
- package/dist/transformation/ng-morph-adapter.d.ts +111 -0
- package/dist/transformation/ng-morph-adapter.d.ts.map +1 -0
- package/dist/transformation/ng-morph-adapter.js +179 -0
- package/dist/transformation/ng-morph-adapter.js.map +1 -0
- package/dist/transformation/ngmodule-manager.d.ts +145 -0
- package/dist/transformation/ngmodule-manager.d.ts.map +1 -0
- package/dist/transformation/ngmodule-manager.js +356 -0
- package/dist/transformation/ngmodule-manager.js.map +1 -0
- package/package.json +62 -0
- package/src/analysis/complexity-metrics-calculator.ts +253 -0
- package/src/analysis/dependency-analyzer.ts +720 -0
- package/src/analysis/external-template-loader.ts +79 -0
- package/src/analysis/selector-mapper.ts +186 -0
- package/src/analysis/service-detector.ts +280 -0
- package/src/analysis/service-pattern-recognizer.ts +545 -0
- package/src/analysis/symbol-locator.ts +170 -0
- package/src/analysis/template-analyzer-usage.ts +614 -0
- package/src/analysis/template-analyzer.ts +208 -0
- package/src/index.ts +51 -0
- package/src/investigation/angular-symbol-finder.ts +263 -0
- package/src/investigation/call-graph-builder.ts +298 -0
- package/src/investigation/codebase-searcher.ts +286 -0
- package/src/investigation/stack-trace-parser.ts +166 -0
- package/src/investigation/type-resolver.ts +144 -0
- package/src/investigation/usage-finder.ts +198 -0
- package/src/metrics/complexity-metrics.ts +617 -0
- package/src/public-api.ts +119 -0
- package/src/transformation/import-manager.ts +626 -0
- package/src/transformation/ng-morph-adapter.ts +252 -0
- package/src/transformation/ngmodule-manager.ts +450 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @angular-modernizer/api - Stack Trace Parser
|
|
3
|
+
*
|
|
4
|
+
* Parses Node.js/Angular/Zone.js/browser stack traces and maps frames to project source files.
|
|
5
|
+
*
|
|
6
|
+
* @remarks
|
|
7
|
+
* Frames are classified as project files when their resolved path starts with `projectRoot`
|
|
8
|
+
* and does not pass through `node_modules`. Browser URL frames (http/https) are always
|
|
9
|
+
* classified as non-project files regardless of origin.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* ```typescript
|
|
13
|
+
* const parser = new StackTraceParser('/my/project/src');
|
|
14
|
+
* const frames = parser.parse(error.stack ?? '');
|
|
15
|
+
* // frames[0].file → '/my/project/src/app/user.service.ts'
|
|
16
|
+
* // frames[0].isProjectFile → true
|
|
17
|
+
*
|
|
18
|
+
* const projectOnly = parser.parseProjectFrames(error.stack ?? '');
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** A single resolved stack frame. */
|
|
23
|
+
export interface StackFrame {
|
|
24
|
+
/** Absolute file path. For browser URL frames, the raw URL string. */
|
|
25
|
+
file: string;
|
|
26
|
+
|
|
27
|
+
/** 1-based line number. */
|
|
28
|
+
line: number;
|
|
29
|
+
|
|
30
|
+
/** 1-based column number. */
|
|
31
|
+
col: number;
|
|
32
|
+
|
|
33
|
+
/** Function or method name. `'<anonymous>'` when not present. */
|
|
34
|
+
functionName: string;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* `true` when the frame's path starts with `projectRoot`
|
|
38
|
+
* and does not pass through `node_modules`.
|
|
39
|
+
*/
|
|
40
|
+
isProjectFile: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Regex matching a V8-style "at" frame in the following forms:
|
|
45
|
+
*
|
|
46
|
+
* ```
|
|
47
|
+
* at FunctionName (file:line:col)
|
|
48
|
+
* at async FunctionName (file:line:col)
|
|
49
|
+
* at new FunctionName (file:line:col)
|
|
50
|
+
* at file:line:col
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
const AT_FRAME_WITH_LOCATION =
|
|
54
|
+
/^\s*at\s+(?:async\s+)?(.+?)\s+\((.+):(\d+):(\d+)\)\s*$/;
|
|
55
|
+
|
|
56
|
+
const AT_FRAME_WITHOUT_FUNCTION =
|
|
57
|
+
/^\s*at\s+((?!async\s|new\s)[^(]+):(\d+):(\d+)\s*$/;
|
|
58
|
+
|
|
59
|
+
const HTTP_SCHEME = /^https?:\/\//;
|
|
60
|
+
const NODE_MODULES_SEGMENT = /[/\\]node_modules[/\\]/;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Parses Node.js/Angular/Zone.js/browser stack traces into structured frames.
|
|
64
|
+
*/
|
|
65
|
+
export class StackTraceParser {
|
|
66
|
+
/**
|
|
67
|
+
* @param projectRoot - Absolute path to the project root used to classify
|
|
68
|
+
* frames as project files vs. external dependencies.
|
|
69
|
+
*/
|
|
70
|
+
constructor(private readonly projectRoot: string) {}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Parses a raw stack trace string into an array of structured frames.
|
|
74
|
+
*
|
|
75
|
+
* @param stackTrace - Raw `Error.stack` string or console-captured stack trace.
|
|
76
|
+
* @returns All frames found, including non-project frames.
|
|
77
|
+
*/
|
|
78
|
+
parse(stackTrace: string): StackFrame[] {
|
|
79
|
+
if (!stackTrace) {
|
|
80
|
+
return [];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const frames: StackFrame[] = [];
|
|
84
|
+
|
|
85
|
+
for (const line of stackTrace.split('\n')) {
|
|
86
|
+
const frame = this.parseLine(line);
|
|
87
|
+
if (frame !== null) {
|
|
88
|
+
frames.push(frame);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return frames;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Parses a stack trace and returns only frames that belong to the project.
|
|
97
|
+
*
|
|
98
|
+
* @param stackTrace - Raw `Error.stack` string or console-captured stack trace.
|
|
99
|
+
* @returns Frames where `isProjectFile` is `true`.
|
|
100
|
+
*/
|
|
101
|
+
parseProjectFrames(stackTrace: string): StackFrame[] {
|
|
102
|
+
return this.parse(stackTrace).filter((f) => f.isProjectFile);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private parseLine(line: string): StackFrame | null {
|
|
106
|
+
const withFunc = AT_FRAME_WITH_LOCATION.exec(line);
|
|
107
|
+
if (withFunc !== null) {
|
|
108
|
+
const [, functionName, file, lineStr, colStr] = withFunc;
|
|
109
|
+
return this.buildFrame(
|
|
110
|
+
this.sanitizeFunctionName(functionName ?? ''),
|
|
111
|
+
file ?? '',
|
|
112
|
+
Number(lineStr),
|
|
113
|
+
Number(colStr),
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const withoutFunc = AT_FRAME_WITHOUT_FUNCTION.exec(line);
|
|
118
|
+
if (withoutFunc !== null) {
|
|
119
|
+
const [, file, lineStr, colStr] = withoutFunc;
|
|
120
|
+
return this.buildFrame(
|
|
121
|
+
'<anonymous>',
|
|
122
|
+
file ?? '',
|
|
123
|
+
Number(lineStr),
|
|
124
|
+
Number(colStr),
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private buildFrame(
|
|
132
|
+
functionName: string,
|
|
133
|
+
file: string,
|
|
134
|
+
line: number,
|
|
135
|
+
col: number,
|
|
136
|
+
): StackFrame {
|
|
137
|
+
return {
|
|
138
|
+
file,
|
|
139
|
+
line,
|
|
140
|
+
col,
|
|
141
|
+
functionName,
|
|
142
|
+
isProjectFile: this.isProjectFile(file),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private isProjectFile(file: string): boolean {
|
|
147
|
+
if (HTTP_SCHEME.test(file)) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
if (NODE_MODULES_SEGMENT.test(file)) {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
return file.startsWith(this.projectRoot);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Strips the `async ` prefix that ts-jest/V8 sometimes leaves in function names
|
|
158
|
+
* when the regex group captures it as part of the name rather than the keyword.
|
|
159
|
+
*/
|
|
160
|
+
private sanitizeFunctionName(name: string): string {
|
|
161
|
+
return name
|
|
162
|
+
.replace(/^async\s+/, '')
|
|
163
|
+
.replace(/^new\s+/, 'new ')
|
|
164
|
+
.trim();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @angular-modernizer/api - Type Resolver
|
|
3
|
+
*
|
|
4
|
+
* Resolves the TypeScript type at a given position (file, line, column)
|
|
5
|
+
* using the ts-morph language service and type checker.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* Useful in bug investigation to understand what type a variable holds
|
|
9
|
+
* at a specific location — including whether it is nullable, async,
|
|
10
|
+
* or wraps a generic parameter.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* const resolver = new TypeResolver(project);
|
|
15
|
+
* const info = resolver.resolveType('/src/app/user.service.ts', 42, 15);
|
|
16
|
+
* // info.typeString → 'UserService'
|
|
17
|
+
* // info.isNullable → false
|
|
18
|
+
* // info.isAsync → false
|
|
19
|
+
* // info.declaredIn → '/src/app/user.service.ts'
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { Project } from 'ts-morph';
|
|
24
|
+
|
|
25
|
+
/** Resolved type information for a source position. */
|
|
26
|
+
export interface ResolvedTypeInfo {
|
|
27
|
+
/** Human-readable resolved type string. */
|
|
28
|
+
typeString: string;
|
|
29
|
+
|
|
30
|
+
/** Generic type parameters extracted from the type string (e.g. `['string']` for `Array<string>`). */
|
|
31
|
+
genericParams: string[];
|
|
32
|
+
|
|
33
|
+
/** Absolute path of the file where this type is declared. Empty string when not determinable. */
|
|
34
|
+
declaredIn: string;
|
|
35
|
+
|
|
36
|
+
/** `true` when the type includes `null` or `undefined` in a union. */
|
|
37
|
+
isNullable: boolean;
|
|
38
|
+
|
|
39
|
+
/** `true` when the type is `Promise<T>` or `Observable<T>`. */
|
|
40
|
+
isAsync: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const ASYNC_TYPE_NAMES = new Set([
|
|
44
|
+
'Promise',
|
|
45
|
+
'Observable',
|
|
46
|
+
'Subject',
|
|
47
|
+
'BehaviorSubject',
|
|
48
|
+
]);
|
|
49
|
+
const FALLBACK: ResolvedTypeInfo = {
|
|
50
|
+
typeString: 'unknown',
|
|
51
|
+
genericParams: [],
|
|
52
|
+
declaredIn: '',
|
|
53
|
+
isNullable: false,
|
|
54
|
+
isAsync: false,
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Resolves TypeScript types at source positions via ts-morph.
|
|
59
|
+
*/
|
|
60
|
+
export class TypeResolver {
|
|
61
|
+
/**
|
|
62
|
+
* @param project - A ts-morph `Project` instance with source files already added.
|
|
63
|
+
*/
|
|
64
|
+
constructor(private readonly project: Project) {}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Resolves the TypeScript type at the given 1-based position.
|
|
68
|
+
*
|
|
69
|
+
* @param file - Absolute path of the source file.
|
|
70
|
+
* @param line - 1-based line number.
|
|
71
|
+
* @param column - 1-based column number.
|
|
72
|
+
* @returns Resolved type information, or a fallback with `typeString: 'unknown'`.
|
|
73
|
+
*/
|
|
74
|
+
resolveType(file: string, line: number, column: number): ResolvedTypeInfo {
|
|
75
|
+
const sourceFile = this.project.getSourceFile(file);
|
|
76
|
+
if (!sourceFile) {
|
|
77
|
+
return FALLBACK;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const pos = sourceFile.compilerNode.getPositionOfLineAndCharacter(
|
|
82
|
+
line - 1,
|
|
83
|
+
column - 1,
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
const node = sourceFile.getDescendantAtPos(pos);
|
|
87
|
+
if (!node) {
|
|
88
|
+
return FALLBACK;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const type = this.project.getTypeChecker().getTypeAtLocation(node);
|
|
92
|
+
const typeString = type.getText(node);
|
|
93
|
+
|
|
94
|
+
const genericParams = this.extractGenericParams(typeString);
|
|
95
|
+
const declaredIn = this.resolveDeclaredIn(type);
|
|
96
|
+
const isNullable = this.checkNullable(typeString);
|
|
97
|
+
const isAsync = this.checkAsync(typeString);
|
|
98
|
+
|
|
99
|
+
return { typeString, genericParams, declaredIn, isNullable, isAsync };
|
|
100
|
+
} catch {
|
|
101
|
+
return FALLBACK;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private extractGenericParams(typeString: string): string[] {
|
|
106
|
+
const match = /<([^>]+)>/.exec(typeString);
|
|
107
|
+
if (!match?.[1]) {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
return match[1]
|
|
111
|
+
.split(',')
|
|
112
|
+
.map((s) => s.trim())
|
|
113
|
+
.filter(Boolean);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private resolveDeclaredIn(
|
|
117
|
+
type: ReturnType<
|
|
118
|
+
ReturnType<Project['getTypeChecker']>['getTypeAtLocation']
|
|
119
|
+
>,
|
|
120
|
+
): string {
|
|
121
|
+
try {
|
|
122
|
+
const symbol = type.getSymbol();
|
|
123
|
+
if (!symbol) {
|
|
124
|
+
return '';
|
|
125
|
+
}
|
|
126
|
+
const declarations = symbol.getDeclarations();
|
|
127
|
+
if (declarations.length === 0) {
|
|
128
|
+
return '';
|
|
129
|
+
}
|
|
130
|
+
return declarations[0]?.getSourceFile().getFilePath() ?? '';
|
|
131
|
+
} catch {
|
|
132
|
+
return '';
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private checkNullable(typeString: string): boolean {
|
|
137
|
+
return typeString.includes('null') || typeString.includes('undefined');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
private checkAsync(typeString: string): boolean {
|
|
141
|
+
const baseName = typeString.split('<')[0]?.trim() ?? '';
|
|
142
|
+
return ASYNC_TYPE_NAMES.has(baseName);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @angular-modernizer/api - Usage Finder
|
|
3
|
+
*
|
|
4
|
+
* Finds all references to a symbol (class, method, function, variable, constant)
|
|
5
|
+
* across a ts-morph Project. Used in bug investigation to determine call paths
|
|
6
|
+
* and blast radius before proposing a fix.
|
|
7
|
+
*
|
|
8
|
+
* @remarks
|
|
9
|
+
* Uses ts-morph `findReferences()` under the hood. The project must already have
|
|
10
|
+
* the relevant source files added before calling `findUsages`.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* const finder = new UsageFinder(project);
|
|
15
|
+
* const usages = finder.findUsages('UserService', { file: '/src/app/user.component.ts' });
|
|
16
|
+
* // usages[0] → { file, line, col, snippet, usageType: 'read' }
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { Project, Node } from 'ts-morph';
|
|
21
|
+
|
|
22
|
+
/** Classification of how a symbol is used at a given location. */
|
|
23
|
+
export type UsageType = 'read' | 'write' | 'call';
|
|
24
|
+
|
|
25
|
+
/** A single resolved symbol usage. */
|
|
26
|
+
export interface SymbolUsage {
|
|
27
|
+
/** Absolute file path containing the usage. */
|
|
28
|
+
file: string;
|
|
29
|
+
|
|
30
|
+
/** 1-based line number. */
|
|
31
|
+
line: number;
|
|
32
|
+
|
|
33
|
+
/** 1-based column number. */
|
|
34
|
+
col: number;
|
|
35
|
+
|
|
36
|
+
/** The source line containing the usage. */
|
|
37
|
+
snippet: string;
|
|
38
|
+
|
|
39
|
+
/** How the symbol is used at this location. */
|
|
40
|
+
usageType: UsageType;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Options for narrowing `findUsages` results. */
|
|
44
|
+
export interface FindUsagesOptions {
|
|
45
|
+
/**
|
|
46
|
+
* Restrict results to a single source file (absolute path).
|
|
47
|
+
* When omitted, all files in the project are searched.
|
|
48
|
+
*/
|
|
49
|
+
file?: string;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Restrict results to a specific usage type.
|
|
53
|
+
* When omitted, all usage types are returned.
|
|
54
|
+
*/
|
|
55
|
+
usageType?: UsageType;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Finds all references to named symbols across a ts-morph Project.
|
|
60
|
+
*/
|
|
61
|
+
export class UsageFinder {
|
|
62
|
+
/**
|
|
63
|
+
* @param project - A ts-morph `Project` instance with source files already added.
|
|
64
|
+
*/
|
|
65
|
+
constructor(private readonly project: Project) {}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Finds all usages of the named symbol across the project.
|
|
69
|
+
*
|
|
70
|
+
* @param symbol - The symbol name to search for (class, method, function, variable, constant).
|
|
71
|
+
* @param options - Optional filters for file scope and usage type.
|
|
72
|
+
* @returns Array of resolved usages, sorted by file path then line number.
|
|
73
|
+
*/
|
|
74
|
+
findUsages(symbol: string, options: FindUsagesOptions = {}): SymbolUsage[] {
|
|
75
|
+
const usages: SymbolUsage[] = [];
|
|
76
|
+
|
|
77
|
+
const sourceFiles = options.file
|
|
78
|
+
? [this.project.getSourceFile(options.file)].filter(Boolean)
|
|
79
|
+
: this.project.getSourceFiles();
|
|
80
|
+
|
|
81
|
+
for (const sourceFile of sourceFiles) {
|
|
82
|
+
if (!sourceFile) {
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
|
|
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
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return this.deduplicate(usages).sort((a, b) =>
|
|
123
|
+
a.file !== b.file ? a.file.localeCompare(b.file) : a.line - b.line,
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private findSymbolNodes(
|
|
128
|
+
sourceFile: ReturnType<Project['getSourceFile']>,
|
|
129
|
+
symbol: string,
|
|
130
|
+
) {
|
|
131
|
+
if (!sourceFile) {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const nodes: Node[] = [];
|
|
136
|
+
|
|
137
|
+
sourceFile.forEachDescendant((node) => {
|
|
138
|
+
if (
|
|
139
|
+
'getName' in node &&
|
|
140
|
+
typeof (node as { getName(): string }).getName === 'function'
|
|
141
|
+
) {
|
|
142
|
+
if ((node as { getName(): string }).getName() === symbol) {
|
|
143
|
+
nodes.push(node);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
return nodes;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private classifyUsage(node: Node): UsageType {
|
|
152
|
+
const parent = node.getParent();
|
|
153
|
+
if (!parent) {
|
|
154
|
+
return 'read';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const parentKindName = parent.getKindName();
|
|
158
|
+
|
|
159
|
+
// Call expression: svc.method() or method()
|
|
160
|
+
if (parentKindName === 'CallExpression') {
|
|
161
|
+
return 'call';
|
|
162
|
+
}
|
|
163
|
+
if (parentKindName === 'PropertyAccessExpression') {
|
|
164
|
+
const grandParent = parent.getParent();
|
|
165
|
+
if (grandParent?.getKindName() === 'CallExpression') {
|
|
166
|
+
return 'call';
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Assignment: x = value or x += value
|
|
171
|
+
if (
|
|
172
|
+
parentKindName === 'BinaryExpression' ||
|
|
173
|
+
parentKindName === 'VariableDeclaration' ||
|
|
174
|
+
parentKindName === 'PropertyAssignment'
|
|
175
|
+
) {
|
|
176
|
+
const parentText = parent.getText();
|
|
177
|
+
const nodeText = node.getText();
|
|
178
|
+
const nodePos = parentText.indexOf(nodeText);
|
|
179
|
+
if (nodePos === 0) {
|
|
180
|
+
return 'write';
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return 'read';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private deduplicate(usages: SymbolUsage[]): SymbolUsage[] {
|
|
188
|
+
const seen = new Set<string>();
|
|
189
|
+
return usages.filter((u) => {
|
|
190
|
+
const key = `${u.file}:${u.line}:${u.col}`;
|
|
191
|
+
if (seen.has(key)) {
|
|
192
|
+
return false;
|
|
193
|
+
}
|
|
194
|
+
seen.add(key);
|
|
195
|
+
return true;
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|