diff --git a/src/lib/schematics/update/rules/checkPropertyAccessMiscRule.ts b/src/lib/schematics/update/rules/checkPropertyAccessMiscRule.ts deleted file mode 100644 index dcd7615df184..000000000000 --- a/src/lib/schematics/update/rules/checkPropertyAccessMiscRule.ts +++ /dev/null @@ -1,60 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import {bold, green, red} from 'chalk'; -import {ProgramAwareRuleWalker, RuleFailure, Rules} from 'tslint'; -import * as ts from 'typescript'; - -/** - * Rule that walks through every identifier that is part of Angular Material and replaces the - * outdated name with the new one. - */ -export class Rule extends Rules.TypedRule { - applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] { - return this.applyWithWalker( - new CheckPropertyAccessMiscWalker(sourceFile, this.getOptions(), program)); - } -} - -export class CheckPropertyAccessMiscWalker extends ProgramAwareRuleWalker { - visitPropertyAccessExpression(prop: ts.PropertyAccessExpression) { - // Recursively call this method for the expression of the current property expression. - // It can happen that there is a chain of property access expressions. - // For example: "mySortInstance.mdSortChange.subscribe()" - if (prop.expression && prop.expression.kind === ts.SyntaxKind.PropertyAccessExpression) { - this.visitPropertyAccessExpression(prop.expression as ts.PropertyAccessExpression); - } - - // TODO(mmalerba): This is probably a bad way to get the property host... - // Tokens are: [..., , '.', ], so back up 3. - const propHost = prop.getChildAt(prop.getChildCount() - 3); - - const type = this.getTypeChecker().getTypeAtLocation(propHost); - const typeSymbol = type && type.getSymbol(); - - if (typeSymbol) { - const typeName = typeSymbol.getName(); - - if (typeName === 'MatListOption' && prop.name.text === 'selectionChange') { - this.addFailureAtNode( - prop, - `Found deprecated property "${red('selectionChange')}" of class` + - ` "${bold('MatListOption')}". Use the "${green('selectionChange')}" property on the` + - ` parent "${bold('MatSelectionList')}" instead.`); - } - - if (typeName === 'MatDatepicker' && prop.name.text === 'selectedChanged') { - this.addFailureAtNode( - prop, - `Found deprecated property "${red('selectedChanged')}" of class` + - ` "${bold('MatDatepicker')}". Use the "${green('dateChange')}" or` + - ` "${green('dateInput')}" methods on "${bold('MatDatepickerInput')}" instead`); - } - } - } -} diff --git a/src/lib/schematics/update/rules/property-names/propertyNamesAccessRule.ts b/src/lib/schematics/update/rules/property-names/propertyNamesAccessRule.ts new file mode 100644 index 000000000000..77b650c6ab41 --- /dev/null +++ b/src/lib/schematics/update/rules/property-names/propertyNamesAccessRule.ts @@ -0,0 +1,45 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {green, red} from 'chalk'; +import {ProgramAwareRuleWalker, RuleFailure, Rules} from 'tslint'; +import * as ts from 'typescript'; +import {propertyNames} from '../../material/data/property-names'; + +/** + * Rule that walks through every property access expression and updates properties that have + * been changed in favor of a new name. + */ +export class Rule extends Rules.TypedRule { + applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] { + return this.applyWithWalker(new Walker(sourceFile, this.getOptions(), program)); + } +} + +export class Walker extends ProgramAwareRuleWalker { + + visitPropertyAccessExpression(node: ts.PropertyAccessExpression) { + const hostType = this.getTypeChecker().getTypeAtLocation(node.expression); + const typeName = hostType && hostType.symbol && hostType.symbol.getName(); + + propertyNames.forEach(data => { + if (node.name.text !== data.replace) { + return; + } + + if (!data.whitelist || data.whitelist.classes.includes(typeName)) { + const replacement = this.createReplacement(node.name.getStart(), + node.name.getWidth(), data.replaceWith); + this.addFailureAtNode(node.name, `Found deprecated property ${red(data.replace)} which ` + + `has been renamed to "${green(data.replaceWith)}"`, replacement); + } + }); + + super.visitPropertyAccessExpression(node); + } +} diff --git a/src/lib/schematics/update/rules/property-names/propertyNamesMiscRule.ts b/src/lib/schematics/update/rules/property-names/propertyNamesMiscRule.ts new file mode 100644 index 000000000000..a06471e21b0b --- /dev/null +++ b/src/lib/schematics/update/rules/property-names/propertyNamesMiscRule.ts @@ -0,0 +1,43 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {bold, green, red} from 'chalk'; +import {ProgramAwareRuleWalker, RuleFailure, Rules} from 'tslint'; +import * as ts from 'typescript'; + +/** + * Rule that walks through every property access expression and and reports to TSLint if + * a given property name is no longer existing but cannot be automatically migrated. + */ +export class Rule extends Rules.TypedRule { + applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] { + return this.applyWithWalker(new Walker(sourceFile, this.getOptions(), program)); + } +} + +export class Walker extends ProgramAwareRuleWalker { + + visitPropertyAccessExpression(node: ts.PropertyAccessExpression) { + const hostType = this.getTypeChecker().getTypeAtLocation(node.expression); + const typeName = hostType && hostType.symbol && hostType.symbol.getName(); + + if (typeName === 'MatListOption' && node.name.text === 'selectionChange') { + this.addFailureAtNode(node, `Found deprecated property "${red('selectionChange')}" of ` + + `class "${bold('MatListOption')}". Use the "${green('selectionChange')}" property on ` + + `the parent "${bold('MatSelectionList')}" instead.`); + } + + if (typeName === 'MatDatepicker' && node.name.text === 'selectedChanged') { + this.addFailureAtNode(node, `Found deprecated property "${red('selectedChanged')}" of ` + + `class "${bold('MatDatepicker')}". Use the "${green('dateChange')}" or ` + + `"${green('dateInput')}" methods on "${bold('MatDatepickerInput')}" instead`); + } + + super.visitPropertyAccessExpression(node); + } +} diff --git a/src/lib/schematics/update/rules/switchPropertyNamesRule.ts b/src/lib/schematics/update/rules/switchPropertyNamesRule.ts deleted file mode 100644 index f98bbb6c9589..000000000000 --- a/src/lib/schematics/update/rules/switchPropertyNamesRule.ts +++ /dev/null @@ -1,65 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import {bold, green, red} from 'chalk'; -import {ProgramAwareRuleWalker, RuleFailure, Rules} from 'tslint'; -import * as ts from 'typescript'; -import {propertyNames} from '../material/data/property-names'; - -/** - * Rule that walks through every property access expression and updates properties that have - * been changed in favor of the new name. - */ -export class Rule extends Rules.TypedRule { - applyWithProgram(sourceFile: ts.SourceFile, program: ts.Program): RuleFailure[] { - return this.applyWithWalker( - new SwitchPropertyNamesWalker(sourceFile, this.getOptions(), program)); - } -} - -export class SwitchPropertyNamesWalker extends ProgramAwareRuleWalker { - visitPropertyAccessExpression(prop: ts.PropertyAccessExpression) { - // Recursively call this method for the expression of the current property expression. - // It can happen that there is a chain of property access expressions. - // For example: "mySortInstance.mdSortChange.subscribe()" - if (prop.expression && prop.expression.kind === ts.SyntaxKind.PropertyAccessExpression) { - this.visitPropertyAccessExpression(prop.expression as ts.PropertyAccessExpression); - } - - // TODO(mmalerba): This is prrobably a bad way to get the property host... - // Tokens are: [..., , '.', ], so back up 3. - const propHost = prop.getChildAt(prop.getChildCount() - 3); - - const type = this.getTypeChecker().getTypeAtLocation(propHost); - const typeSymbol = type && type.getSymbol(); - const typeName = typeSymbol && typeSymbol.getName(); - const propertyData = propertyNames.find(name => { - if (prop.name.text === name.replace) { - // TODO(mmalerba): Verify that this type comes from Angular Material like we do in - // `switchIdentifiersRule`. - return !name.whitelist || !!typeName && new Set(name.whitelist.classes).has(typeName); - } - return false; - }); - - if (!propertyData) { - return; - } - - const replacement = this.createReplacement(prop.name.getStart(), - prop.name.getWidth(), propertyData.replaceWith); - - const typeMessage = propertyData.whitelist ? `of class "${bold(typeName || '')}"` : ''; - - this.addFailureAtNode( - prop.name, - `Found deprecated property "${red(propertyData.replace)}" ${typeMessage} which has been` + - ` renamed to "${green(propertyData.replaceWith)}"`, - replacement); - } -} diff --git a/src/lib/schematics/update/test-cases/index.spec.ts b/src/lib/schematics/update/test-cases/index.spec.ts index 59fe0ff15adf..bc8bd3abc539 100644 --- a/src/lib/schematics/update/test-cases/index.spec.ts +++ b/src/lib/schematics/update/test-cases/index.spec.ts @@ -21,6 +21,7 @@ describe('test cases', () => { 'v5/element-selectors', 'v5/input-names', 'v5/output-names', + 'v5/property-names', ]; // Iterates through every test case directory and generates a jasmine test block that will diff --git a/src/lib/schematics/update/test-cases/v5/property-names_expected_output.ts b/src/lib/schematics/update/test-cases/v5/property-names_expected_output.ts new file mode 100644 index 000000000000..7a7955ee344e --- /dev/null +++ b/src/lib/schematics/update/test-cases/v5/property-names_expected_output.ts @@ -0,0 +1,103 @@ +/* + * Fake definitions because the property name rules can only determine the host type + * properly by using type checking. + */ + +class CdkConnectedOverlay { + _deprecatedOrigin: any; + _deprecatedPositions: any; + _deprecatedOffsetX: any; + _deprecatedOffsetY: any; + _deprecatedWidth: any; + _deprecatedHeight: any; + _deprecatedMinWidth: any; + _deprecatedMinHeight: any; + _deprecatedBackdropClass: any; + _deprecatedScrollStrategy: any; + _deprecatedOpen: any; + _deprecatedHasBackdrop: any; +} + +class MatSelect { + change: any; + onOpen: any; + onClose: any; +} + +class MatRadioGroup { + align: any; +} + +class MatSnackBarConfig { + extraClasses: any; +} + +class CdkPortalOutlet { + _deprecatedPortal: any; + _deprecatedPortalHost: any; +} + +class MatDrawer { + align: any; + onAlignChanged: any; + onOpen: any; + onClose: any; +} + +/* Actual test case using the previously defined definitions. */ + +class A { + self = {me: this}; + + constructor(private a: CdkConnectedOverlay) {} + + onAngularClick() { + this.a.origin = '1'; + this.a.positions = '2'; + this.a.offsetX = '3'; + this.a.offsetY = '4'; + this.a.height = '5'; + + console.log(this.a.width || 10); + console.log(this.a.minWidth || this.a.minHeight); + + this.self.me.a.backdropClass = ['a', 'b', 'c']; + + const x = ({test: true} || this.a); + + if (this.isConnectedOverlay(x)) { + x.scrollStrategy = 'myScrollStrategy'; + x.open = false; + x.hasBackdrop = true; + } + } + + isConnectedOverlay(val: any): val is CdkConnectedOverlay { + return val instanceof CdkConnectedOverlay; + } +} + +class B { + self = {me: this}; + b: MatRadioGroup; + + constructor(private a: MatSelect, + public c: MatSnackBarConfig, + protected d: CdkPortalOutlet, + private e: MatDrawer) {} + + onClick() { + this.a.selectionChange.subscribe(() => console.log('On Change')); + this.a.openedChange.pipe(filter(isOpen => isOpen)).subscribe(() => console.log('On Open')); + this.a.openedChange.pipe(filter(isOpen => !isOpen)).subscribe(() => console.log('On Close')); + + this.b.labelPosition = 'end'; + this.c.panelClass = ['x', 'y', 'z']; + this.d.portal = this.d.portal = 'myNewPortal'; + + this.e.position = 'end'; + this.e.onPositionChanged.subscribe(() => console.log('Align Changed')); + this.e.openedChange.pipe(filter(isOpen => isOpen)).subscribe(() => console.log('Open')); + this.e.openedChange.pipe(filter(isOpen => !isOpen)).subscribe(() => console.log('Close')); + } +} diff --git a/src/lib/schematics/update/test-cases/v5/property-names_input.ts b/src/lib/schematics/update/test-cases/v5/property-names_input.ts new file mode 100644 index 000000000000..ae83d5fd6a38 --- /dev/null +++ b/src/lib/schematics/update/test-cases/v5/property-names_input.ts @@ -0,0 +1,103 @@ +/* + * Fake definitions because the property name rules can only determine the host type + * properly by using type checking. + */ + +class CdkConnectedOverlay { + _deprecatedOrigin: any; + _deprecatedPositions: any; + _deprecatedOffsetX: any; + _deprecatedOffsetY: any; + _deprecatedWidth: any; + _deprecatedHeight: any; + _deprecatedMinWidth: any; + _deprecatedMinHeight: any; + _deprecatedBackdropClass: any; + _deprecatedScrollStrategy: any; + _deprecatedOpen: any; + _deprecatedHasBackdrop: any; +} + +class MatSelect { + change: any; + onOpen: any; + onClose: any; +} + +class MatRadioGroup { + align: any; +} + +class MatSnackBarConfig { + extraClasses: any; +} + +class CdkPortalOutlet { + _deprecatedPortal: any; + _deprecatedPortalHost: any; +} + +class MatDrawer { + align: any; + onAlignChanged: any; + onOpen: any; + onClose: any; +} + +/* Actual test case using the previously defined definitions. */ + +class A { + self = {me: this}; + + constructor(private a: CdkConnectedOverlay) {} + + onAngularClick() { + this.a._deprecatedOrigin = '1'; + this.a._deprecatedPositions = '2'; + this.a._deprecatedOffsetX = '3'; + this.a._deprecatedOffsetY = '4'; + this.a._deprecatedHeight = '5'; + + console.log(this.a._deprecatedWidth || 10); + console.log(this.a._deprecatedMinWidth || this.a._deprecatedMinHeight); + + this.self.me.a._deprecatedBackdropClass = ['a', 'b', 'c']; + + const x = ({test: true} || this.a); + + if (this.isConnectedOverlay(x)) { + x._deprecatedScrollStrategy = 'myScrollStrategy'; + x._deprecatedOpen = false; + x._deprecatedHasBackdrop = true; + } + } + + isConnectedOverlay(val: any): val is CdkConnectedOverlay { + return val instanceof CdkConnectedOverlay; + } +} + +class B { + self = {me: this}; + b: MatRadioGroup; + + constructor(private a: MatSelect, + public c: MatSnackBarConfig, + protected d: CdkPortalOutlet, + private e: MatDrawer) {} + + onClick() { + this.a.change.subscribe(() => console.log('On Change')); + this.a.onOpen.subscribe(() => console.log('On Open')); + this.a.onClose.subscribe(() => console.log('On Close')); + + this.b.align = 'end'; + this.c.extraClasses = ['x', 'y', 'z']; + this.d._deprecatedPortal = this.d._deprecatedPortalHost = 'myNewPortal'; + + this.e.align = 'end'; + this.e.onAlignChanged.subscribe(() => console.log('Align Changed')); + this.e.onOpen.subscribe(() => console.log('Open')); + this.e.onClose.subscribe(() => console.log('Close')); + } +} diff --git a/src/lib/schematics/update/typescript/identifiers.ts b/src/lib/schematics/update/typescript/identifiers.ts deleted file mode 100644 index 04fdfbba77ab..000000000000 --- a/src/lib/schematics/update/typescript/identifiers.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import * as ts from 'typescript'; - -/** Returns the original symbol from an node. */ -export function getOriginalSymbolFromNode(node: ts.Node, checker: ts.TypeChecker) { - const baseSymbol = checker.getSymbolAtLocation(node); - - // tslint:disable-next-line:no-bitwise - if (baseSymbol && baseSymbol.flags & ts.SymbolFlags.Alias) { - return checker.getAliasedSymbol(baseSymbol); - } - - return baseSymbol; -} diff --git a/src/lib/schematics/update/update.ts b/src/lib/schematics/update/update.ts index 34275a973c23..6d094173d061 100644 --- a/src/lib/schematics/update/update.ts +++ b/src/lib/schematics/update/update.ts @@ -34,10 +34,10 @@ export default function(): Rule { path.join(__dirname, 'rules/output-names'), path.join(__dirname, 'rules/css-selectors'), path.join(__dirname, 'rules/element-selectors'), + path.join(__dirname, 'rules/property-names'), ], rules: { // Automatic fixes. - 'switch-property-names': true, 'switch-template-export-as-names': true, // Attribute selector update rules. @@ -66,12 +66,15 @@ export default function(): Rule { // Output name update rules 'output-names-template': true, + // Property name update rules + 'property-names-access': true, + 'property-names-misc': true, + // Additional issues we can detect but not automatically fix. 'check-class-declaration-misc': true, 'check-import-misc': true, 'check-inheritance': true, 'check-method-calls': true, - 'check-property-access-misc': true, 'check-template-misc': true } }, {