Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(core): add tslint rule entry-point for static-query migration #29258

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
"systemjs": "0.18.10",
"tsickle": "0.34.3",
"tslib": "^1.9.0",
"tslint": "5.7.0",
"typescript": "~3.3.3333",
"xhr2": "0.1.4",
"yargs": "9.0.1",
Expand Down Expand Up @@ -144,7 +145,6 @@
"rollup-plugin-commonjs": "8.1.0",
"sauce-connect": "https://saucelabs.com/downloads/sc-4.5.1-linux.tar.gz",
"semver": "5.4.1",
"tslint": "5.7.0",
"tslint-eslint-rules": "4.1.1",
"tsutils": "2.27.2",
"uglify-es": "^3.3.9",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@ load("//tools:defaults.bzl", "ts_library")

ts_library(
name = "static-queries",
srcs = glob(
["**/*.ts"],
exclude = ["index_spec.ts"],
),
srcs = glob(["**/*.ts"]),
tsconfig = "//packages/core/schematics:tsconfig.json",
visibility = [
"//packages/core/schematics:__pkg__",
"//packages/core/schematics/migrations/static-queries/google3:__pkg__",
"//packages/core/schematics/test:__pkg__",
],
deps = [
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
load("//tools:defaults.bzl", "ts_library")

ts_library(
name = "google3",
srcs = glob(["**/*.ts"]),
tsconfig = "//packages/core/schematics:tsconfig.json",
visibility = ["//packages/core/schematics/test:__pkg__"],
deps = [
"//packages/core/schematics/migrations/static-queries",
"@npm//tslint",
],
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {Replacement, RuleFailure, Rules} from 'tslint';
import * as ts from 'typescript';

import {analyzeNgQueryUsage} from '../angular/analyze_query_usage';
import {NgQueryResolveVisitor} from '../angular/ng_query_visitor';
import {QueryTiming} from '../angular/query-definition';
import {getTransformedQueryCallExpr} from '../transform';

const FAILURE_MESSAGE = 'Query does explicitly specify its timing. Read more here: ' +
'https://github.com/angular/angular/pull/28810';

/**
* Rule that reports if an Angular "ViewChild" or "ContentChild" query is not explicitly
* specifying its timing. The rule also provides TSLint automatic replacements that can
* be applied in order to automatically migrate to the explicit query timing API.
*/
export class Rule extends Rules.TypedRule {
applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] {
const typeChecker = program.getTypeChecker();
const queryVisitor = new NgQueryResolveVisitor(program.getTypeChecker());
const rootSourceFiles = program.getRootFileNames().map(f => program.getSourceFile(f) !);
const printer = ts.createPrinter();
const failures: RuleFailure[] = [];

// Analyze source files by detecting queries and class relations.
rootSourceFiles.forEach(sourceFile => queryVisitor.visitNode(sourceFile));

const {resolvedQueries, classMetadata} = queryVisitor;
const queries = resolvedQueries.get(sourceFile);

// No queries detected for the given source file.
if (!queries) {
return [];
}

// Compute the query usage for all resolved queries and update the
// query definitions to explicitly declare the query timing (static or dynamic)
queries.forEach(q => {
const queryExpr = q.decorator.node.expression;
const timing = analyzeNgQueryUsage(q, classMetadata, typeChecker);
const transformedNode = getTransformedQueryCallExpr(q, timing);

if (!transformedNode) {
return;
}

const newText = printer.printNode(ts.EmitHint.Unspecified, transformedNode, sourceFile);

// Replace the existing query decorator call expression with the
// updated call expression node.
const fix = new Replacement(queryExpr.getStart(), queryExpr.getWidth(), newText);
const timingStr = timing === QueryTiming.STATIC ? 'static' : 'dynamic';
const failureMessage = `${FAILURE_MESSAGE}. Based on analysis of the query it can be ` +
`marked as "${timingStr}".`;

failures.push(new RuleFailure(
sourceFile, queryExpr.getStart(), queryExpr.getWidth(), failureMessage, this.ruleName,
fix));
});

return failures;
}
}
70 changes: 69 additions & 1 deletion packages/core/schematics/migrations/static-queries/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,16 @@
*/

import {Rule, SchematicsException, Tree} from '@angular-devkit/schematics';
import {dirname, relative} from 'path';
import * as ts from 'typescript';

import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
import {runStaticQueryMigration} from './migration';

import {analyzeNgQueryUsage} from './angular/analyze_query_usage';
import {NgQueryResolveVisitor} from './angular/ng_query_visitor';
import {getTransformedQueryCallExpr} from './transform';
import {parseTsconfigFile} from './typescript/tsconfig';


/** Entry point for the V8 static-query migration. */
export default function(): Rule {
Expand All @@ -27,3 +35,63 @@ export default function(): Rule {
}
};
}

/**
* Runs the static query migration for the given TypeScript project. The schematic
* analyzes all queries within the project and sets up the query timing based on
* the current usage of the query property. e.g. a view query that is not used in any
* lifecycle hook does not need to be static and can be set up with "static: false".
*/
function runStaticQueryMigration(tree: Tree, tsconfigPath: string, basePath: string) {
const parsed = parseTsconfigFile(tsconfigPath, dirname(tsconfigPath));
const host = ts.createCompilerHost(parsed.options, true);

// We need to overwrite the host "readFile" method, as we want the TypeScript
// program to be based on the file contents in the virtual file tree. Otherwise
// if we run the migration for multiple tsconfig files which have intersecting
// source files, it can end up updating query definitions multiple times.
host.readFile = fileName => {
const buffer = tree.read(relative(basePath, fileName));
return buffer ? buffer.toString() : undefined;
};

const program = ts.createProgram(parsed.fileNames, parsed.options, host);
const typeChecker = program.getTypeChecker();
const queryVisitor = new NgQueryResolveVisitor(typeChecker);
const rootSourceFiles = program.getRootFileNames().map(f => program.getSourceFile(f) !);
const printer = ts.createPrinter();

// Analyze source files by detecting queries and class relations.
rootSourceFiles.forEach(sourceFile => queryVisitor.visitNode(sourceFile));

const {resolvedQueries, classMetadata} = queryVisitor;

// Walk through all source files that contain resolved queries and update
// the source files if needed. Note that we need to update multiple queries
// within a source file within the same recorder in order to not throw off
// the TypeScript node offsets.
resolvedQueries.forEach((queries, sourceFile) => {
const update = tree.beginUpdate(relative(basePath, sourceFile.fileName));

// Compute the query usage for all resolved queries and update the
// query definitions to explicitly declare the query timing (static or dynamic)
queries.forEach(q => {
const queryExpr = q.decorator.node.expression;
const timing = analyzeNgQueryUsage(q, classMetadata, typeChecker);
const transformedNode = getTransformedQueryCallExpr(q, timing);

if (!transformedNode) {
return;
}

const newText = printer.printNode(ts.EmitHint.Unspecified, transformedNode, sourceFile);

// Replace the existing query decorator call expression with the updated
// call expression node.
update.remove(queryExpr.getStart(), queryExpr.getWidth());
update.insertRight(queryExpr.getStart(), newText);
});

tree.commitUpdate(update);
});
}
107 changes: 0 additions & 107 deletions packages/core/schematics/migrations/static-queries/migration.ts

This file was deleted.

48 changes: 48 additions & 0 deletions packages/core/schematics/migrations/static-queries/transform.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import * as ts from 'typescript';

import {NgQueryDefinition, QueryTiming} from './angular/query-definition';
import {getPropertyNameText} from './typescript/property_name';


/**
* Transforms the given query decorator by explicitly specifying the timing based on the
* determined timing. The updated decorator call expression node will be returned.
*/
export function getTransformedQueryCallExpr(
query: NgQueryDefinition, timing: QueryTiming): ts.CallExpression|null {
const queryExpr = query.decorator.node.expression as ts.CallExpression;
const queryArguments = queryExpr.arguments;
const timingPropertyAssignment = ts.createPropertyAssignment(
'static', timing === QueryTiming.STATIC ? ts.createTrue() : ts.createFalse());

// If the query decorator is already called with two arguments, we need to
// keep the existing options untouched and just add the new property if needed.
if (queryArguments.length === 2) {
const existingOptions = queryArguments[1] as ts.ObjectLiteralExpression;

// In case the options already contains a property for the "static" flag, we just
// skip this query and leave it untouched.
if (existingOptions.properties.some(
p => !!p.name && getPropertyNameText(p.name) === 'static')) {
return null;
}

const updatedOptions = ts.updateObjectLiteral(
existingOptions, existingOptions.properties.concat(timingPropertyAssignment));
return ts.updateCall(
queryExpr, queryExpr.expression, queryExpr.typeArguments,
[queryArguments[0], updatedOptions]);
}

return ts.updateCall(
queryExpr, queryExpr.expression, queryExpr.typeArguments,
[queryArguments[0], ts.createObjectLiteral([timingPropertyAssignment])]);
}
2 changes: 2 additions & 0 deletions packages/core/schematics/test/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,11 @@ ts_library(
],
deps = [
"//packages/core/schematics/migrations/static-queries",
"//packages/core/schematics/migrations/static-queries/google3",
"//packages/core/schematics/utils",
"@npm//@angular-devkit/schematics",
"@npm//@types/shelljs",
"@npm//tslint",
],
)

Expand Down