From f5982fd7463fcdd726050b0ac19595b6b9d80633 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Wed, 21 Aug 2019 07:40:30 +0200 Subject: [PATCH] feat(core): add dynamic queries schematic (#32231) Adds a schematic that will remove the explicit `static: false` flag from dynamic queries. E.g. ```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; } ``` ```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; } ``` PR Close #32231 --- packages/core/schematics/BUILD.bazel | 1 + packages/core/schematics/migrations.json | 5 + .../migrations/dynamic-queries/BUILD.bazel | 18 ++ .../migrations/dynamic-queries/README.md | 28 +++ .../migrations/dynamic-queries/index.ts | 85 +++++++++ .../migrations/dynamic-queries/util.ts | 77 ++++++++ .../schematics/migrations/google3/BUILD.bazel | 1 + .../migrations/google3/dynamicQueriesRule.ts | 46 +++++ packages/core/schematics/test/BUILD.bazel | 1 + .../test/dynamic_queries_migration_spec.ts | 165 ++++++++++++++++ .../test/google3/dynamic_queries_spec.ts | 177 ++++++++++++++++++ 11 files changed, 604 insertions(+) create mode 100644 packages/core/schematics/migrations/dynamic-queries/BUILD.bazel create mode 100644 packages/core/schematics/migrations/dynamic-queries/README.md create mode 100644 packages/core/schematics/migrations/dynamic-queries/index.ts create mode 100644 packages/core/schematics/migrations/dynamic-queries/util.ts create mode 100644 packages/core/schematics/migrations/google3/dynamicQueriesRule.ts create mode 100644 packages/core/schematics/test/dynamic_queries_migration_spec.ts create mode 100644 packages/core/schematics/test/google3/dynamic_queries_spec.ts diff --git a/packages/core/schematics/BUILD.bazel b/packages/core/schematics/BUILD.bazel index 23433e4a0d..cdd99beec8 100644 --- a/packages/core/schematics/BUILD.bazel +++ b/packages/core/schematics/BUILD.bazel @@ -10,6 +10,7 @@ npm_package( srcs = ["migrations.json"], visibility = ["//packages/core:__pkg__"], 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", diff --git a/packages/core/schematics/migrations.json b/packages/core/schematics/migrations.json index aac7cb8d82..30fd5090c3 100644 --- a/packages/core/schematics/migrations.json +++ b/packages/core/schematics/migrations.json @@ -34,6 +34,11 @@ "version": "9-beta", "description": "Adds an Angular decorator to undecorated classes that have decorated fields", "factory": "./migrations/undecorated-classes-with-decorated-fields/index" + }, + "migration-v9-dynamic-queries": { + "version": "9-beta", + "description": "Removes the `static` flag from dynamic queries.", + "factory": "./migrations/dynamic-queries/index" } } } diff --git a/packages/core/schematics/migrations/dynamic-queries/BUILD.bazel b/packages/core/schematics/migrations/dynamic-queries/BUILD.bazel new file mode 100644 index 0000000000..686593cb04 --- /dev/null +++ b/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", + ], +) diff --git a/packages/core/schematics/migrations/dynamic-queries/README.md b/packages/core/schematics/migrations/dynamic-queries/README.md new file mode 100644 index 0000000000..9a469d5093 --- /dev/null +++ b/packages/core/schematics/migrations/dynamic-queries/README.md @@ -0,0 +1,28 @@ +## Dynamic queries migration + +Automatically migrates dynamic queries to remove their `static` flag. This flag will no +longer be necessary in version 9 for dynamic queries, as `false` is the default value. + +#### 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; +} +``` diff --git a/packages/core/schematics/migrations/dynamic-queries/index.ts b/packages/core/schematics/migrations/dynamic-queries/index.ts new file mode 100644 index 0000000000..5a1ab2dbac --- /dev/null +++ b/packages/core/schematics/migrations/dynamic-queries/index.ts @@ -0,0 +1,85 @@ +/** + * @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, SchematicContext, 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 the dynamic queries migration for all TypeScript projects in the current CLI workspace. + */ +export default function(): Rule { + return (tree: Tree, ctx: SchematicContext) => { + const {buildPaths, testPaths} = getProjectTsConfigPaths(tree); + const basePath = process.cwd(); + const allPaths = [...buildPaths, ...testPaths]; + + ctx.logger.info('------ Dynamic queries migration ------'); + + 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); + } + }); +} diff --git a/packages/core/schematics/migrations/dynamic-queries/util.ts b/packages/core/schematics/migrations/dynamic-queries/util.ts new file mode 100644 index 0000000000..764be7a50c --- /dev/null +++ b/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')); +} diff --git a/packages/core/schematics/migrations/google3/BUILD.bazel b/packages/core/schematics/migrations/google3/BUILD.bazel index 65f1aa23d9..d078463aef 100644 --- a/packages/core/schematics/migrations/google3/BUILD.bazel +++ b/packages/core/schematics/migrations/google3/BUILD.bazel @@ -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", diff --git a/packages/core/schematics/migrations/google3/dynamicQueriesRule.ts b/packages/core/schematics/migrations/google3/dynamicQueriesRule.ts new file mode 100644 index 0000000000..801e500715 --- /dev/null +++ b/packages/core/schematics/migrations/google3/dynamicQueriesRule.ts @@ -0,0 +1,46 @@ +/** + * @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 = + 'The static flag defaults to false, so setting it false manually is unnecessary.'; + +/** + * 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; + } +} diff --git a/packages/core/schematics/test/BUILD.bazel b/packages/core/schematics/test/BUILD.bazel index f342e28309..c14f4c8010 100644 --- a/packages/core/schematics/test/BUILD.bazel +++ b/packages/core/schematics/test/BUILD.bazel @@ -8,6 +8,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", diff --git a/packages/core/schematics/test/dynamic_queries_migration_spec.ts b/packages/core/schematics/test/dynamic_queries_migration_spec.ts new file mode 100644 index 0000000000..31644d345f --- /dev/null +++ b/packages/core/schematics/test/dynamic_queries_migration_spec.ts @@ -0,0 +1,165 @@ +/** + * @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 {getSystemPath, normalize, virtualFs} from '@angular-devkit/core'; +import {TempScopedNodeJsSyncHost} from '@angular-devkit/core/node/testing'; +import {HostTree} from '@angular-devkit/schematics'; +import {SchematicTestRunner, UnitTestTree} from '@angular-devkit/schematics/testing'; +import * as shx from 'shelljs'; + +describe('dynamic queries migration', () => { + let runner: SchematicTestRunner; + let host: TempScopedNodeJsSyncHost; + let tree: UnitTestTree; + let tmpDirPath: string; + let previousWorkingDir: string; + + beforeEach(() => { + runner = new SchematicTestRunner('test', require.resolve('../migrations.json')); + host = new TempScopedNodeJsSyncHost(); + tree = new UnitTestTree(new HostTree(host)); + + writeFile('/tsconfig.json', JSON.stringify({ + compilerOptions: { + lib: ['es2015'], + } + })); + writeFile('/angular.json', JSON.stringify({ + projects: {t: {architect: {build: {options: {tsConfig: './tsconfig.json'}}}}} + })); + + previousWorkingDir = shx.pwd(); + tmpDirPath = getSystemPath(host.root); + + // Switch into the temporary directory path. This allows us to run + // the schematic against our custom unit test tree. + shx.cd(tmpDirPath); + }); + + afterEach(() => { + shx.cd(previousWorkingDir); + shx.rm('-r', tmpDirPath); + }); + + it('should remove the options object from a dynamic ViewChild query that only has one property', + async() => { + writeFile('/index.ts', ` + import { Directive, ViewChild } from '@angular/core'; + + @Directive() + export class MyDirective { + @ViewChild('child', { static: false }) child: any; + } + `); + + await runMigration(); + expect(tree.readContent('/index.ts')).toContain(`@ViewChild('child') child: any;`); + }); + + it('should remove the options object from a dynamic ContentChild query that only has one property', + async() => { + writeFile('/index.ts', ` + import { Directive, ContentChild } from '@angular/core'; + + @Directive() + export class MyComponent { + @ContentChild('child', { static: false }) child: any; + } + `); + + await runMigration(); + expect(tree.readContent('/index.ts')).toContain(`@ContentChild('child') child: any;`); + }); + + it('should only remove the `static` flag from a ViewChild query if it has more than one property', + async() => { + writeFile('/index.ts', ` + import { Directive, ViewChild, ElementRef } from '@angular/core'; + + @Directive() + export class MyDirective { + @ViewChild('child', { read: ElementRef, static: false }) child: ElementRef; + } + `); + + await runMigration(); + expect(tree.readContent('/index.ts')) + .toContain(`@ViewChild('child', { read: ElementRef }) child: ElementRef;`); + }); + + it('should only remove the `static` flag from a ContentChild query if it has more than one property', + async() => { + writeFile('/index.ts', ` + import { Directive, ContentChild, ElementRef } from '@angular/core'; + + @Directive() + export class MyDirective { + @ContentChild('child', { static: false, read: ElementRef }) child: ElementRef; + } + `); + + await runMigration(); + expect(tree.readContent('/index.ts')) + .toContain(`@ContentChild('child', { read: ElementRef }) child: ElementRef;`); + }); + + it('should not change static ViewChild queries', async() => { + writeFile('/index.ts', ` + import { Directive, ViewChild, ElementRef } from '@angular/core'; + + @Directive() + export class MyDirective { + @ViewChild('child', { read: ElementRef, static: true }) child: ElementRef; + } + `); + + await runMigration(); + expect(tree.readContent('/index.ts')) + .toContain(`@ViewChild('child', { read: ElementRef, static: true }) child: ElementRef;`); + }); + + it('should not change static ContentChild queries', async() => { + writeFile('/index.ts', ` + import { Directive, ContentChild, ElementRef } from '@angular/core'; + + @Directive() + export class MyDirective { + @ContentChild('child', { static: true, read: ElementRef }) child: ElementRef; + } + `); + + await runMigration(); + expect(tree.readContent('/index.ts')) + .toContain(`@ContentChild('child', { static: true, read: ElementRef }) child: ElementRef;`); + }); + + it('should migrate dynamic queries on a setter', async() => { + writeFile('/index.ts', ` + import { Directive, ContentChild, ViewChild } from '@angular/core'; + + @Directive() + export class MyDirective { + @ContentChild('child', { static: false }) set child(c: any) {} + @ViewChild('otherChild', { static: false }) set otherChild(c: any) {} + } + `); + + await runMigration(); + const content = tree.readContent('/index.ts'); + expect(content).toContain(`@ContentChild('child') set child(c: any) {}`); + expect(content).toContain(`@ViewChild('otherChild') set otherChild(c: any) {}`); + }); + + function writeFile(filePath: string, contents: string) { + host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents)); + } + + function runMigration() { + runner.runSchematicAsync('migration-v9-dynamic-queries', {}, tree).toPromise(); + } +}); diff --git a/packages/core/schematics/test/google3/dynamic_queries_spec.ts b/packages/core/schematics/test/google3/dynamic_queries_spec.ts new file mode 100644 index 0000000000..eea72f2add --- /dev/null +++ b/packages/core/schematics/test/google3/dynamic_queries_spec.ts @@ -0,0 +1,177 @@ +/** + * @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 {readFileSync, writeFileSync} from 'fs'; +import {dirname, join} from 'path'; +import * as shx from 'shelljs'; +import {Configuration, Linter} from 'tslint'; + +describe('Google3 dynamic queries TSLint rule', () => { + const rulesDirectory = dirname(require.resolve('../../migrations/google3/dynamicQueriesRule')); + + let tmpDir: string; + + beforeEach(() => { + tmpDir = join(process.env['TEST_TMPDIR'] !, 'google3-test'); + shx.mkdir('-p', tmpDir); + + writeFile('tsconfig.json', JSON.stringify({compilerOptions: {module: 'es2015'}})); + }); + + afterEach(() => shx.rm('-r', tmpDir)); + + function runTSLint(fix = true) { + const program = Linter.createProgram(join(tmpDir, 'tsconfig.json')); + const linter = new Linter({fix, rulesDirectory: [rulesDirectory]}, program); + const config = Configuration.parseConfigFile( + {rules: {'dynamic-queries': true}, linterOptions: {typeCheck: true}}); + + program.getRootFileNames().forEach(fileName => { + linter.lint(fileName, program.getSourceFile(fileName) !.getFullText(), config); + }); + + return linter; + } + + function writeFile(fileName: string, content: string) { + writeFileSync(join(tmpDir, fileName), content); + } + + function getFile(fileName: string) { return readFileSync(join(tmpDir, fileName), 'utf8'); } + + it('should flag dynamic queries', () => { + writeFile('/index.ts', ` + import { Directive, ViewChild, ContentChild } from '@angular/core'; + + @Directive() + export class MyDirective { + @ViewChild('child', { static: false }) child: any; + @ContentChild('otherChild', { static: false }) otherChild: any; + } + `); + + const linter = runTSLint(false); + const failures = linter.getResult().failures; + + expect(failures.length).toBe(2); + expect(failures[0].getFailure()) + .toMatch('The static flag defaults to false, so setting it false manually is unnecessary.'); + expect(failures[1].getFailure()) + .toMatch('The static flag defaults to false, so setting it false manually is unnecessary.'); + }); + + it('should remove the options object from a dynamic ViewChild query that only has one property', + () => { + writeFile('/index.ts', ` + import { Directive, ViewChild } from '@angular/core'; + + @Directive() + export class MyDirective { + @ViewChild('child', { static: false }) child: any; + } + `); + + runTSLint(true); + expect(getFile('/index.ts')).toContain(`@ViewChild('child') child: any;`); + }); + + it('should remove the options object from a dynamic ContentChild query that only has one property', + () => { + writeFile('/index.ts', ` + import { Directive, ContentChild } from '@angular/core'; + + @Directive() + export class MyComponent { + @ContentChild('child', { static: false }) child: any; + } + `); + + runTSLint(true); + expect(getFile('/index.ts')).toContain(`@ContentChild('child') child: any;`); + }); + + it('should only remove the `static` flag from a ViewChild query if it has more than one property', + () => { + writeFile('/index.ts', ` + import { Directive, ViewChild, ElementRef } from '@angular/core'; + + @Directive() + export class MyDirective { + @ViewChild('child', { read: ElementRef, static: false }) child: ElementRef; + } + `); + + runTSLint(true); + expect(getFile('/index.ts')) + .toContain(`@ViewChild('child', { read: ElementRef }) child: ElementRef;`); + }); + + it('should only remove the `static` flag from a ContentChild query if it has more than one property', + () => { + writeFile('/index.ts', ` + import { Directive, ContentChild, ElementRef } from '@angular/core'; + + @Directive() + export class MyDirective { + @ContentChild('child', { static: false, read: ElementRef }) child: ElementRef; + } + `); + + runTSLint(true); + expect(getFile('/index.ts')) + .toContain(`@ContentChild('child', { read: ElementRef }) child: ElementRef;`); + }); + + it('should not change static ViewChild queries', () => { + writeFile('/index.ts', ` + import { Directive, ViewChild, ElementRef } from '@angular/core'; + + @Directive() + export class MyDirective { + @ViewChild('child', { read: ElementRef, static: true }) child: ElementRef; + } + `); + + runTSLint(true); + expect(getFile('/index.ts')) + .toContain(`@ViewChild('child', { read: ElementRef, static: true }) child: ElementRef;`); + }); + + it('should not change static ContentChild queries', () => { + writeFile('/index.ts', ` + import { Directive, ContentChild, ElementRef } from '@angular/core'; + + @Directive() + export class MyDirective { + @ContentChild('child', { static: true, read: ElementRef }) child: ElementRef; + } + `); + + runTSLint(true); + expect(getFile('/index.ts')) + .toContain(`@ContentChild('child', { static: true, read: ElementRef }) child: ElementRef;`); + }); + + it('should migrate dynamic queries on a setter', () => { + writeFile('/index.ts', ` + import { Directive, ContentChild, ViewChild } from '@angular/core'; + + @Directive() + export class MyDirective { + @ContentChild('child', { static: false }) set child(c: any) {} + @ViewChild('otherChild', { static: false }) set otherChild(c: any) {} + } + `); + + runTSLint(true); + const content = getFile('/index.ts'); + expect(content).toContain(`@ContentChild('child') set child(c: any) {}`); + expect(content).toContain(`@ViewChild('otherChild') set otherChild(c: any) {}`); + }); + +});