Skip to content

Commit

Permalink
feat(rules): add fixer for directive-is-now-an-element rules (#58)
Browse files Browse the repository at this point in the history
  • Loading branch information
j3gb3rt authored and imhoffd committed Oct 9, 2018
1 parent d5cc034 commit 126b4f3
Show file tree
Hide file tree
Showing 10 changed files with 260 additions and 18 deletions.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ We are looking for contributors to help build these rules out! See [`CONTRIBUTIN
</td>
</tr>
<tr>
<td></td>
<td>:wrench:</td>
<td>:white_check_mark:</td>
<td>
<code>ion-button-is-now-an-element</code>
Expand All @@ -235,7 +235,7 @@ We are looking for contributors to help build these rules out! See [`CONTRIBUTIN
<th rowspan="3">
<a href="https://github.com/ionic-team/ionic/blob/master/angular/BREAKING.md#fab">FAB</a>
</th>
<td></td>
<td>:wrench:</td>
<td>:white_check_mark:</td>
<td>
<code>ion-fab-button-is-now-an-element</code>
Expand Down Expand Up @@ -294,7 +294,7 @@ We are looking for contributors to help build these rules out! See [`CONTRIBUTIN
<th rowspan="3">
<a href="https://github.com/ionic-team/ionic/blob/master/angular/BREAKING.md#item">Item</a>
</th>
<td></td>
<td>:wrench:</td>
<td>:white_check_mark:</td>
<td>
<code>ion-item-is-now-an-element</code>
Expand Down Expand Up @@ -450,7 +450,7 @@ We are looking for contributors to help build these rules out! See [`CONTRIBUTIN
</td>
</tr>
<tr>
<td></td>
<td>:wrench:</td>
<td>:white_check_mark:</td>
<td>
<code>ion-radio-group-is-now-an-element</code>
Expand Down Expand Up @@ -512,7 +512,7 @@ We are looking for contributors to help build these rules out! See [`CONTRIBUTIN
<th>
<a href="https://github.com/ionic-team/ionic/blob/master/angular/BREAKING.md#text--typography">Text / Typography</a>
</th>
<td></td>
<td>:wrench:</td>
<td>:white_check_mark:</td>
<td>
<code>ion-text-is-now-an-element</code>
Expand Down
58 changes: 53 additions & 5 deletions src/helpers/directiveToElement.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,23 +7,71 @@ import * as ts from 'typescript';
export function generateDescription(directive: string, resultantElement: string) {
return `${directive} is now an ${resultantElement} element instead of an Angular directive.`;
}
export type ReplacementLevel = 'parent' | 'same' | 'child';

export function createDirectiveToElementTemplateVisitorClass(directive: string, resultantElement: string) {
export function createDirectiveToElementTemplateVisitorClass(directive: string, resultantElement: string, level: ReplacementLevel) {
return class extends BasicTemplateAstVisitor {
visitElement(element: ast.ElementAst, context: any): any {
const foundAttr = element.attrs.find(attr => attr.name === directive);

if (foundAttr) {
const start = foundAttr.sourceSpan.start.offset;
this.addFailureAt(start, directive.length, generateDescription(directive, resultantElement));
const attributeStart = foundAttr.sourceSpan.start.offset;
const attributeLength = directive.length;
const attributePosition = this.getSourcePosition(attributeStart);

const fixes = [Lint.Replacement.deleteFromTo(attributePosition - 1, attributePosition + attributeLength)];

switch (level) {
case 'parent':
const parentOpenTag = `<${resultantElement}>\n`;
const parentCloseTag = `\n</${resultantElement}>`;

const angleBracketStartPosition = this.getSourcePosition(element.sourceSpan.start.offset);
const closingAngleBracketEndPosition = this.getSourcePosition(element.endSourceSpan.end.offset);

fixes.push(
Lint.Replacement.replaceFromTo(angleBracketStartPosition, angleBracketStartPosition, parentOpenTag),
Lint.Replacement.replaceFromTo(closingAngleBracketEndPosition, closingAngleBracketEndPosition, parentCloseTag)
);
break;
case 'same':
const tagNameLength = element.name.length;
const tagNameStartPosition = this.getSourcePosition(element.sourceSpan.start.offset) + 1;
const closingTagNameStartPosition = this.getSourcePosition(element.endSourceSpan.start.offset) + 2;

fixes.push(
Lint.Replacement.replaceFromTo(tagNameStartPosition, tagNameStartPosition + tagNameLength, resultantElement),
Lint.Replacement.replaceFromTo(closingTagNameStartPosition, closingTagNameStartPosition + tagNameLength, resultantElement)
);
break;
case 'child':
const childOpenTag = `>\n<${resultantElement}>`;
const childCloseTag = `</${resultantElement}>\n<`;

const angleBracketEndPosition = this.getSourcePosition(element.sourceSpan.end.offset);
const closingAngleBracketStartPosition = this.getSourcePosition(element.endSourceSpan.start.offset);

fixes.push(
Lint.Replacement.replaceFromTo(angleBracketEndPosition - 1, angleBracketEndPosition, childOpenTag),
Lint.Replacement.replaceFromTo(closingAngleBracketStartPosition, closingAngleBracketStartPosition + 1, childCloseTag)
);
break;
}

this.addFailureAt(attributeStart, attributeLength, generateDescription(directive, resultantElement), fixes);
}

super.visitElement(element, context);
}
};
}

export function createDirectiveToElementRuleClass(ruleName: string, directive: string, resultantElement = directive) {
export function createDirectiveToElementRuleClass(
ruleName: string,
directive: string,
resultantElement = directive,
level: ReplacementLevel = 'same'
) {
return class extends Lint.Rules.AbstractRule {
public static metadata: Lint.IRuleMetadata = {
ruleName: ruleName,
Expand All @@ -38,7 +86,7 @@ export function createDirectiveToElementRuleClass(ruleName: string, directive: s
public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
return this.applyWithWalker(
new NgWalker(sourceFile, this.getOptions(), {
templateVisitorCtrl: createDirectiveToElementTemplateVisitorClass(directive, resultantElement)
templateVisitorCtrl: createDirectiveToElementTemplateVisitorClass(directive, resultantElement, level)
})
);
}
Expand Down
2 changes: 1 addition & 1 deletion src/ionMenuToggleIsNowAnElementRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ import { createDirectiveToElementRuleClass } from './helpers/directiveToElement'
const directive = 'menuToggle';

export const ruleName = 'ion-menu-toggle-is-now-an-element';
export const Rule = createDirectiveToElementRuleClass(ruleName, directive, 'ion-menu-toggle');
export const Rule = createDirectiveToElementRuleClass(ruleName, directive, 'ion-menu-toggle', 'parent');
2 changes: 1 addition & 1 deletion src/ionRadioGroupIsNowAnElementRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ import { createDirectiveToElementRuleClass } from './helpers/directiveToElement'
const directive = 'radio-group';

export const ruleName = 'ion-radio-group-is-now-an-element';
export const Rule = createDirectiveToElementRuleClass(ruleName, directive, 'ion-radio-group');
export const Rule = createDirectiveToElementRuleClass(ruleName, directive, 'ion-radio-group', 'child');
2 changes: 1 addition & 1 deletion src/ionTextIsNowAnElementRule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,4 @@ import { createDirectiveToElementRuleClass } from './helpers/directiveToElement'
const directive = 'ion-text';

export const ruleName = 'ion-text-is-now-an-element';
export const Rule = createDirectiveToElementRuleClass(ruleName, directive);
export const Rule = createDirectiveToElementRuleClass(ruleName, directive, undefined, 'parent');
40 changes: 39 additions & 1 deletion test/ionButtonIsNowAnElement.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { expect } from 'chai';
import { ruleName } from '../src/ionButtonIsNowAnElementRule';
import { assertAnnotated, assertSuccess } from './testHelper';
import { assertAnnotated, assertSuccess, assertFailure } from './testHelper';
import { Utils, Replacement } from 'tslint';

describe(ruleName, () => {
describe('success', () => {
Expand Down Expand Up @@ -68,4 +70,40 @@ describe(ruleName, () => {
});
});
});

describe('replacements', () => {
it('should replace button with ion-button and remove attribute', () => {
let source = `
@Component({
template: \`<button ion-button></button>
\`
})
class Bar {}
`;
const fail = {
message: 'ion-button is now an ion-button element instead of an Angular directive.',
startPosition: {
line: 2,
character: 29
},
endPosition: {
line: 2,
character: 39
}
};

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: \`<ion-button></ion-button>
\`
})
class Bar {}
`;
expect(res).to.eq(expected);
});
});
});
40 changes: 39 additions & 1 deletion test/ionFabButtonIsNowAnElement.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { expect } from 'chai';
import { ruleName } from '../src/ionFabButtonIsNowAnElementRule';
import { assertAnnotated, assertSuccess } from './testHelper';
import { assertAnnotated, assertSuccess, assertFailure } from './testHelper';
import { Replacement, Utils } from 'tslint';

describe(ruleName, () => {
describe('success', () => {
Expand Down Expand Up @@ -86,4 +88,40 @@ describe(ruleName, () => {
});
});
});

describe('replacements', () => {
it('should replace button with ion-fab-button and remove attribute', () => {
let source = `
@Component({
template: \`<button ion-fab></button>
\`
})
class Bar {}
`;
const fail = {
message: 'ion-fab is now an ion-fab-button element instead of an Angular directive.',
startPosition: {
line: 2,
character: 29
},
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: \`<ion-fab-button></ion-fab-button>
\`
})
class Bar {}
`;
expect(res).to.eq(expected);
});
});
});
40 changes: 39 additions & 1 deletion test/ionItemIsNowAnElement.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { expect } from 'chai';
import { ruleName } from '../src/ionItemIsNowAnElementRule';
import { assertAnnotated, assertSuccess } from './testHelper';
import { assertAnnotated, assertSuccess, assertFailure } from './testHelper';
import { Utils, Replacement } from 'tslint';

describe(ruleName, () => {
describe('success', () => {
Expand Down Expand Up @@ -68,4 +70,40 @@ describe(ruleName, () => {
});
});
});

describe('replacements', () => {
it('should replace button with ion-item and remove attribute', () => {
let source = `
@Component({
template: \`<button ion-item></button>
\`
})
class Bar {}
`;
const fail = {
message: 'ion-item is now an ion-item element instead of an Angular directive.',
startPosition: {
line: 2,
character: 29
},
endPosition: {
line: 2,
character: 37
}
};

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: \`<ion-item></ion-item>
\`
})
class Bar {}
`;
expect(res).to.eq(expected);
});
});
});
42 changes: 41 additions & 1 deletion test/ionRadioGroupIsNowAnElement.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { expect } from 'chai';
import { ruleName } from '../src/ionRadioGroupIsNowAnElementRule';
import { assertAnnotated, assertSuccess } from './testHelper';
import { assertAnnotated, assertSuccess, assertFailure } from './testHelper';
import { Replacement, Utils } from 'tslint';

describe(ruleName, () => {
describe('success', () => {
Expand Down Expand Up @@ -47,4 +49,42 @@ describe(ruleName, () => {
});
});
});

describe('replacements', () => {
it('should create child ion-radio-group element and remove radio-group attribute', () => {
let source = `
@Component({
template: \`<ion-list radio-group></ion-list>
\`
})
class Bar {}
`;
const fail = {
message: 'radio-group is now an ion-radio-group element instead of an Angular directive.',
startPosition: {
line: 2,
character: 31
},
endPosition: {
line: 2,
character: 42
}
};

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: \`<ion-list>
<ion-radio-group></ion-radio-group>
</ion-list>
\`
})
class Bar {}
`;
expect(res).to.eq(expected);
});
});
});
Loading

0 comments on commit 126b4f3

Please sign in to comment.