From a71d8bc6dffc94b603542bb800ec17839fe407cd Mon Sep 17 00:00:00 2001 From: dwieeb Date: Fri, 29 Jun 2018 11:15:49 -0500 Subject: [PATCH] feat(rules): add ion-fab-attributes-renamed rule (#16) --- README.md | 10 +- src/ionFabAttributesRenamedRule.ts | 63 ++++++ test/ionFabAttributesRenamed.spec.ts | 319 +++++++++++++++++++++++++++ test/testHelper.ts | 8 +- 4 files changed, 395 insertions(+), 5 deletions(-) create mode 100644 src/ionFabAttributesRenamedRule.ts create mode 100644 test/ionFabAttributesRenamed.spec.ts diff --git a/README.md b/README.md index d1c8593..4e7b48b 100644 --- a/README.md +++ b/README.md @@ -209,12 +209,14 @@ We are looking for contributors to help build these rules out! See [`CONTRIBUTIN - - :white_large_square: + :wrench: + :white_check_mark: - ion-fab-attributes-are-renamed + ion-fab-attributes-renamed + + + @dwieeb - diff --git a/src/ionFabAttributesRenamedRule.ts b/src/ionFabAttributesRenamedRule.ts new file mode 100644 index 0000000..95c9490 --- /dev/null +++ b/src/ionFabAttributesRenamedRule.ts @@ -0,0 +1,63 @@ +import * as ast from '@angular/compiler'; +import { NgWalker } from 'codelyzer/angular/ngWalker'; +import { BasicTemplateAstVisitor } from 'codelyzer/angular/templates/basicTemplateAstVisitor'; +import * as Lint from 'tslint'; +import { IOptions } from 'tslint'; +import * as ts from 'typescript'; + +export const ruleName = 'ion-fab-attributes-renamed'; + +function generateErrorMessage(attrName: string, replacement: string) { + return `The ${attrName} attribute of ion-fab has been renamed. Use ${replacement} instead.`; +} + +const attrMap = new Map([ + ['center', 'horizontal="center"'], + ['start', 'horizontal="start"'], + ['end', 'horizontal="end"'], + ['top', 'vertical="top"'], + ['bottom', 'vertical="bottom"'], + ['middle', 'vertical="center"'] +]); + +class IonFabAttributesRenamedTemplateVisitor extends BasicTemplateAstVisitor { + visitElement(element: ast.ElementAst, context: any): any { + if (element.name === 'ion-fab') { + for (const attr of element.attrs) { + const replacement = attrMap.get(attr.name); + + if (replacement) { + const start = attr.sourceSpan.start.offset; + const length = attr.name.length; + const position = this.getSourcePosition(start); + + this.addFailureAt(start, length, generateErrorMessage(attr.name, replacement), [ + Lint.Replacement.replaceFromTo(position, position + length, replacement) + ]); + } + } + } + + super.visitElement(element, context); + } +} + +export class Rule extends Lint.Rules.AbstractRule { + public static metadata: Lint.IRuleMetadata = { + ruleName: ruleName, + type: 'functionality', + description: 'Attributes of ion-fab have been renamed.', + options: null, + optionsDescription: 'Not configurable.', + typescriptOnly: false, + hasFix: true + }; + + public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] { + return this.applyWithWalker( + new NgWalker(sourceFile, this.getOptions(), { + templateVisitorCtrl: IonFabAttributesRenamedTemplateVisitor + }) + ); + } +} diff --git a/test/ionFabAttributesRenamed.spec.ts b/test/ionFabAttributesRenamed.spec.ts new file mode 100644 index 0000000..f808985 --- /dev/null +++ b/test/ionFabAttributesRenamed.spec.ts @@ -0,0 +1,319 @@ +import { expect } from 'chai'; +import { Replacement, Utils } from 'tslint'; +import { ruleName } from '../src/ionFabAttributesRenamedRule'; +import { assertAnnotated, assertFailure, assertFailures, assertMultipleAnnotated, assertSuccess } from './testHelper'; + +describe(ruleName, () => { + describe('success', () => { + it('should work with proper style', () => { + let source = ` + @Component({ + template: \` \` + }) + class Bar {} + `; + assertSuccess(ruleName, source); + }); + }); + + describe('failure', () => { + it('should fail when center is used', () => { + let source = ` + @Component({ + template: \` + + ~~~~~~ + + + + + + + \` + }) + class Bar {} + `; + + assertAnnotated({ + ruleName, + message: 'The center attribute of ion-fab has been renamed. Use horizontal="center" instead.', + source + }); + }); + + it('should fail when start is used', () => { + let source = ` + @Component({ + template: \` + + ~~~~~ + + + + + + + \` + }) + class Bar {} + `; + + assertAnnotated({ + ruleName, + message: 'The start attribute of ion-fab has been renamed. Use horizontal="start" instead.', + source + }); + }); + + it('should fail when end is used', () => { + let source = ` + @Component({ + template: \` + + ~~~ + + + + + + + \` + }) + class Bar {} + `; + + assertAnnotated({ + ruleName, + message: 'The end attribute of ion-fab has been renamed. Use horizontal="end" instead.', + source + }); + }); + + it('should fail when top is used', () => { + let source = ` + @Component({ + template: \` + + ~~~ + + + + + + + \` + }) + class Bar {} + `; + + assertAnnotated({ + ruleName, + message: 'The top attribute of ion-fab has been renamed. Use vertical="top" instead.', + source + }); + }); + + it('should fail when bottom is used', () => { + let source = ` + @Component({ + template: \` + + ~~~~~~ + + + + + + + \` + }) + class Bar {} + `; + + assertAnnotated({ + ruleName, + message: 'The bottom attribute of ion-fab has been renamed. Use vertical="bottom" instead.', + source + }); + }); + + it('should fail when middle is used', () => { + let source = ` + @Component({ + template: \` + + ~~~~~~ + + + + + + + \` + }) + class Bar {} + `; + + assertAnnotated({ + ruleName, + message: 'The middle attribute of ion-fab has been renamed. Use vertical="center" instead.', + source + }); + }); + + it('should fail when multiple are used', () => { + let source = ` + @Component({ + template: \` + + ~~~~~ ^^^^^^ + + + + + + + \` + }) + class Bar {} + `; + + assertMultipleAnnotated({ + ruleName, + source, + failures: [ + { + char: '~', + msg: 'The start attribute of ion-fab has been renamed. Use horizontal="start" instead.' + }, + { + char: '^', + msg: 'The middle attribute of ion-fab has been renamed. Use vertical="center" instead.' + } + ] + }); + }); + }); + + describe('replacements', () => { + it('should replace center with horizontal="center"', () => { + let source = ` + @Component({ + template: \` + \` + }) + class Bar {} + `; + + const fail = { + message: 'The center attribute of ion-fab has been renamed. Use horizontal="center" instead.', + startPosition: { + line: 2, + character: 30 + }, + endPosition: { + line: 2, + character: 36 + } + }; + + const failures = assertFailure(ruleName, source, fail); + const fixes = failures.map(f => f.getFix()); + const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify)); + + let expected = ` + @Component({ + template: \` + \` + }) + class Bar {} + `; + + expect(res).to.eq(expected); + }); + + it('should replace middle with vertical="center"', () => { + let source = ` + @Component({ + template: \` + \` + }) + class Bar {} + `; + + const fail = { + message: 'The middle attribute of ion-fab has been renamed. Use vertical="center" instead.', + startPosition: { + line: 2, + character: 30 + }, + endPosition: { + line: 2, + character: 36 + } + }; + + const failures = assertFailure(ruleName, source, fail); + const fixes = failures.map(f => f.getFix()); + const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify)); + + let expected = ` + @Component({ + template: \` + \` + }) + class Bar {} + `; + + expect(res).to.eq(expected); + }); + + it('should replace multiple', () => { + let source = ` + @Component({ + template: \` + \` + }) + class Bar {} + `; + + const failures = assertFailures(ruleName, source, [ + { + message: 'The end attribute of ion-fab has been renamed. Use horizontal="end" instead.', + startPosition: { + line: 2, + character: 30 + }, + endPosition: { + line: 2, + character: 33 + } + }, + { + message: 'The bottom attribute of ion-fab has been renamed. Use vertical="bottom" instead.', + startPosition: { + line: 2, + character: 34 + }, + endPosition: { + line: 2, + character: 40 + } + } + ]); + + const fixes = failures.map(f => f.getFix()); + const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify)); + + let expected = ` + @Component({ + template: \` + \` + }) + class Bar {} + `; + + expect(res).to.eq(expected); + }); + }); +}); diff --git a/test/testHelper.ts b/test/testHelper.ts index 8c71bef..47ee42d 100644 --- a/test/testHelper.ts +++ b/test/testHelper.ts @@ -244,7 +244,12 @@ export function assertFailure( * @param fails * @param options */ -export function assertFailures(ruleName: string, source: string | ts.SourceFile, fails: IExpectedFailure[], options = null) { +export function assertFailures( + ruleName: string, + source: string | ts.SourceFile, + fails: IExpectedFailure[], + options = null +): Lint.RuleFailure[] { let result; try { result = lint(ruleName, source, options); @@ -257,6 +262,7 @@ export function assertFailures(ruleName: string, source: string | ts.SourceFile, chai.assert.deepEqual(fails[index].startPosition, ruleFail.getStartPosition().getLineAndCharacter(), "start char doesn't match"); chai.assert.deepEqual(fails[index].endPosition, ruleFail.getEndPosition().getLineAndCharacter(), "end char doesn't match"); }); + return result.failures; } /**