ts2famix 2.0.1 → 2.0.4

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.
@@ -1,7 +1,7 @@
1
1
  import * as Famix from "../lib/famix/model/famix";
2
2
  import { logger } from "../analyze";
3
3
  import { ConstructorDeclaration, Identifier, FunctionDeclaration, MethodDeclaration, MethodSignature, PropertyDeclaration, PropertySignature, VariableDeclaration, ParameterDeclaration, GetAccessorDeclaration, SetAccessorDeclaration, EnumMember, TypeAliasDeclaration, Node, SyntaxKind, FunctionExpression } from "ts-morph";
4
- import { TypeDeclaration } from "./EntityDictionary";
4
+ import { TSMorphTypeDeclaration } from "./EntityDictionary";
5
5
 
6
6
  interface SearchParameters {
7
7
  searchArray: string[];
@@ -87,23 +87,31 @@ export function findAncestor(node: Identifier): Node {
87
87
  * @param element A ts-morph element
88
88
  * @returns The ancestor of the ts-morph element
89
89
  */
90
- export function findTypeAncestor(element: TypeDeclaration): Node {
90
+ export function findTypeAncestor(element: Node): Node | undefined {
91
91
  let ancestor: Node | undefined;
92
- ancestor = element.getAncestors().find(a =>
93
- a.getKind() === SyntaxKind.MethodDeclaration ||
94
- a.getKind() === SyntaxKind.Constructor ||
95
- a.getKind() === SyntaxKind.MethodSignature ||
96
- a.getKind() === SyntaxKind.FunctionDeclaration ||
97
- a.getKind() === SyntaxKind.FunctionExpression ||
98
- a.getKind() === SyntaxKind.ModuleDeclaration ||
99
- a.getKind() === SyntaxKind.SourceFile ||
100
- a.getKindName() === "GetAccessor" ||
101
- a.getKindName() === "SetAccessor" ||
102
- a.getKind() === SyntaxKind.ClassDeclaration ||
103
- a.getKind() === SyntaxKind.InterfaceDeclaration);
92
+ const ancestors = element.getAncestors();
93
+ console.log(`Ancestors count: ${ancestors.length}`);
94
+
95
+ ancestor = ancestors.find(a => {
96
+ const kind = a.getKind();
97
+ const kindName = a.getKindName();
98
+ return kind === SyntaxKind.MethodDeclaration ||
99
+ kind === SyntaxKind.Constructor ||
100
+ kind === SyntaxKind.MethodSignature ||
101
+ kind === SyntaxKind.FunctionDeclaration ||
102
+ kind === SyntaxKind.FunctionExpression ||
103
+ kind === SyntaxKind.ModuleDeclaration ||
104
+ kind === SyntaxKind.SourceFile ||
105
+ kindName === "GetAccessor" ||
106
+ kindName === "SetAccessor" ||
107
+ kind === SyntaxKind.ClassDeclaration ||
108
+ kind === SyntaxKind.InterfaceDeclaration;
109
+ });
110
+
104
111
  if (!ancestor) {
105
112
  throw new Error(`Type ancestor not found for ${element.getKindName()}`);
106
- }
113
+ }
114
+
107
115
  return ancestor;
108
116
  }
109
117
 
package/src/fqn.ts CHANGED
@@ -1,71 +1,391 @@
1
- import { ArrowFunction, CallExpression, ClassDeclaration, ClassExpression, ConstructorDeclaration, Decorator, EnumDeclaration, FunctionDeclaration, FunctionExpression, GetAccessorDeclaration, Identifier, ImportDeclaration, ImportEqualsDeclaration, InterfaceDeclaration, MethodDeclaration, MethodSignature, ModuleDeclaration, Node, PropertyDeclaration, SetAccessorDeclaration, SourceFile, SyntaxKind, TypeParameterDeclaration, VariableDeclaration } from "ts-morph";
1
+ import { ArrowFunction, CallExpression, ClassDeclaration, ConstructorDeclaration, Decorator, EnumDeclaration, ExpressionWithTypeArguments, FunctionDeclaration, FunctionExpression, GetAccessorDeclaration, Identifier, ImportDeclaration, ImportEqualsDeclaration, InterfaceDeclaration, MethodDeclaration, MethodSignature, ModuleDeclaration, Node, PropertyDeclaration, SetAccessorDeclaration, SourceFile, SyntaxKind, TypeParameterDeclaration, VariableDeclaration } from "ts-morph";
2
2
  import { entityDictionary, logger } from "./analyze";
3
3
  import path from "path";
4
- import { TypeDeclaration } from "./famix_functions/EntityDictionary";
4
+ import { TSMorphTypeDeclaration } from "./famix_functions/EntityDictionary";
5
5
 
6
- type FQNNode = SourceFile | VariableDeclaration | ArrowFunction | Identifier | MethodDeclaration | MethodSignature | FunctionDeclaration | FunctionExpression | PropertyDeclaration | TypeDeclaration | EnumDeclaration | ImportDeclaration | ImportEqualsDeclaration | CallExpression | GetAccessorDeclaration | SetAccessorDeclaration | ConstructorDeclaration | TypeParameterDeclaration | ClassDeclaration | InterfaceDeclaration | Decorator | ModuleDeclaration;
6
+ type FQNNode = SourceFile | VariableDeclaration | ArrowFunction | Identifier | MethodDeclaration | MethodSignature | FunctionDeclaration | FunctionExpression | PropertyDeclaration | TSMorphTypeDeclaration | EnumDeclaration | ImportDeclaration | ImportEqualsDeclaration | CallExpression | GetAccessorDeclaration | SetAccessorDeclaration | ConstructorDeclaration | TypeParameterDeclaration | ClassDeclaration | InterfaceDeclaration | Decorator | ModuleDeclaration;
7
7
 
8
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
8
9
  function isFQNNode(node: Node): node is FQNNode {
9
10
  return Node.isVariableDeclaration(node) || Node.isArrowFunction(node) || Node.isIdentifier(node) || Node.isMethodDeclaration(node) || Node.isClassDeclaration(node) || Node.isClassExpression(node) || Node.isDecorator(node) || Node.isModuleDeclaration(node) || Node.isCallExpression(node);
11
+ }
12
+
13
+ /**
14
+ * Builds a map of method positions to their property keys in object literals.
15
+ * Scans all variable declarations in a source file, targeting object literals with any keys
16
+ * (e.g., `3: { method() {} }` or `add: { compute() {} }`), and maps each method's start position to its key.
17
+ * Logs each step for debugging.
18
+ *
19
+ * @param sourceFile The TypeScript source file to analyze
20
+ * @returns A Map where keys are method start positions and values are their property keys (e.g., "3", "add")
21
+ */
22
+ function buildStageMethodMap(sourceFile: SourceFile): Map<number, string> {
23
+ const stageMap = new Map<number, string>();
24
+
25
+ sourceFile.getVariableDeclarations().forEach(varDecl => {
26
+ // const varName = varDecl.getName();
27
+ const initializer = varDecl.getInitializer();
28
+
29
+ if (!initializer || !Node.isObjectLiteralExpression(initializer)) {
30
+ return;
31
+ }
32
+
33
+ initializer.getProperties().forEach(prop => {
34
+ let key: string | undefined;
35
+
36
+ if (Node.isPropertyAssignment(prop)) {
37
+ const nameNode = prop.getNameNode();
38
+
39
+ if (Node.isIdentifier(nameNode)) {
40
+ key = nameNode.getText();
41
+ } else if (Node.isStringLiteral(nameNode)) {
42
+ key = nameNode.getText().replace(/^"(.+)"$/, '$1').replace(/^'(.+)'$/, '$1');
43
+ } else if (Node.isNumericLiteral(nameNode)) {
44
+ key = nameNode.getText();
45
+ } else if (Node.isComputedPropertyName(nameNode)) {
46
+ const expression = nameNode.getExpression();
47
+ if (Node.isIdentifier(expression)) {
48
+ // Resolve variable value if possible
49
+ const symbol = expression.getSymbol();
50
+ if (symbol) {
51
+ const decl = symbol.getDeclarations()[0];
52
+ if (Node.isVariableDeclaration(decl) && decl.getInitializer()) {
53
+ const init = decl.getInitializer()!;
54
+ if (Node.isStringLiteral(init) || Node.isNumericLiteral(init)) {
55
+ key = init.getText().replace(/^"(.+)"$/, '$1').replace(/^'(.+)'$/, '$1');
56
+ }
57
+ }
58
+ }
59
+ if (!key) {
60
+ key = expression.getText();
61
+ }
62
+ } else if (Node.isBinaryExpression(expression) && expression.getOperatorToken().getText() === '+') {
63
+ // Handle simple string concatenation (e.g., "A" + "B")
64
+ const left = expression.getLeft();
65
+ const right = expression.getRight();
66
+ if (Node.isStringLiteral(left) && Node.isStringLiteral(right)) {
67
+ key = left.getLiteralText() + right.getLiteralText();
68
+ }
69
+ } else if (Node.isTemplateExpression(expression)) {
70
+ // Handle template literals (e.g., `key-${1}`)
71
+ const head = expression.getHead().getLiteralText();
72
+ const spans = expression.getTemplateSpans();
73
+ if (spans.length === 1 && Node.isNumericLiteral(spans[0].getExpression())) {
74
+ const num = spans[0].getExpression().getText();
75
+ key = `${head}${num}`;
76
+ }
77
+ }
78
+ if (!key) {
79
+ key = expression.getText(); // Fallback
80
+ }
81
+ } else {
82
+ return;
83
+ }
84
+
85
+ const propInitializer = prop.getInitializer();
86
+ if (propInitializer && Node.isObjectLiteralExpression(propInitializer)) {
87
+ propInitializer.getDescendantsOfKind(SyntaxKind.MethodDeclaration).forEach(method => {
88
+ // const methodName = method.getName();
89
+ const pos = method.getStart();
90
+ if (key) {
91
+ stageMap.set(pos, key);
92
+ }
93
+ });
94
+ }
95
+ }
96
+ });
97
+ });
10
98
 
99
+ return stageMap;
11
100
  }
12
101
 
13
- export function getFQN(node: FQNNode | Node): string {
14
- const absolutePathProject = entityDictionary.famixRep.getAbsolutePath();
102
+ /**
103
+ * Builds a map of method positions to their index in class/interface/namespace declarations
104
+ * @param sourceFile The TypeScript source file to analyze
105
+ * @returns A Map where keys are method start positions and values are their positional index (1-based)
106
+ */
107
+ function buildMethodPositionMap(sourceFile: SourceFile): Map<number, number> {
108
+ const positionMap = new Map<number, number>();
109
+ // console.log(`[buildMethodPositionMap] Starting analysis for file: ${sourceFile.getFilePath()}`);
110
+
111
+ // Helper function to process modules recursively
112
+ function processModule(moduleNode: ModuleDeclaration, modulePath: string) {
113
+ // console.log(`[buildMethodPositionMap] Processing module: ${modulePath}`);
114
+
115
+ // Handle functions directly in the module
116
+ const functions = moduleNode.getFunctions();
117
+ const functionCounts = new Map<string, number>();
118
+
119
+ functions.forEach(func => {
120
+ const funcName = func.getName() || `Unnamed_Function(${func.getStart()})`;
121
+ const count = (functionCounts.get(funcName) || 0) + 1;
122
+ functionCounts.set(funcName, count);
123
+ positionMap.set(func.getStart(), count);
124
+ // console.log(`[buildMethodPositionMap] Module function: ${funcName}, position: ${func.getStart()}, index: ${count}`);
125
+ });
126
+
127
+ // Handle classes within the module
128
+ const classes = moduleNode.getClasses();
129
+ classes.forEach(classNode => {
130
+ // console.log(`[buildMethodPositionMap] Processing class in module: ${classNode.getName() || 'Unnamed'}`);
131
+ const methods = classNode.getMethods();
132
+ const methodCounts = new Map<string, number>();
133
+
134
+ methods.forEach(method => {
135
+ const methodName = method.getName();
136
+ const count = (methodCounts.get(methodName) || 0) + 1;
137
+ methodCounts.set(methodName, count);
138
+ positionMap.set(method.getStart(), count);
139
+ // console.log(`[buildMethodPositionMap] Module class method: ${methodName}, position: ${method.getStart()}, index: ${count}`);
140
+ });
141
+ });
142
+
143
+ // Handle interfaces within the module
144
+ const interfaces = moduleNode.getInterfaces();
145
+ interfaces.forEach(interfaceNode => {
146
+ // console.log(`[buildMethodPositionMap] Processing interface in module: ${interfaceNode.getName() || 'Unnamed'}`);
147
+ const methods = interfaceNode.getMethods();
148
+ const methodCounts = new Map<string, number>();
149
+
150
+ methods.forEach(method => {
151
+ const methodName = method.getName();
152
+ const count = (methodCounts.get(methodName) || 0) + 1;
153
+ methodCounts.set(methodName, count);
154
+ positionMap.set(method.getStart(), count);
155
+ // console.log(`[buildMethodPositionMap] Module interface method: ${methodName}, position: ${method.getStart()}, index: ${count}`);
156
+ });
157
+ });
158
+
159
+ // Recursively process nested modules
160
+ const nestedModules = moduleNode.getModules();
161
+ nestedModules.forEach(nestedModule => {
162
+ if (Node.isModuleDeclaration(nestedModule)) {
163
+ const nestedModuleName = nestedModule.getName();
164
+ const newModulePath = `${modulePath}.${nestedModuleName}`;
165
+ processModule(nestedModule, newModulePath);
166
+ }
167
+ });
168
+
169
+ }
170
+
171
+ function trackArrowFunctions(container: Node) {
172
+ const arrows = container.getDescendantsOfKind(SyntaxKind.ArrowFunction);
173
+ arrows.forEach(arrow => {
174
+ const parent = arrow.getParent();
175
+ if (Node.isBlock(parent) || Node.isSourceFile(parent)) {
176
+ // Use negative numbers for arrow functions to distinguish from methods
177
+ positionMap.set(arrow.getStart(), -1 * (positionMap.size + 1));
178
+ // console.log(`[buildMethodPositionMap] Arrow function at ${arrow.getStart()}`);
179
+ }
180
+ });
181
+ }
182
+
183
+ // Handle top-level classes
184
+ sourceFile.getClasses().forEach(classNode => {
185
+ // console.log(`[buildMethodPositionMap] Processing class: ${classNode.getName() || 'Unnamed'}`);
186
+ const methods = classNode.getMethods();
187
+ const methodCounts = new Map<string, number>();
188
+
189
+ methods.forEach(method => {
190
+ const methodName = method.getName();
191
+ const count = (methodCounts.get(methodName) || 0) + 1;
192
+ methodCounts.set(methodName, count);
193
+ positionMap.set(method.getStart(), count);
194
+ // console.log(`[buildMethodPositionMap] Class method: ${methodName}, position: ${method.getStart()}, index: ${count}`);
195
+ });
196
+
197
+ methods.forEach(method => trackArrowFunctions(method));
198
+ });
199
+
200
+ // Handle top-level interfaces
201
+ sourceFile.getInterfaces().forEach(interfaceNode => {
202
+ // console.log(`[buildMethodPositionMap] Processing interface: ${interfaceNode.getName() || 'Unnamed'}`);
203
+ const methods = interfaceNode.getMethods();
204
+ const methodCounts = new Map<string, number>();
205
+
206
+ methods.forEach(method => {
207
+ const methodName = method.getName();
208
+ const count = (methodCounts.get(methodName) || 0) + 1;
209
+ methodCounts.set(methodName, count);
210
+ positionMap.set(method.getStart(), count);
211
+ // console.log(`[buildMethodPositionMap] Interface method: ${methodName}, position: ${method.getStart()}, index: ${count}`);
212
+ });
213
+ methods.forEach(method => trackArrowFunctions(method));
214
+
215
+ });
15
216
 
217
+ // Handle top-level namespaces/modules
218
+ sourceFile.getModules().forEach(moduleNode => {
219
+ if (Node.isModuleDeclaration(moduleNode)) {
220
+ const moduleName = moduleNode.getName();
221
+ processModule(moduleNode, moduleName);
222
+ }
223
+ });
224
+
225
+
226
+ // console.log(`[buildMethodPositionMap] Final positionMap:`, Array.from(positionMap.entries()));
227
+ return positionMap;
228
+ }
229
+
230
+ /**
231
+ * Generates a fully qualified name (FQN) for a given AST node.
232
+ * Constructs an FQN by traversing the node's ancestry, adding names and keys
233
+ * (numeric or string from object literals ...) as needed, prefixed with the file's relative path.
234
+ *
235
+ * @param node The AST node to generate an FQN for
236
+ * @returns A string representing the node's FQN (e.g., "{path}.operations.add.compute[MethodDeclaration]")
237
+ */
238
+ export function getFQN(node: FQNNode | Node): string {
16
239
  const sourceFile = node.getSourceFile();
17
- let parts: string[] = [];
240
+ const absolutePathProject = entityDictionary.famixRep.getAbsolutePath();
241
+ const parts: string[] = [];
18
242
  let currentNode: Node | undefined = node;
19
243
 
244
+ const stageMap = buildStageMethodMap(sourceFile);
245
+ const methodPositionMap = buildMethodPositionMap(sourceFile);
246
+
20
247
  while (currentNode && !Node.isSourceFile(currentNode)) {
21
248
  const { line, column } = sourceFile.getLineAndColumnAtPos(currentNode.getStart());
22
249
  const lc = `${line}:${column}`;
23
- if (Node.isClassDeclaration(currentNode) ||
24
- Node.isClassExpression(currentNode) ||
250
+
251
+ if (Node.isClassDeclaration(currentNode) ||
252
+ Node.isClassExpression(currentNode) ||
25
253
  Node.isInterfaceDeclaration(currentNode) ||
26
- Node.isFunctionDeclaration(currentNode) ||
27
- Node.isMethodDeclaration(currentNode) ||
28
- Node.isModuleDeclaration(currentNode) ||
29
- Node.isVariableDeclaration(currentNode) ||
254
+ Node.isFunctionDeclaration(currentNode) ||
255
+ Node.isMethodDeclaration(currentNode) ||
256
+ Node.isModuleDeclaration(currentNode) ||
257
+ Node.isVariableDeclaration(currentNode) ||
30
258
  Node.isGetAccessorDeclaration(currentNode) ||
31
259
  Node.isSetAccessorDeclaration(currentNode) ||
260
+ Node.isPropertyDeclaration(currentNode) ||
261
+ Node.isParameterDeclaration(currentNode) ||
262
+ Node.isDecorator(currentNode) ||
263
+ Node.isTypeAliasDeclaration(currentNode) ||
264
+ Node.isEnumDeclaration(currentNode) ||
265
+ Node.isEnumMember(currentNode) ||
266
+ Node.isParametered(currentNode) ||
267
+ Node.isPropertySignature(currentNode) ||
268
+ Node.isArrayLiteralExpression(currentNode) ||
269
+ Node.isImportSpecifier(currentNode) ||
32
270
  Node.isIdentifier(currentNode)) {
33
- let name = Node.isIdentifier(currentNode) ? currentNode.getText()
34
- : getNameOfNode(currentNode) /* currentNode.getName() */ || 'Unnamed_' + currentNode.getKindName() + `(${lc})`;
271
+ let name: string;
272
+ if (Node.isImportSpecifier(currentNode)) {
273
+ const alias = currentNode.getAliasNode()?.getText();
274
+ if (alias) {
275
+ let importDecl: Node | undefined = currentNode;
276
+ while (importDecl && !Node.isImportDeclaration(importDecl)) {
277
+ importDecl = importDecl.getParent();
278
+ }
279
+ const moduleSpecifier = importDecl && Node.isImportDeclaration(importDecl)
280
+ ? importDecl.getModuleSpecifier().getLiteralText()
281
+ : "unknown";
282
+ name = currentNode.getName();
283
+ name = `${name} as ${alias}[ImportSpecifier<${moduleSpecifier}>]`;
284
+ } else {
285
+ name = currentNode.getName();
286
+ }
287
+ } else {
288
+ // if constructor, use "constructor" as name
289
+ if (Node.isConstructorDeclaration(currentNode)) {
290
+ name = "constructor";
291
+ } else {
292
+ name = Node.isIdentifier(currentNode) ? currentNode.getText()
293
+ : 'getName' in currentNode && typeof currentNode['getName'] === 'function'
294
+ ? ((currentNode as { getName(): string }).getName() +
295
+ ((Node.isClassDeclaration(currentNode) ||
296
+ Node.isInterfaceDeclaration(currentNode) ||
297
+ Node.isMethodDeclaration(currentNode) ||
298
+ Node.isFunctionDeclaration(currentNode)) &&
299
+ 'getTypeParameters' in currentNode &&
300
+ currentNode.getTypeParameters().length > 0
301
+ ? getParameters(currentNode)
302
+ : ''))
303
+ : `Unnamed_${currentNode.getKindName()}(${lc})`;
304
+ }
305
+ }
306
+
307
+ if (Node.isMethodSignature(currentNode)) {
308
+ const method = currentNode as MethodSignature;
309
+ const params = method.getParameters().map(p => {
310
+ const typeText = p.getType().getText().replace(/\s+/g, "");
311
+ return typeText || "any"; // Fallback for untyped parameters
312
+ });
313
+ const returnType = method.getReturnType().getText().replace(/\s+/g, "") || "void";
314
+ name = `${name}(${params.join(",")}):${returnType}`;
315
+ }
316
+
317
+ parts.unshift(name);
318
+
319
+ // Apply positional index for MethodDeclaration, MethodSignature, and FunctionDeclaration
320
+ if (Node.isMethodDeclaration(currentNode) ||
321
+ Node.isMethodSignature(currentNode) ||
322
+ Node.isFunctionDeclaration(currentNode)) {
323
+ const key = stageMap.get(currentNode.getStart());
324
+ if (key) {
325
+ parts.unshift(key);
326
+ // console.log(`[getFQN] Applied stageMap key: ${key} for ${currentNode.getKindName()} at position ${currentNode.getStart()}`);
327
+ } else {
328
+ const positionIndex = methodPositionMap.get(currentNode.getStart());
329
+ if (positionIndex && positionIndex > 1) {
330
+ parts.unshift(positionIndex.toString());
331
+ // console.log(`[getFQN] Applied positionIndex: ${positionIndex} for ${currentNode.getKindName()} at position ${currentNode.getStart()}`);
332
+ } else {
333
+ console.log(`[getFQN] No positionIndex applied for ${currentNode.getKindName()} at position ${currentNode.getStart()}, positionIndex: ${positionIndex || 'none'}`);
334
+ }
335
+ }
336
+ }
337
+ }
338
+ else if (Node.isArrowFunction(currentNode) ||
339
+ Node.isBlock(currentNode) ||
340
+ Node.isForInStatement(currentNode) ||
341
+ Node.isForOfStatement(currentNode) ||
342
+ Node.isForStatement(currentNode) ||
343
+ Node.isCatchClause(currentNode)) {
344
+ const name = `${currentNode.getKindName()}(${lc})`;
345
+ parts.unshift(name);
346
+ }
347
+ else if (Node.isTypeParameterDeclaration(currentNode)) {
348
+ const arrowParent = currentNode.getFirstAncestorByKind(SyntaxKind.ArrowFunction);
349
+ if (arrowParent) {
350
+ const arrowIndex = Math.abs(methodPositionMap.get(arrowParent.getStart()) || 0);
351
+ if (arrowIndex > 0) {
352
+ parts.unshift(arrowIndex.toString());
353
+ }
354
+ }
355
+ parts.unshift(currentNode.getName());
356
+ // Removed continue to allow ancestor processing
357
+ }
358
+ else if (Node.isConstructorDeclaration(currentNode)) {
359
+ const name = "constructor";
35
360
  parts.unshift(name);
36
- } else if (Node.isArrowFunction(currentNode) ||
37
- Node.isBlock(currentNode) ||
38
- Node.isForInStatement(currentNode) ||
39
- Node.isForOfStatement(currentNode) ||
40
- Node.isForStatement(currentNode) ||
41
- Node.isCatchClause(currentNode)) {
42
- parts.unshift(`${currentNode.getKindName()}(${lc})`);
43
- } else if (Node.isConstructorDeclaration(currentNode)) {
44
- parts.unshift(`constructor`);
45
361
  } else {
46
- // For other kinds, you might want to handle them specifically or ignore
47
- //console.log(`Ignoring node kind: ${currentNode.getKindName()}`);
362
+ console.log(`[getFQN] Ignoring node kind: ${currentNode.getKindName()}`);
48
363
  }
364
+
49
365
  currentNode = currentNode.getParent();
50
366
  }
51
367
 
368
+ let relativePath = entityDictionary.convertToRelativePath(
369
+ path.normalize(sourceFile.getFilePath()),
370
+ absolutePathProject
371
+ ).replace(/\\/g, "/");
52
372
 
53
-
54
- // Prepend the relative path of the source file
55
- const relativePath = entityDictionary.convertToRelativePath(
56
- path.normalize(sourceFile.getFilePath()),
57
- absolutePathProject).replace(/\\/sg, "/");
373
+ // if (relativePath.includes("..")) {
374
+ // }
375
+ if (relativePath.startsWith("/")) {
376
+ relativePath = relativePath.slice(1);
377
+ }
58
378
  parts.unshift(`{${relativePath}}`);
59
- const fqn = parts.join(".") + `[${node.getKindName()}]`; // disambiguate
60
379
 
61
- logger.debug(fqn);
380
+ const fqn = parts.join(".") + `[${node.getKindName()}]`;
381
+ // console.log(`[getFQN] Final FQN: ${fqn}`);
62
382
  return fqn;
63
383
  }
64
384
 
65
385
 
66
386
  export function getUniqueFQN(node: Node): string | undefined {
67
387
  const absolutePathProject = entityDictionary.famixRep.getAbsolutePath();
68
- let parts: string[] = [];
388
+ const parts: string[] = [];
69
389
 
70
390
  if (node instanceof SourceFile) {
71
391
  return entityDictionary.convertToRelativePath(path.normalize(node.getFilePath()), absolutePathProject).replace(/\\/g, "/");
@@ -75,6 +395,9 @@ export function getUniqueFQN(node: Node): string | undefined {
75
395
  while (currentNode) {
76
396
  if (Node.isSourceFile(currentNode)) {
77
397
  const relativePath = entityDictionary.convertToRelativePath(path.normalize(currentNode.getFilePath()), absolutePathProject).replace(/\\/g, "/");
398
+ if (relativePath.includes("..")) {
399
+ logger.error(`Relative path contains ../: ${relativePath}`);
400
+ }
78
401
  parts.unshift(relativePath); // Add file path at the start
79
402
  break;
80
403
  } else if (currentNode.getSymbol()) {
@@ -95,15 +418,20 @@ export function getUniqueFQN(node: Node): string | undefined {
95
418
  * @returns The name of the node, or an empty string if it doesn't have one
96
419
  */
97
420
  export function getNameOfNode(a: Node): string {
421
+ let cKind: ClassDeclaration | undefined;
422
+ let iKind: InterfaceDeclaration | undefined;
423
+ let mKind: MethodDeclaration | undefined;
424
+ let fKind: FunctionDeclaration | undefined;
425
+ let alias: TSMorphTypeDeclaration | undefined;
98
426
  switch (a.getKind()) {
99
427
  case SyntaxKind.SourceFile:
100
428
  return a.asKind(SyntaxKind.SourceFile)!.getBaseName();
101
429
 
102
430
  case SyntaxKind.ModuleDeclaration:
103
- return a.asKind(SyntaxKind.ModuleDeclaration)!.getName();
431
+ return a.asKind(SyntaxKind.ModuleDeclaration)!.getName();
104
432
 
105
433
  case SyntaxKind.ClassDeclaration:
106
- const cKind = a.asKind(SyntaxKind.ClassDeclaration);
434
+ cKind = a.asKind(SyntaxKind.ClassDeclaration);
107
435
  if (cKind && cKind.getTypeParameters().length > 0) {
108
436
  return cKind.getName() + getParameters(a);
109
437
  } else {
@@ -111,21 +439,21 @@ export function getNameOfNode(a: Node): string {
111
439
  }
112
440
 
113
441
  case SyntaxKind.InterfaceDeclaration:
114
- const iKind = a.asKind(SyntaxKind.InterfaceDeclaration);
442
+ iKind = a.asKind(SyntaxKind.InterfaceDeclaration);
115
443
  if (iKind && iKind.getTypeParameters().length > 0) {
116
444
  return iKind.getName() + getParameters(a);
117
445
  } else {
118
446
  return iKind?.getName() || "";
119
447
  }
120
-
448
+
121
449
  case SyntaxKind.PropertyDeclaration:
122
- return a.asKind(SyntaxKind.PropertyDeclaration)!.getName();
450
+ return a.asKind(SyntaxKind.PropertyDeclaration)!.getName();
123
451
 
124
452
  case SyntaxKind.PropertySignature:
125
- return a.asKind(SyntaxKind.PropertySignature)!.getName();
126
-
453
+ return a.asKind(SyntaxKind.PropertySignature)!.getName();
454
+
127
455
  case SyntaxKind.MethodDeclaration:
128
- const mKind = a.asKind(SyntaxKind.MethodDeclaration);
456
+ mKind = a.asKind(SyntaxKind.MethodDeclaration);
129
457
  if (mKind && mKind.getTypeParameters().length > 0) {
130
458
  return mKind.getName() + getParameters(a);
131
459
  } else {
@@ -133,16 +461,16 @@ export function getNameOfNode(a: Node): string {
133
461
  }
134
462
 
135
463
  case SyntaxKind.MethodSignature:
136
- return a.asKind(SyntaxKind.MethodSignature)!.getName();
464
+ return a.asKind(SyntaxKind.MethodSignature)!.getName();
137
465
 
138
466
  case SyntaxKind.GetAccessor:
139
467
  return a.asKind(SyntaxKind.GetAccessor)!.getName();
140
468
 
141
469
  case SyntaxKind.SetAccessor:
142
470
  return a.asKind(SyntaxKind.SetAccessor)!.getName();
143
-
471
+
144
472
  case SyntaxKind.FunctionDeclaration:
145
- const fKind = a.asKind(SyntaxKind.FunctionDeclaration);
473
+ fKind = a.asKind(SyntaxKind.FunctionDeclaration);
146
474
  if (fKind && fKind.getTypeParameters().length > 0) {
147
475
  return fKind.getName() + getParameters(a);
148
476
  } else {
@@ -151,15 +479,15 @@ export function getNameOfNode(a: Node): string {
151
479
 
152
480
  case SyntaxKind.FunctionExpression:
153
481
  return a.asKind(SyntaxKind.FunctionExpression)?.getName() || "anonymous";
154
-
482
+
155
483
  case SyntaxKind.Parameter:
156
484
  return a.asKind(SyntaxKind.Parameter)!.getName();
157
-
485
+
158
486
  case SyntaxKind.VariableDeclaration:
159
487
  return a.asKind(SyntaxKind.VariableDeclaration)!.getName();
160
488
 
161
489
  case SyntaxKind.Decorator:
162
- return "@" + a.asKind(SyntaxKind.Decorator)!.getName();
490
+ return "@" + a.asKind(SyntaxKind.Decorator)!.getName();
163
491
 
164
492
  case SyntaxKind.TypeParameter:
165
493
  return a.asKind(SyntaxKind.TypeParameter)!.getName();
@@ -171,15 +499,21 @@ export function getNameOfNode(a: Node): string {
171
499
  return a.asKind(SyntaxKind.EnumMember)!.getName();
172
500
 
173
501
  case SyntaxKind.TypeAliasDeclaration:
502
+ // special case for parameterized types
503
+ alias = a.asKind(SyntaxKind.TypeAliasDeclaration);
504
+ if (alias && alias.getTypeParameters().length > 0) {
505
+ return alias.getName() + "<" + alias.getTypeParameters().map(tp => tp.getName()).join(", ") + ">";
506
+ }
174
507
  return a.asKind(SyntaxKind.TypeAliasDeclaration)!.getName();
175
508
 
176
509
  case SyntaxKind.Constructor:
177
- return "constructor";
178
-
510
+ return "constructor";
511
+
179
512
  default:
513
+ // throw new Error(`getNameOfNode called on a node that doesn't have a name: ${a.getKindName()}`);
180
514
  // ancestor hasn't got a useful name
181
515
  return "";
182
- }
516
+ }
183
517
  }
184
518
 
185
519
  /**
@@ -207,3 +541,69 @@ export function getParameters(a: Node): string {
207
541
  }
208
542
  return paramString;
209
543
  }
544
+
545
+ /**
546
+ * Gets the FQN of an unresolved interface that is being implemented or extended
547
+ * @param unresolvedInheritedClassOrInterface The expression with type arguments representing the interface
548
+ * @returns The FQN of the unresolved interface
549
+ */
550
+ export function getFQNUnresolvedInheritedClassOrInterface(unresolvedInheritedClassOrInterface: ExpressionWithTypeArguments): string {
551
+ // Check for either ClassDeclaration or InterfaceDeclaration ancestor
552
+ const classAncestor = unresolvedInheritedClassOrInterface.getFirstAncestorByKind(SyntaxKind.ClassDeclaration);
553
+ const interfaceAncestor = unresolvedInheritedClassOrInterface.getFirstAncestorByKind(SyntaxKind.InterfaceDeclaration);
554
+
555
+ // Validate the context
556
+ if (!classAncestor && !interfaceAncestor) {
557
+ throw new Error("getFQNUnresolvedClassOrInterface called on a node that is not in an implements or extends context");
558
+ }
559
+
560
+ // Check if it's a valid implements/extends context
561
+ let isValidContext = false;
562
+
563
+ let classExtendsClass = false;
564
+
565
+ if (classAncestor) {
566
+ // check if the class is extending or implementing an interface
567
+ const extendsClause = classAncestor.getExtends();
568
+ const implementsClause = classAncestor.getImplements();
569
+ isValidContext = (extendsClause !== undefined) || (implementsClause && implementsClause.length > 0);
570
+ classExtendsClass = extendsClause !== undefined;
571
+ } else if (interfaceAncestor) {
572
+ // Check extends clause for interfaces
573
+ const extendsClause = interfaceAncestor.getExtends();
574
+ isValidContext = extendsClause && extendsClause.length > 0;
575
+ }
576
+
577
+ if (!isValidContext) {
578
+ throw new Error("getFQNUnresolvedInterface called on a node that is not in a valid implements or extends context");
579
+ }
580
+
581
+ // get the name of the interface
582
+ const name = unresolvedInheritedClassOrInterface.getExpression().getText();
583
+
584
+ // Find where it's imported - search the entire source file
585
+ const sourceFile = unresolvedInheritedClassOrInterface.getSourceFile();
586
+ const importDecls = sourceFile.getImportDeclarations();
587
+
588
+ for (const importDecl of importDecls) {
589
+ const moduleSpecifier = importDecl.getModuleSpecifierValue();
590
+ const importClause = importDecl.getImportClause();
591
+
592
+ if (importClause) {
593
+ const namedImports = importClause.getNamedImports();
594
+ // declarationName is ClassDeclaration if "class extends class"
595
+ const declarationName = classExtendsClass ? "ClassDeclaration" : "InterfaceDeclaration";
596
+
597
+ for (const namedImport of namedImports) {
598
+ if (namedImport.getName() === name) {
599
+ logger.debug(`Found import for ${name} in ${moduleSpecifier}`);
600
+ return `{module:${moduleSpecifier}}.${name}[${declarationName}]`;
601
+ }
602
+ }
603
+ }
604
+ }
605
+
606
+ // If not found, return a default FQN format
607
+ return `{unknown-module}.${name}[InterfaceDeclaration]`;
608
+ }
609
+