diff --git a/.angulardoc.json b/.angulardoc.json new file mode 100644 index 0000000..f5b8146 --- /dev/null +++ b/.angulardoc.json @@ -0,0 +1,4 @@ +{ + "repoId": "a74b340a-2476-460e-8399-b773fa83fda6", + "lastSync": 0 +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index dbe7cb2..3e44bc8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,6 +4,7 @@ export { Rule as AlertTitleAndSubtitleAreNowHeaderAndSubheader } from './ionAler export { Rule as IonButtonAttributesAreRenamedRule } from './ionButtonAttributesAreRenamedRule'; export { Rule as IonButtonIsNowAnElementRule } from './ionButtonIsNowAnElementRule'; export { Rule as IonChipMarkupChangedRule } from './ionChipMarkupHasChangedRule'; +export { Rule as ionDatetimeCapitalizationChangedRule } from './ionDatetimeCapitalizationChangedRule'; export { Rule as IonNavbarIsNowIonToolbarRule } from './ionNavbarIsNowIonToolbarRule'; export { Rule as IonTabBadgeStyleIsNowBadgeStyle } from './ionTabBadgeStyleIsNowBadgeStyleRule'; export { Rule as IonTabIconIsNowIcon } from './ionTabIconIsNowIconRule'; diff --git a/src/ionDatetimeCapitalizationChangedRule.ts b/src/ionDatetimeCapitalizationChangedRule.ts new file mode 100644 index 0000000..3fb919b --- /dev/null +++ b/src/ionDatetimeCapitalizationChangedRule.ts @@ -0,0 +1,103 @@ +import * as Lint from 'tslint'; +import * as ts from 'typescript'; + +export const ruleName = 'ion-datetime-capitalization-changed'; +export class Rule extends Lint.Rules.AbstractRule { + public static metadata: Lint.IRuleMetadata = { + ruleName, + type: 'functionality', + description: 'Updates the Datetime import', + rationale: 'Datetime exported symbol has changed', + options: null, + optionsDescription: 'Not configurable.', + typescriptOnly: true + }; + + static RuleFailure = 'outdated import path'; + + public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + return this.applyWithWalker(new UpdateImportsWalker(sourceFile, this.getOptions())); + } +} + +class UpdateImportsWalker extends Lint.RuleWalker { + visitImportDeclaration(node: ts.ImportDeclaration): void { + if (ts.isStringLiteral(node.moduleSpecifier) && node.importClause) { + const specifier = node.moduleSpecifier; + const path = (specifier as ts.StringLiteral).text; + const start = specifier.getStart() + 1; + const end = specifier.text.length; + const replacementStart = start; + const replacementEnd = specifier.text.length; + let replacement = null; + + // Try to find updated symbol names. + ImportReplacements.forEach(r => (r.path === path ? this._migrateExportedSymbols(r, node) : void 0)); + + // Try to migrate entire import path updates. + if (ImportMap.has(path)) { + replacement = ImportMap.get(path); + } + if (replacement !== null) { + return this.addFailureAt(start, end, Rule.RuleFailure, this.createReplacement(replacementStart, replacementEnd, replacement)); + } + } + } + + private _migrateExportedSymbols(re: ImportReplacement, node: ts.ImportDeclaration) { + const importClause = node.importClause as ts.ImportClause; + const bindings = importClause.namedBindings as ts.NamedImports | null; + if (!bindings || bindings.kind !== ts.SyntaxKind.NamedImports) { + return; + } + + bindings.elements.forEach((e: ts.ImportSpecifier | null) => { + if (!e || e.kind !== ts.SyntaxKind.ImportSpecifier) { + return; + } + + let toReplace = e.name; + // We don't want to introduce type errors so we alias the old new symbol. + let replacement = `${re.newSymbol} as ${re.symbol}`; + if (e.propertyName) { + toReplace = e.propertyName; + replacement = re.newSymbol; + } + + if (toReplace.getText() !== re.symbol) { + return; + } + + return this.addFailureAt( + toReplace.getStart(), + toReplace.getWidth(), + 'imported symbol no longer exists', + this.createReplacement(toReplace.getStart(), toReplace.getWidth(), replacement) + ); + }); + } +} + +const ImportMap = new Map([['ionic-angular', '@ionic/angular']]); + +interface ImportReplacement { + path: string; + symbol: string; + newPath: string; + newSymbol: string; +} + +const ImportReplacements = [ + { + path: 'ionic-angular', + symbol: 'DateTime', + newPath: '@ionic/angular', + newSymbol: 'Datetime' + }, + { + path: '@ionic/angular', + symbol: 'DateTime', + newPath: '@ionic/angular', + newSymbol: 'Datetime' + } +]; diff --git a/test/ionDatetimeCapitalizationChanged.spec.ts b/test/ionDatetimeCapitalizationChanged.spec.ts new file mode 100644 index 0000000..55c2450 --- /dev/null +++ b/test/ionDatetimeCapitalizationChanged.spec.ts @@ -0,0 +1,55 @@ +import { ruleName } from '../src/ionDatetimeCapitalizationChangedRule'; +import { assertAnnotated, assertSuccess, assertMultipleAnnotated } from './testHelper'; + +describe(ruleName, () => { + describe('success', () => { + it('should work when the import and path are correct', () => { + let source = ` + import {Datetime} from '@ionic/angular'; + `; + assertSuccess(ruleName, source); + }); + }); + describe('failure', () => { + it('should fail when symbol is right, but path is wrong', () => { + let source = ` + import {Datetime} from '@ionic/angular'; + `; + assertAnnotated({ + ruleName, + source, + message: 'outdated import path' + }); + }); + it('should fail when symbol is wrong, but path is right', () => { + let source = ` + import {DateTime} from '@ionic/angular'; + `; + assertAnnotated({ + ruleName, + source, + message: 'imported symbol no longer exists' + }); + }); + it('should fail when symbol and path are wrong', () => { + let source = ` + import {DateTime} from 'ionic-angular'; + `; + assertAnnotated({ + ruleName, + source, + message: 'imported symbol no longer exists' + }); + }); + it('should fail when symbol and path are wrong', () => { + let source = ` + import {DateTime} from 'ionic-angular'; + `; + assertAnnotated({ + ruleName, + source, + message: 'outdated import path' + }); + }); + }); +});