Skip to content

chore(schematics): create upgrade schematic for v8 #15442

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

Merged
merged 1 commit into from
Mar 22, 2019
Merged
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
5 changes: 5 additions & 0 deletions src/lib/schematics/migration.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@
"description": "Updates Angular Material to v7",
"factory": "./ng-update/index#updateToV7"
},
"migration-v8": {
"version": "8",
"description": "Updates Angular Material to v8",
"factory": "./ng-update/index#updateToV8"
},
"ng-post-update": {
"description": "Prints out results after ng-update.",
"factory": "./ng-update/index#postUpdate",
Expand Down
6 changes: 6 additions & 0 deletions src/lib/schematics/ng-update/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const upgradeRules = [
'check-imports-misc',
'check-property-names-misc',
'check-template-misc',
'update-angular-material-imports',

// Ripple misc V7
['ripple-speed-factor-assignment', TargetVersion.V7],
Expand All @@ -46,6 +47,11 @@ export function updateToV7(): Rule {
return createUpgradeRule(TargetVersion.V7, tslintUpgradeConfig);
}

/** Entry point for the migration schematics with target of Angular Material v8 */
export function updateToV8(): Rule {
return createUpgradeRule(TargetVersion.V8, tslintUpgradeConfig);
}

/** Post-update schematic to be called when update is finished. */
export function postUpdate(): Rule {
return () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import {NgModule} from '@angular/core';
import {
MatAutocompleteModule,
MatBadgeModule,
MatButtonModule,
MatButtonToggleModule,
MatCardModule,
MatCheckboxModule,
MatChipsModule,
MatDatepickerModule,
MatDialogModule,
MatExpansionModule,
MatIconModule,
MatInputModule,
MatListModule,
MatMenuModule,
MatNativeDateModule,
MatPaginatorModule,
MatProgressBarModule,
MatProgressSpinnerModule,
MatRadioModule,
MatRippleModule,
MatSelectModule,
MatSidenavModule,
MatSlideToggleModule,
MatSnackBarModule,
MatSortModule,
MatTableModule,
MatTabsModule,
MatToolbarModule,
MatTooltipModule
} from '@angular/material';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is that test-case incorrect? Shouldn't it import from the secondary entry-points?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Your right, it looks like it is still not executing. I am not sure what I am missing to have it included as it should be matched by the glob in the BAZEL file.


@NgModule({
imports: [],
exports: [
MatAutocompleteModule,
MatBadgeModule,
MatButtonModule,
MatButtonToggleModule,
MatCardModule,
MatChipsModule,
MatCheckboxModule,
MatDatepickerModule,
MatDialogModule,
MatExpansionModule,
MatNativeDateModule,
MatIconModule,
MatInputModule,
MatListModule,
MatPaginatorModule,
MatMenuModule,
MatProgressBarModule,
MatProgressSpinnerModule,
MatRadioModule,
MatRippleModule,
MatSelectModule,
MatSidenavModule,
MatSlideToggleModule,
MatSnackBarModule,
MatTableModule,
MatSortModule,
MatTableModule,
MatTabsModule,
MatToolbarModule,
MatTooltipModule,
]
})
export class MaterialModule {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import {NgModule} from '@angular/core';
import {
MatAutocompleteModule,
MatBadgeModule,
MatButtonModule,
MatButtonToggleModule,
MatCardModule,
MatCheckboxModule,
MatChipsModule,
MatDatepickerModule,
MatDialogModule,
MatExpansionModule,
MatIconModule,
MatInputModule,
MatListModule,
MatMenuModule,
MatNativeDateModule,
MatPaginatorModule,
MatProgressBarModule,
MatProgressSpinnerModule,
MatRadioModule,
MatRippleModule,
MatSelectModule,
MatSidenavModule,
MatSlideToggleModule,
MatSnackBarModule,
MatSortModule,
MatTableModule,
MatTabsModule,
MatToolbarModule,
MatTooltipModule
} from '@angular/material';

@NgModule({
imports: [],
exports: [
MatAutocompleteModule,
MatBadgeModule,
MatButtonModule,
MatButtonToggleModule,
MatCardModule,
MatChipsModule,
MatCheckboxModule,
MatDatepickerModule,
MatDialogModule,
MatExpansionModule,
MatNativeDateModule,
MatIconModule,
MatInputModule,
MatListModule,
MatPaginatorModule,
MatMenuModule,
MatProgressBarModule,
MatProgressSpinnerModule,
MatRadioModule,
MatRippleModule,
MatSelectModule,
MatSidenavModule,
MatSlideToggleModule,
MatSnackBarModule,
MatTableModule,
MatSortModule,
MatTableModule,
MatTabsModule,
MatToolbarModule,
MatTooltipModule,
]
})
export class MaterialModule {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
/**
* @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 Lint from 'tslint';
import * as ts from 'typescript';
import {materialModuleSpecifier} from '../../../ng-update/typescript/module-specifiers';

/**
* Regex for testing file paths against to determinte if the file is from the
* Angular Material library.
*/
const ANGULAR_MATERIAL_FILEPATH_REGEX = new RegExp(`${materialModuleSpecifier}/(.*?)/`);

/**
* A TSLint rule correcting symbols imports to using Angular Material
* subpackages (e.g. @angular/material/button) rather than the top level
* package (e.g. @angular/material).
*/
export class Rule extends Lint.Rules.TypedRule {
static metadata: Lint.IRuleMetadata = {
ruleName: 'update-angular-material-imports',
description: Lint.Utils.dedent`
Require all imports for Angular Material to be done via
@angular/material subpackages`,
options: null,
optionsDescription: '',
type: 'functionality',
typescriptOnly: true,
};

static ONLY_SUBPACKAGE_FAILURE_STR = Lint.Utils.dedent`
Importing from @angular/material is deprecated. Instead import from
subpackage the symbol belongs to. e.g. import {MatButtonModule} from
'@angular/material/button'`;
static NO_IMPORT_NAMED_SYMBOLS_FAILURE_STR = Lint.Utils.dedent`
Imports from Angular Material should import specific symbols rather than
importing the entire Angular Material library.`;
static SYMBOL_NOT_FOUND_FAILURE_STR = ` was not found in the Material library.`;
static SYMBOL_FILE_NOT_FOUND_FAILURE_STR =
` was found to be imported from a file outside the Material library.`;

applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] {
return this.applyWithFunction(sourceFile, walk, true, program.getTypeChecker());
}
}

/**
* A walker to walk a given source file to check for imports from the
* @angular/material module.
*/
function walk(ctx: Lint.WalkContext<boolean>, checker: ts.TypeChecker): void {
// The source file to walk.
const sf = ctx.sourceFile;
const cb = (declaration: ts.Node) => {
// Only look at import declarations.
if (!ts.isImportDeclaration(declaration)) {
return;
}
const importLocation = declaration.moduleSpecifier.getText(sf);
// If the import module is not @angular/material, skip check.
if (importLocation !== materialModuleSpecifier) {
return;
}

// If no import clause is found, or nothing is named as a binding in the
// import, add failure saying to import symbols in clause.
if (!declaration.importClause || !declaration.importClause.namedBindings) {
return ctx.addFailureAtNode(declaration, Rule.NO_IMPORT_NAMED_SYMBOLS_FAILURE_STR);
}

// All named bindings in import clauses must be named symbols, otherwise add
// failure saying to import symbols in clause.
if (!ts.isNamedImports(declaration.importClause.namedBindings)) {
return ctx.addFailureAtNode(declaration, Rule.NO_IMPORT_NAMED_SYMBOLS_FAILURE_STR);
}

// If no symbols are in the named bindings then add failure saying to
// import symbols in clause.
if (!declaration.importClause.namedBindings.elements.length) {
return ctx.addFailureAtNode(declaration, Rule.NO_IMPORT_NAMED_SYMBOLS_FAILURE_STR);
}

// Map of submodule locations to arrays of imported symbols.
const importMap = new Map<string, Set<string>>();

// Determine the subpackage each symbol in the namedBinding comes from.
for (const element of declaration.importClause.namedBindings.elements) {
// Confirm the named import is a symbol that can be looked up.
if (!ts.isIdentifier(element.name)) {
return ctx.addFailureAtNode(
element, element.getFullText(sf) + Rule.SYMBOL_NOT_FOUND_FAILURE_STR);
}
// Get the type for the named binding element.
const type = checker.getTypeAtLocation(element.name);
// Get the original symbol where it is declared upstream.
const symbol = type.getSymbol();

// If the symbol can't be found, add failure saying the symbol
// can't be found.
if (!symbol || !symbol.valueDeclaration) {
return ctx.addFailureAtNode(
element, element.getFullText(sf) + Rule.SYMBOL_NOT_FOUND_FAILURE_STR);
}

// If the symbol has no declarations, add failure saying the symbol can't
// be found.
if (!symbol.declarations || !symbol.declarations.length) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should not be necessary now.

return ctx.addFailureAtNode(
element, element.getFullText(sf) + Rule.SYMBOL_NOT_FOUND_FAILURE_STR);
}

// The filename for the source file of the node that contains the
// first declaration of the symbol. All symbol declarations must be
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extra whitespace

// part of a defining node, so parent can be asserted to be defined.
const sourceFile = symbol.valueDeclaration.getSourceFile().fileName;
// File the module the symbol belongs to from a regex match of the
// filename. This will always match since only @angular/material symbols
// are being looked at.
const [, moduleName] = sourceFile.match(ANGULAR_MATERIAL_FILEPATH_REGEX) || [] as undefined[];
if (!moduleName) {
return ctx.addFailureAtNode(
element, element.getFullText(sf) + Rule.SYMBOL_FILE_NOT_FOUND_FAILURE_STR);
}
// The module name where the symbol is defined e.g. card, dialog. The
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extra whitespace

// first capture group is contains the module name.
if (importMap.has(moduleName)) {
importMap.get(moduleName)!.add(symbol.getName());
} else {
importMap.set(moduleName, new Set([symbol.getName()]));
}
}
const fix = buildSecondaryImportStatements(importMap);

// Without a fix to provide, show error message only.
if (!fix) {
return ctx.addFailureAtNode(declaration.moduleSpecifier, Rule.ONLY_SUBPACKAGE_FAILURE_STR);
}
// Mark the lint failure at the module specifier, providing a
// recommended fix.
ctx.addFailureAtNode(
declaration.moduleSpecifier, Rule.ONLY_SUBPACKAGE_FAILURE_STR,
new Lint.Replacement(declaration.getStart(sf), declaration.getWidth(), fix));
};
sf.statements.forEach(cb);
}

/**
* Builds the recommended fix from a map of the imported symbols found in the
* import declaration. Imports declarations are sorted by module, then by
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Extra whitespace.

* symbol within each import declaration.
*
* Example of the format:
*
* import {MatCardModule, MatCardTitle} from '@angular/material/card';
* import {MatRadioModule} from '@angular/material/radio';
*/
function buildSecondaryImportStatements(importMap: Map<string, Set<string>>): string {
return Array.from(importMap.entries())
.sort((a, b) => a[0] > b[0] ? 1 : -1)
.map(entry => {
const imports = Array.from(entry[1]).sort((a, b) => a > b ? 1 : -1).join(', ');
return `import {${imports}} from '@angular/material/${entry[0]}';\n`;
})
.join('');
}