ts2famix 1.3.0 → 1.4.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 (43) hide show
  1. package/dist/analyze.js +37 -27
  2. package/dist/analyze_functions/process_functions.js +723 -0
  3. package/dist/famix_functions/EntityDictionary.js +798 -0
  4. package/dist/famix_functions/helpers_creation.js +97 -0
  5. package/dist/fqn.js +106 -95
  6. package/dist/lib/famix/src/famix_base_element.js +8 -4
  7. package/dist/lib/famix/src/famix_repository.js +32 -15
  8. package/dist/lib/famix/src/model/famix/class.js +1 -1
  9. package/dist/lib/ts-complex/cyclomatic-service.js +2 -3
  10. package/doc-uml/metamodel-full.svg +1 -0
  11. package/doc-uml/metamodel.svg +1 -0
  12. package/package.json +4 -4
  13. package/plantuml.jar +0 -0
  14. package/src/analyze.ts +46 -29
  15. package/src/analyze_functions/process_functions.ts +838 -0
  16. package/src/famix_functions/EntityDictionary.ts +915 -0
  17. package/src/famix_functions/helpers_creation.ts +77 -0
  18. package/src/fqn.ts +101 -92
  19. package/src/generate_uml.sh +3 -0
  20. package/src/lib/famix/src/famix_base_element.ts +9 -5
  21. package/src/lib/famix/src/famix_repository.ts +45 -23
  22. package/src/lib/famix/src/model/famix/class.ts +1 -1
  23. package/src/lib/ts-complex/cyclomatic-service.ts +2 -4
  24. package/src/ts2famix-cli.ts +1 -0
  25. package/dist/analyze_functions/processAccesses.js +0 -56
  26. package/dist/analyze_functions/processFiles.js +0 -554
  27. package/dist/analyze_functions/processImportClauses.js +0 -88
  28. package/dist/analyze_functions/processInheritances.js +0 -74
  29. package/dist/analyze_functions/processInvocations.js +0 -50
  30. package/dist/famix_functions/famix_functions.js +0 -523
  31. package/dist/famix_functions/famix_functions_associations.js +0 -238
  32. package/dist/famix_functions/famix_functions_index.js +0 -135
  33. package/dist/famix_functions/famix_functions_types.js +0 -115
  34. package/docs/.gitkeep +0 -0
  35. package/src/analyze_functions/processAccesses.ts +0 -58
  36. package/src/analyze_functions/processFiles.ts +0 -667
  37. package/src/analyze_functions/processImportClauses.ts +0 -95
  38. package/src/analyze_functions/processInheritances.ts +0 -85
  39. package/src/analyze_functions/processInvocations.ts +0 -52
  40. package/src/famix_functions/famix_functions.ts +0 -562
  41. package/src/famix_functions/famix_functions_associations.ts +0 -242
  42. package/src/famix_functions/famix_functions_index.ts +0 -120
  43. package/src/famix_functions/famix_functions_types.ts +0 -106
@@ -0,0 +1,915 @@
1
+ import { ClassDeclaration, ConstructorDeclaration, FunctionDeclaration, Identifier, InterfaceDeclaration, MethodDeclaration, MethodSignature, ModuleDeclaration, PropertyDeclaration, PropertySignature, SourceFile, TypeParameterDeclaration, VariableDeclaration, ParameterDeclaration, Decorator, GetAccessorDeclaration, SetAccessorDeclaration, ImportSpecifier, CommentRange, EnumDeclaration, EnumMember, TypeAliasDeclaration, FunctionExpression, ExpressionWithTypeArguments, ImportDeclaration, ImportEqualsDeclaration } from "ts-morph";
2
+ import * as Famix from "../lib/famix/src/model/famix";
3
+ import { logger, config } from "../analyze";
4
+ import GraphemeSplitter from "grapheme-splitter";
5
+ import * as Helpers from "./helpers_creation";
6
+ import * as FQNFunctions from "../fqn";
7
+ import { FamixRepository } from "../lib/famix/src/famix_repository";
8
+ import path from "path";
9
+
10
+ export type TSMorphObjectType = ImportDeclaration | ImportEqualsDeclaration | SourceFile | ModuleDeclaration | ClassDeclaration | InterfaceDeclaration | MethodDeclaration | ConstructorDeclaration | MethodSignature | FunctionDeclaration | FunctionExpression | ParameterDeclaration | VariableDeclaration | PropertyDeclaration | PropertySignature | TypeParameterDeclaration | Identifier | Decorator | GetAccessorDeclaration | SetAccessorDeclaration | ImportSpecifier | CommentRange | EnumDeclaration | EnumMember | TypeAliasDeclaration | ExpressionWithTypeArguments;
11
+
12
+ export class EntityDictionary {
13
+
14
+ public famixRep = new FamixRepository();
15
+ private fmxAliasMap = new Map<string, Famix.Alias>(); // Maps the alias names to their Famix model
16
+ private fmxClassMap = new Map<string, Famix.Class | Famix.ParameterizableClass>(); // Maps the fully qualified class names to their Famix model
17
+ private fmxInterfaceMap = new Map<string, Famix.Interface | Famix.ParameterizableInterface>(); // Maps the interface names to their Famix model
18
+ private fmxNamespaceMap = new Map<string, Famix.Namespace>(); // Maps the namespace names to their Famix model
19
+ private fmxFileMap = new Map<string, Famix.ScriptEntity | Famix.Module>(); // Maps the source file names to their Famix model
20
+ private fmxTypeMap = new Map<string, Famix.Type | Famix.PrimitiveType | Famix.ParameterizedType>(); // Maps the type names to their Famix model
21
+ private UNKNOWN_VALUE = '(unknown due to parsing error)'; // The value to use when a name is not usable
22
+ public fmxElementObjectMap = new Map<Famix.Entity,TSMorphObjectType>();
23
+
24
+ constructor() {
25
+ this.famixRep.setFmxElementObjectMap(this.fmxElementObjectMap);
26
+
27
+ }
28
+
29
+ /**
30
+ * Makes a Famix index file anchor
31
+ * @param sourceElement A source element
32
+ * @param famixElement The Famix model of the source element
33
+ */
34
+ public makeFamixIndexFileAnchor(sourceElement: TSMorphObjectType, famixElement: Famix.SourcedEntity): void {
35
+ logger.debug("making index file anchor for '" + sourceElement?.getText() + "' with famixElement " + famixElement.getJSON());
36
+ const fmxIndexFileAnchor = new Famix.IndexedFileAnchor();
37
+ fmxIndexFileAnchor.setElement(famixElement);
38
+ this.fmxElementObjectMap.set(famixElement,sourceElement);
39
+
40
+ if (sourceElement !== null) {
41
+ const absolutePathProject = this.famixRep.getAbsolutePath();
42
+
43
+ const absolutePath = path.normalize(sourceElement.getSourceFile().getFilePath());
44
+
45
+ const positionNodeModules = absolutePath.indexOf('node_modules');
46
+
47
+ let pathInProject: string = "";
48
+
49
+ if (positionNodeModules !== -1) {
50
+ const pathFromNodeModules = absolutePath.substring(positionNodeModules);
51
+ pathInProject = pathFromNodeModules;
52
+ } else {
53
+ pathInProject = this.convertToRelativePath(absolutePath, absolutePathProject);
54
+ }
55
+
56
+ // revert any backslashes to forward slashes (path.normalize on windows introduces them)
57
+ pathInProject = pathInProject.replace(/\\/g, "/");
58
+
59
+ fmxIndexFileAnchor.setFileName(pathInProject);
60
+ let sourceStart, sourceEnd, sourceLineStart, sourceLineEnd: number;
61
+ if (!(sourceElement instanceof CommentRange)) {
62
+ sourceStart = sourceElement.getStart();
63
+ sourceEnd = sourceElement.getEnd();
64
+ sourceLineStart = sourceElement.getStartLineNumber();
65
+ sourceLineEnd = sourceElement.getEndLineNumber();
66
+ } else {
67
+ sourceStart = sourceElement.getPos();
68
+ sourceEnd = sourceElement.getEnd();
69
+ }
70
+ if (config.expectGraphemes) {
71
+ /**
72
+ * The following logic handles the case of multi-code point characters (e.g. emoji) in the source text.
73
+ * This is needed because Pharo/Smalltalk treats multi-code point characters as a single character,
74
+ * but JavaScript treats them as multiple characters. This means that the start and end positions
75
+ * of a source element in Pharo/Smalltalk will be different than the start and end positions of the
76
+ * same source element in JavaScript. This logic finds the start and end positions of the source
77
+ * element in JavaScript and then uses those positions to set the start and end positions of the
78
+ * Famix index file anchor.
79
+ * It depends on code in the 'grapheme-splitter' package in npm.
80
+ */
81
+ const splitter = new GraphemeSplitter();
82
+ const sourceFileText = sourceElement.getSourceFile().getFullText();
83
+ const hasGraphemeClusters = splitter.countGraphemes(sourceFileText) > 1;
84
+ if (hasGraphemeClusters) {
85
+ const sourceElementText = sourceFileText.substring(sourceStart, sourceEnd);
86
+ const sourceElementTextGraphemes = splitter.splitGraphemes(sourceElementText);
87
+ const sourceFileTextGraphemes = splitter.splitGraphemes(sourceFileText);
88
+ const numberOfGraphemeClustersBeforeStart = splitter.countGraphemes(sourceFileText.substring(0, sourceStart));
89
+
90
+ // find the start of the sourceElementTextGraphemes array in the sourceFileTextGraphemes array
91
+ sourceStart = Helpers.indexOfSplitArray({searchArray: sourceFileTextGraphemes,
92
+ targetArray: sourceElementTextGraphemes,
93
+ start: sourceStart - numberOfGraphemeClustersBeforeStart});
94
+ sourceEnd = sourceStart + sourceElementTextGraphemes.length;
95
+ }
96
+ }
97
+ // note: the +1 is because the source anchor is 1-based, but ts-morph is 0-based
98
+ fmxIndexFileAnchor.setStartPos(sourceStart + 1);
99
+ fmxIndexFileAnchor.setEndPos(sourceEnd + 1);
100
+ if (!(sourceElement instanceof CommentRange)) {
101
+ fmxIndexFileAnchor.setStartLine(sourceLineStart);
102
+ fmxIndexFileAnchor.setEndLine(sourceLineEnd);
103
+ }
104
+
105
+ if (!(famixElement instanceof Famix.Association) && !(famixElement instanceof Famix.Comment) && !(sourceElement instanceof CommentRange) && !(sourceElement instanceof Identifier) && !(sourceElement instanceof ImportSpecifier) && !(sourceElement instanceof ExpressionWithTypeArguments)) {
106
+ (famixElement as Famix.NamedEntity).setFullyQualifiedName(FQNFunctions.getFQN(sourceElement));
107
+ }
108
+ } else {
109
+ // sourceElement is null
110
+ logger.warn("sourceElement is null for famixElement " + famixElement.getJSON());
111
+ fmxIndexFileAnchor.setFileName("unknown");
112
+ fmxIndexFileAnchor.setStartPos(0);
113
+ fmxIndexFileAnchor.setEndPos(0);
114
+ }
115
+
116
+ this.famixRep.addElement(fmxIndexFileAnchor);
117
+ }
118
+
119
+ /**
120
+ * Creates or gets a Famix script entity or module
121
+ * @param f A source file
122
+ * @param isModule A boolean indicating if the source file is a module
123
+ * @returns The Famix model of the source file
124
+ */
125
+ public createOrGetFamixFile(f: SourceFile, isModule: boolean): Famix.ScriptEntity | Famix.Module {
126
+ let fmxFile: Famix.ScriptEntity | Famix.Module;
127
+
128
+ const fileName = f.getBaseName();
129
+ const fullyQualifiedFilename = f.getFilePath();
130
+ if (!this.fmxFileMap.has(fullyQualifiedFilename)) {
131
+ if (isModule) {
132
+ fmxFile = new Famix.Module();
133
+ }
134
+ else {
135
+ fmxFile = new Famix.ScriptEntity();
136
+ }
137
+ fmxFile.setName(fileName);
138
+ fmxFile.setNumberOfLinesOfText(f.getEndLineNumber() - f.getStartLineNumber());
139
+ fmxFile.setNumberOfCharacters(f.getFullText().length);
140
+
141
+ this.makeFamixIndexFileAnchor(f, fmxFile);
142
+
143
+ this.fmxFileMap.set(fullyQualifiedFilename, fmxFile);
144
+ this.famixRep.addElement(fmxFile);
145
+ }
146
+ else {
147
+ fmxFile = this.fmxFileMap.get(fullyQualifiedFilename);
148
+ }
149
+
150
+ this.fmxElementObjectMap.set(fmxFile,f);
151
+ return fmxFile;
152
+ }
153
+
154
+ /**
155
+ * Creates or gets a Famix namespace
156
+ * @param m A namespace
157
+ * @returns The Famix model of the namespace
158
+ */
159
+ public createOrGetFamixNamespace(m: ModuleDeclaration): Famix.Namespace {
160
+ let fmxNamespace: Famix.Namespace;
161
+ const namespaceName = m.getName();
162
+ if (!this.fmxNamespaceMap.has(namespaceName)) {
163
+ fmxNamespace = new Famix.Namespace();
164
+ fmxNamespace.setName(namespaceName);
165
+
166
+ this.makeFamixIndexFileAnchor(m, fmxNamespace);
167
+
168
+ this.fmxNamespaceMap.set(namespaceName, fmxNamespace);
169
+
170
+ this.famixRep.addElement(fmxNamespace);
171
+ }
172
+ else {
173
+ fmxNamespace = this.fmxNamespaceMap.get(namespaceName);
174
+ }
175
+
176
+ this.fmxElementObjectMap.set(fmxNamespace,m);
177
+ return fmxNamespace;
178
+ }
179
+
180
+ /**
181
+ * Creates a Famix alias
182
+ * @param a An alias
183
+ * @returns The Famix model of the alias
184
+ */
185
+ public createFamixAlias(a: TypeAliasDeclaration): Famix.Alias {
186
+ let fmxAlias: Famix.Alias;
187
+ const aliasName = a.getName();
188
+ const aliasFullyQualifiedName = a.getType().getText(); // FQNFunctions.getFQN(a);
189
+ if (!this.fmxAliasMap.has(aliasFullyQualifiedName)) {
190
+ fmxAlias = new Famix.Alias();
191
+ fmxAlias.setName(a.getName());
192
+ const aliasNameWithGenerics = aliasName + (a.getTypeParameters().length ? ("<" + a.getTypeParameters().map(tp => tp.getName()).join(", ") + ">") : "");
193
+ logger.debug(`> NOTE: alias ${aliasName} has fully qualified name ${aliasFullyQualifiedName} and name with generics ${aliasNameWithGenerics}.`);
194
+
195
+ const fmxType = this.createOrGetFamixType(aliasNameWithGenerics, a);
196
+ fmxAlias.setAliasedEntity(fmxType);
197
+
198
+ this.makeFamixIndexFileAnchor(a, fmxAlias);
199
+
200
+ this.fmxAliasMap.set(aliasFullyQualifiedName, fmxAlias);
201
+
202
+ this.famixRep.addElement(fmxAlias);
203
+ }
204
+ else {
205
+ fmxAlias = this.fmxAliasMap.get(aliasFullyQualifiedName);
206
+ }
207
+ this.fmxElementObjectMap.set(fmxAlias,a);
208
+
209
+ return fmxAlias;
210
+ }
211
+
212
+ /**
213
+ * Creates or gets a Famix class or parameterizable class
214
+ * @param cls A class
215
+ * @returns The Famix model of the class
216
+ */
217
+ public createOrGetFamixClass(cls: ClassDeclaration): Famix.Class | Famix.ParameterizableClass {
218
+ let fmxClass: Famix.Class | Famix.ParameterizableClass;
219
+ const isAbstract = cls.isAbstract();
220
+ const classFullyQualifiedName = FQNFunctions.getFQN(cls);
221
+ const clsName = cls.getName();
222
+ if (!this.fmxClassMap.has(classFullyQualifiedName)) {
223
+ const isGeneric = cls.getTypeParameters().length;
224
+ if (isGeneric) {
225
+ fmxClass = new Famix.ParameterizableClass();
226
+ }
227
+ else {
228
+ fmxClass = new Famix.Class();
229
+ }
230
+
231
+ fmxClass.setName(clsName);
232
+ fmxClass.setFullyQualifiedName(classFullyQualifiedName);
233
+ fmxClass.setIsAbstract(isAbstract);
234
+
235
+ this.makeFamixIndexFileAnchor(cls, fmxClass);
236
+
237
+ this.fmxClassMap.set(classFullyQualifiedName, fmxClass);
238
+
239
+ this.famixRep.addElement(fmxClass);
240
+
241
+ }
242
+ else {
243
+ fmxClass = this.fmxClassMap.get(classFullyQualifiedName) as (Famix.Class | Famix.ParameterizableClass);
244
+ }
245
+
246
+ this.fmxElementObjectMap.set(fmxClass,cls);
247
+
248
+ return fmxClass;
249
+ }
250
+
251
+ /**
252
+ * Creates or gets a Famix interface or parameterizable interface
253
+ * @param inter An interface
254
+ * @returns The Famix model of the interface
255
+ */
256
+ public createOrGetFamixInterface(inter: InterfaceDeclaration): Famix.Interface | Famix.ParameterizableInterface {
257
+ let fmxInterface: Famix.Interface | Famix.ParameterizableInterface;
258
+ const interName = inter.getName();
259
+ const interFullyQualifiedName = FQNFunctions.getFQN(inter);
260
+ if (!this.fmxInterfaceMap.has(interName)) {
261
+ const isGeneric = inter.getTypeParameters().length;
262
+ if (isGeneric) {
263
+ fmxInterface = new Famix.ParameterizableInterface();
264
+ }
265
+ else {
266
+ fmxInterface = new Famix.Interface();
267
+ }
268
+
269
+ fmxInterface.setName(interName);
270
+
271
+ this.makeFamixIndexFileAnchor(inter, fmxInterface);
272
+
273
+ this.fmxInterfaceMap.set(interFullyQualifiedName, fmxInterface);
274
+
275
+ this.famixRep.addElement(fmxInterface);
276
+ }
277
+ else {
278
+ fmxInterface = this.fmxInterfaceMap.get(interName) as (Famix.Interface | Famix.ParameterizableInterface);
279
+ }
280
+
281
+ this.fmxElementObjectMap.set(fmxInterface,inter);
282
+ return fmxInterface;
283
+ }
284
+
285
+ /**
286
+ * Creates a Famix property
287
+ * @param property A property
288
+ * @returns The Famix model of the property
289
+ */
290
+ public createFamixProperty(property: PropertyDeclaration | PropertySignature): Famix.Property {
291
+ const fmxProperty = new Famix.Property();
292
+ const isSignature = property instanceof PropertySignature;
293
+ fmxProperty.setName(property.getName());
294
+
295
+ let propTypeName = this.UNKNOWN_VALUE;
296
+ try {
297
+ propTypeName = property.getType().getText().trim();
298
+ } catch (error) {
299
+ logger.error(`> WARNING: got exception ${error}. Failed to get usable name for property: ${property.getName()}. Continuing...`);
300
+ }
301
+
302
+ const fmxType = this.createOrGetFamixType(propTypeName, property);
303
+ fmxProperty.setDeclaredType(fmxType);
304
+
305
+ property.getModifiers().forEach(m => fmxProperty.addModifier(m.getText()));
306
+ if (!isSignature && property.getExclamationTokenNode()) {
307
+ fmxProperty.addModifier("!");
308
+ }
309
+ if (property.getQuestionTokenNode()) {
310
+ fmxProperty.addModifier("?");
311
+ }
312
+ if (property.getName().substring(0, 1) === "#") {
313
+ fmxProperty.addModifier("#");
314
+ }
315
+
316
+ if (fmxProperty.getModifiers().has("static")) {
317
+ fmxProperty.setIsClassSide(true);
318
+ }
319
+ else {
320
+ fmxProperty.setIsClassSide(false);
321
+ }
322
+
323
+ this.makeFamixIndexFileAnchor(property, fmxProperty);
324
+
325
+ this.famixRep.addElement(fmxProperty);
326
+
327
+ this.fmxElementObjectMap.set(fmxProperty,property);
328
+
329
+ return fmxProperty;
330
+ }
331
+
332
+ /**
333
+ * Creates a Famix method or accessor
334
+ * @param method A method or an accessor
335
+ * @param currentCC The cyclomatic complexity metrics of the current source file
336
+ * @returns The Famix model of the method or the accessor
337
+ */
338
+ public createFamixMethod(method: MethodDeclaration | ConstructorDeclaration | MethodSignature | GetAccessorDeclaration | SetAccessorDeclaration, currentCC: unknown): Famix.Method | Famix.Accessor {
339
+ let fmxMethod: Famix.Method | Famix.Accessor;
340
+ if (method instanceof GetAccessorDeclaration || method instanceof SetAccessorDeclaration) {
341
+ fmxMethod = new Famix.Accessor();
342
+ const isGetter = method instanceof GetAccessorDeclaration;
343
+ const isSetter = method instanceof SetAccessorDeclaration;
344
+ if (isGetter) {(fmxMethod as Famix.Accessor).setKind("getter");}
345
+ if (isSetter) {(fmxMethod as Famix.Accessor).setKind("setter");}
346
+ this.famixRep.addElement(fmxMethod);
347
+ }
348
+ else {
349
+ fmxMethod = new Famix.Method();
350
+ this.famixRep.addElement(fmxMethod);
351
+ }
352
+ const isConstructor = method instanceof ConstructorDeclaration;
353
+ const isSignature = method instanceof MethodSignature;
354
+ const isGeneric = method.getTypeParameters().length > 0;
355
+ fmxMethod.setIsGeneric(isGeneric);
356
+
357
+ let isAbstract = false;
358
+ let isStatic = false;
359
+ if (method instanceof MethodDeclaration || method instanceof GetAccessorDeclaration || method instanceof SetAccessorDeclaration) {
360
+ isAbstract = method.isAbstract();
361
+ isStatic = method.isStatic();
362
+ }
363
+
364
+ if (isConstructor) {(fmxMethod as Famix.Accessor).setKind("constructor");}
365
+ fmxMethod.setIsAbstract(isAbstract);
366
+ fmxMethod.setIsClassSide(isStatic);
367
+ fmxMethod.setIsPrivate((method instanceof MethodDeclaration || method instanceof GetAccessorDeclaration || method instanceof SetAccessorDeclaration) ? (method.getModifiers().find(x => x.getText() === 'private')) !== undefined : false);
368
+ fmxMethod.setIsProtected((method instanceof MethodDeclaration || method instanceof GetAccessorDeclaration || method instanceof SetAccessorDeclaration) ? (method.getModifiers().find(x => x.getText() === 'protected')) !== undefined : false);
369
+ fmxMethod.setSignature(Helpers.computeSignature(method.getText()));
370
+
371
+ let methodName: string;
372
+ if (isConstructor) {
373
+ methodName = "constructor";
374
+ }
375
+ else {
376
+ methodName = (method as MethodDeclaration | MethodSignature | GetAccessorDeclaration | SetAccessorDeclaration).getName();
377
+ }
378
+ fmxMethod.setName(methodName);
379
+
380
+ if (!isConstructor) {
381
+ if (method.getName().substring(0, 1) === "#") {
382
+ fmxMethod.setIsPrivate(true);
383
+ }
384
+ }
385
+
386
+ if (!fmxMethod.getIsPrivate() && !fmxMethod.getIsProtected()) {
387
+ fmxMethod.setIsPublic(true);
388
+ }
389
+ else {
390
+ fmxMethod.setIsPublic(false);
391
+ }
392
+
393
+ if (!isSignature) {
394
+ fmxMethod.setCyclomaticComplexity(currentCC[fmxMethod.getName()]);
395
+ }
396
+ else {
397
+ fmxMethod.setCyclomaticComplexity(0);
398
+ }
399
+
400
+ let methodTypeName = this.UNKNOWN_VALUE;
401
+ try {
402
+ methodTypeName = method.getReturnType().getText().trim();
403
+ } catch (error) {
404
+ logger.error(`> WARNING: got exception ${error}. Failed to get usable name for return type of method: ${fmxMethod.getName()}. Continuing...`);
405
+ }
406
+
407
+ const fmxType = this.createOrGetFamixType(methodTypeName, method);
408
+ fmxMethod.setDeclaredType(fmxType);
409
+ fmxMethod.setNumberOfLinesOfCode(method.getEndLineNumber() - method.getStartLineNumber());
410
+ const parameters = method.getParameters();
411
+ fmxMethod.setNumberOfParameters(parameters.length);
412
+
413
+ if (!isSignature) {
414
+ fmxMethod.setNumberOfStatements(method.getStatements().length);
415
+ }
416
+ else {
417
+ fmxMethod.setNumberOfStatements(0);
418
+ }
419
+
420
+ this.makeFamixIndexFileAnchor(method, fmxMethod);
421
+
422
+ this.fmxElementObjectMap.set(fmxMethod,method);
423
+
424
+ return fmxMethod;
425
+ }
426
+
427
+ /**
428
+ * Creates a Famix function
429
+ * @param func A function
430
+ * @param currentCC The cyclomatic complexity metrics of the current source file
431
+ * @returns The Famix model of the function
432
+ */
433
+ public createFamixFunction(func: FunctionDeclaration | FunctionExpression, currentCC: unknown): Famix.Function {
434
+ const fmxFunction = new Famix.Function();
435
+ if (func.getName()) {
436
+ fmxFunction.setName(func.getName());
437
+ }
438
+ else {
439
+ fmxFunction.setName("anonymous");
440
+ }
441
+ fmxFunction.setSignature(Helpers.computeSignature(func.getText()));
442
+ fmxFunction.setCyclomaticComplexity(currentCC[fmxFunction.getName()]);
443
+ const isGeneric = func.getTypeParameters().length > 0;
444
+ fmxFunction.setIsGeneric(isGeneric);
445
+
446
+ let functionTypeName = this.UNKNOWN_VALUE;
447
+ try {
448
+ functionTypeName = func.getReturnType().getText().trim();
449
+ } catch (error) {
450
+ logger.error(`> WARNING: got exception ${error}. Failed to get usable name for return type of function: ${func.getName()}. Continuing...`);
451
+ }
452
+
453
+ const fmxType = this.createOrGetFamixType(functionTypeName, func);
454
+ fmxFunction.setDeclaredType(fmxType);
455
+ fmxFunction.setNumberOfLinesOfCode(func.getEndLineNumber() - func.getStartLineNumber());
456
+ const parameters = func.getParameters();
457
+ fmxFunction.setNumberOfParameters(parameters.length);
458
+ fmxFunction.setNumberOfStatements(func.getStatements().length);
459
+
460
+ this.makeFamixIndexFileAnchor(func, fmxFunction);
461
+
462
+ this.famixRep.addElement(fmxFunction);
463
+
464
+ this.fmxElementObjectMap.set(fmxFunction,func);
465
+
466
+ return fmxFunction;
467
+ }
468
+
469
+ /**
470
+ * Creates a Famix parameter
471
+ * @param param A parameter
472
+ * @returns The Famix model of the parameter
473
+ */
474
+ public createFamixParameter(param: ParameterDeclaration): Famix.Parameter {
475
+ const fmxParam = new Famix.Parameter();
476
+
477
+ let paramTypeName = this.UNKNOWN_VALUE;
478
+ try {
479
+ paramTypeName = param.getType().getText().trim();
480
+ } catch (error) {
481
+ logger.error(`> WARNING: got exception ${error}. Failed to get usable name for parameter: ${param.getName()}. Continuing...`);
482
+ }
483
+
484
+ const fmxType = this.createOrGetFamixType(paramTypeName, param);
485
+ fmxParam.setDeclaredType(fmxType);
486
+ fmxParam.setName(param.getName());
487
+
488
+ this.makeFamixIndexFileAnchor(param, fmxParam);
489
+
490
+ this.famixRep.addElement(fmxParam);
491
+
492
+ this.fmxElementObjectMap.set(fmxParam,param);
493
+
494
+ return fmxParam;
495
+ }
496
+
497
+ /**
498
+ * Creates a Famix type parameter
499
+ * @param tp A type parameter
500
+ * @returns The Famix model of the type parameter
501
+ */
502
+ public createFamixParameterType(tp: TypeParameterDeclaration): Famix.ParameterType {
503
+ const fmxParameterType = new Famix.ParameterType();
504
+ fmxParameterType.setName(tp.getName());
505
+
506
+ this.makeFamixIndexFileAnchor(tp, fmxParameterType);
507
+
508
+ this.famixRep.addElement(fmxParameterType);
509
+
510
+ this.fmxElementObjectMap.set(fmxParameterType,tp);
511
+
512
+ return fmxParameterType;
513
+ }
514
+
515
+ /**
516
+ * Creates a Famix variable
517
+ * @param variable A variable
518
+ * @returns The Famix model of the variable
519
+ */
520
+ public createFamixVariable(variable: VariableDeclaration): Famix.Variable {
521
+ const fmxVariable = new Famix.Variable();
522
+
523
+ let variableTypeName = this.UNKNOWN_VALUE;
524
+ try {
525
+ variableTypeName = variable.getType().getText().trim();
526
+ } catch (error) {
527
+ logger.error(`> WARNING: got exception ${error}. Failed to get usable name for variable: ${variable.getName()}. Continuing...`);
528
+ }
529
+
530
+ const fmxType = this.createOrGetFamixType(variableTypeName, variable);
531
+ fmxVariable.setDeclaredType(fmxType);
532
+ fmxVariable.setName(variable.getName());
533
+
534
+ this.makeFamixIndexFileAnchor(variable, fmxVariable);
535
+
536
+ this.famixRep.addElement(fmxVariable);
537
+
538
+ this.fmxElementObjectMap.set(fmxVariable,variable);
539
+
540
+ return fmxVariable;
541
+ }
542
+
543
+ /**
544
+ * Creates a Famix enum
545
+ * @param enumEntity An enum
546
+ * @returns The Famix model of the enum
547
+ */
548
+ public createFamixEnum(enumEntity: EnumDeclaration): Famix.Enum {
549
+ const fmxEnum = new Famix.Enum();
550
+ fmxEnum.setName(enumEntity.getName());
551
+
552
+ this.makeFamixIndexFileAnchor(enumEntity, fmxEnum);
553
+
554
+ this.famixRep.addElement(fmxEnum);
555
+
556
+ this.fmxElementObjectMap.set(fmxEnum,enumEntity);
557
+
558
+ return fmxEnum;
559
+ }
560
+
561
+ /**
562
+ * Creates a Famix enum value
563
+ * @param enumMember An enum member
564
+ * @returns The Famix model of the enum member
565
+ */
566
+ public createFamixEnumValue(enumMember: EnumMember): Famix.EnumValue {
567
+ const fmxEnumValue = new Famix.EnumValue();
568
+
569
+ let enumValueTypeName = this.UNKNOWN_VALUE;
570
+ try {
571
+ enumValueTypeName = enumMember.getType().getText().trim();
572
+ } catch (error) {
573
+ logger.error(`> WARNING: got exception ${error}. Failed to get usable name for enum value: ${enumMember.getName()}. Continuing...`);
574
+ }
575
+
576
+ const fmxType = this.createOrGetFamixType(enumValueTypeName, enumMember);
577
+ fmxEnumValue.setDeclaredType(fmxType);
578
+ fmxEnumValue.setName(enumMember.getName());
579
+
580
+ this.makeFamixIndexFileAnchor(enumMember, fmxEnumValue);
581
+
582
+ this.famixRep.addElement(fmxEnumValue);
583
+
584
+ this.fmxElementObjectMap.set(fmxEnumValue,enumMember);
585
+
586
+ return fmxEnumValue;
587
+ }
588
+
589
+ /**
590
+ * Creates or gets a Famix decorator
591
+ * @param decorator A decorator
592
+ * @param decoratedEntity A class, a method, a parameter or a property
593
+ * @returns The Famix model of the decorator
594
+ */
595
+ public createOrGetFamixDecorator(decorator: Decorator, decoratedEntity: ClassDeclaration | MethodDeclaration | GetAccessorDeclaration | SetAccessorDeclaration | ParameterDeclaration | PropertyDeclaration): Famix.Decorator {
596
+ const fmxDecorator = new Famix.Decorator();
597
+ const decoratorName = "@" + decorator.getName();
598
+ const decoratorExpression = decorator.getText().substring(1);
599
+
600
+ fmxDecorator.setName(decoratorName);
601
+ fmxDecorator.setDecoratorExpression(decoratorExpression);
602
+ const decoratedEntityFullyQualifiedName = FQNFunctions.getFQN(decoratedEntity);
603
+ const fmxDecoratedEntity = this.famixRep.getFamixEntityByFullyQualifiedName(decoratedEntityFullyQualifiedName) as Famix.NamedEntity;
604
+ fmxDecorator.setDecoratedEntity(fmxDecoratedEntity);
605
+
606
+ this.makeFamixIndexFileAnchor(decorator, fmxDecorator);
607
+
608
+ this.famixRep.addElement(fmxDecorator);
609
+
610
+ this.fmxElementObjectMap.set(fmxDecorator,decorator);
611
+
612
+ return fmxDecorator;
613
+ }
614
+
615
+ /**
616
+ * Creates a Famix comment
617
+ * @param comment A comment
618
+ * @param fmxScope The Famix model of the comment's container
619
+ * @param isJSDoc A boolean indicating if the comment is a JSDoc
620
+ * @returns The Famix model of the comment
621
+ */
622
+ public createFamixComment(comment: CommentRange, fmxScope: Famix.NamedEntity, isJSDoc: boolean): Famix.Comment {
623
+ logger.debug(`> NOTE: creating comment ${comment.getText()} in scope ${fmxScope.getName()}.`);
624
+ const fmxComment = new Famix.Comment();
625
+ fmxComment.setContainer(fmxScope); // adds comment to the container's comments collection
626
+ fmxComment.setIsJSDoc(isJSDoc);
627
+
628
+ this.makeFamixIndexFileAnchor(comment, fmxComment);
629
+
630
+ this.famixRep.addElement(fmxComment);
631
+
632
+ this.fmxElementObjectMap.set(fmxComment,comment);
633
+
634
+ return fmxComment;
635
+ }
636
+
637
+ /**
638
+ * Creates or gets a Famix type
639
+ * @param typeName A type name
640
+ * @param element A ts-morph element
641
+ * @returns The Famix model of the type
642
+ */
643
+ public createOrGetFamixType(typeName: string, element: TypeAliasDeclaration | PropertyDeclaration | PropertySignature | MethodDeclaration | ConstructorDeclaration | MethodSignature | GetAccessorDeclaration | SetAccessorDeclaration | FunctionDeclaration | FunctionExpression | ParameterDeclaration | VariableDeclaration | EnumMember): Famix.Type | Famix.PrimitiveType | Famix.ParameterizedType {
644
+ let fmxType: Famix.Type | Famix.PrimitiveType | Famix.ParameterizedType;
645
+ let isPrimitiveType = false;
646
+ let isParameterizedType = false;
647
+
648
+ logger.debug("Creating (or getting) type: '" + typeName + "' of element: " + element?.getText() + " of kind: " + element?.getKindName());
649
+ let ancestor: Famix.ContainerEntity;
650
+ if (element !== undefined) {
651
+ const typeAncestor = Helpers.findTypeAncestor(element);
652
+ const ancestorFullyQualifiedName = FQNFunctions.getFQN(typeAncestor);
653
+ ancestor = this.famixRep.getFamixEntityByFullyQualifiedName(ancestorFullyQualifiedName) as Famix.ContainerEntity;
654
+ if (!ancestor) {
655
+ throw new Error(`Ancestor ${ancestorFullyQualifiedName} not found.`);
656
+ }
657
+ }
658
+
659
+ if (typeName === "number" || typeName === "string" || typeName === "boolean" || typeName === "bigint" || typeName === "symbol" || typeName === "undefined" || typeName === "null" || typeName === "any" || typeName === "unknown" || typeName === "never" || typeName === "void") {
660
+ isPrimitiveType = true;
661
+ }
662
+
663
+ if(!isPrimitiveType && typeName.includes("<") && typeName.includes(">") && !(typeName.includes("=>"))) {
664
+ isParameterizedType = true;
665
+ }
666
+
667
+ if (!this.fmxTypeMap.has(typeName)) {
668
+ if (isPrimitiveType) {
669
+ fmxType = new Famix.PrimitiveType();
670
+ fmxType.setIsStub(true);
671
+ }
672
+ else if (isParameterizedType) {
673
+ fmxType = new Famix.ParameterizedType();
674
+ const parameterTypeNames = typeName.substring(typeName.indexOf("<") + 1, typeName.indexOf(">")).split(",").map(s => s.trim());
675
+ const baseTypeName = typeName.substring(0, typeName.indexOf("<")).trim();
676
+ parameterTypeNames.forEach(parameterTypeName => {
677
+ const fmxParameterType = this.createOrGetFamixType(parameterTypeName, element);
678
+ (fmxType as Famix.ParameterizedType).addArgument(fmxParameterType);
679
+ });
680
+ const fmxBaseType = this.createOrGetFamixType(baseTypeName, element);
681
+ (fmxType as Famix.ParameterizedType).setBaseType(fmxBaseType);
682
+ }
683
+ else {
684
+ fmxType = new Famix.Type();
685
+ }
686
+
687
+ fmxType.setName(typeName);
688
+ fmxType.setContainer(ancestor);
689
+
690
+ this.makeFamixIndexFileAnchor(element, fmxType);
691
+
692
+ this.famixRep.addElement(fmxType);
693
+
694
+ this.fmxTypeMap.set(typeName, fmxType);
695
+ }
696
+ else {
697
+ fmxType = this.fmxTypeMap.get(typeName);
698
+ }
699
+
700
+ this.fmxElementObjectMap.set(fmxType,element);
701
+
702
+ return fmxType;
703
+ }
704
+
705
+ /**
706
+ * Creates a Famix access
707
+ * @param node A node
708
+ * @param id An id of a parameter, a variable, a property or an enum member
709
+ */
710
+ public createFamixAccess(node: Identifier, id: number): void {
711
+ const fmxVar = this.famixRep.getFamixEntityById(id) as Famix.StructuralEntity;
712
+ const nodeReferenceAncestor = Helpers.findAncestor(node);
713
+ const ancestorFullyQualifiedName = FQNFunctions.getFQN(nodeReferenceAncestor);
714
+ const accessor = this.famixRep.getFamixEntityByFullyQualifiedName(ancestorFullyQualifiedName) as Famix.ContainerEntity;
715
+
716
+ const fmxAccess = new Famix.Access();
717
+ fmxAccess.setAccessor(accessor);
718
+ fmxAccess.setVariable(fmxVar);
719
+
720
+ this.makeFamixIndexFileAnchor(node, fmxAccess);
721
+
722
+ this.famixRep.addElement(fmxAccess);
723
+
724
+ this.fmxElementObjectMap.set(fmxAccess,node);
725
+ }
726
+
727
+ /**
728
+ * Creates a Famix invocation
729
+ * @param node A node
730
+ * @param m A method or a function
731
+ * @param id The id of the method or the function
732
+ */
733
+ public createFamixInvocation(node: Identifier, m: MethodDeclaration | ConstructorDeclaration | GetAccessorDeclaration | SetAccessorDeclaration | FunctionDeclaration | FunctionExpression, id: number): void {
734
+ const fmxMethodOrFunction = this.famixRep.getFamixEntityById(id) as Famix.BehavioralEntity;
735
+ const nodeReferenceAncestor = Helpers.findAncestor(node);
736
+ const ancestorFullyQualifiedName = FQNFunctions.getFQN(nodeReferenceAncestor);
737
+ const sender = this.famixRep.getFamixEntityByFullyQualifiedName(ancestorFullyQualifiedName) as Famix.ContainerEntity;
738
+ const receiverFullyQualifiedName = FQNFunctions.getFQN(m.getParent());
739
+ const receiver = this.famixRep.getFamixEntityByFullyQualifiedName(receiverFullyQualifiedName) as Famix.NamedEntity;
740
+
741
+ const fmxInvocation = new Famix.Invocation();
742
+ fmxInvocation.setSender(sender);
743
+ fmxInvocation.setReceiver(receiver);
744
+ fmxInvocation.addCandidate(fmxMethodOrFunction);
745
+ fmxInvocation.setSignature(fmxMethodOrFunction.getSignature());
746
+
747
+ this.makeFamixIndexFileAnchor(node, fmxInvocation);
748
+
749
+ this.famixRep.addElement(fmxInvocation);
750
+
751
+ this.fmxElementObjectMap.set(fmxInvocation,node);
752
+ }
753
+
754
+ /**
755
+ * Creates a Famix inheritance
756
+ * @param cls A class or an interface (subclass)
757
+ * @param inhClass The inherited class or interface (superclass)
758
+ */
759
+ public createFamixInheritance(cls: ClassDeclaration | InterfaceDeclaration, inhClass: ClassDeclaration | InterfaceDeclaration | ExpressionWithTypeArguments): void {
760
+ const fmxInheritance = new Famix.Inheritance();
761
+ // const clsName = cls.getName();
762
+ const classFullyQualifiedName = FQNFunctions.getFQN(cls);
763
+ logger.debug(`createFamixInheritance: classFullyQualifiedName: class fqn = ${classFullyQualifiedName}`);
764
+ let subClass: Famix.Class | Famix.Interface;
765
+ if (cls instanceof ClassDeclaration) {
766
+ subClass = this.fmxClassMap.get(classFullyQualifiedName);
767
+ }
768
+ else {
769
+ subClass = this.fmxInterfaceMap.get(classFullyQualifiedName);
770
+ }
771
+
772
+ let inhClassName: string;
773
+ let inhClassFullyQualifiedName: string;
774
+ let superClass: Famix.Class | Famix.Interface;
775
+ if (inhClass instanceof ClassDeclaration || inhClass instanceof InterfaceDeclaration) {
776
+ inhClassName = inhClass.getName();
777
+ inhClassFullyQualifiedName = FQNFunctions.getFQN(inhClass);
778
+ if (inhClass instanceof ClassDeclaration) {
779
+ superClass = this.fmxClassMap.get(inhClassFullyQualifiedName);
780
+ }
781
+ else {
782
+ superClass = this.fmxInterfaceMap.get(inhClassFullyQualifiedName);
783
+ }
784
+ }
785
+ else {
786
+ // inhClass is an ExpressionWithTypeArguments
787
+ inhClassName = inhClass.getExpression().getText();
788
+ // what is inhClassFullyQualifiedName? TODO
789
+ inhClassFullyQualifiedName = 'Undefined_Scope_from_importer.' + inhClassName;
790
+ }
791
+
792
+ if (superClass === undefined) {
793
+ if (inhClass instanceof ClassDeclaration) {
794
+ superClass = new Famix.Class();
795
+ this.fmxClassMap.set(inhClassFullyQualifiedName, superClass);
796
+ }
797
+ else {
798
+ superClass = new Famix.Interface();
799
+ this.fmxInterfaceMap.set(inhClassFullyQualifiedName, superClass);
800
+ }
801
+
802
+ this.fmxElementObjectMap.set(superClass,inhClass);
803
+
804
+ superClass.setName(inhClassName);
805
+ superClass.setFullyQualifiedName(inhClassFullyQualifiedName);
806
+ superClass.setIsStub(true);
807
+
808
+ this.makeFamixIndexFileAnchor(inhClass, superClass);
809
+
810
+ this.famixRep.addElement(superClass);
811
+ }
812
+
813
+ fmxInheritance.setSubclass(subClass);
814
+ fmxInheritance.setSuperclass(superClass);
815
+
816
+ this.makeFamixIndexFileAnchor(null, fmxInheritance);
817
+
818
+ this.famixRep.addElement(fmxInheritance);
819
+
820
+ this.fmxElementObjectMap.set(fmxInheritance,null);
821
+
822
+ }
823
+
824
+ /**
825
+ * Creates a Famix import clause
826
+ * @param importClauseInfo The information needed to create a Famix import clause
827
+ * @param importDeclaration The import declaration
828
+ * @param importer A source file which is a module
829
+ * @param moduleSpecifierFilePath The path of the module where the export declaration is
830
+ * @param importElement The imported entity
831
+ * @param isInExports A boolean indicating if the imported entity is in the exports
832
+ * @param isDefaultExport A boolean indicating if the imported entity is a default export
833
+ */
834
+ public createFamixImportClause(importClauseInfo: {importDeclaration?: ImportDeclaration | ImportEqualsDeclaration, importer: SourceFile, moduleSpecifierFilePath: string, importElement: ImportSpecifier | Identifier, isInExports: boolean, isDefaultExport: boolean}): void {
835
+ const {importDeclaration, importer, moduleSpecifierFilePath, importElement, isInExports, isDefaultExport} = importClauseInfo;
836
+ logger.debug(`createFamixImportClause: Creating import clause:`);
837
+ const fmxImportClause = new Famix.ImportClause();
838
+
839
+ let importedEntity: Famix.NamedEntity | Famix.StructuralEntity;
840
+ let importedEntityName: string;
841
+
842
+ const absolutePathProject = this.famixRep.getAbsolutePath();
843
+
844
+ const absolutePath = path.normalize(moduleSpecifierFilePath);
845
+ // convert the path and remove any windows backslashes introduced by path.normalize
846
+ const pathInProject: string = this.convertToRelativePath(absolutePath, absolutePathProject).replace(/\\/g, "/");
847
+
848
+ let pathName = "{" + pathInProject + "}.";
849
+
850
+ // Named imports, e.g. import { ClassW } from "./complexExportModule";
851
+ if (importDeclaration instanceof ImportDeclaration
852
+ && importElement instanceof ImportSpecifier) {
853
+ importedEntityName = importElement.getName();
854
+ pathName = pathName + importedEntityName;
855
+ if (isInExports) {
856
+ importedEntity = this.famixRep.getFamixEntityByFullyQualifiedName(pathName) as Famix.NamedEntity;
857
+ }
858
+ if (importedEntity === undefined) {
859
+ importedEntity = new Famix.NamedEntity();
860
+ importedEntity.setName(importedEntityName);
861
+ if (!isInExports) {
862
+ importedEntity.setIsStub(true);
863
+ }
864
+ this.makeFamixIndexFileAnchor(importElement, importedEntity);
865
+ importedEntity.setFullyQualifiedName(pathName);
866
+ }
867
+ }
868
+ // handle import equals declarations, e.g. import myModule = require("./complexExportModule");
869
+ // TypeScript can't determine the type of the imported module, so we create a Module entity
870
+ else if (importDeclaration instanceof ImportEqualsDeclaration) {
871
+ importedEntityName = importDeclaration?.getName();
872
+ pathName = pathName + importedEntityName;
873
+ importedEntity = new Famix.StructuralEntity();
874
+ importedEntity.setName(importedEntityName);
875
+ this.makeFamixIndexFileAnchor(importElement, importedEntity);
876
+ importedEntity.setFullyQualifiedName(pathName);
877
+ const anyType = this.createOrGetFamixType('any', undefined);
878
+ (importedEntity as Famix.StructuralEntity).setDeclaredType(anyType);
879
+ } else { // default imports, e.g. import ClassW from "./complexExportModule";
880
+ importedEntityName = importElement.getText();
881
+ pathName = pathName + (isDefaultExport ? "defaultExport" : "namespaceExport");
882
+ importedEntity = new Famix.NamedEntity();
883
+ importedEntity.setName(importedEntityName);
884
+ this.makeFamixIndexFileAnchor(importElement, importedEntity);
885
+ importedEntity.setFullyQualifiedName(pathName);
886
+ }
887
+
888
+ this.famixRep.addElement(importedEntity);
889
+ const importerFullyQualifiedName = FQNFunctions.getFQN(importer);
890
+ const fmxImporter = this.famixRep.getFamixEntityByFullyQualifiedName(importerFullyQualifiedName) as Famix.Module;
891
+ fmxImportClause.setImportingEntity(fmxImporter);
892
+ fmxImportClause.setImportedEntity(importedEntity);
893
+ if (importDeclaration instanceof ImportEqualsDeclaration) {
894
+ fmxImportClause.setModuleSpecifier(importDeclaration?.getModuleReference().getText() as string);
895
+ } else {
896
+ fmxImportClause.setModuleSpecifier(importDeclaration?.getModuleSpecifierValue() as string);
897
+ }
898
+
899
+ logger.debug(`createFamixImportClause: ${fmxImportClause.getImportedEntity()?.getName()} (of type ${
900
+ Helpers.getSubTypeName(fmxImportClause.getImportedEntity())}) is imported by ${fmxImportClause.getImportingEntity()?.getName()}`);
901
+
902
+ // make an index file anchor for the import clause
903
+ this.makeFamixIndexFileAnchor(importDeclaration, fmxImportClause);
904
+
905
+ fmxImporter.addOutgoingImport(fmxImportClause);
906
+
907
+ this.famixRep.addElement(fmxImportClause);
908
+
909
+ this.fmxElementObjectMap.set(fmxImportClause,importDeclaration);
910
+ }
911
+
912
+ public convertToRelativePath(absolutePath: string, absolutePathProject: string) {
913
+ return absolutePath.replace(absolutePathProject, "").slice(1);
914
+ }
915
+ }