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
This commit is contained in:
crisbeto
2019-08-21 07:40:30 +02:00
committed by Matias Niemelä
parent 4f033235b1
commit f5982fd746
11 changed files with 604 additions and 0 deletions

View File

@ -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",

View File

@ -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();
}
});

View File

@ -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) {}`);
});
});