@@ -11,7 +11,7 @@ import * as ts from 'typescript';
11
11
import { materialModuleSpecifier } from '../../../ng-update/typescript/module-specifiers' ;
12
12
13
13
/**
14
- * Regex for testing file paths against to determinte if the file is from the
14
+ * Regex for testing file paths against to determine if the file is from the
15
15
* Angular Material library.
16
16
*/
17
17
const ANGULAR_MATERIAL_FILEPATH_REGEX = new RegExp ( `${ materialModuleSpecifier } /(.*?)/` ) ;
@@ -51,17 +51,20 @@ export class Rule extends Lint.Rules.TypedRule {
51
51
52
52
/**
53
53
* A walker to walk a given source file to check for imports from the
54
- * @angular /material module.
54
+ * " @angular/material" module.
55
55
*/
56
56
function walk ( ctx : Lint . WalkContext < boolean > , checker : ts . TypeChecker ) : void {
57
57
// The source file to walk.
58
58
const sf = ctx . sourceFile ;
59
+ const printer = ts . createPrinter ( ) ;
59
60
const cb = ( declaration : ts . Node ) => {
60
61
// Only look at import declarations.
61
- if ( ! ts . isImportDeclaration ( declaration ) ) {
62
+ if ( ! ts . isImportDeclaration ( declaration ) ||
63
+ ! ts . isStringLiteralLike ( declaration . moduleSpecifier ) ) {
62
64
return ;
63
65
}
64
- const importLocation = declaration . moduleSpecifier . getText ( sf ) ;
66
+
67
+ const importLocation = declaration . moduleSpecifier . text ;
65
68
// If the import module is not @angular /material, skip check.
66
69
if ( importLocation !== materialModuleSpecifier ) {
67
70
return ;
@@ -85,86 +88,95 @@ function walk(ctx: Lint.WalkContext<boolean>, checker: ts.TypeChecker): void {
85
88
return ctx . addFailureAtNode ( declaration , Rule . NO_IMPORT_NAMED_SYMBOLS_FAILURE_STR ) ;
86
89
}
87
90
88
- // Map of submodule locations to arrays of imported symbols.
89
- const importMap = new Map < string , Set < string > > ( ) ;
91
+ // Map which consists of secondary entry-points and import specifiers which are used
92
+ // within the current import declaration.
93
+ const importMap = new Map < string , ts . ImportSpecifier [ ] > ( ) ;
90
94
91
95
// Determine the subpackage each symbol in the namedBinding comes from.
92
96
for ( const element of declaration . importClause . namedBindings . elements ) {
93
- // Confirm the named import is a symbol that can be looked up .
97
+ // Ensure the import specifier name is statically analyzable .
94
98
if ( ! ts . isIdentifier ( element . name ) ) {
95
- return ctx . addFailureAtNode (
96
- element , element . getFullText ( sf ) + Rule . SYMBOL_NOT_FOUND_FAILURE_STR ) ;
99
+ return ctx . addFailureAtNode ( element , element . getText ( ) + Rule . SYMBOL_NOT_FOUND_FAILURE_STR ) ;
97
100
}
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 ( ) ;
101
+
102
+ // Get the symbol for the named binding element. Note that we cannot determine the
103
+ // value declaration based on the type of the element as types are not necessarily
104
+ // specific to a given secondary entry-point (e.g. exports with the type of "string")
105
+ // would resolve to the module types provided by TypeScript itself.
106
+ const symbol = getDeclarationSymbolOfNode ( element . name , checker ) ;
102
107
103
108
// If the symbol can't be found, add failure saying the symbol
104
109
// can't be found.
105
110
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 ) ;
111
+ return ctx . addFailureAtNode ( element , element . getText ( ) + Rule . SYMBOL_NOT_FOUND_FAILURE_STR ) ;
115
112
}
116
113
117
114
// The filename for the source file of the node that contains the
118
- // first declaration of the symbol. All symbol declarations must be
115
+ // first declaration of the symbol. All symbol declarations must be
119
116
// part of a defining node, so parent can be asserted to be defined.
120
- const sourceFile = symbol . valueDeclaration . getSourceFile ( ) . fileName ;
117
+ const sourceFile : string = symbol . valueDeclaration . getSourceFile ( ) . fileName ;
118
+
121
119
// 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 ) {
120
+ // filename. This will always match since only " @angular/material"
121
+ // elements are analyzed .
122
+ const matches = sourceFile . match ( ANGULAR_MATERIAL_FILEPATH_REGEX ) ;
123
+ if ( ! matches ) {
126
124
return ctx . addFailureAtNode (
127
- element , element . getFullText ( sf ) + Rule . SYMBOL_FILE_NOT_FOUND_FAILURE_STR ) ;
125
+ element , element . getText ( ) + Rule . SYMBOL_FILE_NOT_FOUND_FAILURE_STR ) ;
128
126
}
129
- // The module name where the symbol is defined e.g. card, dialog. The
127
+ const [ , moduleName ] = matches ;
128
+
129
+ // The module name where the symbol is defined e.g. card, dialog. The
130
130
// first capture group is contains the module name.
131
131
if ( importMap . has ( moduleName ) ) {
132
- importMap . get ( moduleName ) ! . add ( symbol . getName ( ) ) ;
132
+ importMap . get ( moduleName ) ! . push ( element ) ;
133
133
} else {
134
- importMap . set ( moduleName , new Set ( [ symbol . getName ( ) ] ) ) ;
134
+ importMap . set ( moduleName , [ element ] ) ;
135
135
}
136
136
}
137
- const fix = buildSecondaryImportStatements ( importMap ) ;
138
137
139
- // Without a fix to provide, show error message only.
140
- if ( ! fix ) {
138
+ // Transforms the import declaration into multiple import declarations that import
139
+ // the given symbols from the individual secondary entry-points. For example:
140
+ // import {MatCardModule, MatCardTitle} from '@angular/material/card';
141
+ // import {MatRadioModule} from '@angular/material/radio';
142
+ const newImportStatements =
143
+ Array . from ( importMap . entries ( ) )
144
+ . sort ( )
145
+ . map ( ( [ name , elements ] ) => {
146
+ const newImport = ts . createImportDeclaration (
147
+ undefined , undefined ,
148
+ ts . createImportClause ( undefined , ts . createNamedImports ( elements ) ) ,
149
+ ts . createStringLiteral ( `${ materialModuleSpecifier } /${ name } ` ) ) ;
150
+ return printer . printNode ( ts . EmitHint . Unspecified , newImport , sf ) ;
151
+ } )
152
+ . join ( '\n' ) ;
153
+
154
+ // Without any import statements that were generated, we can assume that this was an empty
155
+ // import declaration. We still want to add a failure in order to make developers aware that
156
+ // importing from "@angular/material" is deprecated.
157
+ if ( ! newImportStatements ) {
141
158
return ctx . addFailureAtNode ( declaration . moduleSpecifier , Rule . ONLY_SUBPACKAGE_FAILURE_STR ) ;
142
159
}
143
- // Mark the lint failure at the module specifier, providing a
144
- // recommended fix.
160
+
161
+ // Mark the lint failure at the module specifier, providing the replacement that switches
162
+ // the import to individual secondary entry-point imports.
145
163
ctx . addFailureAtNode (
146
164
declaration . moduleSpecifier , Rule . ONLY_SUBPACKAGE_FAILURE_STR ,
147
- new Lint . Replacement ( declaration . getStart ( sf ) , declaration . getWidth ( ) , fix ) ) ;
165
+ new Lint . Replacement ( declaration . getStart ( ) , declaration . getWidth ( ) , newImportStatements ) ) ;
148
166
} ;
167
+
149
168
sf . statements . forEach ( cb ) ;
150
169
}
151
170
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 ( '' ) ;
171
+ /** Gets the symbol that contains the value declaration of the given node. */
172
+ function getDeclarationSymbolOfNode ( node : ts . Node , checker : ts . TypeChecker ) {
173
+ const symbol = checker . getSymbolAtLocation ( node ) ;
174
+
175
+ // Symbols can be aliases of the declaration symbol. e.g. in named import specifiers.
176
+ // We need to resolve the aliased symbol back to the declaration symbol.
177
+ // tslint:disable-next-line:no-bitwise
178
+ if ( symbol && ( symbol . flags & ts . SymbolFlags . Alias ) !== 0 ) {
179
+ return checker . getAliasedSymbol ( symbol ) ;
180
+ }
181
+ return symbol ;
170
182
}
0 commit comments