Skip to content

Commit e852b62

Browse files
committed
refactor(@ngtools/webpack): use type guard based narrowing with TS AST
By leveraging TypeScript's AST type guards, function parameter assumptions and casting can be removed. Many of these cases caused errors when enabling TypeScript's strict option. This is preliminary work to support enabling full TypeScript strict mode within the project.
1 parent 2ab3136 commit e852b62

File tree

6 files changed

+65
-34
lines changed

6 files changed

+65
-34
lines changed

packages/ngtools/webpack/src/entry_resolver.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,16 +8,16 @@
88

99
import { Path, join } from '@angular-devkit/core';
1010
import * as ts from 'typescript';
11-
import { TypeScriptFileRefactor } from './refactor';
11+
import { TypeScriptFileRefactor, findAstNodes } from './refactor';
1212

1313

1414
function _recursiveSymbolExportLookup(refactor: TypeScriptFileRefactor,
1515
symbolName: string,
1616
host: ts.CompilerHost,
1717
program: ts.Program): string | null {
1818
// Check this file.
19-
const hasSymbol = refactor.findAstNodes(null, ts.SyntaxKind.ClassDeclaration)
20-
.some((cd: ts.ClassDeclaration) => {
19+
const hasSymbol = findAstNodes(null, refactor.sourceFile, ts.isClassDeclaration)
20+
.some((cd) => {
2121
return cd.name != undefined && cd.name.text == symbolName;
2222
});
2323
if (hasSymbol) {
@@ -69,8 +69,8 @@ function _recursiveSymbolExportLookup(refactor: TypeScriptFileRefactor,
6969

7070
// Create the source and verify that the symbol is at least a class.
7171
const source = new TypeScriptFileRefactor(module, host, program);
72-
const hasSymbol = source.findAstNodes(null, ts.SyntaxKind.ClassDeclaration)
73-
.some((cd: ts.ClassDeclaration) => {
72+
const hasSymbol = findAstNodes(null, source.sourceFile, ts.isClassDeclaration)
73+
.some((cd) => {
7474
return cd.name != undefined && cd.name.text == symbolName;
7575
});
7676

packages/ngtools/webpack/src/lazy_routes.ts

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -57,24 +57,21 @@ export function findLazyRoutes(
5757
}
5858
const sf: ts.SourceFile = sourceFile;
5959

60-
return findAstNodes(null, sourceFile, ts.SyntaxKind.ObjectLiteralExpression, true)
60+
return findAstNodes(null, sourceFile, ts.isObjectLiteralExpression, true)
6161
// Get all their property assignments.
62-
.map((node: ts.ObjectLiteralExpression) => {
63-
return findAstNodes(node, sf, ts.SyntaxKind.PropertyAssignment, false);
64-
})
62+
.map((node) => findAstNodes(node, sf, ts.isPropertyAssignment, false))
6563
// Take all `loadChildren` elements.
66-
.reduce((acc: ts.PropertyAssignment[], props: ts.PropertyAssignment[]) => {
64+
.reduce((acc, props) => {
6765
return acc.concat(props.filter(literal => {
6866
return _getContentOfKeyLiteral(sf, literal.name) == 'loadChildren';
6967
}));
7068
}, [])
7169
// Get only string values.
72-
.filter((node: ts.PropertyAssignment) => node.initializer.kind == ts.SyntaxKind.StringLiteral)
73-
// Get the string value.
74-
.map((node: ts.PropertyAssignment) => (node.initializer as ts.StringLiteral).text)
70+
.map((node) => node.initializer)
71+
.filter(ts.isStringLiteral)
7572
// Map those to either [path, absoluteModulePath], or [path, null] if the module pointing to
7673
// does not exist.
77-
.map((routePath: string) => {
74+
.map(({ text: routePath }) => {
7875
const moduleName = routePath.split('#')[0];
7976
const compOptions = (program && program.getCompilerOptions()) || compilerOptions || {};
8077
const resolvedModuleName: ts.ResolvedModuleWithFailedLookupLocations = moduleName[0] == '.'
@@ -87,11 +84,11 @@ export function findLazyRoutes(
8784
&& host.fileExists(resolvedModuleName.resolvedModule.resolvedFileName)) {
8885
return [routePath, resolvedModuleName.resolvedModule.resolvedFileName];
8986
} else {
90-
return [routePath, null];
87+
return [routePath, null] as [string, null];
9188
}
9289
})
9390
// Reduce to the LazyRouteMap map.
94-
.reduce((acc: LazyRouteMap, [routePath, resolvedModuleName]: [string, string | null]) => {
91+
.reduce((acc: LazyRouteMap, [routePath, resolvedModuleName]) => {
9592
if (resolvedModuleName) {
9693
acc[routePath] = resolvedModuleName;
9794
}

packages/ngtools/webpack/src/refactor.ts

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,35 @@ import { forwardSlashPath } from './utils';
1919
* @param max The maximum number of items to return.
2020
* @return all nodes of kind, or [] if none is found
2121
*/
22-
// TODO: replace this with collectDeepNodes and add limits to collectDeepNodes
23-
export function findAstNodes<T extends ts.Node>(
22+
export function findAstNodes(
2423
node: ts.Node | null,
2524
sourceFile: ts.SourceFile,
2625
kind: ts.SyntaxKind,
26+
recursive?: boolean,
27+
max?: number,
28+
): ts.Node[];
29+
30+
/**
31+
* Find all nodes from the AST in the subtree of node of SyntaxKind kind.
32+
* @param node The root node to check, or null if the whole tree should be searched.
33+
* @param sourceFile The source file where the node is.
34+
* @param guard
35+
* @param recursive Whether to go in matched nodes to keep matching.
36+
* @param max The maximum number of items to return.
37+
* @return all nodes of kind, or [] if none is found
38+
*/
39+
export function findAstNodes<T extends ts.Node>(
40+
node: ts.Node | null,
41+
sourceFile: ts.SourceFile,
42+
guard: (node: ts.Node) => node is T,
43+
recursive?: boolean,
44+
max?: number,
45+
): T[];
46+
47+
export function findAstNodes<T extends ts.Node>(
48+
node: ts.Node | null,
49+
sourceFile: ts.SourceFile,
50+
kindOrGuard: ts.SyntaxKind | ((node: ts.Node) => node is T),
2751
recursive = false,
2852
max = Infinity,
2953
): T[] {
@@ -35,23 +59,28 @@ export function findAstNodes<T extends ts.Node>(
3559
node = sourceFile;
3660
}
3761

62+
const test =
63+
typeof kindOrGuard === 'function'
64+
? kindOrGuard
65+
: (node: ts.Node): node is T => node.kind === kindOrGuard;
66+
3867
const arr: T[] = [];
39-
if (node.kind === kind) {
68+
if (test(node)) {
4069
// If we're not recursively looking for children, stop here.
4170
if (!recursive) {
4271
return [node as T];
4372
}
4473

45-
arr.push(node as T);
74+
arr.push(node);
4675
max--;
4776
}
4877

4978
if (max > 0) {
5079
for (const child of node.getChildren(sourceFile)) {
51-
findAstNodes(child, sourceFile, kind, recursive, max)
52-
.forEach((node: ts.Node) => {
80+
findAstNodes(child, sourceFile, test, recursive, max)
81+
.forEach((node) => {
5382
if (max > 0) {
54-
arr.push(node as T);
83+
arr.push(node);
5584
}
5685
max--;
5786
});

packages/ngtools/webpack/src/transformers/find_resources.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export function findResources(sourceFile: ts.SourceFile): string[] {
2727

2828
ts.visitNodes(
2929
(args[0] as ts.ObjectLiteralExpression).properties,
30-
(node: ts.ObjectLiteralElementLike) => {
30+
(node) => {
3131
if (!ts.isPropertyAssignment(node) || ts.isComputedPropertyName(node.name)) {
3232
return node;
3333
}
@@ -47,7 +47,7 @@ export function findResources(sourceFile: ts.SourceFile): string[] {
4747
return node;
4848
}
4949

50-
ts.visitNodes(node.initializer.elements, (node: ts.Expression) => {
50+
ts.visitNodes(node.initializer.elements, (node) => {
5151
const url = getResourceUrl(node);
5252

5353
if (url) {

packages/ngtools/webpack/src/transformers/insert_import.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -69,12 +69,13 @@ export function insertImport(
6969
// Find all imports.
7070
const allImports = collectDeepNodes(sourceFile, ts.SyntaxKind.ImportDeclaration);
7171
const maybeImports = allImports
72-
.filter((node: ts.ImportDeclaration) => {
72+
.filter(ts.isImportDeclaration)
73+
.filter((node) => {
7374
// Filter all imports that do not match the modulePath.
7475
return node.moduleSpecifier.kind == ts.SyntaxKind.StringLiteral
7576
&& (node.moduleSpecifier as ts.StringLiteral).text == modulePath;
7677
})
77-
.filter((node: ts.ImportDeclaration) => {
78+
.filter((node) => {
7879
// Filter out import statements that are either `import 'XYZ'` or `import * as X from 'XYZ'`.
7980
const clause = node.importClause as ts.ImportClause;
8081
if (!clause || clause.name || !clause.namedBindings) {
@@ -83,7 +84,7 @@ export function insertImport(
8384

8485
return clause.namedBindings.kind == ts.SyntaxKind.NamedImports;
8586
})
86-
.map((node: ts.ImportDeclaration) => {
87+
.map((node) => {
8788
// Return the `{ ... }` list of the named import.
8889
return (node.importClause as ts.ImportClause).namedBindings as ts.NamedImports;
8990
});

packages/ngtools/webpack/src/transformers/replace_resources.ts

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,10 @@ export function replaceResources(
2929

3030
const visitNode: ts.Visitor = (node: ts.Node) => {
3131
if (ts.isClassDeclaration(node)) {
32-
const decorators = ts.visitNodes(node.decorators, (node: ts.Decorator) =>
33-
visitDecorator(context, node, typeChecker, directTemplateLoading),
32+
const decorators = ts.visitNodes(node.decorators, (node) =>
33+
ts.isDecorator(node)
34+
? visitDecorator(context, node, typeChecker, directTemplateLoading)
35+
: node,
3436
);
3537

3638
return ts.updateClassDeclaration(
@@ -82,8 +84,10 @@ function visitDecorator(
8284
const styleReplacements: ts.Expression[] = [];
8385

8486
// visit all properties
85-
let properties = ts.visitNodes(objectExpression.properties, (node: ts.ObjectLiteralElementLike) =>
86-
visitComponentMetadata(context, node, styleReplacements, directTemplateLoading),
87+
let properties = ts.visitNodes(objectExpression.properties, (node) =>
88+
ts.isObjectLiteralElementLike(node)
89+
? visitComponentMetadata(context, node, styleReplacements, directTemplateLoading)
90+
: node,
8791
);
8892

8993
// replace properties with updated properties
@@ -137,7 +141,7 @@ function visitComponentMetadata(
137141
}
138142

139143
const isInlineStyles = name === 'styles';
140-
const styles = ts.visitNodes(node.initializer.elements, (node: ts.Expression) => {
144+
const styles = ts.visitNodes(node.initializer.elements, (node) => {
141145
if (!ts.isStringLiteral(node) && !ts.isNoSubstitutionTemplateLiteral(node)) {
142146
return node;
143147
}
@@ -161,7 +165,7 @@ function visitComponentMetadata(
161165
}
162166
}
163167

164-
export function getResourceUrl(node: ts.Expression, loader = ''): string | null {
168+
export function getResourceUrl(node: ts.Node, loader = ''): string | null {
165169
// only analyze strings
166170
if (!ts.isStringLiteral(node) && !ts.isNoSubstitutionTemplateLiteral(node)) {
167171
return null;

0 commit comments

Comments
 (0)