diff --git a/src/helpers/directiveToClass.ts b/src/helpers/directiveToClass.ts new file mode 100644 index 0000000..4d9476c --- /dev/null +++ b/src/helpers/directiveToClass.ts @@ -0,0 +1,99 @@ +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 * as ts from 'typescript'; + +export function generateDescription(directiveCategory?: string, resultantClass?: string) { + return `All directives of ${directiveCategory} category are now replaces with respective classes ${resultantClass}`; +} +export type ReplacementLevel = 'parent' | 'same' | 'child'; + +export function createDirectiveToClassTemplateVisitorClass( + transformations: Map, + directiveCategory: string, + className: string +) { + return class extends BasicTemplateAstVisitor { + visitElement(element: ast.ElementAst, context: any): any { + let errors = []; + for (let [directive, resultantClass] of transformations) { + const foundAttr = element.attrs.find(attr => attr.name === directive); + + if (foundAttr) { + errors.push({ + attr: foundAttr, + resultantClass: resultantClass + }); + } + } + if (errors.length) { + let fixes = []; + let classStartPosition, + classEndPosition, + existingClasses = '', + newClasses = ''; + let elementStart = element.sourceSpan.start.offset; + let elementEnd = element.sourceSpan.end.offset; + + for (let item of errors) { + const attributePosition: number = this.getSourcePosition(item.attr.sourceSpan.start.offset); + const attributeEndPosition = this.getSourcePosition(item.attr.sourceSpan.end.offset); + fixes.push(Lint.Replacement.deleteFromTo(attributePosition - 1, attributeEndPosition)); + newClasses += item.resultantClass + ' '; + } + const foundClassAttr = element.attrs.find(attr => attr.name === 'class'); + + if (foundClassAttr) { + classStartPosition = this.getSourcePosition(foundClassAttr.sourceSpan.start.offset); + classEndPosition = this.getSourcePosition(foundClassAttr.sourceSpan.end.offset); + existingClasses = foundClassAttr.value; + if (!existingClasses.length) newClasses = newClasses.slice(0, -1); + fixes.push(Lint.Replacement.replaceFromTo(classStartPosition, classEndPosition, `class="${newClasses}${existingClasses}"`)); + } else { + if (element.attrs.length) { + const lastAttribute = element.attrs[element.attrs.length - 1]; + classStartPosition = this.getSourcePosition(lastAttribute.sourceSpan.end.offset); + } else { + classStartPosition = this.getSourcePosition(element.sourceSpan.end.offset) - 1; + } + + classEndPosition = classStartPosition; + if (!existingClasses.length) newClasses = newClasses.slice(0, -1); + fixes.push(Lint.Replacement.replaceFromTo(classStartPosition, classEndPosition, ` class="${newClasses}${existingClasses}"`)); + } + + this.addFailureAt(elementStart, elementEnd, generateDescription(directiveCategory, className), fixes); + } + + super.visitElement(element, context); + } + }; +} + +export function createDirectiveToClassRuleClass( + ruleName: string, + transformations: Map, + directiveCategory: string, + className: string +) { + return class extends Lint.Rules.AbstractRule { + public static metadata: Lint.IRuleMetadata = { + ruleName: ruleName, + type: 'functionality', + description: generateDescription(directiveCategory, className), + 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: createDirectiveToClassTemplateVisitorClass(transformations, directiveCategory, className) + }) + ); + } + }; +} diff --git a/src/ionActionSheetMethodCreateParametersRenamedRule.ts b/src/ionActionSheetMethodCreateParametersRenamedRule.ts index e4c16a2..5eaae58 100644 --- a/src/ionActionSheetMethodCreateParametersRenamedRule.ts +++ b/src/ionActionSheetMethodCreateParametersRenamedRule.ts @@ -5,7 +5,10 @@ import { createParametersRenamedClass } from './helpers/parametersRenamed'; export const ruleName = 'ion-action-sheet-method-create-parameters-renamed'; -const parameterMap = new Map([['title', 'header'], ['subTitle', 'subHeader']]); +const parameterMap = new Map([ + ['title', 'header'], + ['subTitle', 'subHeader'] +]); const Walker = createParametersRenamedClass('create', 'ActionSheetController', parameterMap); export class Rule extends Lint.Rules.AbstractRule { diff --git a/src/ionAlertMethodCreateParametersRenamedRule.ts b/src/ionAlertMethodCreateParametersRenamedRule.ts index 5213009..34b8dc8 100644 --- a/src/ionAlertMethodCreateParametersRenamedRule.ts +++ b/src/ionAlertMethodCreateParametersRenamedRule.ts @@ -5,7 +5,10 @@ import { createParametersRenamedClass } from './helpers/parametersRenamed'; export const ruleName = 'ion-alert-method-create-parameters-renamed'; -const parameterMap = new Map([['title', 'header'], ['subTitle', 'subHeader']]); +const parameterMap = new Map([ + ['title', 'header'], + ['subTitle', 'subHeader'] +]); const Walker = createParametersRenamedClass('create', 'AlertController', parameterMap); export class Rule extends Lint.Rules.AbstractRule { diff --git a/src/ionColAttributesRenamedRule.ts b/src/ionColAttributesRenamedRule.ts index 76e3dad..e70cca8 100644 --- a/src/ionColAttributesRenamedRule.ts +++ b/src/ionColAttributesRenamedRule.ts @@ -11,7 +11,12 @@ const formatOldAttr = (prefix: string, breakpoint: string | undefined, value: st const formatNewAttr = (prefix: string, breakpoint: string | undefined, value: string) => `${prefix}${typeof breakpoint === 'undefined' ? '' : `-${breakpoint}`}="${value}"`; -const attrPrefixMap = new Map([['col', 'size'], ['offset', 'offset'], ['push', 'push'], ['pull', 'pull']]); +const attrPrefixMap = new Map([ + ['col', 'size'], + ['offset', 'offset'], + ['push', 'push'], + ['pull', 'pull'] +]); const breakpoints = [undefined, 'xs', 'sm', 'md', 'lg', 'xl']; const values = ['auto', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12']; diff --git a/src/ionItemOptionsAttributeValuesRenamedRule.ts b/src/ionItemOptionsAttributeValuesRenamedRule.ts index a1f9493..66de111 100644 --- a/src/ionItemOptionsAttributeValuesRenamedRule.ts +++ b/src/ionItemOptionsAttributeValuesRenamedRule.ts @@ -6,7 +6,15 @@ import { createAttributeValuesRenamedTemplateVisitorClass } from './helpers/attr export const ruleName = 'ion-item-options-attribute-values-renamed'; -const replacementMap = new Map([['side', new Map([['left', 'start'], ['right', 'end']])]]); +const replacementMap = new Map([ + [ + 'side', + new Map([ + ['left', 'start'], + ['right', 'end'] + ]) + ] +]); const TemplateVisitor = createAttributeValuesRenamedTemplateVisitorClass(['ion-item-options'], replacementMap); diff --git a/src/ionLabelAttributesRenamedRule.ts b/src/ionLabelAttributesRenamedRule.ts index cc377f9..cd1ae80 100644 --- a/src/ionLabelAttributesRenamedRule.ts +++ b/src/ionLabelAttributesRenamedRule.ts @@ -6,7 +6,11 @@ import { createAttributesRenamedTemplateVisitorClass } from './helpers/attribute export const ruleName = 'ion-label-attributes-renamed'; -const replacementMap = new Map([['fixed', 'position="fixed"'], ['floating', 'position="floating"'], ['stacked', 'position="stacked"']]); +const replacementMap = new Map([ + ['fixed', 'position="fixed"'], + ['floating', 'position="floating"'], + ['stacked', 'position="stacked"'] +]); const IonButtonAttributesAreRenamedTemplateVisitor = createAttributesRenamedTemplateVisitorClass(['ion-label'], replacementMap); diff --git a/src/ionMarginUtilIsNowAClassRule.ts b/src/ionMarginUtilIsNowAClassRule.ts new file mode 100644 index 0000000..b5fc9bb --- /dev/null +++ b/src/ionMarginUtilIsNowAClassRule.ts @@ -0,0 +1,16 @@ +import { createDirectiveToClassRuleClass } from './helpers/directiveToClass'; + +const transformations = new Map([ + ['margin', 'ion-margin'], + ['no-margin', 'ion-no-margin'], + ['margin-bottom', 'ion-margin-bottom'], + ['margin-top', 'ion-margin-top'], + ['margin-left', 'ion-margin-start'], + ['margin-right', 'ion-margin-end'], + ['margin-horizontal', 'ion-margin-horizontal'], + ['margin-vertical', 'ion-margin-vertical'] +]); +const directiveCategory = 'margin'; +const className = 'ion-margin'; +export const ruleName = 'ion-margin-util-is-now-a-class'; +export const Rule = createDirectiveToClassRuleClass(ruleName, transformations, directiveCategory, className); diff --git a/src/ionMenuEventsRenamedRule.ts b/src/ionMenuEventsRenamedRule.ts index 2dd51b1..5d933b8 100644 --- a/src/ionMenuEventsRenamedRule.ts +++ b/src/ionMenuEventsRenamedRule.ts @@ -6,7 +6,10 @@ import { createAttributesRenamedTemplateVisitorClass } from './helpers/attribute export const ruleName = 'ion-menu-events-renamed'; -const replacementMap = new Map([['ionOpen', 'ionDidOpen'], ['ionClose', 'ionDidClose']]); +const replacementMap = new Map([ + ['ionOpen', 'ionDidOpen'], + ['ionClose', 'ionDidClose'] +]); const TemplateVisitor = createAttributesRenamedTemplateVisitorClass(undefined, replacementMap); diff --git a/src/ionPaddingUtilIsNowAClassRule.ts b/src/ionPaddingUtilIsNowAClassRule.ts new file mode 100644 index 0000000..68551c0 --- /dev/null +++ b/src/ionPaddingUtilIsNowAClassRule.ts @@ -0,0 +1,17 @@ +import { createDirectiveToClassRuleClass } from './helpers/directiveToClass'; + +const transformations = new Map([ + ['padding', 'ion-padding'], + ['no-padding', 'ion-no-padding'], + ['padding-bottom', 'ion-padding-bottom'], + ['padding-top', 'ion-padding-top'], + ['padding-left', 'ion-padding-start'], + ['padding-right', 'ion-padding-end'], + ['padding-horizontal', 'ion-padding-horizontal'], + ['padding-vertical', 'ion-padding-vertical'] +]); + +const directiveCategory = 'padding'; +const className = 'ion-padding'; +export const ruleName = 'ion-padding-util-is-now-a-class'; +export const Rule = createDirectiveToClassRuleClass(ruleName, transformations, directiveCategory, className); diff --git a/src/ionRadioAttributesRenamedRule.ts b/src/ionRadioAttributesRenamedRule.ts index 1fcf060..dd8b53f 100644 --- a/src/ionRadioAttributesRenamedRule.ts +++ b/src/ionRadioAttributesRenamedRule.ts @@ -6,7 +6,10 @@ import { createAttributesRenamedTemplateVisitorClass } from './helpers/attribute export const ruleName = 'ion-radio-attributes-renamed'; -const replacementMap = new Map([['item-left', 'slot="start"'], ['item-right', 'slot="end"']]); +const replacementMap = new Map([ + ['item-left', 'slot="start"'], + ['item-right', 'slot="end"'] +]); const IonFabAttributesRenamedTemplateVisitor = createAttributesRenamedTemplateVisitorClass(['ion-radio'], replacementMap); diff --git a/src/ionSpinnerAttributeValuesRenamedRule.ts b/src/ionSpinnerAttributeValuesRenamedRule.ts index 21e2800..0d1d8e2 100644 --- a/src/ionSpinnerAttributeValuesRenamedRule.ts +++ b/src/ionSpinnerAttributeValuesRenamedRule.ts @@ -6,7 +6,15 @@ import { createAttributeValuesRenamedTemplateVisitorClass } from './helpers/attr export const ruleName = 'ion-spinner-attribute-values-renamed'; -const replacementMap = new Map([['name', new Map([['ios', 'lines'], ['ios-small', 'lines-small']])]]); +const replacementMap = new Map([ + [ + 'name', + new Map([ + ['ios', 'lines'], + ['ios-small', 'lines-small'] + ]) + ] +]); const affectedElements = ['ion-spinner', 'ion-loading', 'ion-infinite-scroll', 'ion-refresher']; const TemplateVisitor = createAttributeValuesRenamedTemplateVisitorClass(affectedElements, replacementMap); diff --git a/src/ionTextUtilIsNowAClassRule.ts b/src/ionTextUtilIsNowAClassRule.ts new file mode 100644 index 0000000..4c1709d --- /dev/null +++ b/src/ionTextUtilIsNowAClassRule.ts @@ -0,0 +1,16 @@ +import { createDirectiveToClassRuleClass } from './helpers/directiveToClass'; + +const transformations = new Map([ + ['text-left', 'ion-text-left'], + ['text-right', 'ion-text-right'], + ['text-start', 'ion-text-start'], + ['text-end', 'ion-text-end'], + ['text-center', 'ion-text-center'], + ['text-justify', 'ion-text-justify'], + ['text-wrap', 'ion-text-wrap'], + ['text-nowrap', 'ion-text-nowrap'] +]); +const directiveCategory = 'text'; +const className = 'ion-text'; +export const ruleName = 'ion-text-util-is-now-a-class'; +export const Rule = createDirectiveToClassRuleClass(ruleName, transformations, directiveCategory, className); diff --git a/test/testHelper.ts b/test/testHelper.ts index 47ee42d..ff81ef4 100644 --- a/test/testHelper.ts +++ b/test/testHelper.ts @@ -56,7 +56,10 @@ function lint(ruleName: string, source: string | ts.SourceFile, options: any): t linter.lint('file.ts', source, configuration); } else { const rules = loadRules(convertRuleOptions(configuration.rules), linterOptions.rulesDirectory, false); - const res = [].concat.apply([], rules.map(r => r.apply(source))) as tslint.RuleFailure[]; + const res = [].concat.apply( + [], + rules.map(r => r.apply(source)) + ) as tslint.RuleFailure[]; const errCount = res.filter(r => !r.getRuleSeverity || r.getRuleSeverity() === 'error').length; return { errorCount: errCount,