Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(core): add dynamic queries schematic
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
Showing
11 changed files
with
599 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
18 changes: 18 additions & 0 deletions
18
packages/core/schematics/migrations/dynamic-queries/BUILD.bazel
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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
27
packages/core/schematics/migrations/dynamic-queries/README.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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
84
packages/core/schematics/migrations/dynamic-queries/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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
77
packages/core/schematics/migrations/dynamic-queries/util.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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')); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
45 changes: 45 additions & 0 deletions
45
packages/core/schematics/migrations/google3/dynamicQueriesRule.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.