├── src ├── material │ ├── data │ │ ├── export-as-names.json │ │ ├── element-selectors.json │ │ ├── attribute-selectors.json │ │ ├── class-names.json │ │ ├── output-names.json │ │ ├── css-names.json │ │ ├── method-call-checks.json │ │ ├── input-names.json │ │ └── property-names.json │ ├── extra-stylsheets.ts │ ├── color.ts │ ├── typescript-specifiers.ts │ └── component-data.ts ├── typescript │ ├── identifiers.ts │ ├── imports.ts │ └── literal.ts ├── tslint │ ├── find-tslint-binary.ts │ ├── component-file.ts │ └── component-walker.ts ├── rules │ ├── tslint-migration.json │ ├── checkImportMiscRule.ts │ ├── checkIdentifierMiscRule.ts │ ├── checkClassDeclarationMiscRule.ts │ ├── checkInheritanceRule.ts │ ├── switchStringLiteralElementSelectorsRule.ts │ ├── switchStringLiteralCssNamesRule.ts │ ├── switchStringLiteralAttributeSelectorsRule.ts │ ├── checkPropertyAccessMiscRule.ts │ ├── switchPropertyNamesRule.ts │ ├── switchTemplateExportAsNamesRule.ts │ ├── switchTemplateCssNamesRule.ts │ ├── switchTemplateElementSelectorsRule.ts │ ├── switchTemplateAttributeSelectorsRule.ts │ ├── checkMethodCallsRule.ts │ ├── switchTemplateInputNamesRule.ts │ ├── switchTemplateOutputNamesRule.ts │ ├── checkTemplateMiscRule.ts │ ├── switchStylesheetCssNamesRule.ts │ ├── switchStylesheetElementSelectorsRule.ts │ ├── switchStylesheetInputNamesRule.ts │ ├── switchStylesheetOutputNamesRule.ts │ ├── switchStylesheetAttributeSelectorsRule.ts │ └── switchIdentifiersRule.ts ├── index.ts └── cli.ts ├── .gitignore ├── DEVELOPMENT.md ├── test └── fixtures │ └── sample-project │ ├── re-export.ts │ ├── global-styles.scss │ ├── app │ ├── app.component.css │ ├── app.component.spec.ts │ ├── app.component.html │ └── app-component.ts │ ├── tsconfig.json │ ├── material.module.ts │ └── index.ts ├── tsconfig.json ├── LICENSE ├── package.json └── README.md /src/material/data/export-as-names.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | node_modules/ 3 | /dist/ 4 | 5 | # VSCode 6 | settings.json -------------------------------------------------------------------------------- /DEVELOPMENT.md: -------------------------------------------------------------------------------- 1 | Build the tool with `npm run build` 2 | 3 | The npm output will be written to `./dist` 4 | -------------------------------------------------------------------------------- /src/material/extra-stylsheets.ts: -------------------------------------------------------------------------------- 1 | export const EXTRA_STYLESHEETS_GLOB_KEY = 'MD_EXTRA_STYLESHEETS_GLOB'; 2 | -------------------------------------------------------------------------------- /test/fixtures/sample-project/re-export.ts: -------------------------------------------------------------------------------- 1 | export {MatSlideToggleModule as SlideToggleCustom} from '@angular/material'; 2 | -------------------------------------------------------------------------------- /test/fixtures/sample-project/global-styles.scss: -------------------------------------------------------------------------------- 1 | // These lines should be changed: 2 | p { font-family: $mat-font-family; } 3 | -------------------------------------------------------------------------------- /test/fixtures/sample-project/app/app.component.css: -------------------------------------------------------------------------------- 1 | /* These lines should be changed: */ 2 | .mat-input-container {} 3 | [cdkPortalHost] {} 4 | 5 | /* These lines should not be changed: */ 6 | [origin] {} 7 | -------------------------------------------------------------------------------- /src/material/data/element-selectors.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pr": "https://github.com/angular/material2/pull/10297", 4 | "changes": [ 5 | { 6 | "replace": "mat-input-container", 7 | "replaceWith": "mat-form-field" 8 | } 9 | ] 10 | } 11 | ] 12 | -------------------------------------------------------------------------------- /test/fixtures/sample-project/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "module": "commonjs", 5 | "target": "es5", 6 | "sourceMap": true 7 | }, 8 | "files": [ 9 | "./index.ts" 10 | ], 11 | "include": [ 12 | "**/*.spec.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "./dist", 4 | "module": "commonjs", 5 | "target": "es5", 6 | "lib": ["es2015", "es5"], 7 | "sourceMap": true, 8 | "declaration": true 9 | }, 10 | "files": [ 11 | "./src/index.ts", 12 | "./src/cli.ts" 13 | ], 14 | "exclude": [ 15 | "./node_modules/" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /src/material/data/attribute-selectors.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pr": "https://github.com/angular/material2/pull/10257", 4 | "changes": [ 5 | { 6 | "replace": "cdkPortalHost", 7 | "replaceWith": "cdkPortalOutlet" 8 | }, 9 | { 10 | "replace": "portalHost", 11 | "replaceWith": "cdkPortalOutlet" 12 | } 13 | ] 14 | } 15 | ] 16 | -------------------------------------------------------------------------------- /src/typescript/identifiers.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | /** Returns the original symbol from an node. */ 4 | export function getOriginalSymbolFromNode(node: ts.Node, checker: ts.TypeChecker) { 5 | const baseSymbol = checker.getSymbolAtLocation(node); 6 | 7 | if (baseSymbol && baseSymbol.flags & ts.SymbolFlags.Alias) { 8 | return checker.getAliasedSymbol(baseSymbol); 9 | } 10 | 11 | return baseSymbol; 12 | } 13 | -------------------------------------------------------------------------------- /src/material/color.ts: -------------------------------------------------------------------------------- 1 | import {bold, green, red} from 'chalk'; 2 | 3 | const colorFns = { 4 | 'b': bold, 5 | 'g': green, 6 | 'r': red, 7 | }; 8 | 9 | export function color(message: string): string { 10 | // 'r{{text}}' with red 'text', 'g{{text}}' with green 'text', and 'b{{text}}' with bold 'text'. 11 | return message.replace(/(.)\{\{(.*?)\}\}/g, (m, fnName, text) => { 12 | const fn = colorFns[fnName]; 13 | return fn ? fn(text) : text; 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /test/fixtures/sample-project/material.module.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {SlideToggleCustom} from './re-export'; 3 | import * as md from '@angular/material'; 4 | import {MatCheckboxModule, MatSidenavModule} from '@angular/material'; 5 | 6 | @NgModule({ 7 | exports: [ 8 | SlideToggleCustom, 9 | MatCheckboxModule, 10 | MatSidenavModule, 11 | 12 | md.MatSelectModule, 13 | md.MatListModule 14 | ] 15 | }) 16 | export class MyAppMaterialModule {} 17 | -------------------------------------------------------------------------------- /test/fixtures/sample-project/index.ts: -------------------------------------------------------------------------------- 1 | import {NgModule} from '@angular/core'; 2 | import {AppComponent} from './app/app-component'; 3 | import {MatButtonToggleModule, MatDatepickerModule} from '@angular/material' 4 | import {MyAppMaterialModule} from './material.module'; 5 | 6 | @NgModule({ 7 | bootstrap: [ 8 | AppComponent 9 | ], 10 | imports: [ 11 | MyAppMaterialModule, 12 | MatButtonToggleModule, 13 | MatDatepickerModule 14 | ] 15 | }) 16 | export class MdSampleProjectModule {} 17 | -------------------------------------------------------------------------------- /src/tslint/find-tslint-binary.ts: -------------------------------------------------------------------------------- 1 | import {resolve} from 'path'; 2 | import {existsSync} from 'fs'; 3 | 4 | // This import lacks of type definitions. 5 | const resolveBinSync = require('resolve-bin').sync; 6 | 7 | /** Finds the path to the TSLint CLI binary. */ 8 | export function findTslintBinaryPath() { 9 | const defaultPath = resolve(__dirname, '..', 'node_modules', 'tslint', 'bin', 'tslint'); 10 | 11 | if (existsSync(defaultPath)) { 12 | return defaultPath; 13 | } else { 14 | return resolveBinSync('tslint', 'tslint'); 15 | } 16 | } -------------------------------------------------------------------------------- /src/tslint/component-file.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | export type ExternalResource = ts.SourceFile; 4 | 5 | /** 6 | * Creates a fake TypeScript source file that can contain content of templates or stylesheets. 7 | * The fake TypeScript source file then can be passed to TSLint in combination with a rule failure. 8 | */ 9 | export function createComponentFile(filePath: string, content: string): ExternalResource { 10 | const sourceFile = ts.createSourceFile(filePath, `\`${content}\``, ts.ScriptTarget.ES5); 11 | const _getFullText = sourceFile.getFullText; 12 | 13 | sourceFile.getFullText = function() { 14 | const text = _getFullText.apply(sourceFile); 15 | return text.substring(1, text.length - 1); 16 | }.bind(sourceFile); 17 | 18 | return sourceFile; 19 | } 20 | -------------------------------------------------------------------------------- /test/fixtures/sample-project/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import {By} from '@angular/platform-browser'; 2 | import {MatSort} from '@angular/material'; 3 | import {Observable} from "rxjs/Observable"; 4 | 5 | describe('App Component', () => { 6 | 7 | it('should change string literals in tests as well', () => { 8 | const debugElement = By.css('[mat-button]'); 9 | 10 | // This can be considered as invalid, since the button is always an attribute and no selector. 11 | const debugElement2 = By.css('mat-button'); 12 | 13 | // Fakes the old MatSort implementation with the old output property. 14 | const sort: {mdSortChange: Observable} & MatSort = null; 15 | 16 | // This property has been updated as part of the prefix switching. 17 | sort.sortChange.subscribe(() => {}); 18 | }); 19 | 20 | }); 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Google LLC 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/rules/tslint-migration.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": ["./"], 3 | "rules": { 4 | "switch-identifiers": true, 5 | "switch-property-names": true, 6 | "switch-string-literal-attribute-selectors": true, 7 | "switch-string-literal-css-names": true, 8 | "switch-string-literal-element-selectors": true, 9 | "switch-stylesheet-attribute-selectors": true, 10 | "switch-stylesheet-css-names": true, 11 | "switch-stylesheet-element-selectors": true, 12 | "switch-stylesheet-input-names": true, 13 | "switch-stylesheet-output-names": true, 14 | "switch-template-attribute-selectors": true, 15 | "switch-template-css-names": true, 16 | "switch-template-element-selectors": true, 17 | "switch-template-export-as-names": true, 18 | "switch-template-input-names": true, 19 | "switch-template-output-names": true, 20 | 21 | "check-inheritance": true, 22 | "check-method-calls": true, 23 | 24 | "check-class-declaration-misc": true, 25 | "check-identifier-misc": true, 26 | "check-import-misc": true, 27 | "check-property-access-misc": true, 28 | "check-template-misc": true 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/material/typescript-specifiers.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | import {getExportDeclaration, getImportDeclaration} from '../typescript/imports'; 3 | 4 | /** Name of the Angular Material module specifier. */ 5 | export const materialModuleSpecifier = '@angular/material'; 6 | 7 | /** Name of the Angular CDK module specifier. */ 8 | export const cdkModuleSpecifier = '@angular/cdk'; 9 | 10 | /** Whether the specified node is part of an Angular Material import declaration. */ 11 | export function isMaterialImportDeclaration(node: ts.Node) { 12 | return isMaterialDeclaration(getImportDeclaration(node)); 13 | } 14 | 15 | /** Whether the specified node is part of an Angular Material export declaration. */ 16 | export function isMaterialExportDeclaration(node: ts.Node) { 17 | return getExportDeclaration(getImportDeclaration(node)); 18 | } 19 | 20 | /** Whether the declaration is part of Angular Material. */ 21 | function isMaterialDeclaration(declaration: ts.ImportDeclaration | ts.ExportDeclaration) { 22 | const moduleSpecifier = declaration.moduleSpecifier.getText(); 23 | return moduleSpecifier.indexOf(materialModuleSpecifier) !== -1|| 24 | moduleSpecifier.indexOf(cdkModuleSpecifier) !== -1; 25 | } -------------------------------------------------------------------------------- /src/rules/checkImportMiscRule.ts: -------------------------------------------------------------------------------- 1 | import {bold, green, red} from 'chalk'; 2 | import {ProgramAwareRuleWalker, RuleFailure, Rules} from 'tslint'; 3 | import * as ts from 'typescript'; 4 | import {isMaterialImportDeclaration} from '../material/typescript-specifiers'; 5 | 6 | /** 7 | * Rule that walks through every identifier that is part of Angular Material and replaces the 8 | * outdated name with the new one. 9 | */ 10 | export class Rule extends Rules.TypedRule { 11 | applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] { 12 | return this.applyWithWalker(new CheckImportMiscWalker(sourceFile, this.getOptions(), program)); 13 | } 14 | } 15 | 16 | export class CheckImportMiscWalker extends ProgramAwareRuleWalker { 17 | visitImportDeclaration(declaration: ts.ImportDeclaration) { 18 | if (isMaterialImportDeclaration(declaration)) { 19 | declaration.importClause.namedBindings.forEachChild(n => { 20 | let importName = n.getFirstToken() && n.getFirstToken().getText(); 21 | if (importName === 'SHOW_ANIMATION' || importName === 'HIDE_ANIMATION') { 22 | this.addFailureAtNode( 23 | n, 24 | `Found deprecated symbol "${red(importName)}" which has been removed`); 25 | } 26 | }); 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/material/data/class-names.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pr": "https://github.com/angular/material2/pull/10161", 4 | "changes": [ 5 | { 6 | "replace": "ConnectedOverlayDirective", 7 | "replaceWith": "CdkConnectedOverlay" 8 | }, 9 | { 10 | "replace": "OverlayOrigin", 11 | "replaceWith": "CdkOverlayOrigin" 12 | } 13 | ] 14 | }, 15 | 16 | 17 | { 18 | "pr": "https://github.com/angular/material2/pull/10267", 19 | "changes": [ 20 | { 21 | "replace": "ObserveContent", 22 | "replaceWith": "CdkObserveContent" 23 | } 24 | ] 25 | }, 26 | 27 | 28 | { 29 | "pr": "https://github.com/angular/material2/pull/10291", 30 | "changes": [ 31 | { 32 | "replace": "FloatPlaceholderType", 33 | "replaceWith": "FloatLabelType" 34 | }, 35 | { 36 | "replace": "MAT_PLACEHOLDER_GLOBAL_OPTIONS", 37 | "replaceWith": "MAT_LABEL_GLOBAL_OPTIONS" 38 | }, 39 | { 40 | "replace": "PlaceholderOptions", 41 | "replaceWith": "LabelOptions" 42 | } 43 | ] 44 | }, 45 | 46 | 47 | { 48 | "pr": "https://github.com/angular/material2/pull/10325", 49 | "changes": [ 50 | { 51 | "replace": "FocusTrapDirective", 52 | "replaceWith": "CdkTrapFocus" 53 | } 54 | ] 55 | } 56 | ] 57 | -------------------------------------------------------------------------------- /src/rules/checkIdentifierMiscRule.ts: -------------------------------------------------------------------------------- 1 | import {bold, red} from 'chalk'; 2 | import {ProgramAwareRuleWalker, RuleFailure, Rules} from 'tslint'; 3 | import * as ts from 'typescript'; 4 | 5 | /** 6 | * Rule that walks through every identifier that is part of Angular Material and replaces the 7 | * outdated name with the new one. 8 | */ 9 | export class Rule extends Rules.TypedRule { 10 | applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] { 11 | return this.applyWithWalker( 12 | new CheckIdentifierMiscWalker(sourceFile, this.getOptions(), program)); 13 | } 14 | } 15 | 16 | export class CheckIdentifierMiscWalker extends ProgramAwareRuleWalker { 17 | visitIdentifier(identifier: ts.Identifier) { 18 | if (identifier.getText() === 'MatDrawerToggleResult') { 19 | this.addFailureAtNode( 20 | identifier, 21 | `Found "${bold('MatDrawerToggleResult')}" which has changed from a class type to a` + 22 | ` string literal type. Code may need to be updated`); 23 | } 24 | 25 | if (identifier.getText() === 'MatListOptionChange') { 26 | this.addFailureAtNode( 27 | identifier, 28 | `Found usage of "${red('MatListOptionChange')}" which has been removed. Please listen` + 29 | ` for ${bold('selectionChange')} on ${bold('MatSelectionList')} instead`); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/rules/checkClassDeclarationMiscRule.ts: -------------------------------------------------------------------------------- 1 | import {bold, green} from 'chalk'; 2 | import {ProgramAwareRuleWalker, RuleFailure, Rules} from 'tslint'; 3 | import * as ts from 'typescript'; 4 | 5 | /** 6 | * Rule that walks through every identifier that is part of Angular Material and replaces the 7 | * outdated name with the new one. 8 | */ 9 | export class Rule extends Rules.TypedRule { 10 | applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] { 11 | return this.applyWithWalker( 12 | new CheckClassDeclarationMiscWalker(sourceFile, this.getOptions(), program)); 13 | } 14 | } 15 | 16 | export class CheckClassDeclarationMiscWalker extends ProgramAwareRuleWalker { 17 | visitClassDeclaration(declaration: ts.ClassDeclaration) { 18 | if (declaration.heritageClauses) { 19 | declaration.heritageClauses.forEach(hc => { 20 | const classes = new Set(hc.types.map(t => t.getFirstToken().getText())); 21 | if (classes.has('MatFormFieldControl')) { 22 | const sfl = declaration.members 23 | .filter(prop => prop.getFirstToken().getText() === 'shouldFloatLabel'); 24 | if (!sfl.length) { 25 | this.addFailureAtNode( 26 | declaration, 27 | `Found class "${bold(declaration.name.text)}" which extends` + 28 | ` "${bold('MatFormFieldControl')}". This class must define` + 29 | ` "${green('shouldLabelFloat')}" which is now a required property.` 30 | ) 31 | } 32 | } 33 | }); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /test/fixtures/sample-project/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 10 | 11 | 12 |
13 | 14 | 15 |
16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-material-updater", 3 | "version": "0.0.7", 4 | "description": "Migrates projects from Angular Material 5.x to 6.0", 5 | "homepage": "https://github.com/angular/material-update-tool", 6 | "bugs": "https://github.com/angular/material-update-tool/issues", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/angular/material-update-tool.git" 10 | }, 11 | "main": "index.js", 12 | "scripts": { 13 | "build": "tsc -p ./ && npm run copy-files && cp package.json ./dist && cp README.md ./dist", 14 | "copy-files": "cpy \"**/*.json\" ../dist --parents --cwd ./src/ && cpy package.json dist/", 15 | "debug": "npm run build && node dist/cli -p test/fixtures/sample-project -v --es test/fixtures/**/*.{css,scss} --fix=false" 16 | }, 17 | "bin": { 18 | "mat-updater": "./cli.js" 19 | }, 20 | "author": "", 21 | "license": "MIT", 22 | "dependencies": { 23 | "chalk": "^2.0.1", 24 | "glob": "^7.1.2", 25 | "ora": "^1.3.0", 26 | "resolve-bin": "^0.4.0", 27 | "tslint": "^5.7.0", 28 | "typescript": "^2.4.2", 29 | "yargs": "^8.0.2" 30 | }, 31 | "devDependencies": { 32 | "@angular/cdk": "^5.0.0", 33 | "@angular/common": "^5.0.0", 34 | "@angular/core": "^5.0.0", 35 | "@angular/material": "^5.0.0", 36 | "@angular/platform-browser": "^5.0.0", 37 | "@types/chalk": "^0.4.31", 38 | "@types/glob": "^5.0.32", 39 | "@types/jasmine": "^2.5.53", 40 | "@types/node": "^8.0.17", 41 | "@types/ora": "^1.3.1", 42 | "@types/yargs": "^8.0.1", 43 | "cpy-cli": "^1.0.1", 44 | "rxjs": "^5.5.0", 45 | "ts-node": "^3.3.0", 46 | "zone.js": "^0.8.16" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/rules/checkInheritanceRule.ts: -------------------------------------------------------------------------------- 1 | import {bold, green, red} from 'chalk'; 2 | import {ProgramAwareRuleWalker, RuleFailure, Rules} from 'tslint'; 3 | import * as ts from 'typescript'; 4 | import {propertyNames} from '../material/component-data'; 5 | 6 | /** 7 | * Rule that walks through every property access expression and updates properties that have 8 | * been changed in favor of the new name. 9 | */ 10 | export class Rule extends Rules.TypedRule { 11 | applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] { 12 | return this.applyWithWalker( 13 | new CheckInheritanceWalker(sourceFile, this.getOptions(), program)); 14 | } 15 | } 16 | 17 | export class CheckInheritanceWalker extends ProgramAwareRuleWalker { 18 | visitClassDeclaration(declaration: ts.ClassDeclaration) { 19 | // Check if user is extending an Angular Material class whose properties have changed. 20 | const type = this.getTypeChecker().getTypeAtLocation(declaration.name); 21 | const baseTypes = this.getTypeChecker().getBaseTypes(type as ts.InterfaceType); 22 | baseTypes.forEach(t => { 23 | const propertyData = propertyNames.find( 24 | data => data.whitelist && new Set(data.whitelist.classes).has(t.symbol.name)); 25 | if (propertyData) { 26 | this.addFailureAtNode( 27 | declaration, 28 | `Found class "${bold(declaration.name.text)}" which extends class` + 29 | ` "${bold(t.symbol.name)}". Please note that the base class property` + 30 | ` "${red(propertyData.replace)}" has changed to "${green(propertyData.replaceWith)}".` + 31 | ` You may need to update your class as well`); 32 | } 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/typescript/imports.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | /** Checks whether the given node is part of an import specifier node. */ 4 | export function isImportSpecifierNode(node: ts.Node) { 5 | return isPartOfKind(node, ts.SyntaxKind.ImportSpecifier); 6 | } 7 | 8 | /** Checks whether the given node is part of an export specifier node. */ 9 | export function isExportSpecifierNode(node: ts.Node) { 10 | return isPartOfKind(node, ts.SyntaxKind.ExportSpecifier); 11 | } 12 | 13 | /** Checks whether the given node is part of a namespace import. */ 14 | export function isNamespaceImportNode(node: ts.Node) { 15 | return isPartOfKind(node, ts.SyntaxKind.NamespaceImport); 16 | } 17 | 18 | /** Finds the parent import declaration of a given TypeScript node. */ 19 | export function getImportDeclaration(node: ts.Node) { 20 | return findDeclaration(node, ts.SyntaxKind.ImportDeclaration) as ts.ImportDeclaration; 21 | } 22 | 23 | /** Finds the parent export declaration of a given TypeScript node */ 24 | export function getExportDeclaration(node: ts.Node) { 25 | return findDeclaration(node, ts.SyntaxKind.ExportDeclaration) as ts.ExportDeclaration; 26 | } 27 | 28 | /** Finds the specified declaration for the given node by walking up the TypeScript nodes. */ 29 | function findDeclaration(node: ts.Node, kind: T) { 30 | while (node.kind !== kind) { 31 | node = node.parent; 32 | } 33 | 34 | return node; 35 | } 36 | 37 | /** Checks whether the given node is part of another TypeScript Node with the specified kind. */ 38 | function isPartOfKind(node: ts.Node, kind: T): boolean { 39 | if (node.kind === kind) { 40 | return true; 41 | } else if (node.kind === ts.SyntaxKind.SourceFile) { 42 | return false; 43 | } 44 | 45 | return isPartOfKind(node.parent, kind); 46 | } 47 | -------------------------------------------------------------------------------- /src/rules/switchStringLiteralElementSelectorsRule.ts: -------------------------------------------------------------------------------- 1 | import {green, red} from 'chalk'; 2 | import {Replacement, RuleFailure, Rules, RuleWalker} from 'tslint'; 3 | import * as ts from 'typescript'; 4 | import {elementSelectors} from '../material/component-data'; 5 | import {findAll} from '../typescript/literal'; 6 | 7 | /** 8 | * Rule that walks through every string literal, which includes the outdated Material name and 9 | * is part of a call expression. Those string literals will be changed to the new name. 10 | */ 11 | export class Rule extends Rules.AbstractRule { 12 | apply(sourceFile: ts.SourceFile): RuleFailure[] { 13 | return this.applyWithWalker( 14 | new SwitchStringLiteralElementSelectorsWalker(sourceFile, this.getOptions())); 15 | } 16 | } 17 | 18 | export class SwitchStringLiteralElementSelectorsWalker extends RuleWalker { 19 | visitStringLiteral(stringLiteral: ts.StringLiteral) { 20 | if (stringLiteral.parent.kind !== ts.SyntaxKind.CallExpression) { 21 | return; 22 | } 23 | 24 | let stringLiteralText = stringLiteral.getFullText(); 25 | 26 | elementSelectors.forEach(selector => { 27 | this.createReplacementsForOffsets(stringLiteral, selector, 28 | findAll(stringLiteralText, selector.replace)).forEach(replacement => { 29 | this.addFailureAtNode( 30 | stringLiteral, 31 | `Found deprecated element selector "${red(selector.replace)}" which has been` + 32 | ` renamed to "${green(selector.replaceWith)}"`, 33 | replacement); 34 | }); 35 | }); 36 | } 37 | 38 | private createReplacementsForOffsets(node: ts.Node, 39 | update: {replace: string, replaceWith: string}, 40 | offsets: number[]): Replacement[] { 41 | return offsets.map(offset => this.createReplacement( 42 | node.getStart() + offset, update.replace.length, update.replaceWith)); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/rules/switchStringLiteralCssNamesRule.ts: -------------------------------------------------------------------------------- 1 | import {green, red} from 'chalk'; 2 | import {Replacement, RuleFailure, Rules, RuleWalker} from 'tslint'; 3 | import * as ts from 'typescript'; 4 | import {cssNames} from '../material/component-data'; 5 | import {findAll} from '../typescript/literal'; 6 | 7 | /** 8 | * Rule that walks through every string literal, which includes the outdated Material name and 9 | * is part of a call expression. Those string literals will be changed to the new name. 10 | */ 11 | export class Rule extends Rules.AbstractRule { 12 | apply(sourceFile: ts.SourceFile): RuleFailure[] { 13 | return this.applyWithWalker( 14 | new SwitchStringLiteralCssNamesWalker(sourceFile, this.getOptions())); 15 | } 16 | } 17 | 18 | export class SwitchStringLiteralCssNamesWalker extends RuleWalker { 19 | visitStringLiteral(stringLiteral: ts.StringLiteral) { 20 | if (stringLiteral.parent.kind !== ts.SyntaxKind.CallExpression) { 21 | return; 22 | } 23 | 24 | let stringLiteralText = stringLiteral.getFullText(); 25 | 26 | cssNames.forEach(name => { 27 | if (!name.whitelist || name.whitelist.strings) { 28 | this.createReplacementsForOffsets(stringLiteral, name, 29 | findAll(stringLiteralText, name.replace)).forEach(replacement => { 30 | this.addFailureAtNode( 31 | stringLiteral, 32 | `Found deprecated CSS class "${red(name.replace)}" which has been renamed to` + 33 | ` "${green(name.replaceWith)}"`, 34 | replacement) 35 | }); 36 | } 37 | }); 38 | } 39 | 40 | private createReplacementsForOffsets(node: ts.Node, 41 | update: {replace: string, replaceWith: string}, 42 | offsets: number[]): Replacement[] { 43 | return offsets.map(offset => this.createReplacement( 44 | node.getStart() + offset, update.replace.length, update.replaceWith)); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/rules/switchStringLiteralAttributeSelectorsRule.ts: -------------------------------------------------------------------------------- 1 | import {green, red} from 'chalk'; 2 | import {Replacement, RuleFailure, Rules, RuleWalker} from 'tslint'; 3 | import * as ts from 'typescript'; 4 | import {attributeSelectors} from '../material/component-data'; 5 | import {findAll} from '../typescript/literal'; 6 | 7 | /** 8 | * Rule that walks through every string literal, which includes the outdated Material name and 9 | * is part of a call expression. Those string literals will be changed to the new name. 10 | */ 11 | export class Rule extends Rules.AbstractRule { 12 | apply(sourceFile: ts.SourceFile): RuleFailure[] { 13 | return this.applyWithWalker( 14 | new SwitchStringLiteralAttributeSelectorsWalker(sourceFile, this.getOptions())); 15 | } 16 | } 17 | 18 | export class SwitchStringLiteralAttributeSelectorsWalker extends RuleWalker { 19 | visitStringLiteral(stringLiteral: ts.StringLiteral) { 20 | if (stringLiteral.parent.kind !== ts.SyntaxKind.CallExpression) { 21 | return; 22 | } 23 | 24 | let stringLiteralText = stringLiteral.getFullText(); 25 | 26 | attributeSelectors.forEach(selector => { 27 | this.createReplacementsForOffsets(stringLiteral, selector, 28 | findAll(stringLiteralText, selector.replace)).forEach(replacement => { 29 | this.addFailureAtNode( 30 | stringLiteral, 31 | `Found deprecated attribute selector "${red('[' + selector.replace + ']')}" which has` + 32 | ` been renamed to "${green('[' + selector.replaceWith + ']')}"`, 33 | replacement); 34 | }); 35 | }); 36 | } 37 | 38 | private createReplacementsForOffsets(node: ts.Node, 39 | update: {replace: string, replaceWith: string}, 40 | offsets: number[]): Replacement[] { 41 | return offsets.map(offset => this.createReplacement( 42 | node.getStart() + offset, update.replace.length, update.replaceWith)); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular Material 5.x to 6.0 updater tool 2 | 3 | **Outdated**: This repository won't be updated anymore. Future major releases for Angular Material will also have an upgrade tool that can be used in **Angular CLI** projects by running **`ng update @angular/material`** 4 | 5 | --- 6 | 7 | ## Installation 8 | ```bash 9 | npm i -g angular-material-updater 10 | ``` 11 | **Note**: Running this tool with Angular Material versions higher than `5.2.4` will not work. The 12 | updater tool requires type information from the outdated `5.x` API. The upgrade to the latest 13 | version of Angular Material should happen _after_ running the tool. 14 | 15 | ## Usage 16 | Run the `mat-updater` command to update your project. The updater tool will attempt to automatically 17 | fix issues that it can and report issues that can't be automatically fixed. The tool may not catch 18 | 100% of the issues, but it should help get most of the way there. The fixes applied by the tool also 19 | may not be perfect. For example it may rename a symbol resulting in an invalid import. If you want 20 | to run the tool and just detect issues without trying to fix them, you can do so by specifying 21 | `--fix=false`. 22 | 23 | ```bash 24 | # Show the help for the tool 25 | mat-updater --help 26 | 27 | # Run the tool to update prefixes 28 | mat-updater -p path/to/project/tsconfig.json 29 | 30 | # Run the tool to update prefixes with additional style 31 | # files not referenced by an Angular component, where --extra-css 32 | # accepts a glob pointing to the style files 33 | mat-updater -p path/to/project/tsconfig.json --extra-css 'custom/**/*.css' 34 | 35 | # Run the tool to but don't automatically change anything 36 | mat-updater -p path/to/project/tsconfig.json --fix=false 37 | ``` 38 | 39 | ## After running the tool 40 | 1. Update the packages below if you are using them: 41 | ```bash 42 | npm i @angular/cdk@latest 43 | npm i @angular/material@latest 44 | npm i @angular/material-moment-adapter@latest 45 | ``` 46 | 2. Address any issues that were reported by `mat-updater` but unable to be 47 | automatically fixed. 48 | 3. Try to build your app and fix any issues that show up (e.g. bad imports). 49 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export {Rule as CheckClassDeclarationMiscRule} from './rules/checkClassDeclarationMiscRule'; 2 | export {Rule as CheckIdentifierMiscRule} from './rules/checkIdentifierMiscRule'; 3 | export {Rule as CheckImportMiscRule} from './rules/checkImportMiscRule'; 4 | export {Rule as CheckInheritanceRule} from './rules/checkInheritanceRule'; 5 | export {Rule as CheckMethodCallsRule} from './rules/checkMethodCallsRule'; 6 | export {Rule as CheckPropertyAccessMiscRule} from './rules/checkPropertyAccessMiscRule'; 7 | export {Rule as CheckTemplateMiscRule} from './rules/checkTemplateMiscRule'; 8 | export {Rule as SwitchIdentifiersRule} from './rules/switchIdentifiersRule'; 9 | export {Rule as SwitchPropertyNamesRule} from './rules/switchPropertyNamesRule'; 10 | export {Rule as SwitchStringLiteralAttributeSelectorsRule} from './rules/switchStringLiteralAttributeSelectorsRule'; 11 | export {Rule as SwitchStringLiteralCssNamesRule} from './rules/switchStringLiteralCssNamesRule'; 12 | export {Rule as SwitchStringLiteralElementSelectorsRule} from './rules/switchStringLiteralElementSelectorsRule'; 13 | export {Rule as SwitchStylesheetAttributeSelectorsRule} from './rules/switchStylesheetAttributeSelectorsRule'; 14 | export {Rule as SwitchStylesheetCssNamesRule} from './rules/switchStylesheetCssNamesRule'; 15 | export {Rule as SwitchStylesheetElementSelectorsRule} from './rules/switchStylesheetElementSelectorsRule'; 16 | export {Rule as SwitchStylesheetInputNamesRule} from './rules/switchStylesheetInputNamesRule'; 17 | export {Rule as SwitchStylesheetOutputNamesRule} from './rules/switchStylesheetOutputNamesRule'; 18 | export {Rule as SwitchTemplateAttributeSelectorsRule} from './rules/switchTemplateAttributeSelectorsRule'; 19 | export {Rule as SwitchTemplateCssNamesRule} from './rules/switchTemplateCssNamesRule'; 20 | export {Rule as SwitchTemplateElementSelectorsRule} from './rules/switchTemplateElementSelectorsRule'; 21 | export {Rule as SwitchTemplateExportAsNamesRule} from './rules/switchTemplateExportAsNamesRule'; 22 | export {Rule as SwitchTemplateInputNamesRule} from './rules/switchTemplateInputNamesRule'; 23 | export {Rule as SwitchTemplateOutputNamesRule} from './rules/switchTemplateOutputNamesRule'; 24 | -------------------------------------------------------------------------------- /src/material/data/output-names.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pr": "https://github.com/angular/material2/pull/10163", 4 | "changes": [ 5 | { 6 | "replace": "change", 7 | "replaceWith": "selectionChange", 8 | "whitelist": { 9 | "elements": ["mat-select"] 10 | } 11 | }, 12 | { 13 | "replace": "onClose", 14 | "replaceWith": "closed", 15 | "whitelist": { 16 | "elements": ["mat-select"] 17 | } 18 | }, 19 | { 20 | "replace": "onOpen", 21 | "replaceWith": "opened", 22 | "whitelist": { 23 | "elements": ["mat-select"] 24 | } 25 | } 26 | ] 27 | }, 28 | 29 | 30 | { 31 | "pr": "https://github.com/angular/material2/pull/10279", 32 | "changes": [ 33 | { 34 | "replace": "align-changed", 35 | "replaceWith": "positionChanged", 36 | "whitelist": { 37 | "elements": ["mat-drawer", "mat-sidenav"] 38 | } 39 | }, 40 | { 41 | "replace": "close", 42 | "replaceWith": "closed", 43 | "whitelist": { 44 | "elements": ["mat-drawer", "mat-sidenav"] 45 | } 46 | }, 47 | { 48 | "replace": "open", 49 | "replaceWith": "opened", 50 | "whitelist": { 51 | "elements": ["mat-drawer", "mat-sidenav"] 52 | } 53 | } 54 | ] 55 | }, 56 | 57 | 58 | { 59 | "pr": "https://github.com/angular/material2/pull/10309", 60 | "changes": [ 61 | { 62 | "replace": "selectChange", 63 | "replaceWith": "selectedTabChange", 64 | "whitelist": { 65 | "elements": ["mat-tab-group"] 66 | } 67 | } 68 | ] 69 | }, 70 | 71 | 72 | { 73 | "pr": "https://github.com/angular/material2/pull/10311", 74 | "changes": [ 75 | { 76 | "replace": "remove", 77 | "replaceWith": "removed", 78 | "whitelist": { 79 | "attributes": ["mat-chip", "mat-basic-chip"], 80 | "elements": ["mat-chip", "mat-basic-chip"] 81 | } 82 | }, 83 | { 84 | "replace": "destroy", 85 | "replaceWith": "destroyed", 86 | "whitelist": { 87 | "attributes": ["mat-chip", "mat-basic-chip"], 88 | "elements": ["mat-chip", "mat-basic-chip"] 89 | } 90 | } 91 | ] 92 | } 93 | ] 94 | -------------------------------------------------------------------------------- /src/rules/checkPropertyAccessMiscRule.ts: -------------------------------------------------------------------------------- 1 | import {bold, green, red} from 'chalk'; 2 | import {ProgramAwareRuleWalker, RuleFailure, Rules} from 'tslint'; 3 | import * as ts from 'typescript'; 4 | 5 | /** 6 | * Rule that walks through every identifier that is part of Angular Material and replaces the 7 | * outdated name with the new one. 8 | */ 9 | export class Rule extends Rules.TypedRule { 10 | applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] { 11 | return this.applyWithWalker( 12 | new CheckPropertyAccessMiscWalker(sourceFile, this.getOptions(), program)); 13 | } 14 | } 15 | 16 | export class CheckPropertyAccessMiscWalker extends ProgramAwareRuleWalker { 17 | visitPropertyAccessExpression(prop: ts.PropertyAccessExpression) { 18 | // Recursively call this method for the expression of the current property expression. 19 | // It can happen that there is a chain of property access expressions. 20 | // For example: "mySortInstance.mdSortChange.subscribe()" 21 | if (prop.expression && prop.expression.kind === ts.SyntaxKind.PropertyAccessExpression) { 22 | this.visitPropertyAccessExpression(prop.expression as ts.PropertyAccessExpression); 23 | } 24 | 25 | // TODO(mmalerba): This is prrobably a bad way to get the property host... 26 | // Tokens are: [..., , '.', ], so back up 3. 27 | const propHost = prop.getChildAt(prop.getChildCount() - 3); 28 | 29 | const type = this.getTypeChecker().getTypeAtLocation(propHost); 30 | const typeName = type && type.getSymbol() && type.getSymbol().getName(); 31 | 32 | if (typeName === 'MatListOption' && prop.name.text === 'selectionChange') { 33 | this.addFailureAtNode( 34 | prop, 35 | `Found deprecated property "${red('selectionChange')}" of class` + 36 | ` "${bold('MatListOption')}". Use the "${green('selectionChange')}" property on the` + 37 | ` parent "${bold('MatSelectionList')}" instead.`); 38 | } 39 | 40 | if (typeName === 'MatDatepicker' && prop.name.text === 'selectedChanged') { 41 | this.addFailureAtNode( 42 | prop, 43 | `Found deprecated property "${red('selectedChanged')}" of class` + 44 | ` "${bold('MatDatepicker')}". Use the "${green('dateChange')}" or` + 45 | ` "${green('dateInput')}" methods on "${bold('MatDatepickerInput')}" instead`); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/material/data/css-names.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pr": "https://github.com/angular/material2/pull/10296", 4 | "changes": [ 5 | { 6 | "replace": "mat-form-field-placeholder", 7 | "replaceWith": "mat-form-field-label" 8 | }, 9 | { 10 | "replace": "mat-form-field-placeholder-wrapper", 11 | "replaceWith": "mat-form-field-label-wrapper" 12 | }, 13 | { 14 | "replace": "mat-input-container", 15 | "replaceWith": "mat-form-field" 16 | }, 17 | { 18 | "replace": "mat-input-flex", 19 | "replaceWith": "mat-form-field-flex" 20 | }, 21 | { 22 | "replace": "mat-input-hint-spacer", 23 | "replaceWith": "mat-form-field-hint-spacer" 24 | }, 25 | { 26 | "replace": "mat-input-hint-wrapper", 27 | "replaceWith": "mat-form-field-hint-wrapper" 28 | }, 29 | { 30 | "replace": "mat-input-infix", 31 | "replaceWith": "mat-form-field-infix" 32 | }, 33 | { 34 | "replace": "mat-input-invalid", 35 | "replaceWith": "mat-form-field-invalid" 36 | }, 37 | { 38 | "replace": "mat-input-placeholder", 39 | "replaceWith": "mat-form-field-label" 40 | }, 41 | { 42 | "replace": "mat-input-placeholder-wrapper", 43 | "replaceWith": "mat-form-field-label-wrapper" 44 | }, 45 | { 46 | "replace": "mat-input-prefix", 47 | "replaceWith": "mat-form-field-prefix" 48 | }, 49 | { 50 | "replace": "mat-input-ripple", 51 | "replaceWith": "mat-form-field-ripple" 52 | }, 53 | { 54 | "replace": "mat-input-subscript-wrapper", 55 | "replaceWith": "mat-form-field-subscript-wrapper" 56 | }, 57 | { 58 | "replace": "mat-input-suffix", 59 | "replaceWith": "mat-form-field-suffix" 60 | }, 61 | { 62 | "replace": "mat-input-underline", 63 | "replaceWith": "mat-form-field-underline" 64 | }, 65 | { 66 | "replace": "mat-input-wrapper", 67 | "replaceWith": "mat-form-field-wrapper" 68 | } 69 | ] 70 | }, 71 | 72 | 73 | { 74 | "pr": "https://github.com/angular/material2/pull/10325", 75 | "changes": [ 76 | { 77 | "replace": "$mat-font-family", 78 | "replaceWith": "Roboto, 'Helvetica Neue', sans-serif", 79 | "whitelist": { 80 | "css": true 81 | } 82 | } 83 | ] 84 | } 85 | ] 86 | -------------------------------------------------------------------------------- /src/rules/switchPropertyNamesRule.ts: -------------------------------------------------------------------------------- 1 | import {bold, green, red} from 'chalk'; 2 | import {ProgramAwareRuleWalker, RuleFailure, Rules} from 'tslint'; 3 | import * as ts from 'typescript'; 4 | import {propertyNames} from '../material/component-data'; 5 | 6 | /** 7 | * Rule that walks through every property access expression and updates properties that have 8 | * been changed in favor of the new name. 9 | */ 10 | export class Rule extends Rules.TypedRule { 11 | applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] { 12 | return this.applyWithWalker( 13 | new SwitchPropertyNamesWalker(sourceFile, this.getOptions(), program)); 14 | } 15 | } 16 | 17 | export class SwitchPropertyNamesWalker extends ProgramAwareRuleWalker { 18 | visitPropertyAccessExpression(prop: ts.PropertyAccessExpression) { 19 | // Recursively call this method for the expression of the current property expression. 20 | // It can happen that there is a chain of property access expressions. 21 | // For example: "mySortInstance.mdSortChange.subscribe()" 22 | if (prop.expression && prop.expression.kind === ts.SyntaxKind.PropertyAccessExpression) { 23 | this.visitPropertyAccessExpression(prop.expression as ts.PropertyAccessExpression); 24 | } 25 | 26 | // TODO(mmalerba): This is prrobably a bad way to get the property host... 27 | // Tokens are: [..., , '.', ], so back up 3. 28 | const propHost = prop.getChildAt(prop.getChildCount() - 3); 29 | 30 | const type = this.getTypeChecker().getTypeAtLocation(propHost); 31 | const typeName = type && type.getSymbol() && type.getSymbol().getName(); 32 | const propertyData = propertyNames.find(name => { 33 | if (prop.name.text === name.replace) { 34 | // TODO(mmalerba): Verify that this type comes from Angular Material like we do in 35 | // `switchIdentifiersRule`. 36 | return !name.whitelist || new Set(name.whitelist.classes).has(typeName); 37 | } 38 | return false; 39 | }); 40 | 41 | if (!propertyData) { 42 | return; 43 | } 44 | 45 | const replacement = this.createReplacement(prop.name.getStart(), 46 | prop.name.getWidth(), propertyData.replaceWith); 47 | 48 | const typeMessage = propertyData.whitelist ? `of class "${bold(typeName)}"` : ''; 49 | 50 | this.addFailureAtNode( 51 | prop.name, 52 | `Found deprecated property "${red(propertyData.replace)}" ${typeMessage} which has been` + 53 | ` renamed to "${green(propertyData.replaceWith)}"`, 54 | replacement); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/material/data/method-call-checks.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pr": "https://github.com/angular/material2/pull/9190", 4 | "changes": [ 5 | { 6 | "className": "NativeDateAdapter", 7 | "method": "constructor", 8 | "invalidArgCounts": [ 9 | { 10 | "count": 1, 11 | "message": "\"g{{platform}}\" is now required as a second argument" 12 | } 13 | ] 14 | } 15 | ] 16 | }, 17 | 18 | 19 | { 20 | "pr": "https://github.com/angular/material2/pull/10319", 21 | "changes": [ 22 | { 23 | "className": "MatAutocomplete", 24 | "method": "constructor", 25 | "invalidArgCounts": [ 26 | { 27 | "count": 2, 28 | "message": "\"g{{default}}\" is now required as a third argument" 29 | } 30 | ] 31 | } 32 | ] 33 | }, 34 | 35 | 36 | { 37 | "pr": "https://github.com/angular/material2/pull/10325", 38 | "changes": [ 39 | { 40 | "className": "FocusMonitor", 41 | "method": "monitor", 42 | "invalidArgCounts": [ 43 | { 44 | "count": 3, 45 | "message": "The \"r{{renderer}}\" argument has been removed" 46 | } 47 | ] 48 | } 49 | ] 50 | }, 51 | 52 | 53 | { 54 | "pr": "https://github.com/angular/material2/pull/10344", 55 | "changes": [ 56 | { 57 | "className": "MatTooltip", 58 | "method": "constructor", 59 | "invalidArgCounts": [ 60 | { 61 | "count": 11, 62 | "message": "\"g{{_defaultOptions}}\" is now required as a twelfth argument" 63 | } 64 | ] 65 | } 66 | ] 67 | }, 68 | 69 | 70 | { 71 | "pr": "https://github.com/angular/material2/pull/10389", 72 | "changes": [ 73 | { 74 | "className": "MatIconRegistry", 75 | "method": "constructor", 76 | "invalidArgCounts": [ 77 | { 78 | "count": 2, 79 | "message": "\"g{{document}}\" is now required as a third argument" 80 | } 81 | ] 82 | } 83 | ] 84 | }, 85 | 86 | 87 | { 88 | "pr": "https://github.com/angular/material2/pull/9775", 89 | "changes": [ 90 | { 91 | "className": "MatCalendar", 92 | "method": "constructor", 93 | "invalidArgCounts": [ 94 | { 95 | "count": 6, 96 | "message": "\"r{{_elementRef}}\" and \"r{{_ngZone}}\" arguments have been removed" 97 | }, 98 | { 99 | "count": 7, 100 | "message": "\"r{{_elementRef}}\", \"r{{_ngZone}}\", and \"r{{_dir}}\" arguments have been removed" 101 | } 102 | ] 103 | } 104 | ] 105 | } 106 | ] 107 | -------------------------------------------------------------------------------- /src/rules/switchTemplateExportAsNamesRule.ts: -------------------------------------------------------------------------------- 1 | import {green, red} from 'chalk'; 2 | import {Replacement, RuleFailure, Rules} from 'tslint'; 3 | import * as ts from 'typescript'; 4 | import {exportAsNames} from '../material/component-data'; 5 | import {ExternalResource} from '../tslint/component-file'; 6 | import {ComponentWalker} from '../tslint/component-walker'; 7 | import {findAll} from '../typescript/literal'; 8 | 9 | /** 10 | * Rule that walks through every component decorator and updates their inline or external 11 | * templates. 12 | */ 13 | export class Rule extends Rules.AbstractRule { 14 | apply(sourceFile: ts.SourceFile): RuleFailure[] { 15 | return this.applyWithWalker( 16 | new SwitchTemplateExportAsNamesWalker(sourceFile, this.getOptions())); 17 | } 18 | } 19 | 20 | export class SwitchTemplateExportAsNamesWalker extends ComponentWalker { 21 | visitInlineTemplate(template: ts.StringLiteral) { 22 | this.replaceNamesInTemplate(template, template.getText()).forEach(replacement => { 23 | const fix = replacement.replacement; 24 | const ruleFailure = new RuleFailure(template.getSourceFile(), fix.start, fix.end, 25 | replacement.message, this.getRuleName(), fix); 26 | this.addFailure(ruleFailure); 27 | }); 28 | } 29 | 30 | visitExternalTemplate(template: ExternalResource) { 31 | this.replaceNamesInTemplate(template, template.getFullText()).forEach(replacement => { 32 | const fix = replacement.replacement; 33 | const ruleFailure = new RuleFailure(template, fix.start + 1, fix.end + 1, 34 | replacement.message, this.getRuleName(), fix); 35 | this.addFailure(ruleFailure); 36 | }); 37 | } 38 | 39 | /** 40 | * Replaces the outdated name in the template with the new one and returns an updated template. 41 | */ 42 | private replaceNamesInTemplate(node: ts.Node, templateContent: string): 43 | {message: string, replacement: Replacement}[] { 44 | const replacements: {message: string, replacement: Replacement}[] = []; 45 | 46 | exportAsNames.forEach(name => { 47 | this.createReplacementsForOffsets(node, name, findAll(templateContent, name.replace)) 48 | .forEach(replacement => { 49 | replacements.push({ 50 | message: `Found deprecated exportAs reference "${red(name.replace)}" which has been` + 51 | ` renamed to "${green(name.replaceWith)}"`, 52 | replacement 53 | }); 54 | }) 55 | }); 56 | 57 | return replacements; 58 | } 59 | 60 | private createReplacementsForOffsets(node: ts.Node, 61 | update: {replace: string, replaceWith: string}, 62 | offsets: number[]): Replacement[] { 63 | return offsets.map(offset => this.createReplacement( 64 | node.getStart() + offset, update.replace.length, update.replaceWith)); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/rules/switchTemplateCssNamesRule.ts: -------------------------------------------------------------------------------- 1 | import {green, red} from 'chalk'; 2 | import {Replacement, RuleFailure, Rules} from 'tslint'; 3 | import * as ts from 'typescript'; 4 | import {cssNames} from '../material/component-data'; 5 | import {ExternalResource} from '../tslint/component-file'; 6 | import {ComponentWalker} from '../tslint/component-walker'; 7 | import {findAll} from '../typescript/literal'; 8 | 9 | /** 10 | * Rule that walks through every component decorator and updates their inline or external 11 | * templates. 12 | */ 13 | export class Rule extends Rules.AbstractRule { 14 | apply(sourceFile: ts.SourceFile): RuleFailure[] { 15 | return this.applyWithWalker(new SwitchTemplateCaaNamesWalker(sourceFile, this.getOptions())); 16 | } 17 | } 18 | 19 | export class SwitchTemplateCaaNamesWalker extends ComponentWalker { 20 | visitInlineTemplate(template: ts.StringLiteral) { 21 | this.replaceNamesInTemplate(template, template.getText()).forEach(replacement => { 22 | const fix = replacement.replacement; 23 | const ruleFailure = new RuleFailure(template.getSourceFile(), fix.start, fix.end, 24 | replacement.message, this.getRuleName(), fix); 25 | this.addFailure(ruleFailure); 26 | }); 27 | } 28 | 29 | visitExternalTemplate(template: ExternalResource) { 30 | this.replaceNamesInTemplate(template, template.getFullText()).forEach(replacement => { 31 | const fix = replacement.replacement; 32 | const ruleFailure = new RuleFailure(template, fix.start + 1, fix.end + 1, 33 | replacement.message, this.getRuleName(), fix); 34 | this.addFailure(ruleFailure); 35 | }); 36 | } 37 | 38 | /** 39 | * Replaces the outdated name in the template with the new one and returns an updated template. 40 | */ 41 | private replaceNamesInTemplate(node: ts.Node, templateContent: string): 42 | {message: string, replacement: Replacement}[] { 43 | const replacements: {message: string, replacement: Replacement}[] = []; 44 | 45 | cssNames.forEach(name => { 46 | if (!name.whitelist || name.whitelist.html) { 47 | this.createReplacementsForOffsets(node, name, findAll(templateContent, name.replace)) 48 | .forEach(replacement => { 49 | replacements.push({ 50 | message: `Found deprecated CSS class "${red(name.replace)}" which has been` + 51 | ` renamed to "${green(name.replaceWith)}"`, 52 | replacement 53 | }); 54 | }); 55 | } 56 | }); 57 | 58 | return replacements; 59 | } 60 | 61 | private createReplacementsForOffsets(node: ts.Node, 62 | update: {replace: string, replaceWith: string}, 63 | offsets: number[]): Replacement[] { 64 | return offsets.map(offset => this.createReplacement( 65 | node.getStart() + offset, update.replace.length, update.replaceWith)); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /test/fixtures/sample-project/app/app-component.ts: -------------------------------------------------------------------------------- 1 | import {FocusMonitor} from '@angular/cdk/a11y'; 2 | import {ConnectedOverlayDirective} from '@angular/cdk/overlay'; 3 | import {Component, ElementRef, EventEmitter, ViewChild} from '@angular/core'; 4 | import { 5 | MAT_PLACEHOLDER_GLOBAL_OPTIONS, MatDatepicker, 6 | MatDrawerToggleResult, 7 | MatFormFieldControl, MatListOption, MatListOptionChange, 8 | MatSelect, 9 | MatSidenav, 10 | NativeDateAdapter 11 | } from '@angular/material'; 12 | import {Observable} from 'rxjs/Observable'; 13 | 14 | @Component({ 15 | selector: 'md-app-component', 16 | templateUrl: './app.component.html', 17 | styleUrls: ['./app.component.css'] 18 | }) 19 | export class AppComponent {} 20 | 21 | @Component({ 22 | selector: 'md-test-component', 23 | template: ` 24 | 25 | 26 | `, 27 | styles: [ 28 | ` 29 | mat-checkbox { 30 | font-weight: bold; 31 | } 32 | `, 33 | ` 34 | button[mat-button] { 35 | text-transform: none; 36 | } 37 | 38 | /* This line should be changed: */ 39 | mat-input-container {} 40 | ` 41 | ] 42 | }) 43 | export class TestComponent { 44 | @ViewChild('cod') cod: ConnectedOverlayDirective; 45 | @ViewChild('snav') snav: MatSidenav; 46 | @ViewChild('select') select: MatSelect; 47 | @ViewChild('opt') opt: MatListOption; 48 | @ViewChild('dp') dp: MatDatepicker; 49 | 50 | thing = new MyThing(); 51 | blah: MatDrawerToggleResult; 52 | 53 | constructor(el: ElementRef, fm: FocusMonitor) { 54 | fm.monitor(el.nativeElement, null, true); 55 | let cods = [this.cod]; 56 | this.opt.selectionChange.subscribe((x: MatListOptionChange) => {}); 57 | this.dp.selectedChanged.subscribe(() => {}); 58 | 59 | // These lines should be changed: 60 | this.cod._deprecatedBackdropClass = 'test'; 61 | let stuff = this.snav.onAlignChanged; 62 | cods[0]._deprecatedHeight = 1; 63 | let x = MAT_PLACEHOLDER_GLOBAL_OPTIONS; 64 | let y = el.nativeElement.querySelector('mat-input-container'); 65 | this.select.onOpen.subscribe(() => {}); 66 | 67 | // These lines should not be changed: 68 | this.thing._deprecatedBackdropClass = 'test'; 69 | stuff = this.thing.onAlignChanged; 70 | } 71 | } 72 | 73 | class MyThing { 74 | _deprecatedBackdropClass: string; 75 | onAlignChanged = new EventEmitter(); 76 | } 77 | 78 | class MySidenav extends MatSidenav {} 79 | 80 | class MyDateAdapter extends NativeDateAdapter { 81 | constructor() { super(null); } 82 | } 83 | 84 | class MyFormFiledControl implements MatFormFieldControl { 85 | value: any; 86 | stateChanges: Observable; 87 | id: string; 88 | placeholder: string; 89 | ngControl: | null; 90 | focused: boolean; 91 | empty: boolean; 92 | required: boolean; 93 | disabled: boolean; 94 | errorState: boolean; 95 | setDescribedByIds(ids: string[]): void {} 96 | onContainerClick(event: MouseEvent): void {} 97 | } 98 | -------------------------------------------------------------------------------- /src/rules/switchTemplateElementSelectorsRule.ts: -------------------------------------------------------------------------------- 1 | import {green, red} from 'chalk'; 2 | import {Replacement, RuleFailure, Rules} from 'tslint'; 3 | import * as ts from 'typescript'; 4 | import {elementSelectors} from '../material/component-data'; 5 | import {ExternalResource} from '../tslint/component-file'; 6 | import {ComponentWalker} from '../tslint/component-walker'; 7 | import {findAll} from '../typescript/literal'; 8 | 9 | /** 10 | * Rule that walks through every component decorator and updates their inline or external 11 | * templates. 12 | */ 13 | export class Rule extends Rules.AbstractRule { 14 | apply(sourceFile: ts.SourceFile): RuleFailure[] { 15 | return this.applyWithWalker( 16 | new SwitchTemplateElementSelectorsWalker(sourceFile, this.getOptions())); 17 | } 18 | } 19 | 20 | export class SwitchTemplateElementSelectorsWalker extends ComponentWalker { 21 | visitInlineTemplate(template: ts.StringLiteral) { 22 | this.replaceNamesInTemplate(template, template.getText()).forEach(replacement => { 23 | const fix = replacement.replacement; 24 | const ruleFailure = new RuleFailure(template.getSourceFile(), fix.start, fix.end, 25 | replacement.message, this.getRuleName(), fix); 26 | this.addFailure(ruleFailure); 27 | }); 28 | } 29 | 30 | visitExternalTemplate(template: ExternalResource) { 31 | this.replaceNamesInTemplate(template, template.getFullText()).forEach(replacement => { 32 | const fix = replacement.replacement; 33 | const ruleFailure = new RuleFailure(template, fix.start + 1, fix.end + 1, 34 | replacement.message, this.getRuleName(), fix); 35 | this.addFailure(ruleFailure); 36 | }); 37 | } 38 | 39 | /** 40 | * Replaces the outdated name in the template with the new one and returns an updated template. 41 | */ 42 | private replaceNamesInTemplate(node: ts.Node, templateContent: string): 43 | {message: string, replacement: Replacement}[] { 44 | const replacements: {message: string, replacement: Replacement}[] = []; 45 | 46 | elementSelectors.forEach(selector => { 47 | // Being more aggressive with that replacement here allows us to also handle inline 48 | // style elements. Normally we would check if the selector is surrounded by the HTML tag 49 | // characters. 50 | this.createReplacementsForOffsets(node, selector, findAll(templateContent, selector.replace)) 51 | .forEach(replacement => { 52 | replacements.push({ 53 | message: `Found deprecated element selector "${red(selector.replace)}" which has` + 54 | ` been renamed to "${green(selector.replaceWith)}"`, 55 | replacement 56 | }); 57 | }); 58 | }); 59 | 60 | return replacements; 61 | } 62 | 63 | private createReplacementsForOffsets(node: ts.Node, 64 | update: {replace: string, replaceWith: string}, 65 | offsets: number[]): Replacement[] { 66 | return offsets.map(offset => this.createReplacement( 67 | node.getStart() + offset, update.replace.length, update.replaceWith)); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/rules/switchTemplateAttributeSelectorsRule.ts: -------------------------------------------------------------------------------- 1 | import {green, red} from 'chalk'; 2 | import {Replacement, RuleFailure, Rules} from 'tslint'; 3 | import * as ts from 'typescript'; 4 | import {attributeSelectors} from '../material/component-data'; 5 | import {ExternalResource} from '../tslint/component-file'; 6 | import {ComponentWalker} from '../tslint/component-walker'; 7 | import {findAll} from '../typescript/literal'; 8 | 9 | /** 10 | * Rule that walks through every component decorator and updates their inline or external 11 | * templates. 12 | */ 13 | export class Rule extends Rules.AbstractRule { 14 | apply(sourceFile: ts.SourceFile): RuleFailure[] { 15 | return this.applyWithWalker( 16 | new SwitchTemplateAttributeSelectorsWalker(sourceFile, this.getOptions())); 17 | } 18 | } 19 | 20 | export class SwitchTemplateAttributeSelectorsWalker extends ComponentWalker { 21 | visitInlineTemplate(template: ts.StringLiteral) { 22 | this.replaceNamesInTemplate(template, template.getText()).forEach(replacement => { 23 | const fix = replacement.replacement; 24 | const ruleFailure = new RuleFailure(template.getSourceFile(), fix.start, fix.end, 25 | replacement.message, this.getRuleName(), fix); 26 | this.addFailure(ruleFailure); 27 | }); 28 | } 29 | 30 | visitExternalTemplate(template: ExternalResource) { 31 | this.replaceNamesInTemplate(template, template.getFullText()).forEach(replacement => { 32 | const fix = replacement.replacement; 33 | const ruleFailure = new RuleFailure(template, fix.start + 1, fix.end + 1, 34 | replacement.message, this.getRuleName(), fix); 35 | this.addFailure(ruleFailure); 36 | }); 37 | } 38 | 39 | /** 40 | * Replaces the outdated name in the template with the new one and returns an updated template. 41 | */ 42 | private replaceNamesInTemplate(node: ts.Node, templateContent: string): 43 | {message: string, replacement: Replacement}[] { 44 | const replacements: {message: string, replacement: Replacement}[] = []; 45 | 46 | attributeSelectors.forEach(selector => { 47 | // Being more aggressive with that replacement here allows us to also handle inline 48 | // style elements. Normally we would check if the selector is surrounded by the HTML tag 49 | // characters. 50 | this.createReplacementsForOffsets(node, selector, findAll(templateContent, selector.replace)) 51 | .forEach(replacement => { 52 | replacements.push({ 53 | message: `Found deprecated attribute selector` + 54 | ` "${red('[' + selector.replace + ']')}" which has been renamed to` + 55 | ` "${green('[' + selector.replaceWith + ']')}"`, 56 | replacement 57 | }); 58 | }); 59 | }); 60 | 61 | return replacements; 62 | } 63 | 64 | private createReplacementsForOffsets(node: ts.Node, 65 | update: {replace: string, replaceWith: string}, 66 | offsets: number[]): Replacement[] { 67 | return offsets.map(offset => this.createReplacement( 68 | node.getStart() + offset, update.replace.length, update.replaceWith)); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/rules/checkMethodCallsRule.ts: -------------------------------------------------------------------------------- 1 | import {bold} from 'chalk'; 2 | import {ProgramAwareRuleWalker, RuleFailure, Rules} from 'tslint'; 3 | import * as ts from 'typescript'; 4 | import {color} from '../material/color'; 5 | import {methodCallChecks} from '../material/component-data'; 6 | 7 | /** 8 | * Rule that walks through every property access expression and updates properties that have 9 | * been changed in favor of the new name. 10 | */ 11 | export class Rule extends Rules.TypedRule { 12 | applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] { 13 | return this.applyWithWalker( 14 | new CheckMethodCallsWalker(sourceFile, this.getOptions(), program)); 15 | } 16 | } 17 | 18 | export class CheckMethodCallsWalker extends ProgramAwareRuleWalker { 19 | visitNewExpression(expression: ts.NewExpression) { 20 | const className = this.getTypeChecker().getTypeAtLocation(expression).symbol.name; 21 | this.checkConstructor(expression, className); 22 | } 23 | 24 | visitCallExpression(expression: ts.CallExpression) { 25 | if (expression.expression.kind !== ts.SyntaxKind.PropertyAccessExpression) { 26 | const methodName = expression.getFirstToken().getText(); 27 | 28 | if (methodName === 'super') { 29 | const type = this.getTypeChecker().getTypeAtLocation(expression.expression); 30 | const className = type.symbol && type.symbol.name; 31 | this.checkConstructor(expression, className); 32 | } 33 | return; 34 | } 35 | 36 | // TODO(mmalerba): This is probably a bad way to get the class node... 37 | // Tokens are: [..., , '.', ], so back up 3. 38 | const accessExp = expression.expression; 39 | const classNode = accessExp.getChildAt(accessExp.getChildCount() - 3); 40 | const methodNode = accessExp.getChildAt(accessExp.getChildCount() - 1); 41 | const methodName = methodNode.getText(); 42 | const type = this.getTypeChecker().getTypeAtLocation(classNode); 43 | const className = type.symbol && type.symbol.name; 44 | 45 | const currentCheck = methodCallChecks 46 | .find(data => data.method === methodName && data.className === className); 47 | if (!currentCheck) { 48 | return; 49 | } 50 | 51 | const failure = currentCheck.invalidArgCounts 52 | .find(countData => countData.count === expression.arguments.length); 53 | if (failure) { 54 | this.addFailureAtNode( 55 | expression, 56 | `Found call to "${bold(className + '.' + methodName)}" with` + 57 | ` ${bold(String(failure.count))} arguments. ${color(failure.message)}`); 58 | } 59 | } 60 | 61 | private checkConstructor(node: ts.NewExpression | ts.CallExpression, className: string) { 62 | const currentCheck = methodCallChecks 63 | .find(data => data.method === 'constructor' && data.className === className); 64 | if (!currentCheck) { 65 | return; 66 | } 67 | 68 | const failure = currentCheck.invalidArgCounts 69 | .find(countData => countData.count === node.arguments.length); 70 | if (failure) { 71 | this.addFailureAtNode( 72 | node, 73 | `Found "${bold(className)}" constructed with ${bold(String(failure.count))} arguments.` + 74 | ` ${color(failure.message)}`); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/rules/switchTemplateInputNamesRule.ts: -------------------------------------------------------------------------------- 1 | import {green, red} from 'chalk'; 2 | import {Replacement, RuleFailure, Rules} from 'tslint'; 3 | import * as ts from 'typescript'; 4 | import {inputNames} from '../material/component-data'; 5 | import {ExternalResource} from '../tslint/component-file'; 6 | import {ComponentWalker} from '../tslint/component-walker'; 7 | import {findAll, findAllInputsInElWithAttr, findAllInputsInElWithTag} from '../typescript/literal'; 8 | 9 | /** 10 | * Rule that walks through every component decorator and updates their inline or external 11 | * templates. 12 | */ 13 | export class Rule extends Rules.AbstractRule { 14 | apply(sourceFile: ts.SourceFile): RuleFailure[] { 15 | return this.applyWithWalker(new SwitchTemplateInputNamesWalker(sourceFile, this.getOptions())); 16 | } 17 | } 18 | 19 | export class SwitchTemplateInputNamesWalker extends ComponentWalker { 20 | visitInlineTemplate(template: ts.StringLiteral) { 21 | this.replaceNamesInTemplate(template, template.getText()).forEach(replacement => { 22 | const fix = replacement.replacement; 23 | const ruleFailure = new RuleFailure(template.getSourceFile(), fix.start, fix.end, 24 | replacement.message, this.getRuleName(), fix); 25 | this.addFailure(ruleFailure); 26 | }); 27 | } 28 | 29 | visitExternalTemplate(template: ExternalResource) { 30 | this.replaceNamesInTemplate(template, template.getFullText()).forEach(replacement => { 31 | const fix = replacement.replacement; 32 | const ruleFailure = new RuleFailure(template, fix.start + 1, fix.end + 1, 33 | replacement.message, this.getRuleName(), fix); 34 | this.addFailure(ruleFailure); 35 | }); 36 | } 37 | 38 | /** 39 | * Replaces the outdated name in the template with the new one and returns an updated template. 40 | */ 41 | private replaceNamesInTemplate(node: ts.Node, templateContent: string): 42 | {message: string, replacement: Replacement}[] { 43 | const replacements: {message: string, replacement: Replacement}[] = []; 44 | 45 | inputNames.forEach(name => { 46 | let offsets; 47 | if (name.whitelist && name.whitelist.attributes && name.whitelist.attributes.length) { 48 | offsets = 49 | findAllInputsInElWithAttr(templateContent, name.replace, name.whitelist.attributes); 50 | } 51 | if (name.whitelist && name.whitelist.elements && name.whitelist.elements.length) { 52 | offsets = 53 | findAllInputsInElWithTag(templateContent, name.replace, name.whitelist.elements); 54 | } 55 | if (!name.whitelist) { 56 | offsets = findAll(templateContent, name.replace); 57 | } 58 | this.createReplacementsForOffsets(node, name, offsets).forEach(replacement => { 59 | replacements.push({ 60 | message: `Found deprecated @Input() "${red(name.replace)}" which has been renamed to` + 61 | ` "${green(name.replaceWith)}"`, 62 | replacement 63 | }); 64 | }); 65 | }); 66 | 67 | return replacements; 68 | } 69 | 70 | private createReplacementsForOffsets(node: ts.Node, 71 | update: {replace: string, replaceWith: string}, 72 | offsets: number[]): Replacement[] { 73 | return offsets.map(offset => this.createReplacement( 74 | node.getStart() + offset, update.replace.length, update.replaceWith)); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/rules/switchTemplateOutputNamesRule.ts: -------------------------------------------------------------------------------- 1 | import {green, red} from 'chalk'; 2 | import {Replacement, RuleFailure, Rules} from 'tslint'; 3 | import * as ts from 'typescript'; 4 | import {outputNames} from '../material/component-data'; 5 | import {ExternalResource} from '../tslint/component-file'; 6 | import {ComponentWalker} from '../tslint/component-walker'; 7 | import { 8 | findAll, 9 | findAllOutputsInElWithAttr, 10 | findAllOutputsInElWithTag 11 | } from '../typescript/literal'; 12 | 13 | /** 14 | * Rule that walks through every component decorator and updates their inline or external 15 | * templates. 16 | */ 17 | export class Rule extends Rules.AbstractRule { 18 | apply(sourceFile: ts.SourceFile): RuleFailure[] { 19 | return this.applyWithWalker(new SwitchTemplateOutputNamesWalker(sourceFile, this.getOptions())); 20 | } 21 | } 22 | 23 | export class SwitchTemplateOutputNamesWalker extends ComponentWalker { 24 | visitInlineTemplate(template: ts.StringLiteral) { 25 | this.replaceNamesInTemplate(template, template.getText()).forEach(replacement => { 26 | const fix = replacement.replacement; 27 | const ruleFailure = new RuleFailure(template.getSourceFile(), fix.start, fix.end, 28 | replacement.message, this.getRuleName(), fix); 29 | this.addFailure(ruleFailure); 30 | }); 31 | } 32 | 33 | visitExternalTemplate(template: ExternalResource) { 34 | this.replaceNamesInTemplate(template, template.getFullText()).forEach(replacement => { 35 | const fix = replacement.replacement; 36 | const ruleFailure = new RuleFailure(template, fix.start + 1, fix.end + 1, 37 | replacement.message, this.getRuleName(), fix); 38 | this.addFailure(ruleFailure); 39 | }); 40 | } 41 | 42 | /** 43 | * Replaces the outdated name in the template with the new one and returns an updated template. 44 | */ 45 | private replaceNamesInTemplate(node: ts.Node, templateContent: string): 46 | {message: string, replacement: Replacement}[] { 47 | const replacements: {message: string, replacement: Replacement}[] = []; 48 | 49 | outputNames.forEach(name => { 50 | let offsets = []; 51 | if (name.whitelist && name.whitelist.attributes && name.whitelist.attributes.length) { 52 | offsets = offsets.concat(findAllOutputsInElWithAttr( 53 | templateContent, name.replace, name.whitelist.attributes)); 54 | } 55 | if (name.whitelist && name.whitelist.elements && name.whitelist.elements.length) { 56 | offsets = offsets.concat(findAllOutputsInElWithTag( 57 | templateContent, name.replace, name.whitelist.elements)); 58 | } 59 | if (!name.whitelist) { 60 | offsets = offsets.concat(findAll(templateContent, name.replace)); 61 | } 62 | this.createReplacementsForOffsets(node, name, offsets).forEach(replacement => { 63 | replacements.push({ 64 | message: `Found deprecated @Output() "${red(name.replace)}" which has been renamed to` + 65 | ` "${green(name.replaceWith)}"`, 66 | replacement 67 | }); 68 | }); 69 | }); 70 | 71 | return replacements; 72 | } 73 | 74 | private createReplacementsForOffsets(node: ts.Node, 75 | update: {replace: string, replaceWith: string}, 76 | offsets: number[]): Replacement[] { 77 | return offsets.map(offset => this.createReplacement( 78 | node.getStart() + offset, update.replace.length, update.replaceWith)); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/rules/checkTemplateMiscRule.ts: -------------------------------------------------------------------------------- 1 | import {bold, green, red} from 'chalk'; 2 | import {RuleFailure, Rules} from 'tslint'; 3 | import * as ts from 'typescript'; 4 | import {ExternalResource} from '../tslint/component-file'; 5 | import {ComponentWalker} from '../tslint/component-walker'; 6 | import {findAll, findAllInputsInElWithTag, findAllOutputsInElWithTag} from '../typescript/literal'; 7 | 8 | /** 9 | * Rule that walks through every component decorator and updates their inline or external 10 | * templates. 11 | */ 12 | export class Rule extends Rules.AbstractRule { 13 | apply(sourceFile: ts.SourceFile): RuleFailure[] { 14 | return this.applyWithWalker(new CheckTemplateMiscWalker(sourceFile, this.getOptions())); 15 | } 16 | } 17 | 18 | export class CheckTemplateMiscWalker extends ComponentWalker { 19 | visitInlineTemplate(template: ts.StringLiteral) { 20 | this.checkTemplate(template, template.getText()).forEach(failure => { 21 | const ruleFailure = new RuleFailure(template.getSourceFile(), failure.start, failure.end, 22 | failure.message, this.getRuleName()); 23 | this.addFailure(ruleFailure); 24 | }); 25 | } 26 | 27 | visitExternalTemplate(template: ExternalResource) { 28 | this.checkTemplate(template, template.getFullText()).forEach(failure => { 29 | const ruleFailure = new RuleFailure(template, failure.start + 1, failure.end + 1, 30 | failure.message, this.getRuleName()); 31 | this.addFailure(ruleFailure); 32 | }); 33 | } 34 | 35 | /** 36 | * Replaces the outdated name in the template with the new one and returns an updated template. 37 | */ 38 | private checkTemplate(node: ts.Node, templateContent: string): 39 | {start: number, end: number, message: string}[] { 40 | let failures: {message: string, start: number, end: number}[] = []; 41 | 42 | failures = failures.concat(findAll(templateContent, 'cdk-focus-trap').map(offset => ({ 43 | start: offset, 44 | end: offset + 'cdk-focus-trap'.length, 45 | message: `Found deprecated element selector "${red('cdk-focus-trap')}" which has been` + 46 | ` changed to an attribute selector "${green('[cdkTrapFocus]')}"` 47 | }))); 48 | 49 | failures = failures.concat( 50 | findAllOutputsInElWithTag(templateContent, 'selectionChange', ['mat-list-option']) 51 | .map(offset => ({ 52 | start: offset, 53 | end: offset + 'selectionChange'.length, 54 | message: `Found deprecated @Output() "${red('selectionChange')}" on` + 55 | ` "${bold('mat-list-option')}". Use "${green('selectionChange')}" on` + 56 | ` "${bold('mat-selection-list')}" instead` 57 | }))); 58 | 59 | failures = failures.concat( 60 | findAllOutputsInElWithTag(templateContent, 'selectedChanged', ['mat-datepicker']) 61 | .map(offset => ({ 62 | start: offset, 63 | end: offset + 'selectionChange'.length, 64 | message: `Found deprecated @Output() "${red('selectedChanged')}" on` + 65 | ` "${bold('mat-datepicker')}". Use "${green('dateChange')}" or` + 66 | ` "${green('dateInput')}" on "${bold('')}" instead` 67 | }))); 68 | 69 | failures = failures.concat( 70 | findAllInputsInElWithTag(templateContent, 'selected', ['mat-button-toggle-group']) 71 | .map(offset => ({ 72 | start: offset, 73 | end: offset + 'selected'.length, 74 | message: `Found deprecated @Input() "${red('selected')}" on`+ 75 | ` "${bold('mat-radio-button-group')}". Use "${green('value')}" instead` 76 | }))); 77 | 78 | return failures; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/typescript/literal.ts: -------------------------------------------------------------------------------- 1 | import * as ts from 'typescript'; 2 | 3 | /** Returns the text of a string literal without the quotes. */ 4 | export function getLiteralTextWithoutQuotes(literal: ts.StringLiteral) { 5 | return literal.getText().substring(1, literal.getText().length - 1); 6 | } 7 | 8 | /** Method that can be used to replace all search occurrences in a string. */ 9 | export function findAll(str: string, search: string): number[] { 10 | const result = []; 11 | let i = -1; 12 | while ((i = str.indexOf(search, i + 1)) !== -1) { 13 | result.push(i); 14 | } 15 | return result; 16 | } 17 | 18 | export function findAllInputsInElWithTag(html: string, name: string, tagNames: string[]): number[] { 19 | return findAllIoInElWithTag(html, name, tagNames, String.raw`\[?`, String.raw`\]?`); 20 | } 21 | 22 | export function findAllOutputsInElWithTag(html: string, name: string, tagNames: string[]): 23 | number[] { 24 | return findAllIoInElWithTag(html, name, tagNames, String.raw`\(`, String.raw`\)`); 25 | } 26 | 27 | /** 28 | * Method that can be used to rename all occurrences of an `@Input()` in a HTML string that occur 29 | * inside an element with any of the given attributes. This is useful for replacing an `@Input()` on 30 | * a `@Directive()` with an attribute selector. 31 | */ 32 | export function findAllInputsInElWithAttr(html: string, name: string, attrs: string[]): number[] { 33 | return findAllIoInElWithAttr(html, name, attrs, String.raw`\[?`, String.raw`\]?`); 34 | } 35 | 36 | /** 37 | * Method that can be used to rename all occurrences of an `@Output()` in a HTML string that occur 38 | * inside an element with any of the given attributes. This is useful for replacing an `@Output()` 39 | * on a `@Directive()` with an attribute selector. 40 | */ 41 | export function findAllOutputsInElWithAttr(html: string, name: string, attrs: string[]): number[] { 42 | return findAllIoInElWithAttr(html, name, attrs, String.raw`\(`, String.raw`\)`); 43 | } 44 | 45 | function findAllIoInElWithTag(html:string, name: string, tagNames: string[], startIoPattern: string, 46 | endIoPattern: string): number[] { 47 | const skipPattern = String.raw`[^>]*\s`; 48 | const openTagPattern = String.raw`<\s*`; 49 | const tagNamesPattern = String.raw`(?:${tagNames.join('|')})`; 50 | const replaceIoPattern = String.raw` 51 | (${openTagPattern}${tagNamesPattern}\s(?:${skipPattern})?${startIoPattern}) 52 | ${name} 53 | ${endIoPattern}[=\s>]`; 54 | const replaceIoRegex = new RegExp(replaceIoPattern.replace(/\s/g, ''), 'g'); 55 | const result = []; 56 | let match; 57 | while (match = replaceIoRegex.exec(html)) { 58 | result.push(match.index + match[1].length); 59 | } 60 | return result; 61 | } 62 | 63 | function findAllIoInElWithAttr(html: string, name: string, attrs: string[], startIoPattern: string, 64 | endIoPattern: string): number[] { 65 | const skipPattern = String.raw`[^>]*\s`; 66 | const openTagPattern = String.raw`<\s*\S`; 67 | const attrsPattern = String.raw`(?:${attrs.join('|')})`; 68 | const inputAfterAttrPattern = String.raw` 69 | (${openTagPattern}${skipPattern}${attrsPattern}[=\s](?:${skipPattern})?${startIoPattern}) 70 | ${name} 71 | ${endIoPattern}[=\s>]`; 72 | const inputBeforeAttrPattern = String.raw` 73 | (${openTagPattern}${skipPattern}${startIoPattern}) 74 | ${name} 75 | ${endIoPattern}[=\s](?:${skipPattern})?${attrsPattern}[=\s>]`; 76 | const replaceIoPattern = String.raw`${inputAfterAttrPattern}|${inputBeforeAttrPattern}`; 77 | const replaceIoRegex = new RegExp(replaceIoPattern.replace(/\s/g, ''), 'g'); 78 | const result = []; 79 | let match; 80 | while (match = replaceIoRegex.exec(html)) { 81 | result.push(match.index + (match[1] || match[2]).length); 82 | } 83 | return result; 84 | } 85 | -------------------------------------------------------------------------------- /src/rules/switchStylesheetCssNamesRule.ts: -------------------------------------------------------------------------------- 1 | import {green, red} from 'chalk'; 2 | import {sync as globSync} from 'glob'; 3 | import {resolve} from 'path'; 4 | import {IOptions, Replacement, RuleFailure, Rules} from 'tslint'; 5 | import * as ts from 'typescript'; 6 | import {cssNames} from '../material/component-data'; 7 | import {EXTRA_STYLESHEETS_GLOB_KEY} from '../material/extra-stylsheets'; 8 | import {ExternalResource} from '../tslint/component-file'; 9 | import {ComponentWalker} from '../tslint/component-walker'; 10 | import {findAll} from '../typescript/literal'; 11 | 12 | /** 13 | * Rule that walks through every component decorator and updates their inline or external 14 | * stylesheets. 15 | */ 16 | export class Rule extends Rules.AbstractRule { 17 | apply(sourceFile: ts.SourceFile): RuleFailure[] { 18 | return this.applyWithWalker(new SwitchStylesheetCssNamesWalker(sourceFile, this.getOptions())); 19 | } 20 | } 21 | 22 | export class SwitchStylesheetCssNamesWalker extends ComponentWalker { 23 | 24 | constructor(sourceFile: ts.SourceFile, options: IOptions) { 25 | // This is a special feature. In some applications, developers will have global stylesheets 26 | // that are not specified in any Angular component. Those stylesheets can be also migrated 27 | // if the developer specifies the `--extra-stylesheets` option which accepts a glob for files. 28 | const extraFiles = []; 29 | if (process.env[EXTRA_STYLESHEETS_GLOB_KEY]) { 30 | process.env[EXTRA_STYLESHEETS_GLOB_KEY].split(' ') 31 | .map(glob => globSync(glob)) 32 | .forEach(files => files.forEach(styleUrl => { 33 | extraFiles.push(resolve(styleUrl)); 34 | })); 35 | } 36 | 37 | super(sourceFile, options, extraFiles); 38 | 39 | extraFiles.forEach(styleUrl => this._reportExternalStyle(styleUrl)); 40 | } 41 | 42 | visitInlineStylesheet(stylesheet: ts.StringLiteral) { 43 | this.replaceNamesInStylesheet(stylesheet, stylesheet.getText()).forEach(replacement => { 44 | const fix = replacement.replacement; 45 | const ruleFailure = new RuleFailure(stylesheet.getSourceFile(), fix.start, fix.end, 46 | replacement.message, this.getRuleName(), fix); 47 | this.addFailure(ruleFailure); 48 | }); 49 | } 50 | 51 | visitExternalStylesheet(stylesheet: ExternalResource) { 52 | this.replaceNamesInStylesheet(stylesheet, stylesheet.getFullText()).forEach(replacement => { 53 | const fix = replacement.replacement; 54 | const ruleFailure = new RuleFailure(stylesheet, fix.start + 1, fix.end + 1, 55 | replacement.message, this.getRuleName(), fix); 56 | this.addFailure(ruleFailure); 57 | }); 58 | } 59 | 60 | /** 61 | * Replaces the outdated name in the stylesheet with the new one and returns an updated 62 | * stylesheet. 63 | */ 64 | private replaceNamesInStylesheet(node: ts.Node, stylesheetContent: string): 65 | {message: string, replacement: Replacement}[] { 66 | const replacements: {message: string, replacement: Replacement}[] = []; 67 | 68 | cssNames.forEach(name => { 69 | if (!name.whitelist || name.whitelist.css) { 70 | this.createReplacementsForOffsets(node, name, findAll(stylesheetContent, name.replace)) 71 | .forEach(replacement => { 72 | replacements.push({ 73 | message: `Found CSS class "${red(name.replace)}" which has been renamed to` + 74 | ` "${green(name.replaceWith)}"`, 75 | replacement 76 | }); 77 | }); 78 | } 79 | }); 80 | 81 | return replacements; 82 | } 83 | 84 | private createReplacementsForOffsets(node: ts.Node, 85 | update: {replace: string, replaceWith: string}, 86 | offsets: number[]): Replacement[] { 87 | return offsets.map(offset => this.createReplacement( 88 | node.getStart() + offset, update.replace.length, update.replaceWith)); 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /src/rules/switchStylesheetElementSelectorsRule.ts: -------------------------------------------------------------------------------- 1 | import {green, red} from 'chalk'; 2 | import {sync as globSync} from 'glob'; 3 | import {resolve} from "path"; 4 | import {IOptions, Replacement, RuleFailure, Rules} from 'tslint'; 5 | import * as ts from 'typescript'; 6 | import {elementSelectors} from '../material/component-data'; 7 | import {EXTRA_STYLESHEETS_GLOB_KEY} from '../material/extra-stylsheets'; 8 | import {ExternalResource} from '../tslint/component-file'; 9 | import {ComponentWalker} from '../tslint/component-walker'; 10 | import {findAll} from '../typescript/literal'; 11 | 12 | /** 13 | * Rule that walks through every component decorator and updates their inline or external 14 | * stylesheets. 15 | */ 16 | export class Rule extends Rules.AbstractRule { 17 | apply(sourceFile: ts.SourceFile): RuleFailure[] { 18 | return this.applyWithWalker( 19 | new SwitchStylesheetElementSelectorsWalker(sourceFile, this.getOptions())); 20 | } 21 | } 22 | 23 | export class SwitchStylesheetElementSelectorsWalker extends ComponentWalker { 24 | 25 | constructor(sourceFile: ts.SourceFile, options: IOptions) { 26 | // This is a special feature. In some applications, developers will have global stylesheets 27 | // that are not specified in any Angular component. Those stylesheets can be also migrated 28 | // if the developer specifies the `--extra-stylesheets` option which accepts a glob for files. 29 | const extraFiles = []; 30 | if (process.env[EXTRA_STYLESHEETS_GLOB_KEY]) { 31 | process.env[EXTRA_STYLESHEETS_GLOB_KEY].split(' ') 32 | .map(glob => globSync(glob)) 33 | .forEach(files => files.forEach(styleUrl => { 34 | extraFiles.push(resolve(styleUrl)); 35 | })); 36 | } 37 | 38 | super(sourceFile, options, extraFiles); 39 | 40 | extraFiles.forEach(styleUrl => this._reportExternalStyle(styleUrl)); 41 | } 42 | 43 | visitInlineStylesheet(stylesheet: ts.StringLiteral) { 44 | this.replaceNamesInStylesheet(stylesheet, stylesheet.getText()).forEach(replacement => { 45 | const fix = replacement.replacement; 46 | const ruleFailure = new RuleFailure(stylesheet.getSourceFile(), fix.start, fix.end, 47 | replacement.message, this.getRuleName(), fix); 48 | this.addFailure(ruleFailure); 49 | }); 50 | } 51 | 52 | visitExternalStylesheet(stylesheet: ExternalResource) { 53 | this.replaceNamesInStylesheet(stylesheet, stylesheet.getFullText()).forEach(replacement => { 54 | const fix = replacement.replacement; 55 | const ruleFailure = new RuleFailure(stylesheet, fix.start + 1, fix.end + 1, 56 | replacement.message, this.getRuleName(), fix); 57 | this.addFailure(ruleFailure); 58 | }); 59 | } 60 | 61 | /** 62 | * Replaces the outdated name in the stylesheet with the new one and returns an updated 63 | * stylesheet. 64 | */ 65 | private replaceNamesInStylesheet(node: ts.Node, stylesheetContent: string): 66 | {message: string, replacement: Replacement}[] { 67 | const replacements: {message: string, replacement: Replacement}[] = []; 68 | 69 | elementSelectors.forEach(selector => { 70 | this.createReplacementsForOffsets(node, selector, 71 | findAll(stylesheetContent, selector.replace)).forEach(replacement => { 72 | replacements.push({ 73 | message: `Found deprecated element selector "${red(selector.replace)}" which has` + 74 | ` been renamed to "${green(selector.replaceWith)}"`, 75 | replacement 76 | }); 77 | }); 78 | }); 79 | 80 | return replacements; 81 | } 82 | 83 | private createReplacementsForOffsets(node: ts.Node, 84 | update: {replace: string, replaceWith: string}, 85 | offsets: number[]): Replacement[] { 86 | return offsets.map(offset => this.createReplacement( 87 | node.getStart() + offset, update.replace.length, update.replaceWith)); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/cli.ts: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env node 2 | 3 | import {red, yellow} from 'chalk'; 4 | import {spawn} from 'child_process'; 5 | import {existsSync, statSync} from 'fs'; 6 | import * as ora from 'ora'; 7 | import {resolve} from 'path'; 8 | import {argv, help, option} from 'yargs'; 9 | import {EXTRA_STYLESHEETS_GLOB_KEY} from './material/extra-stylsheets'; 10 | import {findTslintBinaryPath} from './tslint/find-tslint-binary'; 11 | 12 | // Register a help page in yargs. 13 | help(); 14 | 15 | // Register the project option in yargs. 16 | option('project', { 17 | alias: 'p', 18 | describe: 'Path to the tsconfig.json file of the project', 19 | string: true, 20 | required: true 21 | }); 22 | 23 | option('extra-stylesheets', { 24 | alias: ['es', 'extra-css'], 25 | describe: 'Glob that matches additional stylesheets that should be migrated', 26 | string: true, 27 | array: true, 28 | required: false 29 | }); 30 | 31 | option('fix', { 32 | describe: 'Whether to attempt to automatically fix issues', 33 | boolean: true, 34 | required: false, 35 | default: true 36 | }); 37 | 38 | /** Path to the TypeScript project. */ 39 | let projectPath: string = argv.project; 40 | 41 | // Exit the process if the specified project does not exist. 42 | if (!existsSync(projectPath)) { 43 | console.error(red('Specified project path is not valid. File or directory does not exist!')); 44 | process.exit(1); 45 | } 46 | 47 | // If the project path links to a directory, automatically reference the "tsconfig.json" file. 48 | if (statSync(projectPath).isDirectory()) { 49 | projectPath = `${projectPath}/tsconfig.json`; 50 | } 51 | 52 | if (projectPath) { 53 | const migrationConfig = resolve(__dirname, 'rules', 'tslint-migration.json'); 54 | 55 | // Command line arguments for dispatching the TSLint executable. 56 | const tslintArgs = ['-c', migrationConfig, '-p', projectPath]; 57 | if (argv.fix) { 58 | tslintArgs.push('--fix'); 59 | } 60 | const childProcessEnv = { ...process.env }; 61 | 62 | if (argv.extraStylesheets) { 63 | // Since TSLint runs in another node process and we want to apply the fixes for extra 64 | // stylesheets through TSLint we need to transfer the glob of stylesheets to the child 65 | // process. 66 | childProcessEnv[EXTRA_STYLESHEETS_GLOB_KEY] = argv.extraStylesheets.join(' '); 67 | } 68 | 69 | migrateProject(tslintArgs, childProcessEnv); 70 | } 71 | 72 | /** Starts the migration of the specified project in the TSLint arguments. */ 73 | function migrateProject(tslintArgs: string[], env?: any) { 74 | const tslintBin = findTslintBinaryPath(); 75 | const spinner = ora('Migrating the specified Angular Material project').start(); 76 | 77 | // Run the TSLint CLI with the configuration file from the migration tool. 78 | const tslintProcess = spawn('node', [tslintBin, ...tslintArgs], {env}); 79 | 80 | let stderr = ''; 81 | 82 | tslintProcess.stderr.on('data', data => stderr += data.toString()); 83 | 84 | tslintProcess.stdout.pipe(process.stdout); 85 | tslintProcess.stderr.pipe(process.stderr); 86 | 87 | tslintProcess.on('close', () => { 88 | // Clear the spinner output before printing messages, because Ora is not able to clear the 89 | // spinner properly if there is console output after the previous spinner output. 90 | spinner.clear(); 91 | 92 | if (stderr.trim()) { 93 | console.error(yellow('Make sure the following things are done correctly:')); 94 | console.error(yellow(' • Angular Material is installed in the project (for type checking)')); 95 | console.error(yellow(' • The Angular Material version is not higher than "5.2.4"')); 96 | console.error(yellow(' • Project "tsconfig.json" configuration matches the desired files')); 97 | console.error(); 98 | spinner.fail('Errors occurred while migrating the Angular Material project.'); 99 | } else { 100 | spinner.succeed(`Successfully migrated the project source files. Please check above output ` + 101 | `for issues that couldn't be automatically fixed.`); 102 | } 103 | }); 104 | } 105 | -------------------------------------------------------------------------------- /src/rules/switchStylesheetInputNamesRule.ts: -------------------------------------------------------------------------------- 1 | import {green, red} from 'chalk'; 2 | import {sync as globSync} from 'glob'; 3 | import {resolve} from "path"; 4 | import {IOptions, Replacement, RuleFailure, Rules} from 'tslint'; 5 | import * as ts from 'typescript'; 6 | import {inputNames} from '../material/component-data'; 7 | import {EXTRA_STYLESHEETS_GLOB_KEY} from '../material/extra-stylsheets'; 8 | import {ExternalResource} from '../tslint/component-file'; 9 | import {ComponentWalker} from '../tslint/component-walker'; 10 | import {findAll} from '../typescript/literal'; 11 | 12 | /** 13 | * Rule that walks through every component decorator and updates their inline or external 14 | * stylesheets. 15 | */ 16 | export class Rule extends Rules.AbstractRule { 17 | apply(sourceFile: ts.SourceFile): RuleFailure[] { 18 | return this.applyWithWalker( 19 | new SwitchStylesheetInputNamesWalker(sourceFile, this.getOptions())); 20 | } 21 | } 22 | 23 | export class SwitchStylesheetInputNamesWalker extends ComponentWalker { 24 | 25 | constructor(sourceFile: ts.SourceFile, options: IOptions) { 26 | // This is a special feature. In some applications, developers will have global stylesheets 27 | // that are not specified in any Angular component. Those stylesheets can be also migrated 28 | // if the developer specifies the `--extra-stylesheets` option which accepts a glob for files. 29 | const extraFiles = []; 30 | if (process.env[EXTRA_STYLESHEETS_GLOB_KEY]) { 31 | process.env[EXTRA_STYLESHEETS_GLOB_KEY].split(' ') 32 | .map(glob => globSync(glob)) 33 | .forEach(files => files.forEach(styleUrl => { 34 | extraFiles.push(resolve(styleUrl)); 35 | })); 36 | } 37 | 38 | super(sourceFile, options, extraFiles); 39 | 40 | extraFiles.forEach(styleUrl => this._reportExternalStyle(styleUrl)); 41 | } 42 | 43 | visitInlineStylesheet(stylesheet: ts.StringLiteral) { 44 | this.replaceNamesInStylesheet(stylesheet, stylesheet.getText()).forEach(replacement => { 45 | const fix = replacement.replacement; 46 | const ruleFailure = new RuleFailure(stylesheet.getSourceFile(), fix.start, fix.end, 47 | replacement.message, this.getRuleName(), fix); 48 | this.addFailure(ruleFailure); 49 | }); 50 | } 51 | 52 | visitExternalStylesheet(stylesheet: ExternalResource) { 53 | this.replaceNamesInStylesheet(stylesheet, stylesheet.getFullText()).forEach(replacement => { 54 | const fix = replacement.replacement; 55 | const ruleFailure = new RuleFailure(stylesheet, fix.start + 1, fix.end + 1, 56 | replacement.message, this.getRuleName(), fix); 57 | this.addFailure(ruleFailure); 58 | }); 59 | } 60 | 61 | /** 62 | * Replaces the outdated name in the stylesheet with the new one and returns an updated 63 | * stylesheet. 64 | */ 65 | private replaceNamesInStylesheet(node: ts.Node, stylesheetContent: string): 66 | {message: string, replacement: Replacement}[] { 67 | const replacements: {message: string, replacement: Replacement}[] = []; 68 | 69 | inputNames.forEach(name => { 70 | if (!name.whitelist || name.whitelist.css) { 71 | const bracketedName = {replace: `[${name.replace}]`, replaceWith: `[${name.replaceWith}]`}; 72 | this.createReplacementsForOffsets(node, name, 73 | findAll(stylesheetContent, bracketedName.replace)).forEach(replacement => { 74 | replacements.push({ 75 | message: `Found deprecated @Input() "${red(name.replace)}" which has been renamed` + 76 | ` to "${green(name.replaceWith)}"`, 77 | replacement 78 | }); 79 | }); 80 | } 81 | }); 82 | 83 | return replacements; 84 | } 85 | 86 | private createReplacementsForOffsets(node: ts.Node, 87 | update: {replace: string, replaceWith: string}, 88 | offsets: number[]): Replacement[] { 89 | return offsets.map(offset => this.createReplacement( 90 | node.getStart() + offset, update.replace.length, update.replaceWith)); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/rules/switchStylesheetOutputNamesRule.ts: -------------------------------------------------------------------------------- 1 | import {green, red} from 'chalk'; 2 | import {sync as globSync} from 'glob'; 3 | import {resolve} from "path"; 4 | import {IOptions, Replacement, RuleFailure, Rules} from 'tslint'; 5 | import * as ts from 'typescript'; 6 | import {outputNames} from '../material/component-data'; 7 | import {EXTRA_STYLESHEETS_GLOB_KEY} from '../material/extra-stylsheets'; 8 | import {ExternalResource} from '../tslint/component-file'; 9 | import {ComponentWalker} from '../tslint/component-walker'; 10 | import {findAll} from '../typescript/literal'; 11 | 12 | /** 13 | * Rule that walks through every component decorator and updates their inline or external 14 | * stylesheets. 15 | */ 16 | export class Rule extends Rules.AbstractRule { 17 | apply(sourceFile: ts.SourceFile): RuleFailure[] { 18 | return this.applyWithWalker( 19 | new SwitchStylesheetOutputNamesWalker(sourceFile, this.getOptions())); 20 | } 21 | } 22 | 23 | export class SwitchStylesheetOutputNamesWalker extends ComponentWalker { 24 | 25 | constructor(sourceFile: ts.SourceFile, options: IOptions) { 26 | // This is a special feature. In some applications, developers will have global stylesheets 27 | // that are not specified in any Angular component. Those stylesheets can be also migrated 28 | // if the developer specifies the `--extra-stylesheets` option which accepts a glob for files. 29 | const extraFiles = []; 30 | if (process.env[EXTRA_STYLESHEETS_GLOB_KEY]) { 31 | process.env[EXTRA_STYLESHEETS_GLOB_KEY].split(' ') 32 | .map(glob => globSync(glob)) 33 | .forEach(files => files.forEach(styleUrl => { 34 | extraFiles.push(resolve(styleUrl)); 35 | })); 36 | } 37 | 38 | super(sourceFile, options, extraFiles); 39 | 40 | extraFiles.forEach(styleUrl => this._reportExternalStyle(styleUrl)); 41 | } 42 | 43 | visitInlineStylesheet(stylesheet: ts.StringLiteral) { 44 | this.replaceNamesInStylesheet(stylesheet, stylesheet.getText()).forEach(replacement => { 45 | const fix = replacement.replacement; 46 | const ruleFailure = new RuleFailure(stylesheet.getSourceFile(), fix.start, fix.end, 47 | replacement.message, this.getRuleName(), fix); 48 | this.addFailure(ruleFailure); 49 | }); 50 | } 51 | 52 | visitExternalStylesheet(stylesheet: ExternalResource) { 53 | this.replaceNamesInStylesheet(stylesheet, stylesheet.getFullText()).forEach(replacement => { 54 | const fix = replacement.replacement; 55 | const ruleFailure = new RuleFailure(stylesheet, fix.start + 1, fix.end + 1, 56 | replacement.message, this.getRuleName(), fix); 57 | this.addFailure(ruleFailure); 58 | }); 59 | } 60 | 61 | /** 62 | * Replaces the outdated name in the stylesheet with the new one and returns an updated 63 | * stylesheet. 64 | */ 65 | private replaceNamesInStylesheet(node: ts.Node, stylesheetContent: string): 66 | {message: string, replacement: Replacement}[] { 67 | const replacements: {message: string, replacement: Replacement}[] = []; 68 | 69 | outputNames.forEach(name => { 70 | if (!name.whitelist || name.whitelist.css) { 71 | const bracketedName = {replace: `[${name.replace}]`, replaceWith: `[${name.replaceWith}]`}; 72 | this.createReplacementsForOffsets(node, name, 73 | findAll(stylesheetContent, bracketedName.replace)).forEach(replacement => { 74 | replacements.push({ 75 | message: `Found deprecated @Output() "${red(name.replace)}" which has been` + 76 | ` renamed to "${green(name.replaceWith)}"`, 77 | replacement 78 | }); 79 | }); 80 | } 81 | }); 82 | 83 | return replacements; 84 | } 85 | 86 | private createReplacementsForOffsets(node: ts.Node, 87 | update: {replace: string, replaceWith: string}, 88 | offsets: number[]): Replacement[] { 89 | return offsets.map(offset => this.createReplacement( 90 | node.getStart() + offset, update.replace.length, update.replaceWith)); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/rules/switchStylesheetAttributeSelectorsRule.ts: -------------------------------------------------------------------------------- 1 | import {green, red} from 'chalk'; 2 | import {sync as globSync} from 'glob'; 3 | import {resolve} from 'path'; 4 | import {IOptions, Replacement, RuleFailure, Rules} from 'tslint'; 5 | import * as ts from 'typescript'; 6 | import {attributeSelectors} from '../material/component-data'; 7 | import {EXTRA_STYLESHEETS_GLOB_KEY} from '../material/extra-stylsheets'; 8 | import {ExternalResource} from '../tslint/component-file'; 9 | import {ComponentWalker} from '../tslint/component-walker'; 10 | import {findAll} from '../typescript/literal'; 11 | 12 | /** 13 | * Rule that walks through every component decorator and updates their inline or external 14 | * stylesheets. 15 | */ 16 | export class Rule extends Rules.AbstractRule { 17 | apply(sourceFile: ts.SourceFile): RuleFailure[] { 18 | return this.applyWithWalker( 19 | new SwitchStylesheetAtributeSelectorsWalker(sourceFile, this.getOptions())); 20 | } 21 | } 22 | 23 | export class SwitchStylesheetAtributeSelectorsWalker extends ComponentWalker { 24 | 25 | constructor(sourceFile: ts.SourceFile, options: IOptions) { 26 | // This is a special feature. In some applications, developers will have global stylesheets 27 | // that are not specified in any Angular component. Those stylesheets can be also migrated 28 | // if the developer specifies the `--extra-stylesheets` option which accepts a glob for files. 29 | const extraFiles = []; 30 | if (process.env[EXTRA_STYLESHEETS_GLOB_KEY]) { 31 | process.env[EXTRA_STYLESHEETS_GLOB_KEY].split(' ') 32 | .map(glob => globSync(glob)) 33 | .forEach(files => files.forEach(styleUrl => { 34 | extraFiles.push(resolve(styleUrl)); 35 | })); 36 | } 37 | 38 | super(sourceFile, options, extraFiles); 39 | 40 | extraFiles.forEach(styleUrl => this._reportExternalStyle(styleUrl)); 41 | } 42 | 43 | visitInlineStylesheet(stylesheet: ts.StringLiteral) { 44 | this.replaceNamesInStylesheet(stylesheet, stylesheet.getText()).forEach(replacement => { 45 | const fix = replacement.replacement; 46 | const ruleFailure = new RuleFailure(stylesheet.getSourceFile(), fix.start, fix.end, 47 | replacement.message, this.getRuleName(), fix); 48 | this.addFailure(ruleFailure); 49 | }); 50 | } 51 | 52 | visitExternalStylesheet(stylesheet: ExternalResource) { 53 | this.replaceNamesInStylesheet(stylesheet, stylesheet.getFullText()).forEach(replacement => { 54 | const fix = replacement.replacement; 55 | const ruleFailure = new RuleFailure(stylesheet, fix.start + 1, fix.end + 1, 56 | replacement.message, this.getRuleName(), fix); 57 | this.addFailure(ruleFailure); 58 | }); 59 | } 60 | 61 | /** 62 | * Replaces the outdated name in the stylesheet with the new one and returns an updated 63 | * stylesheet. 64 | */ 65 | private replaceNamesInStylesheet(node: ts.Node, stylesheetContent: string): 66 | {message: string, replacement: Replacement}[] { 67 | const replacements: {message: string, replacement: Replacement}[] = []; 68 | 69 | attributeSelectors.forEach(selector => { 70 | const bracketedSelector = { 71 | replace: `[${selector.replace}]`, 72 | replaceWith: `[${selector.replaceWith}]` 73 | }; 74 | this.createReplacementsForOffsets(node, bracketedSelector, 75 | findAll(stylesheetContent, bracketedSelector.replace)).forEach(replacement => { 76 | replacements.push({ 77 | message: `Found deprecated attribute selector "${red(bracketedSelector.replace)}"` + 78 | ` which has been renamed to "${green(bracketedSelector.replaceWith)}"`, 79 | replacement 80 | }); 81 | }); 82 | }); 83 | 84 | return replacements; 85 | } 86 | 87 | private createReplacementsForOffsets(node: ts.Node, 88 | update: {replace: string, replaceWith: string}, 89 | offsets: number[]): Replacement[] { 90 | return offsets.map(offset => this.createReplacement( 91 | node.getStart() + offset, update.replace.length, update.replaceWith)); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/material/component-data.ts: -------------------------------------------------------------------------------- 1 | export interface MaterialExportAsNameData { 2 | /** The exportAs name to replace. */ 3 | replace: string; 4 | /** The new exportAs name. */ 5 | replaceWith: string; 6 | } 7 | 8 | export interface MaterialElementSelectorData { 9 | /** The element name to replace. */ 10 | replace: string; 11 | /** The new name for the element. */ 12 | replaceWith: string; 13 | } 14 | 15 | export interface MaterialCssNameData { 16 | /** The CSS name to replace. */ 17 | replace: string; 18 | /** The new CSS name. */ 19 | replaceWith: string; 20 | /** Whitelist where this replacement is made. If omitted it is made in all files. */ 21 | whitelist: { 22 | /** Replace this name in CSS files. */ 23 | css?: boolean, 24 | /** Replace this name in HTML files. */ 25 | html?: boolean, 26 | /** Replace this name in TypeScript strings. */ 27 | strings?: boolean 28 | } 29 | } 30 | 31 | export interface MaterialAttributeSelectorData { 32 | /** The attribute name to replace. */ 33 | replace: string; 34 | /** The new name for the attribute. */ 35 | replaceWith: string; 36 | } 37 | 38 | export interface MaterialPropertyNameData { 39 | /** The property name to replace. */ 40 | replace: string; 41 | /** The new name for the property. */ 42 | replaceWith: string; 43 | /** Whitelist where this replacement is made. If omitted it is made for all Classes. */ 44 | whitelist: { 45 | /** Replace the property only when its type is one of the given Classes. */ 46 | classes?: string[]; 47 | } 48 | } 49 | 50 | export interface MaterialClassNameData { 51 | /** The Class name to replace. */ 52 | replace: string; 53 | /** The new name for the Class. */ 54 | replaceWith: string; 55 | } 56 | 57 | export interface MaterialInputNameData { 58 | /** The @Input() name to replace. */ 59 | replace: string; 60 | /** The new name for the @Input(). */ 61 | replaceWith: string; 62 | /** Whitelist where this replacement is made. If omitted it is made in all HTML & CSS */ 63 | whitelist?: { 64 | /** Limit to elements with any of these element tags. */ 65 | elements?: string[], 66 | /** Limit to elements with any of these attributes. */ 67 | attributes?: string[], 68 | /** Whether to ignore CSS attribute selectors when doing this replacement. */ 69 | css?: boolean, 70 | } 71 | } 72 | 73 | export interface MaterialOutputNameData { 74 | /** The @Output() name to replace. */ 75 | replace: string; 76 | /** The new name for the @Output(). */ 77 | replaceWith: string; 78 | /** Whitelist where this replacement is made. If omitted it is made in all HTML & CSS */ 79 | whitelist?: { 80 | /** Limit to elements with any of these element tags. */ 81 | elements?: string[], 82 | /** Limit to elements with any of these attributes. */ 83 | attributes?: string[], 84 | /** Whether to ignore CSS attribute selectors when doing this replacement. */ 85 | css?: boolean, 86 | } 87 | } 88 | 89 | export interface MaterialMethodCallData { 90 | className: string; 91 | method: string; 92 | invalidArgCounts: { 93 | count: number, 94 | message: string 95 | }[] 96 | } 97 | 98 | type Changes = { 99 | pr: string; 100 | changes: T[] 101 | } 102 | 103 | function getChanges(allChanges: Changes[]): T[] { 104 | return allChanges.reduce((result, changes) => result.concat(changes.changes), []); 105 | } 106 | 107 | /** Export the class name data as part of a module. This means that the data is cached. */ 108 | export const classNames = getChanges(require('./data/class-names.json')); 109 | 110 | /** Export the input names data as part of a module. This means that the data is cached. */ 111 | export const inputNames = getChanges(require('./data/input-names.json')); 112 | 113 | /** Export the output names data as part of a module. This means that the data is cached. */ 114 | export const outputNames = getChanges(require('./data/output-names.json')); 115 | 116 | /** Export the element selectors data as part of a module. This means that the data is cached. */ 117 | export const elementSelectors = 118 | getChanges(require('./data/element-selectors.json')); 119 | 120 | /** Export the attribute selectors data as part of a module. This means that the data is cached. */ 121 | export const exportAsNames = 122 | getChanges(require('./data/export-as-names.json')); 123 | 124 | /** Export the attribute selectors data as part of a module. This means that the data is cached. */ 125 | export const attributeSelectors = 126 | getChanges(require('./data/attribute-selectors.json')); 127 | 128 | /** Export the property names as part of a module. This means that the data is cached. */ 129 | export const propertyNames = 130 | getChanges(require('./data/property-names.json')); 131 | 132 | export const methodCallChecks = 133 | getChanges(require('./data/method-call-checks.json')); 134 | 135 | export const cssNames = getChanges(require('./data/css-names.json')); 136 | -------------------------------------------------------------------------------- /src/tslint/component-walker.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * TSLint custom walker implementation that also visits external and inline templates. 3 | */ 4 | import {existsSync, readFileSync} from 'fs' 5 | import {dirname, join, resolve} from 'path'; 6 | import {Fix, IOptions, RuleFailure, RuleWalker} from 'tslint'; 7 | import * as ts from 'typescript'; 8 | import {getLiteralTextWithoutQuotes} from '../typescript/literal'; 9 | import {createComponentFile, ExternalResource} from "./component-file"; 10 | 11 | /** 12 | * Custom TSLint rule walker that identifies Angular components and visits specific parts of 13 | * the component metadata. 14 | */ 15 | export class ComponentWalker extends RuleWalker { 16 | 17 | protected visitInlineTemplate(template: ts.StringLiteral) {} 18 | protected visitInlineStylesheet(stylesheet: ts.StringLiteral) {} 19 | 20 | protected visitExternalTemplate(template: ExternalResource) {} 21 | protected visitExternalStylesheet(stylesheet: ExternalResource) {} 22 | 23 | private skipFiles: Set; 24 | 25 | constructor(sourceFile: ts.SourceFile, options: IOptions, skipFiles: string[] = []) { 26 | super(sourceFile, options); 27 | this.skipFiles = new Set(skipFiles.map(p => resolve(p))); 28 | } 29 | 30 | visitNode(node: ts.Node) { 31 | if (node.kind === ts.SyntaxKind.CallExpression) { 32 | const callExpression = node as ts.CallExpression; 33 | const callExpressionName = callExpression.expression.getText(); 34 | 35 | if (callExpressionName === 'Component' || callExpressionName === 'Directive') { 36 | this._visitDirectiveCallExpression(callExpression); 37 | } 38 | } 39 | 40 | super.visitNode(node); 41 | } 42 | 43 | private _visitDirectiveCallExpression(callExpression: ts.CallExpression) { 44 | const directiveMetadata = callExpression.arguments[0] as ts.ObjectLiteralExpression; 45 | 46 | if (!directiveMetadata) { 47 | return; 48 | } 49 | 50 | for (const property of directiveMetadata.properties as ts.NodeArray) { 51 | const propertyName = property.name.getText(); 52 | const initializerKind = property.initializer.kind; 53 | 54 | if (propertyName === 'template') { 55 | this.visitInlineTemplate(property.initializer as ts.StringLiteral) 56 | } 57 | 58 | if (propertyName === 'templateUrl' && initializerKind === ts.SyntaxKind.StringLiteral) { 59 | this._reportExternalTemplate(property.initializer as ts.StringLiteral); 60 | } 61 | 62 | if (propertyName === 'styles' && initializerKind === ts.SyntaxKind.ArrayLiteralExpression) { 63 | this._reportInlineStyles(property.initializer as ts.ArrayLiteralExpression); 64 | } 65 | 66 | if (propertyName === 'styleUrls' && initializerKind === ts.SyntaxKind.ArrayLiteralExpression) { 67 | this._visitExternalStylesArrayLiteral(property.initializer as ts.ArrayLiteralExpression); 68 | } 69 | } 70 | } 71 | 72 | private _reportInlineStyles(inlineStyles: ts.ArrayLiteralExpression) { 73 | inlineStyles.elements.forEach(element => { 74 | this.visitInlineStylesheet(element as ts.StringLiteral); 75 | }); 76 | } 77 | 78 | private _visitExternalStylesArrayLiteral(styleUrls: ts.ArrayLiteralExpression) { 79 | styleUrls.elements.forEach(styleUrlLiteral => { 80 | const styleUrl = getLiteralTextWithoutQuotes(styleUrlLiteral as ts.StringLiteral); 81 | const stylePath = resolve(join(dirname(this.getSourceFile().fileName), styleUrl)); 82 | 83 | if (!this.skipFiles.has(stylePath)) { 84 | this._reportExternalStyle(stylePath); 85 | } 86 | }) 87 | } 88 | 89 | private _reportExternalTemplate(templateUrlLiteral: ts.StringLiteral) { 90 | const templateUrl = getLiteralTextWithoutQuotes(templateUrlLiteral); 91 | const templatePath = resolve(join(dirname(this.getSourceFile().fileName), templateUrl)); 92 | 93 | if (this.skipFiles.has(templatePath)) { 94 | return; 95 | } 96 | 97 | // Check if the external template file exists before proceeding. 98 | if (!existsSync(templatePath)) { 99 | console.error(`PARSE ERROR: ${this.getSourceFile().fileName}:` + 100 | ` Could not find template: "${templatePath}".`); 101 | process.exit(1); 102 | } 103 | 104 | // Create a fake TypeScript source file that includes the template content. 105 | const templateFile = createComponentFile(templatePath, readFileSync(templatePath, 'utf8')); 106 | 107 | this.visitExternalTemplate(templateFile); 108 | } 109 | 110 | public _reportExternalStyle(stylePath: string) { 111 | // Check if the external stylesheet file exists before proceeding. 112 | if (!existsSync(stylePath)) { 113 | console.error(`PARSE ERROR: ${this.getSourceFile().fileName}:` + 114 | ` Could not find stylesheet: "${stylePath}".`); 115 | process.exit(1); 116 | } 117 | 118 | // Create a fake TypeScript source file that includes the stylesheet content. 119 | const stylesheetFile = createComponentFile(stylePath, readFileSync(stylePath, 'utf8')); 120 | 121 | this.visitExternalStylesheet(stylesheetFile); 122 | } 123 | 124 | /** Creates a TSLint rule failure for the given external resource. */ 125 | protected addExternalResourceFailure(file: ExternalResource, message: string, fix?: Fix) { 126 | const ruleFailure = new RuleFailure(file, file.getStart(), file.getEnd(), 127 | message, this.getRuleName(), fix); 128 | 129 | this.addFailure(ruleFailure); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/material/data/input-names.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pr": "https://github.com/angular/material2/pull/10161", 4 | "changes": [ 5 | { 6 | "replace": "origin", 7 | "replaceWith": "cdkConnectedOverlayOrigin", 8 | "whitelist": { 9 | "attributes": ["cdk-connected-overlay", "connected-overlay", "cdkConnectedOverlay"] 10 | } 11 | }, 12 | { 13 | "replace": "positions", 14 | "replaceWith": "cdkConnectedOverlayPositions", 15 | "whitelist": { 16 | "attributes": ["cdk-connected-overlay", "connected-overlay", "cdkConnectedOverlay"] 17 | } 18 | }, 19 | { 20 | "replace": "offsetX", 21 | "replaceWith": "cdkConnectedOverlayOffsetX", 22 | "whitelist": { 23 | "attributes": ["cdk-connected-overlay", "connected-overlay", "cdkConnectedOverlay"] 24 | } 25 | }, 26 | { 27 | "replace": "offsetY", 28 | "replaceWith": "cdkConnectedOverlayOffsetY", 29 | "whitelist": { 30 | "attributes": ["cdk-connected-overlay", "connected-overlay", "cdkConnectedOverlay"] 31 | } 32 | }, 33 | { 34 | "replace": "width", 35 | "replaceWith": "cdkConnectedOverlayWidth", 36 | "whitelist": { 37 | "attributes": ["cdk-connected-overlay", "connected-overlay", "cdkConnectedOverlay"] 38 | } 39 | }, 40 | { 41 | "replace": "height", 42 | "replaceWith": "cdkConnectedOverlayHeight", 43 | "whitelist": { 44 | "attributes": ["cdk-connected-overlay", "connected-overlay", "cdkConnectedOverlay"] 45 | } 46 | }, 47 | { 48 | "replace": "minWidth", 49 | "replaceWith": "cdkConnectedOverlayMinWidth", 50 | "whitelist": { 51 | "attributes": ["cdk-connected-overlay", "connected-overlay", "cdkConnectedOverlay"] 52 | } 53 | }, 54 | { 55 | "replace": "minHeight", 56 | "replaceWith": "cdkConnectedOverlayMinHeight", 57 | "whitelist": { 58 | "attributes": ["cdk-connected-overlay", "connected-overlay", "cdkConnectedOverlay"] 59 | } 60 | }, 61 | { 62 | "replace": "backdropClass", 63 | "replaceWith": "cdkConnectedOverlayBackdropClass", 64 | "whitelist": { 65 | "attributes": ["cdk-connected-overlay", "connected-overlay", "cdkConnectedOverlay"] 66 | } 67 | }, 68 | { 69 | "replace": "scrollStrategy", 70 | "replaceWith": "cdkConnectedOverlayScrollStrategy", 71 | "whitelist": { 72 | "attributes": ["cdk-connected-overlay", "connected-overlay", "cdkConnectedOverlay"] 73 | } 74 | }, 75 | { 76 | "replace": "open", 77 | "replaceWith": "cdkConnectedOverlayOpen", 78 | "whitelist": { 79 | "attributes": ["cdk-connected-overlay", "connected-overlay", "cdkConnectedOverlay"] 80 | } 81 | }, 82 | { 83 | "replace": "hasBackdrop", 84 | "replaceWith": "cdkConnectedOverlayHasBackdrop", 85 | "whitelist": { 86 | "attributes": ["cdk-connected-overlay", "connected-overlay", "cdkConnectedOverlay"] 87 | } 88 | } 89 | ] 90 | }, 91 | 92 | 93 | { 94 | "pr": "https://github.com/angular/material2/pull/10218", 95 | "changes": [ 96 | { 97 | "replace": "align", 98 | "replaceWith": "labelPosition", 99 | "whitelist": { 100 | "elements": ["mat-radio-group", "mat-radio-button"] 101 | } 102 | } 103 | ] 104 | }, 105 | 106 | 107 | { 108 | "pr": "https://github.com/angular/material2/pull/10279", 109 | "changes": [ 110 | { 111 | "replace": "align", 112 | "replaceWith": "position", 113 | "whitelist": { 114 | "elements": ["mat-drawer", "mat-sidenav"] 115 | } 116 | } 117 | ] 118 | }, 119 | 120 | 121 | { 122 | "pr": "https://github.com/angular/material2/pull/10294", 123 | "changes": [ 124 | { 125 | "replace": "dividerColor", 126 | "replaceWith": "color", 127 | "whitelist": { 128 | "elements": ["mat-form-field"] 129 | } 130 | }, 131 | { 132 | "replace": "floatPlaceholder", 133 | "replaceWith": "floatLabel", 134 | "whitelist": { 135 | "elements": ["mat-form-field"] 136 | } 137 | } 138 | ] 139 | }, 140 | 141 | 142 | { 143 | "pr": "https://github.com/angular/material2/pull/10309", 144 | "changes": [ 145 | { 146 | "replace": "mat-dynamic-height", 147 | "replaceWith": "dynamicHeight", 148 | "whitelist": { 149 | "elements": ["mat-tab-group"] 150 | } 151 | } 152 | ] 153 | }, 154 | 155 | 156 | { 157 | "pr": "https://github.com/angular/material2/pull/10342", 158 | "changes": [ 159 | { 160 | "replace": "align", 161 | "replaceWith": "labelPosition", 162 | "whitelist": { 163 | "elements": ["mat-checkbox"] 164 | } 165 | } 166 | ] 167 | }, 168 | 169 | 170 | { 171 | "pr": "https://github.com/angular/material2/pull/10344", 172 | "changes": [ 173 | { 174 | "replace": "tooltip-position", 175 | "replaceWith": "matTooltipPosition", 176 | "whitelist": { 177 | "attributes": ["matTooltip"] 178 | } 179 | } 180 | ] 181 | }, 182 | 183 | 184 | { 185 | "pr": "https://github.com/angular/material2/pull/10373", 186 | "changes": [ 187 | { 188 | "replace": "thumb-label", 189 | "replaceWith": "thumbLabel", 190 | "whitelist": { 191 | "elements": ["mat-slider"] 192 | } 193 | }, 194 | { 195 | "replace": "tick-interval", 196 | "replaceWith": "tickInterval", 197 | "whitelist": { 198 | "elements": ["mat-slider"] 199 | } 200 | } 201 | ] 202 | } 203 | ] 204 | -------------------------------------------------------------------------------- /src/rules/switchIdentifiersRule.ts: -------------------------------------------------------------------------------- 1 | import {green, red} from 'chalk'; 2 | import {relative} from 'path'; 3 | import {ProgramAwareRuleWalker, RuleFailure, Rules} from 'tslint'; 4 | import * as ts from 'typescript'; 5 | import {classNames} from '../material/component-data'; 6 | import { 7 | isMaterialExportDeclaration, 8 | isMaterialImportDeclaration, 9 | } from '../material/typescript-specifiers'; 10 | import {getOriginalSymbolFromNode} from '../typescript/identifiers'; 11 | import { 12 | isExportSpecifierNode, 13 | isImportSpecifierNode, 14 | isNamespaceImportNode 15 | } from '../typescript/imports'; 16 | 17 | /** 18 | * Rule that walks through every identifier that is part of Angular Material and replaces the 19 | * outdated name with the new one. 20 | */ 21 | export class Rule extends Rules.TypedRule { 22 | applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] { 23 | return this.applyWithWalker( 24 | new SwitchIdentifiersWalker(sourceFile, this.getOptions(), program)); 25 | } 26 | } 27 | 28 | export class SwitchIdentifiersWalker extends ProgramAwareRuleWalker { 29 | 30 | /** List of Angular Material declarations inside of the current source file. */ 31 | materialDeclarations: ts.Declaration[] = []; 32 | 33 | /** List of Angular Material namespace declarations in the current source file. */ 34 | materialNamespaceDeclarations: ts.Declaration[] = []; 35 | 36 | /** Method that is called for every identifier inside of the specified project. */ 37 | visitIdentifier(identifier: ts.Identifier) { 38 | // Store Angular Material namespace identifers in a list of declarations. 39 | // Namespace identifiers can be: `import * as md from '@angular/material';` 40 | this._storeNamespaceImports(identifier); 41 | 42 | // For identifiers that aren't listed in the className data, the whole check can be 43 | // skipped safely. 44 | if (!classNames.some(data => data.replace === identifier.text)) { 45 | return; 46 | } 47 | 48 | const symbol = getOriginalSymbolFromNode(identifier, this.getTypeChecker()); 49 | 50 | // If the symbol is not defined or could not be resolved, just skip the following identifier 51 | // checks. 52 | if (!symbol || !symbol.name || symbol.name === 'unknown') { 53 | console.error(`Could not resolve symbol for identifier "${identifier.text}" ` + 54 | `in file ${this._getRelativeFileName()}`); 55 | return; 56 | } 57 | 58 | // For export declarations that are referring to Angular Material, the identifier should be 59 | // switched to the new name. 60 | if (isExportSpecifierNode(identifier) && isMaterialExportDeclaration(identifier)) { 61 | return this.createIdentifierFailure(identifier, symbol); 62 | } 63 | 64 | // For import declarations that are referring to Angular Material, the value declarations 65 | // should be stored so that other identifiers in the file can be compared. 66 | if (isImportSpecifierNode(identifier) && isMaterialImportDeclaration(identifier)) { 67 | this.materialDeclarations.push(symbol.valueDeclaration); 68 | } 69 | 70 | // For identifiers that are not part of an import or export, the list of Material declarations 71 | // should be checked to ensure that only identifiers of Angular Material are updated. 72 | // Identifiers that are imported through an Angular Material namespace will be updated. 73 | else if (this.materialDeclarations.indexOf(symbol.valueDeclaration) === -1 && 74 | !this._isIdentifierFromNamespace(identifier)) { 75 | return; 76 | } 77 | 78 | return this.createIdentifierFailure(identifier, symbol); 79 | } 80 | 81 | /** Creates a failure and replacement for the specified identifier. */ 82 | private createIdentifierFailure(identifier: ts.Identifier, symbol: ts.Symbol) { 83 | let classData = classNames.find( 84 | data => data.replace === symbol.name || data.replace === identifier.text); 85 | 86 | if (!classData) { 87 | console.error(`Could not find updated name for identifier "${identifier.getText()}" in ` + 88 | ` in file ${this._getRelativeFileName()}.`); 89 | return; 90 | } 91 | 92 | const replacement = this.createReplacement( 93 | identifier.getStart(), identifier.getWidth(), classData.replaceWith); 94 | 95 | this.addFailureAtNode( 96 | identifier, 97 | `Found deprecated identifier "${red(classData.replace)}" which has been renamed to` + 98 | ` "${green(classData.replaceWith)}"`, 99 | replacement); 100 | } 101 | 102 | /** Checks namespace imports from Angular Material and stores them in a list. */ 103 | private _storeNamespaceImports(identifier: ts.Identifier) { 104 | // In some situations, developers will import Angular Material completely using a namespace 105 | // import. This is not recommended, but should be still handled in the migration tool. 106 | if (isNamespaceImportNode(identifier) && isMaterialImportDeclaration(identifier)) { 107 | const symbol = getOriginalSymbolFromNode(identifier, this.getTypeChecker()); 108 | 109 | if (symbol) { 110 | return this.materialNamespaceDeclarations.push(symbol.valueDeclaration); 111 | } 112 | } 113 | } 114 | 115 | /** Checks whether the given identifier is part of the Material namespace. */ 116 | private _isIdentifierFromNamespace(identifier: ts.Identifier) { 117 | if (identifier.parent.kind !== ts.SyntaxKind.PropertyAccessExpression) { 118 | return; 119 | } 120 | 121 | const propertyExpression = identifier.parent as ts.PropertyAccessExpression; 122 | const expressionSymbol = getOriginalSymbolFromNode(propertyExpression.expression, 123 | this.getTypeChecker()); 124 | 125 | return this.materialNamespaceDeclarations.indexOf(expressionSymbol.valueDeclaration) !== -1; 126 | } 127 | 128 | /** Returns the current source file path relative to the root directory of the project. */ 129 | private _getRelativeFileName(): string { 130 | return relative(this.getProgram().getCurrentDirectory(), this.getSourceFile().fileName); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/material/data/property-names.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "pr": "https://github.com/angular/material2/pull/10161", 4 | "changes": [ 5 | { 6 | "replace": "_deprecatedOrigin", 7 | "replaceWith": "origin", 8 | "whitelist": { 9 | "classes": ["CdkConnectedOverlay", "ConnectedOverlayDirective"] 10 | } 11 | }, 12 | { 13 | "replace": "_deprecatedPositions", 14 | "replaceWith": "positions", 15 | "whitelist": { 16 | "classes": ["CdkConnectedOverlay", "ConnectedOverlayDirective"] 17 | } 18 | }, 19 | { 20 | "replace": "_deprecatedOffsetX", 21 | "replaceWith": "offsetX", 22 | "whitelist": { 23 | "classes": ["CdkConnectedOverlay", "ConnectedOverlayDirective"] 24 | } 25 | }, 26 | { 27 | "replace": "_deprecatedOffsetY", 28 | "replaceWith": "offsetY", 29 | "whitelist": { 30 | "classes": ["CdkConnectedOverlay", "ConnectedOverlayDirective"] 31 | } 32 | }, 33 | { 34 | "replace": "_deprecatedWidth", 35 | "replaceWith": "width", 36 | "whitelist": { 37 | "classes": ["CdkConnectedOverlay", "ConnectedOverlayDirective"] 38 | } 39 | }, 40 | { 41 | "replace": "_deprecatedHeight", 42 | "replaceWith": "height", 43 | "whitelist": { 44 | "classes": ["CdkConnectedOverlay", "ConnectedOverlayDirective"] 45 | } 46 | }, 47 | { 48 | "replace": "_deprecatedMinWidth", 49 | "replaceWith": "minWidth", 50 | "whitelist": { 51 | "classes": ["CdkConnectedOverlay", "ConnectedOverlayDirective"] 52 | } 53 | }, 54 | { 55 | "replace": "_deprecatedMinHeight", 56 | "replaceWith": "minHeight", 57 | "whitelist": { 58 | "classes": ["CdkConnectedOverlay", "ConnectedOverlayDirective"] 59 | } 60 | }, 61 | { 62 | "replace": "_deprecatedBackdropClass", 63 | "replaceWith": "backdropClass", 64 | "whitelist": { 65 | "classes": ["CdkConnectedOverlay", "ConnectedOverlayDirective"] 66 | } 67 | }, 68 | { 69 | "replace": "_deprecatedScrollStrategy", 70 | "replaceWith": "scrollStrategy", 71 | "whitelist": { 72 | "classes": ["CdkConnectedOverlay", "ConnectedOverlayDirective"] 73 | } 74 | }, 75 | { 76 | "replace": "_deprecatedOpen", 77 | "replaceWith": "open", 78 | "whitelist": { 79 | "classes": ["CdkConnectedOverlay", "ConnectedOverlayDirective"] 80 | } 81 | }, 82 | { 83 | "replace": "_deprecatedHasBackdrop", 84 | "replaceWith": "hasBackdrop", 85 | "whitelist": { 86 | "classes": ["CdkConnectedOverlay", "ConnectedOverlayDirective"] 87 | } 88 | } 89 | ] 90 | }, 91 | 92 | 93 | { 94 | "pr": "https://github.com/angular/material2/pull/10163", 95 | "changes": [ 96 | { 97 | "replace": "change", 98 | "replaceWith": "selectionChange", 99 | "whitelist": { 100 | "classes": ["MatSelect"] 101 | } 102 | }, 103 | { 104 | "replace": "onOpen", 105 | "replaceWith": "openedChange.pipe(filter(isOpen => isOpen))", 106 | "whitelist": { 107 | "classes": ["MatSelect"] 108 | } 109 | }, 110 | { 111 | "replace": "onClose", 112 | "replaceWith": "openedChange.pipe(filter(isOpen => !isOpen))", 113 | "whitelist": { 114 | "classes": ["MatSelect"] 115 | } 116 | } 117 | ] 118 | }, 119 | 120 | 121 | { 122 | "pr": "https://github.com/angular/material2/pull/10218", 123 | "changes": [ 124 | { 125 | "replace": "align", 126 | "replaceWith": "labelPosition", 127 | "whitelist": { 128 | "classes": ["MatRadioGroup", "MatRadioButton"] 129 | } 130 | } 131 | ] 132 | }, 133 | 134 | 135 | { 136 | "pr": "https://github.com/angular/material2/pull/10253", 137 | "changes": [ 138 | { 139 | "replace": "extraClasses", 140 | "replaceWith": "panelClass", 141 | "whitelist": { 142 | "classes": ["MatSnackBarConfig"] 143 | } 144 | } 145 | ] 146 | }, 147 | 148 | 149 | { 150 | "pr": "https://github.com/angular/material2/pull/10257", 151 | "changes": [ 152 | { 153 | "replace": "_deprecatedPortal", 154 | "replaceWith": "portal", 155 | "whitelist": { 156 | "classes": ["CdkPortalOutlet"] 157 | } 158 | }, 159 | { 160 | "replace": "_deprecatedPortalHost", 161 | "replaceWith": "portal", 162 | "whitelist": { 163 | "classes": ["CdkPortalOutlet"] 164 | } 165 | } 166 | ] 167 | }, 168 | 169 | 170 | { 171 | "pr": "https://github.com/angular/material2/pull/10279", 172 | "changes": [ 173 | { 174 | "replace": "align", 175 | "replaceWith": "position", 176 | "whitelist": { 177 | "classes": ["MatDrawer", "MatSidenav"] 178 | } 179 | }, 180 | { 181 | "replace": "onAlignChanged", 182 | "replaceWith": "onPositionChanged", 183 | "whitelist": { 184 | "classes": ["MatDrawer", "MatSidenav"] 185 | } 186 | }, 187 | { 188 | "replace": "onOpen", 189 | "replaceWith": "openedChange.pipe(filter(isOpen => isOpen))", 190 | "whitelist": { 191 | "classes": ["MatDrawer", "MatSidenav"] 192 | } 193 | }, 194 | { 195 | "replace": "onClose", 196 | "replaceWith": "openedChange.pipe(filter(isOpen => !isOpen))", 197 | "whitelist": { 198 | "classes": ["MatDrawer", "MatSidenav"] 199 | } 200 | } 201 | ] 202 | }, 203 | 204 | 205 | { 206 | "pr": "https://github.com/angular/material2/pull/10293", 207 | "changes": [ 208 | { 209 | "replace": "shouldPlaceholderFloat", 210 | "replaceWith": "shouldLabelFloat", 211 | "whitelist": { 212 | "classes": ["MatFormFieldControl", "MatSelect"] 213 | } 214 | } 215 | ] 216 | }, 217 | 218 | 219 | { 220 | "pr": "https://github.com/angular/material2/pull/10294", 221 | "changes": [ 222 | { 223 | "replace": "dividerColor", 224 | "replaceWith": "color", 225 | "whitelist": { 226 | "classes": ["MatFormField"] 227 | } 228 | }, 229 | { 230 | "replace": "floatPlaceholder", 231 | "replaceWith": "floatLabel", 232 | "whitelist": { 233 | "classes": ["MatFormField"] 234 | } 235 | } 236 | ] 237 | }, 238 | 239 | 240 | { 241 | "pr": "https://github.com/angular/material2/pull/10309", 242 | "changes": [ 243 | { 244 | "replace": "selectChange", 245 | "replaceWith": "selectedTabChange", 246 | "whitelist": { 247 | "classes": ["MatTabGroup"] 248 | } 249 | }, 250 | { 251 | "replace": "_dynamicHeightDeprecated", 252 | "replaceWith": "dynamicHeight", 253 | "whitelist": { 254 | "classes": ["MatTabGroup"] 255 | } 256 | } 257 | ] 258 | }, 259 | 260 | 261 | { 262 | "pr": "https://github.com/angular/material2/pull/10311", 263 | "changes": [ 264 | { 265 | "replace": "destroy", 266 | "replaceWith": "destroyed", 267 | "whitelist": { 268 | "classes": ["MatChip"] 269 | } 270 | }, 271 | { 272 | "replace": "onRemove", 273 | "replaceWith": "removed", 274 | "whitelist": { 275 | "classes": ["MatChip"] 276 | } 277 | } 278 | ] 279 | }, 280 | 281 | 282 | { 283 | "pr": "https://github.com/angular/material2/pull/10342", 284 | "changes": [ 285 | { 286 | "replace": "align", 287 | "replaceWith": "labelPosition", 288 | "whitelist": { 289 | "classes": ["MatCheckbox"] 290 | } 291 | } 292 | ] 293 | }, 294 | 295 | 296 | { 297 | "pr": "https://github.com/angular/material2/pull/10344", 298 | "changes": [ 299 | { 300 | "replace": "_positionDeprecated", 301 | "replaceWith": "position", 302 | "whitelist": { 303 | "classes": ["MatTooltip"] 304 | } 305 | } 306 | ] 307 | }, 308 | 309 | 310 | { 311 | "pr": "https://github.com/angular/material2/pull/10373", 312 | "changes": [ 313 | { 314 | "replace": "_thumbLabelDeprecated", 315 | "replaceWith": "thumbLabel", 316 | "whitelist": { 317 | "classes": ["MatSlider"] 318 | } 319 | }, 320 | { 321 | "replace": "_tickIntervalDeprecated", 322 | "replaceWith": "tickInterval", 323 | "whitelist": { 324 | "classes": ["MatSlider"] 325 | } 326 | } 327 | ] 328 | } 329 | ] 330 | --------------------------------------------------------------------------------