Skip to content

Commit

Permalink
feat(core): add dynamic queries schematic
Browse files Browse the repository at this point in the history
Adds a schematic that will remove the explicit `static: false` flag from dynamic queries. E.g.

#### Before
```ts
import { Directive, ViewChild, ContentChild, ElementRef } from '@angular/core';

@directive()
export class MyDirective {
  @ViewChild('child', { static: false }) child: any;
  @ViewChild('secondChild', { read: ElementRef, static: false }) secondChild: ElementRef;
  @ContentChild('thirdChild', { static: false }) thirdChild: any;
}
```

#### After
```ts
import { Directive, ViewChild, ContentChild, ElementRef } from '@angular/core';

@directive()
export class MyDirective {
  @ViewChild('child') child: any;
  @ViewChild('secondChild', { read: ElementRef }) secondChild: ElementRef;
  @ContentChild('thirdChild') thirdChild: any;
}
```
  • Loading branch information
crisbeto committed Aug 20, 2019
1 parent 3dbc4ab commit fea5add
Show file tree
Hide file tree
Showing 11 changed files with 599 additions and 0 deletions.
1 change: 1 addition & 0 deletions packages/core/schematics/BUILD.bazel
Expand Up @@ -10,6 +10,7 @@ npm_package(
srcs = ["migrations.json"],
visibility = ["//packages/core:__pkg__"],
deps = [
"//packages/core/schematics/migrations/dynamic-queries",
"//packages/core/schematics/migrations/move-document",
"//packages/core/schematics/migrations/renderer-to-renderer2",
"//packages/core/schematics/migrations/static-queries",
Expand Down
5 changes: 5 additions & 0 deletions packages/core/schematics/migrations.json
Expand Up @@ -24,6 +24,11 @@
"version": "9-beta",
"description": "Decorates undecorated base classes of directives/components that use dependency injection. Copies metadata from base classes to derived directives/components/pipes that are not decorated.",
"factory": "./migrations/undecorated-classes-with-di/index"
},
"migration-v9-dynamic-queries": {
"version": "9-beta",
"description": "Removes the `static` flag from dynamic queries.",
"factory": "./migrations/dynamic-queries/index"
}
}
}
18 changes: 18 additions & 0 deletions packages/core/schematics/migrations/dynamic-queries/BUILD.bazel
@@ -0,0 +1,18 @@
load("//tools:defaults.bzl", "ts_library")

ts_library(
name = "dynamic-queries",
srcs = glob(["**/*.ts"]),
tsconfig = "//packages/core/schematics:tsconfig.json",
visibility = [
"//packages/core/schematics:__pkg__",
"//packages/core/schematics/migrations/google3:__pkg__",
"//packages/core/schematics/test:__pkg__",
],
deps = [
"//packages/core/schematics/utils",
"@npm//@angular-devkit/schematics",
"@npm//@types/node",
"@npm//typescript",
],
)
27 changes: 27 additions & 0 deletions packages/core/schematics/migrations/dynamic-queries/README.md
@@ -0,0 +1,27 @@
## Dynamic queries migration

Automatically migrates dynamic queries to remove their `static` flag.

#### Before
```ts
import { Directive, ViewChild, ContentChild, ElementRef } from '@angular/core';

@Directive()
export class MyDirective {
@ViewChild('child', { static: false }) child: any;
@ViewChild('secondChild', { read: ElementRef, static: false }) secondChild: ElementRef;
@ContentChild('thirdChild', { static: false }) thirdChild: any;
}
```

#### After
```ts
import { Directive, ViewChild, ContentChild, ElementRef } from '@angular/core';

@Directive()
export class MyDirective {
@ViewChild('child') child: any;
@ViewChild('secondChild', { read: ElementRef }) secondChild: ElementRef;
@ContentChild('thirdChild') thirdChild: any;
}
```
84 changes: 84 additions & 0 deletions packages/core/schematics/migrations/dynamic-queries/index.ts
@@ -0,0 +1,84 @@
/**
* @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 {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 {parseTsconfigFile} from '../../utils/typescript/parse_tsconfig'
import {identifyDynamicQueryNodes, removeOptionsParameter, removeStaticFlag} from './util';


/**
* Runs a migration over a TypeScript project that removes
* the `static: false` flag from query annotations.
*/
export default function(): Rule {
return (tree: Tree) => {
const {buildPaths, testPaths} = getProjectTsConfigPaths(tree);
const basePath = process.cwd();
const allPaths = [...buildPaths, ...testPaths];

if (!allPaths.length) {
throw new SchematicsException(
'Could not find any tsconfig file. Cannot migrate dynamic queries.');
}

for (const tsconfigPath of allPaths) {
runDynamicQueryMigration(tree, tsconfigPath, basePath);
}
};
}

function runDynamicQueryMigration(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));
// Strip BOM as otherwise TSC methods (Ex: getWidth) will return an offset which
// which breaks the CLI UpdateRecorder.
// See: https://github.com/angular/angular/pull/30719
return buffer ? buffer.toString().replace(/^\uFEFF/, '') : undefined;
};

const program = ts.createProgram(parsed.fileNames, parsed.options, host);
const typeChecker = program.getTypeChecker();
const sourceFiles = program.getSourceFiles().filter(
f => !f.isDeclarationFile && !program.isSourceFileFromExternalLibrary(f));
const printer = ts.createPrinter();

sourceFiles.forEach(sourceFile => {
const result = identifyDynamicQueryNodes(typeChecker, sourceFile);

if (result.removeProperty.length || result.removeParameter.length) {
const update = tree.beginUpdate(relative(basePath, sourceFile.fileName));

result.removeProperty.forEach(node => {
update.remove(node.getStart(), node.getWidth());
update.insertRight(
node.getStart(),
printer.printNode(ts.EmitHint.Unspecified, removeStaticFlag(node), sourceFile));
});

result.removeParameter.forEach(node => {
update.remove(node.getStart(), node.getWidth());
update.insertRight(
node.getStart(),
printer.printNode(ts.EmitHint.Unspecified, removeOptionsParameter(node), sourceFile));
});

tree.commitUpdate(update);
}
});
}
77 changes: 77 additions & 0 deletions packages/core/schematics/migrations/dynamic-queries/util.ts
@@ -0,0 +1,77 @@
/**
* @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 {getAngularDecorators} from '../../utils/ng_decorators';

/**
* Identifies the nodes that should be migrated by the dynamic
* queries schematic. Splits the nodes into the following categories:
* - `removeProperty` - queries from which we should only remove the `static` property of the
* `options` parameter (e.g. `@ViewChild('child', {static: false, read: ElementRef})`).
* - `removeParameter` - queries from which we should drop the entire `options` parameter.
* (e.g. `@ViewChild('child', {static: false})`).
*/
export function identifyDynamicQueryNodes(typeChecker: ts.TypeChecker, sourceFile: ts.SourceFile) {
const removeProperty: ts.ObjectLiteralExpression[] = [];
const removeParameter: ts.CallExpression[] = [];

sourceFile.forEachChild(function walk(node: ts.Node) {
if (ts.isClassDeclaration(node)) {
node.members.forEach(member => {
const angularDecorators =
member.decorators && getAngularDecorators(typeChecker, member.decorators);

if (angularDecorators) {
angularDecorators
// Filter out the queries that can have the `static` flag.
.filter(decorator => {
return decorator.name === 'ViewChild' || decorator.name === 'ContentChild';
})
// Filter out the queries where the `static` flag is explicitly set to `false`.
.filter(decorator => {
const options = decorator.node.expression.arguments[1];
return options && ts.isObjectLiteralExpression(options) &&
options.properties.some(
property => ts.isPropertyAssignment(property) &&
property.initializer.kind === ts.SyntaxKind.FalseKeyword);
})
.forEach(decorator => {
const options =
decorator.node.expression.arguments[1] as ts.ObjectLiteralExpression;

// At this point we know that at least one property is the `static` flag. If this is
// the only property we can drop the entire object literal, otherwise we have to
// drop only the property.
if (options.properties.length === 1) {
removeParameter.push(decorator.node.expression);
} else {
removeProperty.push(options);
}
});
}
});
}

node.forEachChild(walk);
});

return {removeProperty, removeParameter};
}

/** Removes the `options` parameter from the call expression of a query decorator. */
export function removeOptionsParameter(node: ts.CallExpression): ts.CallExpression {
return ts.updateCall(node, node.expression, node.typeArguments, [node.arguments[0]]);
}

/** Removes the `static` property from an object literal expression. */
export function removeStaticFlag(node: ts.ObjectLiteralExpression): ts.ObjectLiteralExpression {
return ts.updateObjectLiteral(
node,
node.properties.filter(property => property.name && property.name.getText() !== 'static'));
}
1 change: 1 addition & 0 deletions packages/core/schematics/migrations/google3/BUILD.bazel
Expand Up @@ -6,6 +6,7 @@ ts_library(
tsconfig = "//packages/core/schematics:tsconfig.json",
visibility = ["//packages/core/schematics/test/google3:__pkg__"],
deps = [
"//packages/core/schematics/migrations/dynamic-queries",
"//packages/core/schematics/migrations/missing-injectable",
"//packages/core/schematics/migrations/missing-injectable/google3",
"//packages/core/schematics/migrations/renderer-to-renderer2",
Expand Down
45 changes: 45 additions & 0 deletions packages/core/schematics/migrations/google3/dynamicQueriesRule.ts
@@ -0,0 +1,45 @@
/**
* @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 {identifyDynamicQueryNodes, removeOptionsParameter, removeStaticFlag} from '../dynamic-queries/util';

const RULE_NAME = 'dynamic-queries';
const FAILURE_MESSAGE = 'Static flag on queries is no longer required.';

/**
* TSLint rule that removes the `static` flag from dynamic queries.
*/
export class Rule extends Rules.TypedRule {
applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] {
const printer = ts.createPrinter();
const failures: RuleFailure[] = [];
const result = identifyDynamicQueryNodes(program.getTypeChecker(), sourceFile);

result.removeProperty.forEach(node => {
failures.push(new RuleFailure(
sourceFile, node.getStart(), node.getEnd(), FAILURE_MESSAGE, RULE_NAME,
new Replacement(
node.getStart(), node.getWidth(),
printer.printNode(ts.EmitHint.Unspecified, removeStaticFlag(node), sourceFile))));
});

result.removeParameter.forEach(node => {
failures.push(new RuleFailure(
sourceFile, node.getStart(), node.getEnd(), FAILURE_MESSAGE, RULE_NAME,
new Replacement(
node.getStart(), node.getWidth(),
printer.printNode(
ts.EmitHint.Unspecified, removeOptionsParameter(node), sourceFile))));
});

return failures;
}
}
1 change: 1 addition & 0 deletions packages/core/schematics/test/BUILD.bazel
Expand Up @@ -9,6 +9,7 @@ ts_library(
"//packages/core/schematics:migrations.json",
],
deps = [
"//packages/core/schematics/migrations/dynamic-queries",
"//packages/core/schematics/migrations/missing-injectable",
"//packages/core/schematics/migrations/move-document",
"//packages/core/schematics/migrations/renderer-to-renderer2",
Expand Down

0 comments on commit fea5add

Please sign in to comment.