Skip to content

Commit de06c55

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 10968cd commit de06c55

File tree

6 files changed

+323
-0
lines changed

6 files changed

+323
-0
lines changed

src/lib/schematics/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ ts_library(
2525
"@npm//@types/jasmine",
2626
"@npm//@types/node",
2727
"@npm//tslint",
28+
"@npm//tsutils",
2829
"@npm//typescript",
2930
],
3031
tsconfig = ":tsconfig.json",

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: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,11 @@ export function updateToV7(): Rule {
4646
return createUpgradeRule(TargetVersion.V7, tslintUpgradeConfig);
4747
}
4848

49+
/** Entry point for the migration schematics with target of Angular Material v8 */
50+
export function updateToV8(): Rule {
51+
return createUpgradeRule(TargetVersion.V8, tslintUpgradeConfig);
52+
}
53+
4954
/** Post-update schematic to be called when update is finished. */
5055
export function postUpdate(): Rule {
5156
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: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
/**
2+
* This schematic corrects to importing symbols from Angular Material through
3+
* subpackages (e.g. @angular/material/button) rather than the top level
4+
* package (e.g. @angular/material).
5+
*/
6+
import * as Lint from 'tslint';
7+
import * as tsutils from 'tsutils';
8+
import * as ts from 'typescript';
9+
10+
/** The import path for Angular Material top level module. */
11+
const ANGULAR_MATERIAL_IMPORT_PATH = `'@angular/material'`;
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 = /\@angular\/material\/(.*?)\//;
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+
45+
applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): Lint.RuleFailure[] {
46+
return this.applyWithFunction(sourceFile, walk, true, program.getTypeChecker());
47+
}
48+
}
49+
50+
/**
51+
* A walker to walk a given source file to check for imports from the
52+
* @angular/material module.
53+
*/
54+
function walk(ctx: Lint.WalkContext<boolean>, checker: ts.TypeChecker): void {
55+
// The source file to walk.
56+
const sf = ctx.sourceFile;
57+
const cb = (declaration: ts.Node) => {
58+
// Only look at import declarations.
59+
if (!ts.isImportDeclaration(declaration)) {
60+
return;
61+
}
62+
const importLocation = declaration.moduleSpecifier.getText(sf);
63+
// If the import module is not @angular/material, skip check.
64+
if (importLocation !== ANGULAR_MATERIAL_IMPORT_PATH) {
65+
return;
66+
}
67+
68+
// If no import clause is found, or nothing is named as a binding in the
69+
// import, add failure saying to import symbols in clause.
70+
if (!declaration.importClause || !declaration.importClause.namedBindings) {
71+
return ctx.addFailureAtNode(declaration, Rule.NO_IMPORT_NAMED_SYMBOLS_FAILURE_STR);
72+
}
73+
74+
// All named bindings in import clauses must be named symbols, otherwise add
75+
// failure saying to import symbols in clause.
76+
if (!ts.isNamedImports(declaration.importClause.namedBindings)) {
77+
return ctx.addFailureAtNode(declaration, Rule.NO_IMPORT_NAMED_SYMBOLS_FAILURE_STR);
78+
}
79+
80+
// If no symbols are in the named bindings then add failure saying to
81+
// import symbols in clause.
82+
if (!declaration.importClause.namedBindings.elements.length) {
83+
return ctx.addFailureAtNode(declaration, Rule.NO_IMPORT_NAMED_SYMBOLS_FAILURE_STR);
84+
}
85+
86+
// Map of submodule locations to arrays of imported symbols.
87+
const importMap = new Map<string, Set<string>>();
88+
89+
// Determine the subpackage each symbol in the namedBinding comes from.
90+
for (const importedSymbol of declaration.importClause.namedBindings.elements) {
91+
// Confirm the named import is a symbol that can be looked up.
92+
if (!ts.isIdentifier(importedSymbol.name)) {
93+
return ctx.addFailureAtNode(
94+
importedSymbol, importedSymbol.getFullText(sf) + Rule.SYMBOL_NOT_FOUND_FAILURE_STR);
95+
}
96+
// Get the original symbol where it is declared upstream.
97+
const symbol = getOriginalSymbol(importedSymbol.name, checker);
98+
99+
// If the symbol can't be found, add failure saying the symbol
100+
// can't be found.
101+
if (!symbol) {
102+
return ctx.addFailureAtNode(
103+
importedSymbol, importedSymbol.getFullText(sf) + Rule.SYMBOL_NOT_FOUND_FAILURE_STR);
104+
}
105+
106+
// If the symbol has no declarations, add failure saying the symbol can't
107+
// be found.
108+
if (!symbol.declarations || !symbol.declarations.length) {
109+
return ctx.addFailureAtNode(
110+
importedSymbol, importedSymbol.getFullText(sf) + Rule.SYMBOL_NOT_FOUND_FAILURE_STR);
111+
}
112+
113+
// The filename for the source file of the node that contains the
114+
// first declaration of the symbol. All symbol declarations must be
115+
// part of a defining node, so parent can be asserted to be defined.
116+
const fileName = symbol.declarations[0].parent.getSourceFile().fileName;
117+
// File the module the symbol belongs to from a regex match of the
118+
// filename. This will always match since only @angular/material symbols
119+
// are being looked at.
120+
const matched = fileName.match(ANGULAR_MATERIAL_FILEPATH_REGEX)!;
121+
// The module name where the symbol is defined e.g. card, dialog. The
122+
// first capture group is contains the module name.
123+
const moduleName = `${matched[1]}`;
124+
if (importMap.has(moduleName)) {
125+
importMap.get(moduleName)!.add(symbol.getName());
126+
} else {
127+
importMap.set(moduleName, new Set([symbol.getName()]));
128+
}
129+
}
130+
const fix = buildFixerString(importMap);
131+
132+
// Without a fix to provide, show error message only.
133+
if (!fix) {
134+
return ctx.addFailureAtNode(declaration.moduleSpecifier, Rule.ONLY_SUBPACKAGE_FAILURE_STR);
135+
}
136+
// Mark the lint failure at the module specifier, providing a
137+
// recommended fix.
138+
ctx.addFailureAtNode(
139+
declaration.moduleSpecifier, Rule.ONLY_SUBPACKAGE_FAILURE_STR,
140+
new Lint.Replacement(declaration.getStart(sf), declaration.getWidth(), fix));
141+
};
142+
sf.statements.forEach(cb);
143+
}
144+
145+
/** Gets the original defintion of a symbol for the node provided. */
146+
function getOriginalSymbol(node: ts.Node, checker: ts.TypeChecker): ts.Symbol|undefined {
147+
const baseSymbol = checker.getSymbolAtLocation(node);
148+
if (baseSymbol && tsutils.isSymbolFlagSet(baseSymbol, ts.SymbolFlags.Alias)) {
149+
return checker.getAliasedSymbol(baseSymbol);
150+
}
151+
return baseSymbol;
152+
}
153+
154+
/**
155+
* Builds the recommended fix from a map of the imported symbols found in the
156+
* import declaration. Imports declarations are sorted by module, then by
157+
* symbol within each import declaration.
158+
*
159+
* Example of the format:
160+
*
161+
* import {MatCardModule, MatCardTitle} from '@angular/material/card';
162+
* import {MatRadioModule} from '@angular/material/radio';
163+
*/
164+
function buildFixerString(importMap: Map<string, Set<string>>): string {
165+
return Array.from(importMap.entries())
166+
.sort((a, b) => a[0] > b[0] ? 1 : -1)
167+
.map(entry => {
168+
const imports = Array.from(entry[1]).sort((a, b) => a > b ? 1 : -1).join(', ');
169+
return `import {${imports}} from '@angular/material/${entry[0]}';\n`;
170+
})
171+
.join('');
172+
}

0 commit comments

Comments
 (0)