feat(router): add migration for ActivatedRouteSnapshot.fragment (#41092)
Adds a migration that casts the value of `ActivatedRouteSnapshot.fragment` to be non-nullable. Also moves some code from the `AbstractControl.parent` migration so that it can be reused. Relates to #37336. PR Close #41092
This commit is contained in:
		
							parent
							
								
									1eba57eb00
								
							
						
					
					
						commit
						190fa07b9a
					
				| @ -11,6 +11,7 @@ pkg_npm( | |||||||
|     visibility = ["//packages/core:__pkg__"], |     visibility = ["//packages/core:__pkg__"], | ||||||
|     deps = [ |     deps = [ | ||||||
|         "//packages/core/schematics/migrations/abstract-control-parent", |         "//packages/core/schematics/migrations/abstract-control-parent", | ||||||
|  |         "//packages/core/schematics/migrations/activated-route-snapshot-fragment", | ||||||
|         "//packages/core/schematics/migrations/can-activate-with-redirect-to", |         "//packages/core/schematics/migrations/can-activate-with-redirect-to", | ||||||
|         "//packages/core/schematics/migrations/dynamic-queries", |         "//packages/core/schematics/migrations/dynamic-queries", | ||||||
|         "//packages/core/schematics/migrations/initial-navigation", |         "//packages/core/schematics/migrations/initial-navigation", | ||||||
|  | |||||||
| @ -84,6 +84,11 @@ | |||||||
|       "version": "11.1.0-beta", |       "version": "11.1.0-beta", | ||||||
|       "description": "Removes `canActivate` from a `Route` config when `redirectTo` is also present", |       "description": "Removes `canActivate` from a `Route` config when `redirectTo` is also present", | ||||||
|       "factory": "./migrations/can-activate-with-redirect-to/index" |       "factory": "./migrations/can-activate-with-redirect-to/index" | ||||||
|  |     }, | ||||||
|  |     "migration-v12-activated-route-snapshot-fragment": { | ||||||
|  |       "version": "12.0.0-beta", | ||||||
|  |       "description": "In Angular version 12, the type of ActivatedRouteSnapshot.fragment is nullable. This migration automatically adds non-null assertions to it.", | ||||||
|  |       "factory": "./migrations/activated-route-snapshot-fragment/index" | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @ -8,14 +8,11 @@ | |||||||
| 
 | 
 | ||||||
| import {normalize} from 'path'; | import {normalize} from 'path'; | ||||||
| import * as ts from 'typescript'; | import * as ts from 'typescript'; | ||||||
|  | import {isNullCheck, isSafeAccess} from '../../utils/typescript/nodes'; | ||||||
|  | import {hasOneOfTypes, isNullableType} from '../../utils/typescript/symbol'; | ||||||
| 
 | 
 | ||||||
| /** Names of symbols from `@angular/forms` whose `parent` accesses have to be migrated. */ | /** Names of symbols from `@angular/forms` whose `parent` accesses have to be migrated. */ | ||||||
| const abstractControlSymbols = new Set<string>([ | const abstractControlSymbols = ['AbstractControl', 'FormArray', 'FormControl', 'FormGroup']; | ||||||
|   'AbstractControl', |  | ||||||
|   'FormArray', |  | ||||||
|   'FormControl', |  | ||||||
|   'FormGroup', |  | ||||||
| ]); |  | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Finds the `PropertyAccessExpression`-s that are accessing the `parent` property in |  * Finds the `PropertyAccessExpression`-s that are accessing the `parent` property in | ||||||
| @ -38,78 +35,6 @@ export function findParentAccesses( | |||||||
|   return results; |   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`. */ | /** Checks whether a property access is on an `AbstractControl` coming from `@angular/forms`. */ | ||||||
| function isAbstractControlReference( | function isAbstractControlReference( | ||||||
|     typeChecker: ts.TypeChecker, node: ts.PropertyAccessExpression): boolean { |     typeChecker: ts.TypeChecker, node: ts.PropertyAccessExpression): boolean { | ||||||
| @ -119,37 +44,14 @@ function isAbstractControlReference( | |||||||
|   // If such a node is found, we check whether the type is one of the `AbstractControl` symbols
 |   // 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`.
 |   // and whether it comes from the `@angular/forms` directory in the `node_modules`.
 | ||||||
|   while (ts.isPropertyAccessExpression(current)) { |   while (ts.isPropertyAccessExpression(current)) { | ||||||
|     const type = typeChecker.getTypeAtLocation(current.expression); |     const symbol = typeChecker.getTypeAtLocation(current.expression)?.getSymbol(); | ||||||
|     const symbol = type.getSymbol(); |     if (symbol) { | ||||||
|     if (symbol && type) { |  | ||||||
|       const sourceFile = symbol.valueDeclaration?.getSourceFile(); |       const sourceFile = symbol.valueDeclaration?.getSourceFile(); | ||||||
|       return sourceFile != null && |       return sourceFile != null && | ||||||
|           formsPattern.test(normalize(sourceFile.fileName).replace(/\\/g, '/')) && |           formsPattern.test(normalize(sourceFile.fileName).replace(/\\/g, '/')) && | ||||||
|           hasAbstractControlType(typeChecker, type); |           hasOneOfTypes(typeChecker, current.expression, abstractControlSymbols); | ||||||
|     } |     } | ||||||
|     current = current.expression; |     current = current.expression; | ||||||
|   } |   } | ||||||
|   return false; |   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; |  | ||||||
| } |  | ||||||
|  | |||||||
| @ -0,0 +1,18 @@ | |||||||
|  | load("//tools:defaults.bzl", "ts_library") | ||||||
|  | 
 | ||||||
|  | ts_library( | ||||||
|  |     name = "activated-route-snapshot-fragment", | ||||||
|  |     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 @@ | |||||||
|  | ## `ActivatedRouteSnapshot.fragment` migration | ||||||
|  | 
 | ||||||
|  | The value if `ActivatedRouteSnapshot.fragment` is becoming nullable. This migration adds non-null | ||||||
|  | assertions to it. | ||||||
|  | 
 | ||||||
|  | #### Before | ||||||
|  | ```ts | ||||||
|  | import { Component } from '@angular/core'; | ||||||
|  | import { ActivatedRouteSnapshot } from '@angular/router'; | ||||||
|  | 
 | ||||||
|  | @Component({}) | ||||||
|  | export class YourComponent { | ||||||
|  |   private _activatedRouteSnapshot: ActivatedRouteSnapshot; | ||||||
|  | 
 | ||||||
|  |   getFragmentValue() { | ||||||
|  |     return this._activatedRouteSnapshot.fragment.value; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | #### After | ||||||
|  | ```ts | ||||||
|  | import { Component } from '@angular/core'; | ||||||
|  | import { ActivatedRoute } from '@angular/router'; | ||||||
|  | 
 | ||||||
|  | @Component({}) | ||||||
|  | export class YourComponent { | ||||||
|  |   private _activatedRouteSnapshot: ActivatedRouteSnapshot; | ||||||
|  | 
 | ||||||
|  |   getFragmentValue() { | ||||||
|  |     return this._activatedRouteSnapshot.fragment!.value; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | ``` | ||||||
| @ -0,0 +1,62 @@ | |||||||
|  | /** | ||||||
|  |  * @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 * as ts from 'typescript'; | ||||||
|  | 
 | ||||||
|  | import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths'; | ||||||
|  | import {canMigrateFile, createMigrationProgram} from '../../utils/typescript/compiler_host'; | ||||||
|  | import {findFragmentAccesses, migrateActivatedRouteSnapshotFragment} from './util'; | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Migration that marks accesses of `ActivatedRouteSnapshot.fragment` 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 ' + | ||||||
|  |           '`ActivatedRouteSnapshot.fragment` accesses.'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     for (const tsconfigPath of allPaths) { | ||||||
|  |       runActivatedRouteSnapshotFragmentMigration(tree, tsconfigPath, basePath); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | function runActivatedRouteSnapshotFragmentMigration( | ||||||
|  |     tree: Tree, tsconfigPath: string, basePath: string) { | ||||||
|  |   const {program} = createMigrationProgram(tree, tsconfigPath, basePath); | ||||||
|  |   const typeChecker = program.getTypeChecker(); | ||||||
|  |   const sourceFiles = | ||||||
|  |       program.getSourceFiles().filter(sourceFile => canMigrateFile(basePath, sourceFile, program)); | ||||||
|  |   const printer = ts.createPrinter(); | ||||||
|  | 
 | ||||||
|  |   sourceFiles.forEach(sourceFile => { | ||||||
|  |     const nodesToMigrate = findFragmentAccesses(typeChecker, sourceFile); | ||||||
|  | 
 | ||||||
|  |     if (nodesToMigrate.size > 0) { | ||||||
|  |       const update = tree.beginUpdate(relative(basePath, sourceFile.fileName)); | ||||||
|  |       nodesToMigrate.forEach(node => { | ||||||
|  |         update.remove(node.getStart(), node.getWidth()); | ||||||
|  |         update.insertRight( | ||||||
|  |             node.getStart(), | ||||||
|  |             printer.printNode( | ||||||
|  |                 ts.EmitHint.Unspecified, migrateActivatedRouteSnapshotFragment(node), sourceFile)); | ||||||
|  |       }); | ||||||
|  |       tree.commitUpdate(update); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  | } | ||||||
| @ -0,0 +1,39 @@ | |||||||
|  | /** | ||||||
|  |  * @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 * as ts from 'typescript'; | ||||||
|  | import {isNullCheck, isSafeAccess} from '../../utils/typescript/nodes'; | ||||||
|  | import {hasOneOfTypes, isNullableType} from '../../utils/typescript/symbol'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Finds all the accesses of `ActivatedRouteSnapshot.fragment` | ||||||
|  |  * that need to be migrated within a particular file. | ||||||
|  |  */ | ||||||
|  | export function findFragmentAccesses( | ||||||
|  |     typeChecker: ts.TypeChecker, sourceFile: ts.SourceFile): Set<ts.PropertyAccessExpression> { | ||||||
|  |   const results = new Set<ts.PropertyAccessExpression>(); | ||||||
|  | 
 | ||||||
|  |   sourceFile.forEachChild(function walk(node: ts.Node) { | ||||||
|  |     if (ts.isPropertyAccessExpression(node) && node.name.text === 'fragment' && | ||||||
|  |         !results.has(node) && !isNullCheck(node) && !isSafeAccess(node) && | ||||||
|  |         hasOneOfTypes(typeChecker, node.expression, ['ActivatedRouteSnapshot']) && | ||||||
|  |         isNullableType(typeChecker, node)) { | ||||||
|  |       results.add(node); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     node.forEachChild(walk); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   return results; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** Migrates an `ActivatedRouteSnapshot.fragment` access. */ | ||||||
|  | export function migrateActivatedRouteSnapshotFragment(node: ts.PropertyAccessExpression): ts.Node { | ||||||
|  |   // Turns `foo.fragment` into `foo.fragment!`.
 | ||||||
|  |   return ts.createNonNullExpression(node); | ||||||
|  | } | ||||||
| @ -6,6 +6,7 @@ ts_library( | |||||||
|     tsconfig = "//packages/core/schematics:tsconfig.json", |     tsconfig = "//packages/core/schematics:tsconfig.json", | ||||||
|     visibility = ["//packages/core/schematics/test/google3:__pkg__"], |     visibility = ["//packages/core/schematics/test/google3:__pkg__"], | ||||||
|     deps = [ |     deps = [ | ||||||
|  |         "//packages/core/schematics/migrations/activated-route-snapshot-fragment", | ||||||
|         "//packages/core/schematics/migrations/can-activate-with-redirect-to", |         "//packages/core/schematics/migrations/can-activate-with-redirect-to", | ||||||
|         "//packages/core/schematics/migrations/dynamic-queries", |         "//packages/core/schematics/migrations/dynamic-queries", | ||||||
|         "//packages/core/schematics/migrations/initial-navigation", |         "//packages/core/schematics/migrations/initial-navigation", | ||||||
|  | |||||||
| @ -0,0 +1,40 @@ | |||||||
|  | /** | ||||||
|  |  * @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 {Replacement, RuleFailure, Rules} from 'tslint'; | ||||||
|  | import * as ts from 'typescript'; | ||||||
|  | 
 | ||||||
|  | import {findFragmentAccesses, migrateActivatedRouteSnapshotFragment} from '../activated-route-snapshot-fragment/util'; | ||||||
|  | 
 | ||||||
|  | export class Rule extends Rules.TypedRule { | ||||||
|  |   applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] { | ||||||
|  |     if (sourceFile.isDeclarationFile || program.isSourceFileFromExternalLibrary(sourceFile)) { | ||||||
|  |       return []; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     const failures: RuleFailure[] = []; | ||||||
|  |     const typeChecker = program.getTypeChecker(); | ||||||
|  |     const nodesToMigrate = findFragmentAccesses(typeChecker, sourceFile); | ||||||
|  | 
 | ||||||
|  |     if (nodesToMigrate.size > 0) { | ||||||
|  |       const printer = ts.createPrinter(); | ||||||
|  |       nodesToMigrate.forEach(node => { | ||||||
|  |         const sourceFile = node.getSourceFile(); | ||||||
|  |         const migratedNode = migrateActivatedRouteSnapshotFragment(node); | ||||||
|  |         const replacement = new Replacement( | ||||||
|  |             node.getStart(), node.getWidth(), | ||||||
|  |             printer.printNode(ts.EmitHint.Unspecified, migratedNode, sourceFile)); | ||||||
|  |         failures.push(new RuleFailure( | ||||||
|  |             sourceFile, node.getStart(), node.getEnd(), | ||||||
|  |             '`ActivatedRouteSnapshot.fragment` is nullable.', this.ruleName, replacement)); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return failures; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -9,6 +9,7 @@ ts_library( | |||||||
|     ], |     ], | ||||||
|     deps = [ |     deps = [ | ||||||
|         "//packages/core/schematics/migrations/abstract-control-parent", |         "//packages/core/schematics/migrations/abstract-control-parent", | ||||||
|  |         "//packages/core/schematics/migrations/activated-route-snapshot-fragment", | ||||||
|         "//packages/core/schematics/migrations/can-activate-with-redirect-to", |         "//packages/core/schematics/migrations/can-activate-with-redirect-to", | ||||||
|         "//packages/core/schematics/migrations/dynamic-queries", |         "//packages/core/schematics/migrations/dynamic-queries", | ||||||
|         "//packages/core/schematics/migrations/initial-navigation", |         "//packages/core/schematics/migrations/initial-navigation", | ||||||
|  | |||||||
| @ -0,0 +1,186 @@ | |||||||
|  | /** | ||||||
|  |  * @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('ActivatedRouteSnapshot.fragment 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/router.d.ts', ` | ||||||
|  |       export declare class ActivatedRoute { | ||||||
|  |         get children(): ActivatedRoute[]; | ||||||
|  |         fragment: Observable<string | null>; | ||||||
|  |         snapshot: ActivatedRouteSnapshot; | ||||||
|  |         url: Observable<unknown[]>; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       export declare class ActivatedRouteSnapshot { | ||||||
|  |         fragment: string | null; | ||||||
|  |         url: unknown[]; | ||||||
|  |       } | ||||||
|  |     `);
 | ||||||
|  | 
 | ||||||
|  |     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 `ActivatedRouteSnapshot.fragment`', | ||||||
|  |      async () => { | ||||||
|  |        writeFile('/index.ts', ` | ||||||
|  |           import {ActivatedRoute} from '@angular/router'; | ||||||
|  | 
 | ||||||
|  |           class App { | ||||||
|  |             private _route: ActivatedRoute; | ||||||
|  | 
 | ||||||
|  |             getFragment() { | ||||||
|  |               return this._getSnapshot().fragment.foo; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             private _getSnapshot() { | ||||||
|  |               return this._route.snapshot; | ||||||
|  |             } | ||||||
|  |           } | ||||||
|  |         `);
 | ||||||
|  | 
 | ||||||
|  |        await runMigration(); | ||||||
|  | 
 | ||||||
|  |        expect(tree.readContent('/index.ts')).toContain('return this._getSnapshot().fragment!.foo'); | ||||||
|  |      }); | ||||||
|  | 
 | ||||||
|  |   it('should not add non-null assertions to accesses of `ActivatedRouteSnapshot.fragment` if there is one already', | ||||||
|  |      async () => { | ||||||
|  |        writeFile('/index.ts', ` | ||||||
|  |         import {ActivatedRoute} from '@angular/router'; | ||||||
|  | 
 | ||||||
|  |         class App { | ||||||
|  |           private _route: ActivatedRoute; | ||||||
|  | 
 | ||||||
|  |           getFragment() { | ||||||
|  |             return this._route.snapshot.fragment!.foo; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       `);
 | ||||||
|  | 
 | ||||||
|  |        await runMigration(); | ||||||
|  | 
 | ||||||
|  |        expect(tree.readContent('/index.ts')) | ||||||
|  |            .toContain('return this._route.snapshot.fragment!.foo;'); | ||||||
|  |      }); | ||||||
|  | 
 | ||||||
|  |   it('should not add non-null assertions if the `ActivatedRouteSnapshot.fragment` has been null checked in an if statement', | ||||||
|  |      async () => { | ||||||
|  |        writeFile('/index.ts', ` | ||||||
|  |         import {ActivatedRouteSnapshot} from '@angular/router'; | ||||||
|  | 
 | ||||||
|  |         function getFragmentValue(snapshot: ActivatedRouteSnapshot) { | ||||||
|  |           if (snapshot.fragment) { | ||||||
|  |             return snapshot.fragment.value; | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           return null; | ||||||
|  |         } | ||||||
|  |       `);
 | ||||||
|  | 
 | ||||||
|  |        await runMigration(); | ||||||
|  | 
 | ||||||
|  |        const content = tree.readContent('/index.ts'); | ||||||
|  |        expect(content).toContain(`if (snapshot.fragment) {`); | ||||||
|  |        expect(content).toContain(`return snapshot.fragment.value;`); | ||||||
|  |      }); | ||||||
|  | 
 | ||||||
|  |   it('should not add non-null assertions if the `ActivatedRouteSnapshot.fragment` has been null checked in an else if statement', | ||||||
|  |      async () => { | ||||||
|  |        writeFile('/index.ts', ` | ||||||
|  |         import {ActivatedRouteSnapshot} from '@angular/router'; | ||||||
|  | 
 | ||||||
|  |         function getSnapshotValue(foo: boolean, snapshot: ActivatedRouteSnapshot) { | ||||||
|  |           if (foo) { | ||||||
|  |             return foo; | ||||||
|  |           } else if (snapshot.fragment) { | ||||||
|  |             return snapshot.fragment.value; | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           return null; | ||||||
|  |         } | ||||||
|  |       `);
 | ||||||
|  | 
 | ||||||
|  |        await runMigration(); | ||||||
|  | 
 | ||||||
|  |        const content = tree.readContent('/index.ts'); | ||||||
|  |        expect(content).toContain(`} else if (snapshot.fragment) {`); | ||||||
|  |        expect(content).toContain(`return snapshot.fragment.value;`); | ||||||
|  |      }); | ||||||
|  | 
 | ||||||
|  |   it('should not add non-null assertions if the `ActivatedRouteSnapshot.fragment` has been null checked in a ternary expression', | ||||||
|  |      async () => { | ||||||
|  |        writeFile('/index.ts', ` | ||||||
|  |         import {ActivatedRouteSnapshot} from '@angular/router'; | ||||||
|  | 
 | ||||||
|  |         function getSnapshotValue(snapshot: ActivatedRouteSnapshot) { | ||||||
|  |           return snapshot.fragment ? snapshot.fragment.value : null; | ||||||
|  |         } | ||||||
|  |       `);
 | ||||||
|  | 
 | ||||||
|  |        await runMigration(); | ||||||
|  | 
 | ||||||
|  |        expect(tree.readContent('/index.ts')) | ||||||
|  |            .toContain(`return snapshot.fragment ? snapshot.fragment.value : null;`); | ||||||
|  |      }); | ||||||
|  | 
 | ||||||
|  |   it('should not add non-null assertion to `ActivatedRouteSnapshot.fragment` if there is a safe access', | ||||||
|  |      async () => { | ||||||
|  |        writeFile('/index.ts', ` | ||||||
|  |         import {ActivatedRouteSnapshot} from '@angular/router'; | ||||||
|  | 
 | ||||||
|  |         function getSnapshotValue(snapshot: ActivatedRouteSnapshot) { | ||||||
|  |           return snapshot.fragment?.value; | ||||||
|  |         } | ||||||
|  |       `);
 | ||||||
|  | 
 | ||||||
|  |        await runMigration(); | ||||||
|  |        expect(tree.readContent('/index.ts')).toContain(`return snapshot.fragment?.value;`); | ||||||
|  |      }); | ||||||
|  | 
 | ||||||
|  |   function writeFile(filePath: string, contents: string) { | ||||||
|  |     host.sync.write(normalize(filePath), virtualFs.stringToFileBuffer(contents)); | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   function runMigration() { | ||||||
|  |     return runner.runSchematicAsync('migration-v12-activated-route-snapshot-fragment', {}, tree) | ||||||
|  |         .toPromise(); | ||||||
|  |   } | ||||||
|  | }); | ||||||
| @ -0,0 +1,210 @@ | |||||||
|  | /** | ||||||
|  |  * @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 {readFileSync, writeFileSync} from 'fs'; | ||||||
|  | import {dirname, join} from 'path'; | ||||||
|  | import * as shx from 'shelljs'; | ||||||
|  | import {Configuration, Linter} from 'tslint'; | ||||||
|  | 
 | ||||||
|  | describe('Google3 ActivatedRouteSnapshot.fragment TSLint rule', () => { | ||||||
|  |   const rulesDirectory = | ||||||
|  |       dirname(require.resolve('../../migrations/google3/activatedRouteSnapshotFragmentRule')); | ||||||
|  | 
 | ||||||
|  |   let tmpDir: string; | ||||||
|  | 
 | ||||||
|  |   beforeEach(() => { | ||||||
|  |     tmpDir = join(process.env['TEST_TMPDIR']!, 'google3-test'); | ||||||
|  |     shx.mkdir('-p', tmpDir); | ||||||
|  | 
 | ||||||
|  |     // We need to declare the Angular symbols we're testing for, otherwise type checking won't work.
 | ||||||
|  |     writeFile('router.d.ts', ` | ||||||
|  |       export declare class ActivatedRoute { | ||||||
|  |         get children(): ActivatedRoute[]; | ||||||
|  |         fragment: Observable<string | null>; | ||||||
|  |         snapshot: ActivatedRouteSnapshot; | ||||||
|  |         url: Observable<UrlSegment[]>; | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       export declare class ActivatedRouteSnapshot { | ||||||
|  |         fragment: string | null; | ||||||
|  |         url: UrlSegment[]; | ||||||
|  |       } | ||||||
|  |     `);
 | ||||||
|  | 
 | ||||||
|  |     writeFile('tsconfig.json', JSON.stringify({ | ||||||
|  |       compilerOptions: { | ||||||
|  |         module: 'es2015', | ||||||
|  |         baseUrl: './', | ||||||
|  |         strictNullChecks: true, | ||||||
|  |         paths: { | ||||||
|  |           '@angular/router': ['router.d.ts'], | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |     })); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   afterEach(() => shx.rm('-r', tmpDir)); | ||||||
|  | 
 | ||||||
|  |   function runTSLint(fix: boolean) { | ||||||
|  |     const program = Linter.createProgram(join(tmpDir, 'tsconfig.json')); | ||||||
|  |     const linter = new Linter({fix, rulesDirectory: [rulesDirectory]}, program); | ||||||
|  |     const config = | ||||||
|  |         Configuration.parseConfigFile({rules: {'activated-route-snapshot-fragment': 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 accesses to `ActivatedRouteSnapshot.fragment`', () => { | ||||||
|  |     writeFile('/index.ts', ` | ||||||
|  |       import {ActivatedRoute} from '@angular/router'; | ||||||
|  | 
 | ||||||
|  |       class App { | ||||||
|  |         private _route: ActivatedRoute; | ||||||
|  | 
 | ||||||
|  |         ngOnInit() { | ||||||
|  |           this._route.fragment.subscribe(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         getFragment() { | ||||||
|  |           return this._route.snapshot.fragment.foo; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |   `);
 | ||||||
|  | 
 | ||||||
|  |     const linter = runTSLint(false); | ||||||
|  |     const failures = linter.getResult().failures.map(failure => failure.getFailure()); | ||||||
|  |     expect(failures).toEqual(['`ActivatedRouteSnapshot.fragment` is nullable.']); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('should add non-null assertions to accesses of `ActivatedRouteSnapshot.fragment`', () => { | ||||||
|  |     writeFile('/index.ts', ` | ||||||
|  |       import {ActivatedRoute} from '@angular/router'; | ||||||
|  | 
 | ||||||
|  |       class App { | ||||||
|  |         private _route: ActivatedRoute; | ||||||
|  | 
 | ||||||
|  |         getFragment() { | ||||||
|  |           return this._getSnapshot().fragment.foo; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         private _getSnapshot() { | ||||||
|  |           return this._route.snapshot; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     `);
 | ||||||
|  | 
 | ||||||
|  |     runTSLint(true); | ||||||
|  | 
 | ||||||
|  |     expect(getFile('/index.ts')).toContain('return this._getSnapshot().fragment!.foo'); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('should not add non-null assertions to accesses of `ActivatedRouteSnapshot.fragment` if there is one already', | ||||||
|  |      () => { | ||||||
|  |        writeFile('/index.ts', ` | ||||||
|  |         import {ActivatedRoute} from '@angular/router'; | ||||||
|  | 
 | ||||||
|  |         class App { | ||||||
|  |           private _route: ActivatedRoute; | ||||||
|  | 
 | ||||||
|  |           getFragment() { | ||||||
|  |             return this._route.snapshot.fragment!.foo; | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       `);
 | ||||||
|  | 
 | ||||||
|  |        runTSLint(true); | ||||||
|  | 
 | ||||||
|  |        expect(getFile('/index.ts')).toContain('return this._route.snapshot.fragment!.foo;'); | ||||||
|  |      }); | ||||||
|  | 
 | ||||||
|  |   it('should not add non-null assertions if the `ActivatedRouteSnapshot.fragment` has been null checked in an if statement', | ||||||
|  |      () => { | ||||||
|  |        writeFile('/index.ts', ` | ||||||
|  |         import {ActivatedRouteSnapshot} from '@angular/router'; | ||||||
|  | 
 | ||||||
|  |         function getFragmentValue(snapshot: ActivatedRouteSnapshot) { | ||||||
|  |           if (snapshot.fragment) { | ||||||
|  |             return snapshot.fragment.value; | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           return null; | ||||||
|  |         } | ||||||
|  |       `);
 | ||||||
|  | 
 | ||||||
|  |        runTSLint(true); | ||||||
|  | 
 | ||||||
|  |        const content = getFile('/index.ts'); | ||||||
|  |        expect(content).toContain(`if (snapshot.fragment) {`); | ||||||
|  |        expect(content).toContain(`return snapshot.fragment.value;`); | ||||||
|  |      }); | ||||||
|  | 
 | ||||||
|  |   it('should not add non-null assertions if the `ActivatedRouteSnapshot.fragment` has been null checked in an else if statement', | ||||||
|  |      () => { | ||||||
|  |        writeFile('/index.ts', ` | ||||||
|  |         import {ActivatedRouteSnapshot} from '@angular/router'; | ||||||
|  | 
 | ||||||
|  |         function getSnapshotValue(foo: boolean, snapshot: ActivatedRouteSnapshot) { | ||||||
|  |           if (foo) { | ||||||
|  |             return foo; | ||||||
|  |           } else if (snapshot.fragment) { | ||||||
|  |             return snapshot.fragment.value; | ||||||
|  |           } | ||||||
|  | 
 | ||||||
|  |           return null; | ||||||
|  |         } | ||||||
|  |       `);
 | ||||||
|  | 
 | ||||||
|  |        runTSLint(true); | ||||||
|  | 
 | ||||||
|  |        const content = getFile('/index.ts'); | ||||||
|  |        expect(content).toContain(`} else if (snapshot.fragment) {`); | ||||||
|  |        expect(content).toContain(`return snapshot.fragment.value;`); | ||||||
|  |      }); | ||||||
|  | 
 | ||||||
|  |   it('should not add non-null assertions if the `ActivatedRouteSnapshot.fragment` has been null checked in a ternary expression', | ||||||
|  |      () => { | ||||||
|  |        writeFile('/index.ts', ` | ||||||
|  |         import {ActivatedRouteSnapshot} from '@angular/router'; | ||||||
|  | 
 | ||||||
|  |         function getSnapshotValue(snapshot: ActivatedRouteSnapshot) { | ||||||
|  |           return snapshot.fragment ? snapshot.fragment.value : null; | ||||||
|  |         } | ||||||
|  |       `);
 | ||||||
|  | 
 | ||||||
|  |        runTSLint(true); | ||||||
|  | 
 | ||||||
|  |        expect(getFile('/index.ts')) | ||||||
|  |            .toContain(`return snapshot.fragment ? snapshot.fragment.value : null;`); | ||||||
|  |      }); | ||||||
|  | 
 | ||||||
|  |   it('should not add non-null assertion to `ActivatedRouteSnapshot.fragment` if there is a safe access', | ||||||
|  |      () => { | ||||||
|  |        writeFile('/index.ts', ` | ||||||
|  |         import {ActivatedRouteSnapshot} from '@angular/router'; | ||||||
|  | 
 | ||||||
|  |         function getSnapshotValue(snapshot: ActivatedRouteSnapshot) { | ||||||
|  |           return snapshot.fragment?.value; | ||||||
|  |         } | ||||||
|  |       `);
 | ||||||
|  | 
 | ||||||
|  |        runTSLint(true); | ||||||
|  |        expect(getFile('/index.ts')).toContain(`return snapshot.fragment?.value;`); | ||||||
|  |      }); | ||||||
|  | }); | ||||||
| @ -26,3 +26,43 @@ export function closestNode<T extends ts.Node>(node: ts.Node, kind: ts.SyntaxKin | |||||||
| 
 | 
 | ||||||
|   return null; |   return null; | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Checks whether a particular node is part of a null check. E.g. given: | ||||||
|  |  * `foo.bar ? foo.bar.value : null` the null check would be `foo.bar`. | ||||||
|  |  */ | ||||||
|  | export function isNullCheck(node: ts.Node): boolean { | ||||||
|  |   if (!node.parent) { | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // `foo.bar && foo.bar.value` where `node` is `foo.bar`.
 | ||||||
|  |   if (ts.isBinaryExpression(node.parent) && node.parent.left === node) { | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // `foo.bar && foo.bar.parent && foo.bar.parent.value`
 | ||||||
|  |   // where `node` is `foo.bar`.
 | ||||||
|  |   if (node.parent.parent && ts.isBinaryExpression(node.parent.parent) && | ||||||
|  |       node.parent.parent.left === node.parent) { | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // `if (foo.bar) {...}` where `node` is `foo.bar`.
 | ||||||
|  |   if (ts.isIfStatement(node.parent) && node.parent.expression === node) { | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   // `foo.bar ? foo.bar.value : null` where `node` is `foo.bar`.
 | ||||||
|  |   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`). */ | ||||||
|  | export function isSafeAccess(node: ts.Node): boolean { | ||||||
|  |   return node.parent != null && ts.isPropertyAccessExpression(node.parent) && | ||||||
|  |       node.parent.expression === node && node.parent.questionDotToken != null; | ||||||
|  | } | ||||||
|  | |||||||
| @ -27,3 +27,57 @@ export function isReferenceToImport( | |||||||
|   return !!(nodeSymbol && importSymbol) && |   return !!(nodeSymbol && importSymbol) && | ||||||
|       nodeSymbol.valueDeclaration === importSymbol.valueDeclaration; |       nodeSymbol.valueDeclaration === importSymbol.valueDeclaration; | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | /** Checks whether a node's type is nullable (`null`, `undefined` or `void`). */ | ||||||
|  | export 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; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Walks through the types and sub-types of a node, looking for a | ||||||
|  |  * type that has the same name as one of the passed-in ones. | ||||||
|  |  */ | ||||||
|  | export function hasOneOfTypes( | ||||||
|  |     typeChecker: ts.TypeChecker, node: ts.Node, types: string[]): boolean { | ||||||
|  |   const type = typeChecker.getTypeAtLocation(node); | ||||||
|  |   const typeNode = | ||||||
|  |       type ? typeChecker.typeToTypeNode(type, undefined, ts.NodeBuilderFlags.None) : undefined; | ||||||
|  |   let hasMatch = false; | ||||||
|  |   if (typeNode) { | ||||||
|  |     (function walk(current: ts.Node) { | ||||||
|  |       if (ts.isIdentifier(current) && types.includes(current.text)) { | ||||||
|  |         hasMatch = true; | ||||||
|  |       } else if (!hasMatch && !ts.isTypeLiteralNode(current)) { | ||||||
|  |         current.forEachChild(walk); | ||||||
|  |       } | ||||||
|  |     })(typeNode); | ||||||
|  |   } | ||||||
|  |   return hasMatch; | ||||||
|  | } | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user