Skip to content

Commit cd15de9

Browse files
Added Type Extraction Strategy documentation
1 parent 97d16c8 commit cd15de9

File tree

5 files changed

+101
-49
lines changed

5 files changed

+101
-49
lines changed

docs/development/Extraction Strategies.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
**Currently Implemented Extraction Strategies:**
1616
- [[Extraction Strategy - Dependencies|Dependencies and FQNs]]
17-
- Types
17+
- [[Extraction Strategy - Types|Types]]
1818
- Values
1919
- Imports
2020
- Exports
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Extraction Strategy: Types
2+
**Goal:** Enrichment of the graph structures with type information in the form of [[Node - TS Type|:TS:Type]] nodes
3+
4+
**Strategy:**
5+
1. Use the ESLint Parser Services to get the native TS Compiler node for the AST node of a construct for which a type should be determined
6+
2. Use TS Compiler TypeChecker to extract all information necessary to construct the type [[Concepts|concepts]]
7+
8+
9+
**Central Components:**
10+
- `LCEType` and all of it's sub-classes: concepts that represent various type constructs that TypeScript offers
11+
- `type.utils.ts`: collection of functions that utilize the TS Compiler API to extract type [[Concepts|concepts]]
12+
- internally, the `parseType` and `parseAnonymousType` functions contain the central extraction logic
13+
- `parseESNodeType` and other exported `parseX` functions serve as shortcuts to extract type information for specific syntax constructs

docs/index.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,5 @@ This plugin is based on the [LCE Architecture](https://jqassistant-plugin.github
5858

5959
**Extraction Strategies:**
6060
- *[[Extraction Strategies|Overview]]*
61-
- [[Extraction Strategy - Dependencies|Dependencies and FQNs]]
61+
- [[Extraction Strategy - Dependencies|Dependencies and FQNs]]
62+
- [[Extraction Strategy - Types|Types]]

typescript/src/core/concepts/type.concept.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ export class LCETypeDeclared extends LCEType {
4242
}
4343

4444
/**
45-
* Represents an union type (e.g. `string | number`)
45+
* Represents a union type (e.g. `string | number`)
4646
*/
4747
export class LCETypeUnion extends LCEType {
4848
public static override conceptId = "union-type";

typescript/src/core/processors/type.utils.ts

Lines changed: 84 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ export function parseMethodType(
8383
esClassLikeDecl: ESNode,
8484
esMethodDecl: MethodDefinitionNonComputedName | TSAbstractMethodDefinitionNonComputedName | TSMethodSignatureNonComputedName,
8585
methodName: string,
86-
jsPrivate: boolean
86+
jsPrivate: boolean,
8787
): LCETypeFunction | undefined {
8888
const globalContext = processingContext.globalContext;
8989
const tc = globalContext.typeChecker;
@@ -131,8 +131,8 @@ export function parseMethodType(
131131
i,
132132
(esParam as Identifier).name,
133133
esParam.optional ?? false,
134-
parseType(processingContext, paramType, paramNode)
135-
)
134+
parseType(processingContext, paramType, paramNode),
135+
),
136136
);
137137
}
138138
return new LCETypeFunction(LCETypeNotIdentified.CONSTRUCTOR, parameters, false, []);
@@ -149,7 +149,7 @@ export function parseMethodType(
149149
LCETypeNotIdentified.SETTER,
150150
[new LCETypeFunctionParameter(0, paramName, false, parseType(processingContext, paramType, methodNode))],
151151
false,
152-
[]
152+
[],
153153
);
154154
}
155155
}
@@ -162,8 +162,8 @@ export function parseMethodType(
162162

163163
// determine if method is async
164164
let async = false;
165-
if("modifiers" in methodNode) {
166-
async = !!(methodNode.modifiers as {kind: number}[])?.find(m => m.kind === ts.SyntaxKind.AsyncKeyword);
165+
if ("modifiers" in methodNode) {
166+
async = !!(methodNode.modifiers as { kind: number }[])?.find((m) => m.kind === ts.SyntaxKind.AsyncKeyword);
167167
}
168168

169169
// parse type parameters
@@ -177,7 +177,7 @@ export function parseMethodType(
177177
*/
178178
export function parseFunctionType(
179179
processingContext: ProcessingContext,
180-
esFunctionDecl: FunctionDeclaration | TSDeclareFunction | FunctionExpression | ArrowFunctionExpression
180+
esFunctionDecl: FunctionDeclaration | TSDeclareFunction | FunctionExpression | ArrowFunctionExpression,
181181
): LCETypeFunction {
182182
const globalContext = processingContext.globalContext;
183183
const tc = globalContext.typeChecker;
@@ -194,7 +194,7 @@ export function parseFunctionType(
194194
// parse parameters
195195
const parameters = parseFunctionParameters(processingContext, functionSignature, functionNode);
196196

197-
const async = !!functionNode.modifiers?.find(m => m.kind === ts.SyntaxKind.AsyncKeyword);
197+
const async = !!functionNode.modifiers?.find((m) => m.kind === ts.SyntaxKind.AsyncKeyword);
198198

199199
return new LCETypeFunction(returnType, parameters, async, typeParameters);
200200
}
@@ -206,14 +206,14 @@ export function parseFunctionType(
206206
*/
207207
export function parseClassLikeTypeParameters(
208208
processingContext: ProcessingContext,
209-
esElement: ClassDeclaration | TSInterfaceDeclaration
209+
esElement: ClassDeclaration | TSInterfaceDeclaration,
210210
): LCETypeParameterDeclaration[] {
211211
const globalContext = processingContext.globalContext;
212212
const node = globalContext.services.esTreeNodeToTSNodeMap.get(esElement);
213213
const type = globalContext.typeChecker.getTypeAtLocation(node);
214214
const tc = globalContext.typeChecker;
215215
const result: LCETypeParameterDeclaration[] = [];
216-
for (let i = 0; i < tc.getTypeArguments(type as TypeReference).length; i++){
216+
for (let i = 0; i < tc.getTypeArguments(type as TypeReference).length; i++) {
217217
const typeParam = tc.getTypeArguments(type as TypeReference)[i];
218218
const name = typeParam.symbol.name;
219219
let constraintType: LCEType;
@@ -245,7 +245,7 @@ export function parseTypeAliasTypeParameters(processingContext: ProcessingContex
245245
const result: LCETypeParameterDeclaration[] = [];
246246

247247
const esTypeParameters = esElement.typeParameters?.params ?? [];
248-
for (let i = 0; i < esTypeParameters.length; i++){
248+
for (let i = 0; i < esTypeParameters.length; i++) {
249249
const esTypeParam = esTypeParameters[i];
250250
const typeParam = tc.getTypeAtLocation(globalContext.services.esTreeNodeToTSNodeMap.get(esTypeParam));
251251
const name = typeParam.symbol.name;
@@ -276,7 +276,7 @@ export function parseTypeAliasTypeParameters(processingContext: ProcessingContex
276276
export function parseClassLikeBaseType(
277277
processingContext: ProcessingContext,
278278
esTypeIdentifier: MemberExpression | Identifier | TSClassImplements | TSInterfaceHeritage,
279-
esTypeArguments?: TypeNode[]
279+
esTypeArguments?: TypeNode[],
280280
): LCETypeDeclared | undefined {
281281
const globalContext = processingContext.globalContext;
282282
const tc = globalContext.typeChecker;
@@ -307,10 +307,16 @@ export function parseESNodeType(processingContext: ProcessingContext, esNode: ES
307307
return result;
308308
}
309309

310-
function parseType(processingContext: ProcessingContext, type: Type, node: Node, excludedFQN?: string, ignoreDependencies = false, typeResolutionDepth = 0): LCEType {
311-
310+
function parseType(
311+
processingContext: ProcessingContext,
312+
type: Type,
313+
node: Node,
314+
excludedFQN?: string,
315+
ignoreDependencies = false,
316+
typeResolutionDepth = 0,
317+
): LCEType {
312318
// cut off type resolution at a certain depth to prevent graph clutter and potential infinite recursion
313-
if(typeResolutionDepth > MAX_TYPE_RESOLUTION_DEPTH) {
319+
if (typeResolutionDepth > MAX_TYPE_RESOLUTION_DEPTH) {
314320
return LCETypeNotIdentified.RESOLUTION_LIMIT;
315321
}
316322

@@ -328,23 +334,21 @@ function parseType(processingContext: ProcessingContext, type: Type, node: Node,
328334
symbol = undefined;
329335
}
330336
}
331-
if(!globalFqn) {
332-
if(symbol && (symbol.flags & ts.SymbolFlags.EnumMember) && "parent" in symbol) {
337+
if (!globalFqn) {
338+
if (symbol && symbol.flags & ts.SymbolFlags.EnumMember && "parent" in symbol) {
333339
// Normalize enum member symbols to avoid enum member declared types
334340
symbol = symbol.parent as Symbol;
335341
}
336342
globalFqn = symbol ? tc.getFullyQualifiedName(symbol) : undefined;
337343
}
338344

339345
if (
340-
(
341-
!globalFqn ||
346+
(!globalFqn ||
342347
globalFqn === "(Anonymous function)" ||
343348
globalFqn.startsWith("__type") ||
344349
globalFqn.startsWith("__object") ||
345350
globalFqn === excludedFQN ||
346-
(symbol && symbol.getEscapedName().toString().startsWith("__object"))
347-
) &&
351+
(symbol && symbol.getEscapedName().toString().startsWith("__object"))) &&
348352
!isPrimitiveType(tc.typeToString(type))
349353
) {
350354
// TODO: handle recursive types like `_DeepPartialObject`
@@ -373,8 +377,12 @@ function parseType(processingContext: ProcessingContext, type: Type, node: Node,
373377
// normalize TypeChecker FQN and determine if type is part of the project
374378
const sourceFile = symbol?.valueDeclaration?.getSourceFile() ?? symbol?.declarations?.find((d) => !!d.getSourceFile())?.getSourceFile();
375379
const isStandardLibrary = !!sourceFile && globalContext.services.program.isSourceFileDefaultLibrary(sourceFile);
376-
const relativeSrcPath = !!sourceFile ? FileUtils.normalizePath(path.relative(globalContext.projectInfo.rootPath, sourceFile.fileName)) : undefined;
377-
const isExternal = !!sourceFile && (globalContext.services.program.isSourceFileFromExternalLibrary(sourceFile) || relativeSrcPath!.startsWith("node_modules"));
380+
const relativeSrcPath = !!sourceFile
381+
? FileUtils.normalizePath(path.relative(globalContext.projectInfo.rootPath, sourceFile.fileName))
382+
: undefined;
383+
const isExternal =
384+
!!sourceFile &&
385+
(globalContext.services.program.isSourceFileFromExternalLibrary(sourceFile) || relativeSrcPath!.startsWith("node_modules"));
378386

379387
let normalizedFqn = new FQN("");
380388
let scheduleFqnResolution = false;
@@ -385,7 +393,10 @@ function parseType(processingContext: ProcessingContext, type: Type, node: Node,
385393
if (globalFqn.startsWith('"')) {
386394
// path that *probably* points to node modules
387395
// -> resolve absolute path
388-
const packageName = NodeUtils.getPackageNameForPath(globalContext.projectInfo.rootPath, ModulePathUtils.extractFQNPath(globalFqn));
396+
const packageName = NodeUtils.getPackageNameForPath(
397+
globalContext.projectInfo.rootPath,
398+
ModulePathUtils.extractFQNPath(globalFqn),
399+
);
389400
if (packageName) {
390401
normalizedFqn.globalFqn = `"${packageName}".${ModulePathUtils.extractFQNIdentifier(globalFqn)}`;
391402
} else {
@@ -397,7 +408,10 @@ function parseType(processingContext: ProcessingContext, type: Type, node: Node,
397408
if (packageName) {
398409
normalizedFqn.globalFqn = `"${packageName}".${globalFqn}`;
399410
} else {
400-
normalizedFqn.globalFqn = ModulePathUtils.normalizeTypeCheckerFQN(`"${sourceFile.fileName}".${globalFqn}`, globalContext.sourceFilePathAbsolute);
411+
normalizedFqn.globalFqn = ModulePathUtils.normalizeTypeCheckerFQN(
412+
`"${sourceFile.fileName}".${globalFqn}`,
413+
globalContext.sourceFilePathAbsolute,
414+
);
401415
}
402416
}
403417
} else if (globalFqn.startsWith('"')) {
@@ -433,7 +447,16 @@ function parseType(processingContext: ProcessingContext, type: Type, node: Node,
433447
const ta = tc.getTypeArguments(type as TypeReference)[i];
434448
if ("typeArguments" in node && node.typeArguments) {
435449
// if type argument child node is available, pass it on
436-
typeArguments.push(parseType(processingContext, ta, (node.typeArguments as Node[]).at(i) ?? node, excludedFQN, ignoreDependencies, typeResolutionDepth + 1))
450+
typeArguments.push(
451+
parseType(
452+
processingContext,
453+
ta,
454+
(node.typeArguments as Node[]).at(i) ?? node,
455+
excludedFQN,
456+
ignoreDependencies,
457+
typeResolutionDepth + 1,
458+
),
459+
);
437460
} else {
438461
typeArguments.push(parseType(processingContext, ta, node, excludedFQN, ignoreDependencies, typeResolutionDepth + 1));
439462
}
@@ -450,7 +473,7 @@ function parseType(processingContext: ProcessingContext, type: Type, node: Node,
450473
}
451474
return result;
452475
}
453-
} catch(e) {
476+
} catch (e) {
454477
console.error("Error occurred during type resolution:");
455478
console.error(e);
456479
return new LCETypeNotIdentified(tc.typeToString(type));
@@ -464,23 +487,34 @@ function parseAnonymousType(
464487
symbol?: Symbol,
465488
excludedFQN?: string,
466489
ignoreDependencies = false,
467-
typeResolutionDepth = 0
490+
typeResolutionDepth = 0,
468491
): LCEType {
469492
const globalContext = processingContext.globalContext;
470493
const tc = globalContext.typeChecker;
471494

472495
// complex anonymous type
473496
if (type.isUnion()) {
474497
// union type
475-
return new LCETypeUnion(type.types.map((t) => parseType(processingContext, t, node, excludedFQN, ignoreDependencies, typeResolutionDepth + 1)));
498+
return new LCETypeUnion(
499+
type.types.map((t) => parseType(processingContext, t, node, excludedFQN, ignoreDependencies, typeResolutionDepth + 1)),
500+
);
476501
} else if (type.isIntersection()) {
477502
// intersection type
478-
return new LCETypeIntersection(type.types.map((t) => parseType(processingContext, t, node, excludedFQN, ignoreDependencies, typeResolutionDepth + 1)));
503+
return new LCETypeIntersection(
504+
type.types.map((t) => parseType(processingContext, t, node, excludedFQN, ignoreDependencies, typeResolutionDepth + 1)),
505+
);
479506
} else if (type.getCallSignatures().length > 0) {
480507
if (type.getCallSignatures().length > 1) return new LCETypeNotIdentified(tc.typeToString(type));
481508
// function type
482509
const signature = type.getCallSignatures()[0];
483-
const returnType = parseType(processingContext, tc.getReturnTypeOfSignature(signature), node, excludedFQN, ignoreDependencies, typeResolutionDepth + 1);
510+
const returnType = parseType(
511+
processingContext,
512+
tc.getReturnTypeOfSignature(signature),
513+
node,
514+
excludedFQN,
515+
ignoreDependencies,
516+
typeResolutionDepth + 1,
517+
);
484518
const parameters: LCETypeFunctionParameter[] = [];
485519
const paramSyms = signature.getParameters();
486520
for (let i = 0; i < paramSyms.length; i++) {
@@ -493,8 +527,8 @@ function parseAnonymousType(
493527
i,
494528
parameterSym.name,
495529
optional,
496-
parseType(processingContext, paramType, node, excludedFQN, ignoreDependencies, typeResolutionDepth + 1)
497-
)
530+
parseType(processingContext, paramType, node, excludedFQN, ignoreDependencies, typeResolutionDepth + 1),
531+
),
498532
);
499533
}
500534
const typeParameters = parseFunctionTypeParameters(processingContext, signature, node);
@@ -504,18 +538,20 @@ function parseAnonymousType(
504538
// TODO: test for methods, callables, index signatures, etc.
505539
const members: LCETypeObjectMember[] = [];
506540
for (const prop of type.getProperties()) {
507-
if(prop.valueDeclaration) {
508-
const propSignature = prop.valueDeclaration as PropertySignature
541+
if (prop.valueDeclaration) {
542+
const propSignature = prop.valueDeclaration as PropertySignature;
509543
const optional = !!propSignature.questionToken;
510-
const readonly = !!propSignature.modifiers && propSignature.modifiers.some(mod => mod.kind === ts.SyntaxKind.ReadonlyKeyword);
544+
const readonly = !!propSignature.modifiers && propSignature.modifiers.some((mod) => mod.kind === ts.SyntaxKind.ReadonlyKeyword);
511545

512546
const propType = tc.getTypeOfSymbolAtLocation(prop, node);
513-
members.push(new LCETypeObjectMember(
514-
prop.name,
515-
parseType(processingContext, propType, node, undefined, ignoreDependencies, typeResolutionDepth + 1),
516-
optional,
517-
readonly
518-
));
547+
members.push(
548+
new LCETypeObjectMember(
549+
prop.name,
550+
parseType(processingContext, propType, node, undefined, ignoreDependencies, typeResolutionDepth + 1),
551+
optional,
552+
readonly,
553+
),
554+
);
519555
}
520556
}
521557
return new LCETypeObject(members);
@@ -529,9 +565,11 @@ function parseAnonymousType(
529565
} else if (tc.typeToString(type) === "false") {
530566
// boolean false literal
531567
return new LCETypeLiteral(false);
532-
} else if((type.flags & ts.TypeFlags.Object) &&
533-
((type as ObjectType).objectFlags & ts.ObjectFlags.Reference) &&
534-
((type as TypeReference).target.objectFlags & ts.ObjectFlags.Tuple)) {
568+
} else if (
569+
type.flags & ts.TypeFlags.Object &&
570+
(type as ObjectType).objectFlags & ts.ObjectFlags.Reference &&
571+
(type as TypeReference).target.objectFlags & ts.ObjectFlags.Tuple
572+
) {
535573
// tuple type
536574
const typeArgs = tc.getTypeArguments(type as TypeReference);
537575
const types: LCEType[] = [];
@@ -560,7 +598,7 @@ function parseFunctionTypeParameters(processingContext: ProcessingContext, signa
560598
const result: LCETypeParameterDeclaration[] = [];
561599
const typeParameters = signature.getTypeParameters();
562600
if (typeParameters) {
563-
for (let i = 0; i < typeParameters.length; i++){
601+
for (let i = 0; i < typeParameters.length; i++) {
564602
const typeParam = typeParameters[i];
565603
const name = typeParam.symbol.name;
566604
let constraintType: LCEType;

0 commit comments

Comments
 (0)