feat(forms): add migration for AbstractControl.parent accesses (#39009)
As of #32671, the type of `AbstractControl.parent` can be null which can cause compilation errors in existing apps. These changes add a migration that will append non-null assertions to existing unsafe accesses. ```` // Before console.log(control.parent.value); // After console.log(control.parent!.value); ``` The migration also tries its best to avoid cases where the non-null assertions aren't necessary (e.g. if the `parent` was null checked already). PR Close #39009
This commit is contained in:
parent
f2fca6d58e
commit
aeec223db1
|
@ -10,6 +10,7 @@ pkg_npm(
|
||||||
srcs = ["migrations.json"],
|
srcs = ["migrations.json"],
|
||||||
visibility = ["//packages/core:__pkg__"],
|
visibility = ["//packages/core:__pkg__"],
|
||||||
deps = [
|
deps = [
|
||||||
|
"//packages/core/schematics/migrations/abstract-control-parent",
|
||||||
"//packages/core/schematics/migrations/dynamic-queries",
|
"//packages/core/schematics/migrations/dynamic-queries",
|
||||||
"//packages/core/schematics/migrations/missing-injectable",
|
"//packages/core/schematics/migrations/missing-injectable",
|
||||||
"//packages/core/schematics/migrations/module-with-providers",
|
"//packages/core/schematics/migrations/module-with-providers",
|
||||||
|
|
|
@ -54,6 +54,11 @@
|
||||||
"version": "11.0.0-beta",
|
"version": "11.0.0-beta",
|
||||||
"description": "The default value for `relativeLinkResolution` is changing from 'legacy' to 'corrected'.\nThis migration updates `RouterModule` configurations that use the default value to \nnow specifically use 'legacy' to prevent breakages when updating.",
|
"description": "The default value for `relativeLinkResolution` is changing from 'legacy' to 'corrected'.\nThis migration updates `RouterModule` configurations that use the default value to \nnow specifically use 'legacy' to prevent breakages when updating.",
|
||||||
"factory": "./migrations/relative-link-resolution/index"
|
"factory": "./migrations/relative-link-resolution/index"
|
||||||
|
},
|
||||||
|
"migration-v11-abstract-control-parent": {
|
||||||
|
"version": "11.0.0-beta",
|
||||||
|
"description": "In Angular version 11, the type of `AbstractControl.parent` can be `null` to reflect the runtime value more accurately. This migration automatically adds non-null assertions to existing accesses of the `parent` property on types like `FormControl`, `FormArray` and `FormGroup`.",
|
||||||
|
"factory": "./migrations/abstract-control-parent/index"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
load("//tools:defaults.bzl", "ts_library")
|
||||||
|
|
||||||
|
ts_library(
|
||||||
|
name = "abstract-control-parent",
|
||||||
|
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",
|
||||||
|
],
|
||||||
|
)
|
|
@ -0,0 +1,34 @@
|
||||||
|
## `AbstractControl.parent` migration
|
||||||
|
|
||||||
|
As of Angular v11, the type of `AbstractControl.parent` can be null. This migration automatically
|
||||||
|
identifies usages and adds non-null assertions.
|
||||||
|
|
||||||
|
#### Before
|
||||||
|
```ts
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { FormControl } from '@angular/forms';
|
||||||
|
|
||||||
|
@Component()
|
||||||
|
export class MyComponent {
|
||||||
|
private _control = new FormControl();
|
||||||
|
|
||||||
|
getParentValue() {
|
||||||
|
return this._control.parent.value; // <- Compilation error in v11.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### After
|
||||||
|
```ts
|
||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { FormControl } from '@angular/forms';
|
||||||
|
|
||||||
|
@Component()
|
||||||
|
export class MyComponent {
|
||||||
|
private _control = new FormControl();
|
||||||
|
|
||||||
|
getParentValue() {
|
||||||
|
return this._control.parent!.value; // <- Non-null assertion added during the migration.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
|
@ -0,0 +1,57 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google LLC 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 {relative} from 'path';
|
||||||
|
|
||||||
|
import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
|
||||||
|
import {createMigrationProgram} from '../../utils/typescript/compiler_host';
|
||||||
|
import {findParentAccesses} from './util';
|
||||||
|
|
||||||
|
|
||||||
|
/** Migration that marks accesses of `AbstractControl.parent` as non-null. */
|
||||||
|
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 AbstractControl.parent accesses.');
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const tsconfigPath of allPaths) {
|
||||||
|
runNativeAbstractControlParentMigration(tree, tsconfigPath, basePath);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function runNativeAbstractControlParentMigration(
|
||||||
|
tree: Tree, tsconfigPath: string, basePath: string) {
|
||||||
|
const {program} = createMigrationProgram(tree, tsconfigPath, basePath);
|
||||||
|
const typeChecker = program.getTypeChecker();
|
||||||
|
const sourceFiles = program.getSourceFiles().filter(
|
||||||
|
f => !f.isDeclarationFile && !program.isSourceFileFromExternalLibrary(f));
|
||||||
|
|
||||||
|
sourceFiles.forEach(sourceFile => {
|
||||||
|
// We sort the nodes based on their position in the file and we offset the positions by one
|
||||||
|
// for each non-null assertion that we've added. We have to do it this way, rather than
|
||||||
|
// creating and printing a new AST node like in other migrations, because property access
|
||||||
|
// expressions can be nested (e.g. `control.parent.parent.value`), but the node positions
|
||||||
|
// aren't being updated as we're inserting new code. If we were to go through the AST,
|
||||||
|
// we'd have to update the `SourceFile` and start over after each operation.
|
||||||
|
findParentAccesses(typeChecker, sourceFile)
|
||||||
|
.sort((a, b) => a.getStart() - b.getStart())
|
||||||
|
.forEach((node, index) => {
|
||||||
|
const update = tree.beginUpdate(relative(basePath, sourceFile.fileName));
|
||||||
|
update.insertRight(node.getStart() + node.getWidth() + index, '!');
|
||||||
|
tree.commitUpdate(update);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
|
@ -0,0 +1,155 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google LLC 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 {normalize} from 'path';
|
||||||
|
import * as ts from 'typescript';
|
||||||
|
|
||||||
|
/** Names of symbols from `@angular/forms` whose `parent` accesses have to be migrated. */
|
||||||
|
const abstractControlSymbols = new Set<string>([
|
||||||
|
'AbstractControl',
|
||||||
|
'FormArray',
|
||||||
|
'FormControl',
|
||||||
|
'FormGroup',
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the `PropertyAccessExpression`-s that are accessing the `parent` property in
|
||||||
|
* such a way that may result in a compilation error after the v11 type changes.
|
||||||
|
*/
|
||||||
|
export function findParentAccesses(
|
||||||
|
typeChecker: ts.TypeChecker, sourceFile: ts.SourceFile): ts.PropertyAccessExpression[] {
|
||||||
|
const results: ts.PropertyAccessExpression[] = [];
|
||||||
|
|
||||||
|
sourceFile.forEachChild(function walk(node: ts.Node) {
|
||||||
|
if (ts.isPropertyAccessExpression(node) && node.name.text === 'parent' && !isNullCheck(node) &&
|
||||||
|
!isSafeAccess(node) && results.indexOf(node) === -1 &&
|
||||||
|
isAbstractControlReference(typeChecker, node) && isNullableType(typeChecker, node)) {
|
||||||
|
results.unshift(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
node.forEachChild(walk);
|
||||||
|
});
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Checks whether a node's type is nullable (`null`, `undefined` or `void`). */
|
||||||
|
function isNullableType(typeChecker: ts.TypeChecker, node: ts.Node) {
|
||||||
|
// Skip expressions in the form of `foo.bar!.baz` since the `TypeChecker` seems
|
||||||
|
// to identify them as null, even though the user indicated that it won't be.
|
||||||
|
if (node.parent && ts.isNonNullExpression(node.parent)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const type = typeChecker.getTypeAtLocation(node);
|
||||||
|
const typeNode = typeChecker.typeToTypeNode(type, undefined, ts.NodeBuilderFlags.None);
|
||||||
|
let hasSeenNullableType = false;
|
||||||
|
|
||||||
|
// Trace the type of the node back to a type node, walk
|
||||||
|
// through all of its sub-nodes and look for nullable tyes.
|
||||||
|
if (typeNode) {
|
||||||
|
(function walk(current: ts.Node) {
|
||||||
|
if (current.kind === ts.SyntaxKind.NullKeyword ||
|
||||||
|
current.kind === ts.SyntaxKind.UndefinedKeyword ||
|
||||||
|
current.kind === ts.SyntaxKind.VoidKeyword) {
|
||||||
|
hasSeenNullableType = true;
|
||||||
|
// Note that we don't descend into type literals, because it may cause
|
||||||
|
// us to mis-identify the root type as nullable, because it has a nullable
|
||||||
|
// property (e.g. `{ foo: string | null }`).
|
||||||
|
} else if (!hasSeenNullableType && !ts.isTypeLiteralNode(current)) {
|
||||||
|
current.forEachChild(walk);
|
||||||
|
}
|
||||||
|
})(typeNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
return hasSeenNullableType;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks whether a particular node is part of a null check. E.g. given:
|
||||||
|
* `control.parent ? control.parent.value : null` the null check would be `control.parent`.
|
||||||
|
*/
|
||||||
|
function isNullCheck(node: ts.PropertyAccessExpression): boolean {
|
||||||
|
if (!node.parent) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// `control.parent && control.parent.value` where `node` is `control.parent`.
|
||||||
|
if (ts.isBinaryExpression(node.parent) && node.parent.left === node) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// `control.parent && control.parent.parent && control.parent.parent.value`
|
||||||
|
// where `node` is `control.parent`.
|
||||||
|
if (node.parent.parent && ts.isBinaryExpression(node.parent.parent) &&
|
||||||
|
node.parent.parent.left === node.parent) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// `if (control.parent) {...}` where `node` is `control.parent`.
|
||||||
|
if (ts.isIfStatement(node.parent) && node.parent.expression === node) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// `control.parent ? control.parent.value : null` where `node` is `control.parent`.
|
||||||
|
if (ts.isConditionalExpression(node.parent) && node.parent.condition === node) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Checks whether a property access is safe (e.g. `foo.parent?.value`). */
|
||||||
|
function isSafeAccess(node: ts.PropertyAccessExpression): boolean {
|
||||||
|
return node.parent != null && ts.isPropertyAccessExpression(node.parent) &&
|
||||||
|
node.parent.expression === node && node.parent.questionDotToken != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Checks whether a property access is on an `AbstractControl` coming from `@angular/forms`. */
|
||||||
|
function isAbstractControlReference(
|
||||||
|
typeChecker: ts.TypeChecker, node: ts.PropertyAccessExpression): boolean {
|
||||||
|
let current: ts.Expression = node;
|
||||||
|
const formsPattern = /node_modules\/?.*\/@angular\/forms/;
|
||||||
|
// Walks up the property access chain and tries to find a symbol tied to a `SourceFile`.
|
||||||
|
// If such a node is found, we check whether the type is one of the `AbstractControl` symbols
|
||||||
|
// and whether it comes from the `@angular/forms` directory in the `node_modules`.
|
||||||
|
while (ts.isPropertyAccessExpression(current)) {
|
||||||
|
const type = typeChecker.getTypeAtLocation(current.expression);
|
||||||
|
const symbol = type.getSymbol();
|
||||||
|
if (symbol && type) {
|
||||||
|
const sourceFile = symbol.valueDeclaration?.getSourceFile();
|
||||||
|
return sourceFile != null &&
|
||||||
|
formsPattern.test(normalize(sourceFile.fileName).replace(/\\/g, '/')) &&
|
||||||
|
hasAbstractControlType(typeChecker, type);
|
||||||
|
}
|
||||||
|
current = current.expression;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Walks through the sub-types of a type, looking for a type that
|
||||||
|
* has the same name as one of the `AbstractControl` types.
|
||||||
|
*/
|
||||||
|
function hasAbstractControlType(typeChecker: ts.TypeChecker, type: ts.Type): boolean {
|
||||||
|
const typeNode = typeChecker.typeToTypeNode(type, undefined, ts.NodeBuilderFlags.None);
|
||||||
|
let hasMatch = false;
|
||||||
|
if (typeNode) {
|
||||||
|
(function walk(current: ts.Node) {
|
||||||
|
if (ts.isIdentifier(current) && abstractControlSymbols.has(current.text)) {
|
||||||
|
hasMatch = true;
|
||||||
|
// Note that we don't descend into type literals, because it may cause
|
||||||
|
// us to mis-identify the root type as nullable, because it has a nullable
|
||||||
|
// property (e.g. `{ foo: FormControl }`).
|
||||||
|
} else if (!hasMatch && !ts.isTypeLiteralNode(current)) {
|
||||||
|
current.forEachChild(walk);
|
||||||
|
}
|
||||||
|
})(typeNode);
|
||||||
|
}
|
||||||
|
return hasMatch;
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ ts_library(
|
||||||
"//packages/core/schematics:migrations.json",
|
"//packages/core/schematics:migrations.json",
|
||||||
],
|
],
|
||||||
deps = [
|
deps = [
|
||||||
|
"//packages/core/schematics/migrations/abstract-control-parent",
|
||||||
"//packages/core/schematics/migrations/dynamic-queries",
|
"//packages/core/schematics/migrations/dynamic-queries",
|
||||||
"//packages/core/schematics/migrations/missing-injectable",
|
"//packages/core/schematics/migrations/missing-injectable",
|
||||||
"//packages/core/schematics/migrations/module-with-providers",
|
"//packages/core/schematics/migrations/module-with-providers",
|
||||||
|
|
|
@ -0,0 +1,289 @@
|
||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright Google LLC 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('AbstractControl.parent 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'], strictNullChecks: true},
|
||||||
|
}));
|
||||||
|
writeFile('/angular.json', JSON.stringify({
|
||||||
|
projects: {t: {architect: {build: {options: {tsConfig: './tsconfig.json'}}}}}
|
||||||
|
}));
|
||||||
|
// We need to declare the Angular symbols we're testing for, otherwise type checking won't work.
|
||||||
|
writeFile('/node_modules/@angular/forms/index.d.ts', `
|
||||||
|
export declare abstract class AbstractControl {
|
||||||
|
get dirty(): boolean;
|
||||||
|
get disabled(): boolean;
|
||||||
|
get parent(): FormGroup | FormArray | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare class FormArray extends AbstractControl {
|
||||||
|
getRawValue(): any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare class FormControl extends AbstractControl {
|
||||||
|
setValue(value: any): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare class FormGroup extends AbstractControl {
|
||||||
|
getRawValue(): any;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Fake non-Angular package to make sure that we don't migrate packages we don't own.
|
||||||
|
writeFile('/node_modules/@not-angular/forms/index.d.ts', `
|
||||||
|
export declare abstract class AbstractControl {
|
||||||
|
get dirty(): boolean;
|
||||||
|
get disabled(): boolean;
|
||||||
|
get parent(): FormGroup | FormArray | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export declare class FormControl extends AbstractControl {
|
||||||
|
setValue(value: any): void;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
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 add non-null assertions to accesses of AbstractControl.parent', async () => {
|
||||||
|
writeFile('/index.ts', `
|
||||||
|
import {AbstractControl} from '@angular/forms';
|
||||||
|
|
||||||
|
class App {
|
||||||
|
private _control: AbstractControl;
|
||||||
|
|
||||||
|
getParentValue() {
|
||||||
|
return this._control.parent.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
await runMigration();
|
||||||
|
expect(tree.readContent('/index.ts')).toContain(`return this._control.parent!.value;`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add non-null assertions to accesses of FormArray.parent', async () => {
|
||||||
|
writeFile('/index.ts', `
|
||||||
|
import {FormArray} from '@angular/forms';
|
||||||
|
|
||||||
|
class App {
|
||||||
|
getParentValueOf(control: FormArray) {
|
||||||
|
return control.parent.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
await runMigration();
|
||||||
|
expect(tree.readContent('/index.ts')).toContain(`return control.parent!.value;`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add non-null assertions to accesses of FormControl.parent', async () => {
|
||||||
|
writeFile('/index.ts', `
|
||||||
|
import {FormControl} from '@angular/forms';
|
||||||
|
|
||||||
|
class App {
|
||||||
|
getBlankControlParentValue() {
|
||||||
|
return this._getControl().parent.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getControl() {
|
||||||
|
return new FormControl();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
await runMigration();
|
||||||
|
expect(tree.readContent('/index.ts')).toContain(`return this._getControl().parent!.value;`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add non-null assertions to accesses of FormGroup.parent', async () => {
|
||||||
|
writeFile('/index.ts', `
|
||||||
|
import {FormGroup} from '@angular/forms';
|
||||||
|
|
||||||
|
class App {
|
||||||
|
getGlobalGroupParentValue() {
|
||||||
|
const parent = (window.foo as FormGroup).parent;
|
||||||
|
return parent.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
await runMigration();
|
||||||
|
expect(tree.readContent('/index.ts'))
|
||||||
|
.toContain(`const parent = (window.foo as FormGroup).parent!;`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add non-null assertions to nested accesses of `AbstractControl.parent`', async () => {
|
||||||
|
writeFile('/index.ts', `
|
||||||
|
import {FormControl} from '@angular/forms';
|
||||||
|
|
||||||
|
class App {
|
||||||
|
private _control = new FormControl();
|
||||||
|
|
||||||
|
getGreatGrandParentValue() {
|
||||||
|
return this._control.parent.parent.parent.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
await runMigration();
|
||||||
|
expect(tree.readContent('/index.ts'))
|
||||||
|
.toContain(`return this._control.parent!.parent!.parent!.value;`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not add non-null assertions if the `parent` has been null checked in an if statement',
|
||||||
|
async () => {
|
||||||
|
writeFile('/index.ts', `
|
||||||
|
import {FormControl} from '@angular/forms';
|
||||||
|
|
||||||
|
function getParentValue(control: FormControl) {
|
||||||
|
if (control.parent) {
|
||||||
|
return control.parent.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
await runMigration();
|
||||||
|
|
||||||
|
const content = tree.readContent('/index.ts');
|
||||||
|
expect(content).toContain(`if (control.parent) {`);
|
||||||
|
expect(content).toContain(`return control.parent.value;`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not add non-null assertions if the `parent` has been null checked in an else if statement',
|
||||||
|
async () => {
|
||||||
|
writeFile('/index.ts', `
|
||||||
|
import {FormControl} from '@angular/forms';
|
||||||
|
|
||||||
|
function getParentValue(foo: boolean, control: FormControl) {
|
||||||
|
if (foo) {
|
||||||
|
return foo;
|
||||||
|
} else if (control.parent) {
|
||||||
|
return control.parent.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
await runMigration();
|
||||||
|
|
||||||
|
const content = tree.readContent('/index.ts');
|
||||||
|
expect(content).toContain(`} else if (control.parent) {`);
|
||||||
|
expect(content).toContain(`return control.parent.value;`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not add non-null assertions if the `parent` has been null checked in a ternary expression',
|
||||||
|
async () => {
|
||||||
|
writeFile('/index.ts', `
|
||||||
|
import {FormControl} from '@angular/forms';
|
||||||
|
|
||||||
|
function getParentValue(control: FormControl) {
|
||||||
|
return control.parent ? control.parent.value : null;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
await runMigration();
|
||||||
|
|
||||||
|
expect(tree.readContent('/index.ts'))
|
||||||
|
.toContain(`return control.parent ? control.parent.value : null;`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not add non-null assertions if a nested `parent` has been null checked', async () => {
|
||||||
|
writeFile('/index.ts', `
|
||||||
|
import {FormControl} from '@angular/forms';
|
||||||
|
|
||||||
|
function getGreatGrandParentValue(control: FormControl) {
|
||||||
|
return control.parent && control.parent.parent && control.parent.parent.parent && control.parent.parent.parent.value;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
await runMigration();
|
||||||
|
|
||||||
|
expect(tree.readContent('/index.ts'))
|
||||||
|
.toContain(
|
||||||
|
`return control.parent && control.parent.parent && control.parent.parent.parent && control.parent.parent.parent.value;`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not add non-null assertions if there is one already', async () => {
|
||||||
|
writeFile('/index.ts', `
|
||||||
|
import {FormControl} from '@angular/forms';
|
||||||
|
|
||||||
|
function getParentValue(control: FormControl) {
|
||||||
|
return control.parent!.value;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
await runMigration();
|
||||||
|
expect(tree.readContent('/index.ts')).toContain(`return control.parent!.value;`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not add non-null assertions if there is a safe access', async () => {
|
||||||
|
writeFile('/index.ts', `
|
||||||
|
import {FormControl} from '@angular/forms';
|
||||||
|
|
||||||
|
function getParentValue(control: FormControl) {
|
||||||
|
return control.parent?.value;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
await runMigration();
|
||||||
|
expect(tree.readContent('/index.ts')).toContain(`return control.parent?.value;`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not add non-null assertions if the symbol does not come from @angular/forms',
|
||||||
|
async () => {
|
||||||
|
writeFile('/index.ts', `
|
||||||
|
import {FormControl} from '@not-angular/forms';
|
||||||
|
|
||||||
|
function getParentValue(control: FormControl) {
|
||||||
|
return control.parent.value;
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
|
||||||
|
await runMigration();
|
||||||
|
expect(tree.readContent('/index.ts')).toContain(`return control.parent.value;`);
|
||||||
|
});
|
||||||
|
|
||||||
|
function writeFile(filePath: string, contents: string) {
|
||||||
|
host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents));
|
||||||
|
}
|
||||||
|
|
||||||
|
function runMigration() {
|
||||||
|
return runner.runSchematicAsync('migration-v11-abstract-control-parent', {}, tree).toPromise();
|
||||||
|
}
|
||||||
|
});
|
Loading…
Reference in New Issue