Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add "banana in a box" rule #322

Merged
merged 4 commits into from
Jun 15, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions src/bananaInBoxRule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import * as Lint from 'tslint';
import * as ts from 'typescript';
import * as ast from '@angular/compiler';
import {BasicTemplateAstVisitor} from './angular/templates/basicTemplateAstVisitor';
import {NgWalker} from './angular/ngWalker';


const InvalidSyntaxBoxOpen = '([';
const InvalidSyntaxBoxClose = '])';
const ValidSyntaxOpen = '[(';
const ValidSyntaxClose = ')]';
const InvalidSyntaxBoxRe = new RegExp('\\(\\[(.*?)\\]\\)(.*?)');

const getReplacements = (text: ast.BoundEventAst, absolutePosition: number) => {
const expr: string = (text.sourceSpan as any).toString();
const internalStart = expr.indexOf(InvalidSyntaxBoxOpen);
const internalEnd = expr.lastIndexOf(InvalidSyntaxBoxClose);
const len = internalEnd - internalStart - InvalidSyntaxBoxClose.length;
const trimmed = expr.substr(internalStart + InvalidSyntaxBoxOpen.length, len).trim();

return [
new Lint.Replacement(absolutePosition,
internalEnd - internalStart + ValidSyntaxClose.length,
`${ValidSyntaxOpen}${trimmed}${ValidSyntaxClose}`)
];
};

class BananaInBoxTemplateVisitor extends BasicTemplateAstVisitor {

visitEvent(prop: ast.BoundEventAst, context: any): any {

if (prop.sourceSpan) {
// Note that will not be reliable for different interpolation symbols
let error = null;
const expr: any = (<any>prop.sourceSpan).toString();
if (InvalidSyntaxBoxRe.test(expr)) {
error = 'Invalid binding syntax. Use [(expr)] instead';
}

if (error) {
const internalStart = expr.indexOf(InvalidSyntaxBoxOpen) + 1;
const start = prop.sourceSpan.start.offset + internalStart;
const absolutePosition = this.getSourcePosition(start-1);

this.addFailure(this.createFailure(start, expr.trim().length,
error, getReplacements(prop, absolutePosition))
);
}
}
}
}



export class Rule extends Lint.Rules.AbstractRule {
public static metadata: Lint.IRuleMetadata = {
ruleName: 'banana-in-box',
type: 'functionality',
description: `Ensure that the two-way data binding syntax is correct.`,
rationale: `The parens "()" should have been inside the brackets "[]".`,
options: null,
optionsDescription: `Not configurable.`,
typescriptOnly: true,
};

static FAILURE: string = 'The %s "%s" that you\'re trying to access does not exist in the class declaration.';

public apply(sourceFile:ts.SourceFile): Lint.RuleFailure[] {

return this.applyWithWalker(
new NgWalker(sourceFile,
this.getOptions(), {
templateVisitorCtrl: BananaInBoxTemplateVisitor,
}));
}
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@ export { Rule as UsePipeTransformInterfaceRule } from './usePipeTransformInterfa
export { Rule as TemplateToNgTemplateRule } from './templateToNgTemplateRule';
export { Rule as UsePipeDecoratorRule } from './usePipeDecoratorRule';
export { Rule as UseViewEncapsulationRule } from './useViewEncapsulationRule';
export { Rule as BananaInBoxRule } from './bananaInBoxRule';
export * from './angular/config';

65 changes: 65 additions & 0 deletions test/bananaInBoxRule.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { assertSuccess, assertAnnotated } from './testHelper';
import { Replacement } from 'tslint';
import { expect } from 'chai';

describe('banana-in-box', () => {
describe('success', () => {
it('should work with proper style', () => {
let source = `
@Component({
template: \` <input type="text" [(ngModel)]="foo" name="foo"> \`
})
class Bar {}
`;
assertSuccess('banana-in-box', source);
});

});

describe('failure', () => {
it('should fail when the box is in the banana', () => {
let source = `
@Component({
template: \` <input type="text" ([ngModel])="foo" name="foo">
~~~~~~~~~~~~~~~~~
\`
})
class Bar {}
`;
assertAnnotated({
ruleName: 'banana-in-box',
message: 'Invalid binding syntax. Use [(expr)] instead',
source
});
});
});

describe('replacements', () => {
it('should fail when the box is in the banana', () => {
let source = `
@Component({
template: \` <input type="text" ([ngModel])="foo" name="foo">
~~~~~~~~~~~~~~~~~
\`
})
class Bar {}
`;
const failures = assertAnnotated({
ruleName: 'banana-in-box',
message: 'Invalid binding syntax. Use [(expr)] instead',
source
});

const res = Replacement.applyAll(source, failures[0].getFix());
expect(res).to.eq(`
@Component({
template: \` <input type="text" [(ngModel)]="foo" name="foo">
~~~~~~~~~~~~~~~~~
\`
})
class Bar {}
`);
});

});
});