Skip to content

Commit 8ef46f3

Browse files
devversionmatsko
authored andcommitted
refactor(core): add tslint rule entry-point for static-query migration (angular#29258)
In order to be able to use the static-query migration logic within Google, we need to provide a TSLint rule entry-point that wires up the schematic logic and provides reporting and automatic fixes. PR Close angular#29258
1 parent 7b70760 commit 8ef46f3

File tree

10 files changed

+332
-117
lines changed

10 files changed

+332
-117
lines changed

package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@
9797
"systemjs": "0.18.10",
9898
"tsickle": "0.34.3",
9999
"tslib": "^1.9.0",
100+
"tslint": "5.7.0",
100101
"typescript": "~3.3.3333",
101102
"xhr2": "0.1.4",
102103
"yargs": "9.0.1",
@@ -144,7 +145,6 @@
144145
"rewire": "2.5.2",
145146
"sauce-connect": "https://saucelabs.com/downloads/sc-4.5.1-linux.tar.gz",
146147
"semver": "5.4.1",
147-
"tslint": "5.7.0",
148148
"tslint-eslint-rules": "4.1.1",
149149
"tsutils": "2.27.2",
150150
"universal-analytics": "0.4.15",

packages/core/schematics/migrations/static-queries/BUILD.bazel

+2-4
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,11 @@ load("//tools:defaults.bzl", "ts_library")
22

33
ts_library(
44
name = "static-queries",
5-
srcs = glob(
6-
["**/*.ts"],
7-
exclude = ["index_spec.ts"],
8-
),
5+
srcs = glob(["**/*.ts"]),
96
tsconfig = "//packages/core/schematics:tsconfig.json",
107
visibility = [
118
"//packages/core/schematics:__pkg__",
9+
"//packages/core/schematics/migrations/static-queries/google3:__pkg__",
1210
"//packages/core/schematics/test:__pkg__",
1311
],
1412
deps = [
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
load("//tools:defaults.bzl", "ts_library")
2+
3+
ts_library(
4+
name = "google3",
5+
srcs = glob(["**/*.ts"]),
6+
tsconfig = "//packages/core/schematics:tsconfig.json",
7+
visibility = ["//packages/core/schematics/test:__pkg__"],
8+
deps = [
9+
"//packages/core/schematics/migrations/static-queries",
10+
"@npm//tslint",
11+
],
12+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {Replacement, RuleFailure, Rules} from 'tslint';
10+
import * as ts from 'typescript';
11+
12+
import {analyzeNgQueryUsage} from '../angular/analyze_query_usage';
13+
import {NgQueryResolveVisitor} from '../angular/ng_query_visitor';
14+
import {QueryTiming} from '../angular/query-definition';
15+
import {getTransformedQueryCallExpr} from '../transform';
16+
17+
/**
18+
* Rule that reports if an Angular "ViewChild" or "ContentChild" query is not explicitly
19+
* specifying its timing. The rule also provides TSLint automatic replacements that can
20+
* be applied in order to automatically migrate to the explicit query timing API.
21+
*/
22+
export class Rule extends Rules.TypedRule {
23+
applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] {
24+
const typeChecker = program.getTypeChecker();
25+
const queryVisitor = new NgQueryResolveVisitor(program.getTypeChecker());
26+
const rootSourceFiles = program.getRootFileNames().map(f => program.getSourceFile(f) !);
27+
const printer = ts.createPrinter();
28+
const failures: RuleFailure[] = [];
29+
30+
// Analyze source files by detecting queries and class relations.
31+
rootSourceFiles.forEach(sourceFile => queryVisitor.visitNode(sourceFile));
32+
33+
const {resolvedQueries, classMetadata} = queryVisitor;
34+
const queries = resolvedQueries.get(sourceFile);
35+
36+
// No queries detected for the given source file.
37+
if (!queries) {
38+
return [];
39+
}
40+
41+
// Compute the query usage for all resolved queries and update the
42+
// query definitions to explicitly declare the query timing (static or dynamic)
43+
queries.forEach(q => {
44+
const queryExpr = q.decorator.node.expression;
45+
const timing = analyzeNgQueryUsage(q, classMetadata, typeChecker);
46+
const transformedNode = getTransformedQueryCallExpr(q, timing);
47+
48+
if (!transformedNode) {
49+
return;
50+
}
51+
52+
const newText = printer.printNode(ts.EmitHint.Unspecified, transformedNode, sourceFile);
53+
54+
// Replace the existing query decorator call expression with the
55+
// updated call expression node.
56+
const fix = new Replacement(queryExpr.getStart(), queryExpr.getWidth(), newText);
57+
const timingStr = timing === QueryTiming.STATIC ? 'static' : 'dynamic';
58+
59+
failures.push(new RuleFailure(
60+
sourceFile, queryExpr.getStart(), queryExpr.getWidth(),
61+
`Query is not explicitly marked as "${timingStr}"`, this.ruleName, fix));
62+
});
63+
64+
return failures;
65+
}
66+
}

packages/core/schematics/migrations/static-queries/index.ts

+69-1
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,16 @@
77
*/
88

99
import {Rule, SchematicsException, Tree} from '@angular-devkit/schematics';
10+
import {dirname, relative} from 'path';
11+
import * as ts from 'typescript';
12+
1013
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
11-
import {runStaticQueryMigration} from './migration';
14+
15+
import {analyzeNgQueryUsage} from './angular/analyze_query_usage';
16+
import {NgQueryResolveVisitor} from './angular/ng_query_visitor';
17+
import {getTransformedQueryCallExpr} from './transform';
18+
import {parseTsconfigFile} from './typescript/tsconfig';
19+
1220

1321
/** Entry point for the V8 static-query migration. */
1422
export default function(): Rule {
@@ -27,3 +35,63 @@ export default function(): Rule {
2735
}
2836
};
2937
}
38+
39+
/**
40+
* Runs the static query migration for the given TypeScript project. The schematic
41+
* analyzes all queries within the project and sets up the query timing based on
42+
* the current usage of the query property. e.g. a view query that is not used in any
43+
* lifecycle hook does not need to be static and can be set up with "static: false".
44+
*/
45+
function runStaticQueryMigration(tree: Tree, tsconfigPath: string, basePath: string) {
46+
const parsed = parseTsconfigFile(tsconfigPath, dirname(tsconfigPath));
47+
const host = ts.createCompilerHost(parsed.options, true);
48+
49+
// We need to overwrite the host "readFile" method, as we want the TypeScript
50+
// program to be based on the file contents in the virtual file tree. Otherwise
51+
// if we run the migration for multiple tsconfig files which have intersecting
52+
// source files, it can end up updating query definitions multiple times.
53+
host.readFile = fileName => {
54+
const buffer = tree.read(relative(basePath, fileName));
55+
return buffer ? buffer.toString() : undefined;
56+
};
57+
58+
const program = ts.createProgram(parsed.fileNames, parsed.options, host);
59+
const typeChecker = program.getTypeChecker();
60+
const queryVisitor = new NgQueryResolveVisitor(typeChecker);
61+
const rootSourceFiles = program.getRootFileNames().map(f => program.getSourceFile(f) !);
62+
const printer = ts.createPrinter();
63+
64+
// Analyze source files by detecting queries and class relations.
65+
rootSourceFiles.forEach(sourceFile => queryVisitor.visitNode(sourceFile));
66+
67+
const {resolvedQueries, classMetadata} = queryVisitor;
68+
69+
// Walk through all source files that contain resolved queries and update
70+
// the source files if needed. Note that we need to update multiple queries
71+
// within a source file within the same recorder in order to not throw off
72+
// the TypeScript node offsets.
73+
resolvedQueries.forEach((queries, sourceFile) => {
74+
const update = tree.beginUpdate(relative(basePath, sourceFile.fileName));
75+
76+
// Compute the query usage for all resolved queries and update the
77+
// query definitions to explicitly declare the query timing (static or dynamic)
78+
queries.forEach(q => {
79+
const queryExpr = q.decorator.node.expression;
80+
const timing = analyzeNgQueryUsage(q, classMetadata, typeChecker);
81+
const transformedNode = getTransformedQueryCallExpr(q, timing);
82+
83+
if (!transformedNode) {
84+
return;
85+
}
86+
87+
const newText = printer.printNode(ts.EmitHint.Unspecified, transformedNode, sourceFile);
88+
89+
// Replace the existing query decorator call expression with the updated
90+
// call expression node.
91+
update.remove(queryExpr.getStart(), queryExpr.getWidth());
92+
update.insertRight(queryExpr.getStart(), newText);
93+
});
94+
95+
tree.commitUpdate(update);
96+
});
97+
}

packages/core/schematics/migrations/static-queries/migration.ts

-107
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
/**
2+
* @license
3+
* Copyright Google Inc. All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import * as ts from 'typescript';
10+
11+
import {NgQueryDefinition, QueryTiming} from './angular/query-definition';
12+
import {getPropertyNameText} from './typescript/property_name';
13+
14+
15+
/**
16+
* Transforms the given query decorator by explicitly specifying the timing based on the
17+
* determined timing. The updated decorator call expression node will be returned.
18+
*/
19+
export function getTransformedQueryCallExpr(
20+
query: NgQueryDefinition, timing: QueryTiming): ts.CallExpression|null {
21+
const queryExpr = query.decorator.node.expression as ts.CallExpression;
22+
const queryArguments = queryExpr.arguments;
23+
const timingPropertyAssignment = ts.createPropertyAssignment(
24+
'static', timing === QueryTiming.STATIC ? ts.createTrue() : ts.createFalse());
25+
26+
// If the query decorator is already called with two arguments, we need to
27+
// keep the existing options untouched and just add the new property if needed.
28+
if (queryArguments.length === 2) {
29+
const existingOptions = queryArguments[1] as ts.ObjectLiteralExpression;
30+
31+
// In case the options already contains a property for the "static" flag, we just
32+
// skip this query and leave it untouched.
33+
if (existingOptions.properties.some(
34+
p => !!p.name && getPropertyNameText(p.name) === 'static')) {
35+
return null;
36+
}
37+
38+
const updatedOptions = ts.updateObjectLiteral(
39+
existingOptions, existingOptions.properties.concat(timingPropertyAssignment));
40+
return ts.updateCall(
41+
queryExpr, queryExpr.expression, queryExpr.typeArguments,
42+
[queryArguments[0], updatedOptions]);
43+
}
44+
45+
return ts.updateCall(
46+
queryExpr, queryExpr.expression, queryExpr.typeArguments,
47+
[queryArguments[0], ts.createObjectLiteral([timingPropertyAssignment])]);
48+
}

packages/core/schematics/test/BUILD.bazel

+2
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ ts_library(
1010
],
1111
deps = [
1212
"//packages/core/schematics/migrations/static-queries",
13+
"//packages/core/schematics/migrations/static-queries/google3",
1314
"//packages/core/schematics/utils",
1415
"@npm//@angular-devkit/schematics",
1516
"@npm//@types/shelljs",
17+
"@npm//tslint",
1618
],
1719
)
1820

0 commit comments

Comments
 (0)