Skip to content

Commit

Permalink
feat(rules): add ion-option-is-now-ion-select-option rule
Browse files Browse the repository at this point in the history
  • Loading branch information
imhoffd committed Jul 7, 2018
1 parent d3306e8 commit 6274f6a
Show file tree
Hide file tree
Showing 3 changed files with 256 additions and 0 deletions.
53 changes: 53 additions & 0 deletions src/helpers/elementRename.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
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';

function generateDescription(elementName: string, newElementName: string) {
return `The ${elementName} component is now named ${newElementName}.`;
}

export function createElementRenameTemplateVisitorClass(elementName: string, newElementName: string) {
return class extends BasicTemplateAstVisitor {
visitElement(element: ast.ElementAst, context: any): any {
if (element.name && element.name === elementName) {
const startTagStart = element.sourceSpan.start.offset;
const startTagLength = element.name.length;
const startTagPosition = this.getSourcePosition(startTagStart) + 1;
const endTagStart = element.endSourceSpan.start.offset;
const endTagLength = element.name.length;
const endTagPosition = this.getSourcePosition(endTagStart) + 2;

this.addFailureAt(startTagStart + 1, startTagLength, generateDescription(element.name, newElementName), [
Lint.Replacement.replaceFromTo(startTagPosition, startTagPosition + startTagLength, newElementName),
Lint.Replacement.replaceFromTo(endTagPosition, endTagPosition + endTagLength, newElementName)
]);
}

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

export function createElementRenameRuleClass(ruleName: string, elementName: string, newElementName: string) {
return class extends Lint.Rules.AbstractRule {
public static metadata: Lint.IRuleMetadata = {
ruleName: ruleName,
type: 'functionality',
description: generateDescription(elementName, newElementName),
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: createElementRenameTemplateVisitorClass(elementName, newElementName)
})
);
}
};
}
5 changes: 5 additions & 0 deletions src/ionOptionIsNowIonSelectOptionRule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { createElementRenameRuleClass } from './helpers/elementRename';

export const ruleName = 'ion-option-is-now-ion-select-option';

export const Rule = createElementRenameRuleClass(ruleName, 'ion-option', 'ion-select-option');
198 changes: 198 additions & 0 deletions test/ionOptionIsNowIonSelectOption.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import { assertSuccess, assertAnnotated, assertMultipleAnnotated, assertFailures } from './testHelper';
import { Replacement, Utils } from 'tslint';
import { expect } from 'chai';
import { ruleName } from '../src/ionOptionIsNowIonSelectOptionRule';

describe(ruleName, () => {
describe('success', () => {
it('should work with proper style', () => {
let source = `
@Component({
template: \`
<ion-select>
<ion-select-option>Option 1</ion-select-option>
<ion-select-option>Option 2</ion-select-option>
<ion-select-option>Option 3</ion-select-option>
</ion-select>
\`
})
class Bar {}
`;
assertSuccess(ruleName, source);
});
});

describe('failure', () => {
it('should fail when ion-option is used', () => {
let source = `
@Component({
template: \`
<ion-select>
<ion-option>Option 1</ion-option>
~~~~~~~~~~
<ion-option>Option 2</ion-option>
^^^^^^^^^^
<ion-option>Option 3</ion-option>
zzzzzzzzzz
</ion-select>
\`
})
class Bar {}
`;

assertMultipleAnnotated({
ruleName,
source,
failures: [
{
char: '~',
msg: 'The ion-option component is now named ion-select-option.'
},
{
char: '^',
msg: 'The ion-option component is now named ion-select-option.'
},
{
char: 'z',
msg: 'The ion-option component is now named ion-select-option.'
}
]
});
});
});

describe('replacements', () => {
it('should replace ion-option with ion-select-option', () => {
let source = `
@Component({
template: \`
<ion-select>
<ion-option>Option 1</ion-option>
<ion-option>Option 2</ion-option>
<ion-option>Option 3</ion-option>
</ion-select>
\`
})
class Bar {}
`;

const failures = assertFailures(ruleName, source, [
{
message: 'The ion-option component is now named ion-select-option.',
startPosition: {
line: 4,
character: 15
},
endPosition: {
line: 4,
character: 25
}
},
{
message: 'The ion-option component is now named ion-select-option.',
startPosition: {
line: 5,
character: 15
},
endPosition: {
line: 5,
character: 25
}
},
{
message: 'The ion-option component is now named ion-select-option.',
startPosition: {
line: 6,
character: 15
},
endPosition: {
line: 6,
character: 25
}
}
]);

const fixes = failures.map(f => f.getFix());
const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify));

let expected = `
@Component({
template: \`
<ion-select>
<ion-select-option>Option 1</ion-select-option>
<ion-select-option>Option 2</ion-select-option>
<ion-select-option>Option 3</ion-select-option>
</ion-select>
\`
})
class Bar {}
`;

expect(res).to.eq(expected);
});

it('should replace ion-option with ion-select-option on multiline', () => {
let source = `
@Component({
template: \`
<ion-select>
<ion-option value="f">
Female
</ion-option>
<ion-option value="m">
Male
</ion-option>
</ion-select>
\`
})
class Bar {}
`;

const failures = assertFailures(ruleName, source, [
{
message: 'The ion-option component is now named ion-select-option.',
startPosition: {
line: 4,
character: 15
},
endPosition: {
line: 4,
character: 25
}
},
{
message: 'The ion-option component is now named ion-select-option.',
startPosition: {
line: 7,
character: 15
},
endPosition: {
line: 7,
character: 25
}
}
]);

const fixes = failures.map(f => f.getFix());
const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify));

let expected = `
@Component({
template: \`
<ion-select>
<ion-select-option value="f">
Female
</ion-select-option>
<ion-select-option value="m">
Male
</ion-select-option>
</ion-select>
\`
})
class Bar {}
`;

expect(res).to.eq(expected);
});
});
});

0 comments on commit 6274f6a

Please sign in to comment.