diff --git a/src/bananaInBoxRule.ts b/src/bananaInBoxRule.ts new file mode 100644 index 000000000..1588cdad1 --- /dev/null +++ b/src/bananaInBoxRule.ts @@ -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 = (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, + })); + } +} diff --git a/src/index.ts b/src/index.ts index 12c2ccf67..4b8a53402 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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'; diff --git a/test/bananaInBoxRule.spec.ts b/test/bananaInBoxRule.spec.ts new file mode 100644 index 000000000..6a0900b9f --- /dev/null +++ b/test/bananaInBoxRule.spec.ts @@ -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: \` \` + }) + class Bar {} + `; + assertSuccess('banana-in-box', source); + }); + + }); + + describe('failure', () => { + it('should fail when the box is in the banana', () => { + let source = ` + @Component({ + template: \` + ~~~~~~~~~~~~~~~~~ + \` + }) + 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: \` + ~~~~~~~~~~~~~~~~~ + \` + }) + 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: \` + ~~~~~~~~~~~~~~~~~ + \` + }) + class Bar {} + `); + }); + + }); +});