8000 feat(core): add ModuleWithProviders generic type migration by CaerusKaru · Pull Request #33217 · angular/angular · GitHub
[go: up one dir, main page]

Skip to content

feat(core): add ModuleWithProviders generic type migration #33217

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/core/schematics/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ npm_package(
deps = [
"//packages/core/schematics/migrations/dynamic-queries",
"//packages/core/schematics/migrations/missing-injectable",
"//packages/core/schematics/migrations/module-with-providers",
"//packages/core/schematics/migrations/move-document",
"//packages/core/schematics/migrations/postinstall-ngcc",
"//packages/core/schematics/migrations/renderer-to-renderer2",
Expand Down
5 changes: 5 additions & 0 deletions packages/core/schematics/migrations.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@
"version": "9-beta",
"description": "Adds an ngcc call as a postinstall hook in package.json",
"factory": "./migrations/postinstall-ngcc/index"
},
"migration-v9-module-with-providers": {
"version": "9.0.0-beta",
"description": "Adds explicit typing to `ModuleWithProviders`",
"factory": "./migrations/module-with-providers/index"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
load("//tools:defaults.bzl", "ts_library")

ts_library(
name = "module-with-providers",
srcs = glob(["**/*.ts"]),
tsconfig = "//packages/core/schematics:tsconfig.json",
visibility = [
"//packages/core/schematics:__pkg__",
"//packages/core/schematics/test:__pkg__",
],
deps = [
"//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/partial_evaluator",
"//packages/compiler-cli/src/ngtsc/reflection",
"//packages/core/schematics/utils",
"@npm//@angular-devkit/schematics",
"@npm//@types/node",
"@npm//typescript",
],
)
28 changes: 28 additions & 0 deletions 10000 packages/core/schematics/migrations/module-with-providers/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
## ModuleWithProviders migration

`ModuleWithProviders` type will not default to the `any` type for its generic in a future version of Angular.
This migration adds a generic to any `ModuleWithProvider` types found.

#### Before
```ts
import { NgModule, ModuleWithProviders } from '@angular/core';

@NgModule({})
export class MyModule {
static forRoot(): ModuleWithProviders {
ngModule: MyModule
}
}
```

#### After
```ts
import { NgModule, ModuleWithProviders } from '@angular/core';

@NgModule({})
export class MyModule {
static forRoot(): ModuleWithProviders<MyModule> {
ngModule: MyModule
}
}
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/**
* @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 {NgDecorator, getAngularDecorators} from '../../utils/ng_decorators';
import {isModuleWithProvidersNotGeneric} from './util';

export interface ResolvedNgModule {
name: string;
node: ts.ClassDeclaration;
decorator: NgDecorator;
/**
* List of found static method declarations on the module which do not
* declare an explicit return type.
*/
staticMethodsWithoutType: ts.MethodDeclaration[];
}

/**
* Visitor that walks through specified TypeScript nodes and collects all
* found NgModule static methods without types and all ModuleWithProviders
* usages without generic types attached.
*/
export class Collector {
resolvedModules: ResolvedNgModule[] = [];
resolvedNonGenerics: ts.TypeReferenceNode[] = [];

constructor(public typeChecker: ts.TypeChecker) {}

visitNode(node: ts.Node) {
if (ts.isClassDeclaration(node)) {
this.visitClassDeclaration(node);
} else if (isModuleWithProvidersNotGeneric(this.typeChecker, node)) {
this.resolvedNonGenerics.push(node);
}

ts.forEachChild(node, n => this.visitNode(n));
}

private visitClassDeclaration(node: ts.ClassDeclaration) {
if (!node.decorators || !node.decorators.length) {
return;
}

const ngDecorators = getAngularDecorators(this.typeChecker, node.decorators);
const ngModuleDecorator = ngDecorators.find(({name}) => name === 'NgModule');

if (ngModuleDecorator) {
this._visitNgModuleClass(node, ngModuleDecorator);
}
}

private _visitNgModuleClass(node: ts.ClassDeclaration, decorator: NgDecorator) {
const decoratorCall = decorator.node.expression;
const metadata = decoratorCall.arguments[0];

if (!metadata || !ts.isObjectLiteralExpression(metadata)) {
return;
}

this.resolvedModules.push({
name: node.name ? node.name.text : 'default',
node,
decorator,
staticMethodsWithoutType: node.members.filter(isStaticMethodNoType),
});
}
}

function isStaticMethodNoType(node: ts.ClassElement): node is ts.MethodDeclaration {
return ts.isMethodDeclaration(node) && !!node.modifiers &&
node.modifiers.findIndex(m => m.kind === ts.SyntaxKind.StaticKeyword) > -1 && !node.type;
}
101 changes: 101 additions & 0 deletions packages/core/schematics/migrations/module-with-providers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/**
* @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, UpdateRecorder} from '@angular-devkit/schematics';
import {dirname, relative} from 'path';
import * as ts from 'typescript';

import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
import {createMigrationCompilerHost} from '../../utils/typescript/compiler_host';
import {parseTsconfigFile} from '../../utils/typescript/parse_tsconfig';

import {Collector} from './collector';
import {AnalysisFailure, ModuleWithProvidersTransform} from './transform';



/**
* Runs the ModuleWithProviders 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];
const failures: string[] = [];

ctx.logger.info('------ ModuleWithProviders migration ------');

if (!allPaths.length) {
throw new SchematicsException(
'Could not find any tsconfig file. Cannot migrate ModuleWithProviders.');
}

for (const tsconfigPath of allPaths) {
failures.push(...runModuleWithProvidersMigration(tree, tsconfigPath, basePath));
}

if (failures.length) {
ctx.logger.info('Could not migrate all instances of ModuleWithProviders');
ctx.logger.info('Please manually fix the following failures:');
failures.forEach(message => ctx.logger.warn(`⮑ ${message}`));
} else {
ctx.logger.info('Successfully migrated all found ModuleWithProviders.');
}

ctx.logger.info('----------------------------------------------');
};
}

function runModuleWithProvidersMigration(tree: Tree, tsconfigPath: string, basePath: string) {
const parsed = parseTsconfigFile(tsconfigPath, dirname(tsconfigPath));
const host = createMigrationCompilerHost(tree, parsed.options, basePath);
const failures: string[] = [];

const program = ts.createProgram(parsed.fileNames, parsed.options, host);
const typeChecker = program.getTypeChecker();
const collector = new Collector(typeChecker);
const sourceFiles = program.getSourceFiles().filter(
f => !f.isDeclarationFile && !program.isSourceFileFromExternalLibrary(f));

// Analyze source files by detecting all modules.
sourceFiles.forEach(sourceFile => collector.visitNode(sourceFile));

const {resolvedModules, resolvedNonGenerics} = collector;
const transformer = new ModuleWithProvidersTransform(typeChecker, getUpdateRecorder);
const updateRecorders = new Map<ts.SourceFile, UpdateRecorder>();

[...resolvedModules.reduce(
(failures, m) => failures.concat(transformer.migrateModule(m)), [] as AnalysisFailure[]),
...resolvedNonGenerics.reduce(
(failures, t) => failures.concat(transformer.migrateType(t)), [] as AnalysisFailure[])]
.forEach(({message, node}) => {
const nodeSourceFile = node.getSourceFile();
const relativeFilePath = relative(basePath, nodeSourceFile.fileName);
const {line, character} =
ts.getLineAndCharacterOfPosition(node.getSourceFile(), node.getStart());
failures.push(`${relativeFilePath}@${line + 1}:${character + 1}: ${message}`);
});

// Walk through each update recorder and commit the update. We need to commit the
// updates in batches per source file as there can be only one recorder per source
// file in order to avoid shift character offsets.
updateRecorders.forEach(recorder => tree.commitUpdate(recorder));

return failures;

/** Gets the update recorder for the specified source file. */
function getUpdateRecorder(sourceFile: ts.SourceFile): UpdateRecorder {
if (updateRecorders.has(sourceFile)) {
return updateRecorders.get(sourceFile) !;
}
const recorder = tree.beginUpdate(relative(basePath, sourceFile.fileName));
updateRecorders.set(sourceFile, recorder);
return recorder;
}
}
Loading
0