Skip to content

Commit 272f96d

Browse files
committed
chore(schematics): create upgrade schematic for v8
- Create the initial v8 upgrade schematic - Create TSLint rule schematic to update imports to subpackage imports for @angular/material
1 parent 59818d1 commit 272f96d

File tree

5 files changed

+321
-0
lines changed

5 files changed

+321
-0
lines changed

src/lib/schematics/migration.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@
1111
"description": "Updates Angular Material to v7",
1212
"factory": "./ng-update/index#updateToV7"
1313
},
14+
"migration-v8": {
15+
"version": "8",
16+
"description": "Updates Angular Material to v8",
17+
"factory": "./ng-update/index#updateToV8"
18+
},
1419
"ng-post-update": {
1520
"description": "Prints out results after ng-update.",
1621
"factory": "./ng-update/index#postUpdate",

src/lib/schematics/ng-update/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const upgradeRules = [
2020
'check-imports-misc',
2121
'check-property-names-misc',
2222
'check-template-misc',
23+
'update-angular-material-imports',
2324

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

50+
/** Entry point for the migration schematics with target of Angular Material v8 */
51+
export function updateToV8(): Rule {
52+
return createUpgradeRule(TargetVersion.V8, tslintUpgradeConfig);
53+
}
54+
4955
/** Post-update schematic to be called when update is finished. */
5056
export function postUpdate(): Rule {
5157
return () => {
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import {NgModule} from '@angular/core';
2+
import {
3+
MatAutocompleteModule,
4+
MatBadgeModule,
5+
MatButtonModule,
6+
MatButtonToggleModule,
7+
MatCardModule,
8+
MatCheckboxModule,
9+
MatChipsModule,
10+
MatDatepickerModule,
11+
MatDialogModule,
12+
MatExpansionModule,
13+
MatIconModule,
14+
MatInputModule,
15+
MatListModule,
16+
MatMenuModule,
17+
MatNativeDateModule,
18+
MatPaginatorModule,
19+
MatProgressBarModule,
20+
MatProgressSpinnerModule,
21+
MatRadioModule,
22+
MatRippleModule,
23+
MatSelectModule,
24+
MatSidenavModule,
25+
MatSlideToggleModule,
26+
MatSnackBarModule,
27+
MatSortModule,
28+
MatTableModule,
29+
MatTabsModule,
30+
MatToolbarModule,
31+
MatTooltipModule
32+
} from '@angular/material';
33+
34+
@NgModule({
35+
imports: [],
36+
exports: [
37+
MatAutocompleteModule,
38+
MatBadgeModule,
39+
MatButtonModule,
40+
MatButtonToggleModule,
41+
MatCardModule,
42+
MatChipsModule,
43+
MatCheckboxModule,
44+
MatDatepickerModule,
45+
MatDialogModule,
46+
MatExpansionModule,
47+
MatNativeDateModule,
48+
MatIconModule,
49+
MatInputModule,
50+
MatListModule,
51+
MatPaginatorModule,
52+
MatMenuModule,
53+
MatProgressBarModule,
54+
MatProgressSpinnerModule,
55+
MatRadioModule,
56+
MatRippleModule,
57+
MatSelectModule,
58+
MatSidenavModule,
59+
MatSlideToggleModule,
60+
MatSnackBarModule,
61+
MatTableModule,
62+
MatSortModule,
63+
MatTableModule,
64+
MatTabsModule,
65+
MatToolbarModule,
66+
MatTooltipModule,
67+
]
68+
})
69+
export class MaterialModule {
70+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import {NgModule} from '@angular/core';
2+
import {
3+
MatAutocompleteModule,
4+
MatBadgeModule,
5+
MatButtonModule,
6+
MatButtonToggleModule,
7+
MatCardModule,
8+
MatCheckboxModule,
9+
MatChipsModule,
10+
MatDatepickerModule,
11+
MatDialogModule,
12+
MatExpansionModule,
13+
MatIconModule,
14+
MatInputModule,
15+
MatListModule,
16+
MatMenuModule,
17+
MatNativeDateModule,
18+
MatPaginatorModule,
19+
MatProgressBarModule,
20+
MatProgressSpinnerModule,
21+
MatRadioModule,
22+
MatRippleModule,
23+
MatSelectModule,
24+
MatSidenavModule,
25+
MatSlideToggleModule,
26+
MatSnackBarModule,
27+
MatSortModule,
28+
MatTableModule,
29+
MatTabsModule,
30+
MatToolbarModule,
31+
MatTooltipModule
32+
} from '@angular/material';
33+
34+
@NgModule({
35+
imports: [],
36+
exports: [
37+
MatAutocompleteModule,
38+
MatBadgeModule,
39+
MatButtonModule,
40+
MatButtonToggleModule,
41+
MatCardModule,
42+
MatChipsModule,
43+
MatCheckboxModule,
44+
MatDatepickerModule,
45+
MatDialogModule,
46+
MatExpansionModule,
47+
MatNativeDateModule,
48+
MatIconModule,
49+
MatInputModule,
50+
MatListModule,
51+
MatPaginatorModule,
52+
MatMenuModule,
53+
MatProgressBarModule,
54+
MatProgressSpinnerModule,
55+
MatRadioModule,
56+
MatRippleModule,
57+
MatSelectModule,
58+
MatSidenavModule,
59+
MatSlideToggleModule,
60+
MatSnackBarModule,
61+
MatTableModule,
62+
MatSortModule,
63+
MatTableModule,
64+
MatTabsModule,
65+
MatToolbarModule,
66+
MatTooltipModule,
67+
]
68+
})
69+
export class MaterialModule {
70+
}
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import * as Lint from 'tslint';
10+
import * as ts from 'typescript';
11+
import {materialModuleSpecifier} from '../../../ng-update/typescript/module-specifiers';
12+
13+
/**
14+
* Regex for testing file paths against to determinte if the file is from the
15+
* Angular Material library.
16+
*/
17+
const ANGULAR_MATERIAL_FILEPATH_REGEX = new RegExp(`${materialModuleSpecifier}/(.*?)/`);
18+
19+
/**
20+
* A TSLint rule correcting symbols imports to using Angular Material
21+
* subpackages (e.g. @angular/material/button) rather than the top level
22+
* package (e.g. @angular/material).
23+
*/
24+
export class Rule extends Lint.Rules.TypedRule {
25+
static metadata: Lint.IRuleMetadata = {
26+
ruleName: 'update-angular-material-imports',
27+
description: Lint.Utils.dedent`
28+
Require all imports for Angular Material to be done via
29+
@angular/material subpackages`,
30+
options: null,
31+
optionsDescription: '',
32+
type: 'functionality',
33+
typescriptOnly: true,
34+
};
35+
36+
static ONLY_SUBPACKAGE_FAILURE_STR = Lint.Utils.dedent`
37+
Importing from @angular/material is deprecated. Instead import from
38+
subpackage the symbol belongs to. e.g. import {MatButtonModule} from
39+
'@angular/material/button'`;
40+
static NO_IMPORT_NAMED_SYMBOLS_FAILURE_STR = Lint.Utils.dedent`
41+
Imports from Angular Material should import specific symbols rather than
42+
importing the entire Angular Material library.`;
43+
static SYMBOL_NOT_FOUND_FAILURE_STR = ` was not found in the Material library.`;
44+
static SYMBOL_FILE_NOT_FOUND_FAILURE_STR =
45+
` was found to be imported from a file outside the Material library.`;
46+
47+
applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] {
48+
return this.applyWithFunction(sourceFile, walk, true, program.getTypeChecker());
49+
}
50+
}
51+
52+
/**
53+
* A walker to walk a given source file to check for imports from the
54+
* @angular/material module.
55+
*/
56+
function walk(ctx: Lint.WalkContext<boolean>, checker: ts.TypeChecker): void {
57+
// The source file to walk.
58+
const sf = ctx.sourceFile;
59+
const cb = (declaration: ts.Node) => {
60+
// Only look at import declarations.
61+
if (!ts.isImportDeclaration(declaration)) {
62+
return;
63+
}
64+
const importLocation = declaration.moduleSpecifier.getText(sf);
65+
// If the import module is not @angular/material, skip check.
66+
if (importLocation !== materialModuleSpecifier) {
67+
return;
68+
}
69+
70+
// If no import clause is found, or nothing is named as a binding in the
71+
// import, add failure saying to import symbols in clause.
72+
if (!declaration.importClause || !declaration.importClause.namedBindings) {
73+
return ctx.addFailureAtNode(declaration, Rule.NO_IMPORT_NAMED_SYMBOLS_FAILURE_STR);
74+
}
75+
76+
// All named bindings in import clauses must be named symbols, otherwise add
77+
// failure saying to import symbols in clause.
78+
if (!ts.isNamedImports(declaration.importClause.namedBindings)) {
79+
return ctx.addFailureAtNode(declaration, Rule.NO_IMPORT_NAMED_SYMBOLS_FAILURE_STR);
80+
}
81+
82+
// If no symbols are in the named bindings then add failure saying to
83+
// import symbols in clause.
84+
if (!declaration.importClause.namedBindings.elements.length) {
85+
return ctx.addFailureAtNode(declaration, Rule.NO_IMPORT_NAMED_SYMBOLS_FAILURE_STR);
86+
}
87+
88+
// Map of submodule locations to arrays of imported symbols.
89+
const importMap = new Map<string, Set<string>>();
90+
91+
// Determine the subpackage each symbol in the namedBinding comes from.
92+
for (const element of declaration.importClause.namedBindings.elements) {
93+
// Confirm the named import is a symbol that can be looked up.
94+
if (!ts.isIdentifier(element.name)) {
95+
return ctx.addFailureAtNode(
96+
element, element.getFullText(sf) + Rule.SYMBOL_NOT_FOUND_FAILURE_STR);
97+
}
98+
// Get the type for the named binding element.
99+
const type = checker.getTypeAtLocation(element.name);
100+
// Get the original symbol where it is declared upstream.
101+
const symbol = type.getSymbol();
102+
103+
// If the symbol can't be found, add failure saying the symbol
104+
// can't be found.
105+
if (!symbol || !symbol.valueDeclaration) {
106+
return ctx.addFailureAtNode(
107+
element, element.getFullText(sf) + Rule.SYMBOL_NOT_FOUND_FAILURE_STR);
108+
}
109+
110+
// If the symbol has no declarations, add failure saying the symbol can't
111+
// be found.
112+
if (!symbol.declarations || !symbol.declarations.length) {
113+
return ctx.addFailureAtNode(
114+
element, element.getFullText(sf) + Rule.SYMBOL_NOT_FOUND_FAILURE_STR);
115+
}
116+
117+
// The filename for the source file of the node that contains the
118+
// first declaration of the symbol. All symbol declarations must be
119+
// part of a defining node, so parent can be asserted to be defined.
120+
const sourceFile = symbol.valueDeclaration.getSourceFile().fileName;
121+
// File the module the symbol belongs to from a regex match of the
122+
// filename. This will always match since only @angular/material symbols
123+
// are being looked at.
124+
const [, moduleName] = sourceFile.match(ANGULAR_MATERIAL_FILEPATH_REGEX) || [] as undefined[];
125+
if (!moduleName) {
126+
return ctx.addFailureAtNode(
127+
element, element.getFullText(sf) + Rule.SYMBOL_FILE_NOT_FOUND_FAILURE_STR);
128+
}
129+
// The module name where the symbol is defined e.g. card, dialog. The
130+
// first capture group is contains the module name.
131+
if (importMap.has(moduleName)) {
132+
importMap.get(moduleName)!.add(symbol.getName());
133+
} else {
134+
importMap.set(moduleName, new Set([symbol.getName()]));
135+
}
136+
}
137+
const fix = buildSecondaryImportStatements(importMap);
138+
139+
// Without a fix to provide, show error message only.
140+
if (!fix) {
141+
return ctx.addFailureAtNode(declaration.moduleSpecifier, Rule.ONLY_SUBPACKAGE_FAILURE_STR);
142+
}
143+
// Mark the lint failure at the module specifier, providing a
144+
// recommended fix.
145+
ctx.addFailureAtNode(
146+
declaration.moduleSpecifier, Rule.ONLY_SUBPACKAGE_FAILURE_STR,
147+
new Lint.Replacement(declaration.getStart(sf), declaration.getWidth(), fix));
148+
};
149+
sf.statements.forEach(cb);
150+
}
151+
152+
/**
153+
* Builds the recommended fix from a map of the imported symbols found in the
154+
* import declaration. Imports declarations are sorted by module, then by
155+
* symbol within each import declaration.
156+
*
157+
* Example of the format:
158+
*
159+
* import {MatCardModule, MatCardTitle} from '@angular/material/card';
160+
* import {MatRadioModule} from '@angular/material/radio';
161+
*/
162+
function buildSecondaryImportStatements(importMap: Map<string, Set<string>>): string {
163+
return Array.from(importMap.entries())
164+
.sort((a, b) => a[0] > b[0] ? 1 : -1)
165+
.map(entry => {
166+
const imports = Array.from(entry[1]).sort((a, b) => a > b ? 1 : -1).join(', ');
167+
return `import {${imports}} from '@angular/material/${entry[0]}';\n`;
168+
})
169+
.join('');
170+
}

0 commit comments

Comments
 (0)