From d9d7da425b0e92178914d5bde142ace43cbd95d5 Mon Sep 17 00:00:00 2001 From: mgechev Date: Sun, 11 Jun 2017 14:28:15 -0700 Subject: [PATCH] feat: angular specific whitespace rule 1. Implement whitespace check for interpolation (fix #320). --- src/angularWhitespaceRule.ts | 71 +++++++++++++++++++ test/angularWhitespaceRule.spec.ts | 105 +++++++++++++++++++++++++++++ 2 files changed, 176 insertions(+) create mode 100644 src/angularWhitespaceRule.ts create mode 100644 test/angularWhitespaceRule.spec.ts diff --git a/src/angularWhitespaceRule.ts b/src/angularWhitespaceRule.ts new file mode 100644 index 000000000..b5338eaf0 --- /dev/null +++ b/src/angularWhitespaceRule.ts @@ -0,0 +1,71 @@ +import * as Lint from 'tslint'; +import * as ts from 'typescript'; +import {stringDistance} from './util/utils'; +import {getDeclaredProperties, getDeclaredMethods} from './util/classDeclarationUtils'; +import {NgWalker} from './angular/ngWalker'; +import {RecursiveAngularExpressionVisitor} from './angular/templates/recursiveAngularExpressionVisitor'; +import * as e from '@angular/compiler/src/expression_parser/ast'; +import * as ast from '@angular/compiler'; +import { BasicTemplateAstVisitor } from './angular/templates/basicTemplateAstVisitor'; +import { ExpTypes } from './angular/expressionTypes'; +import { Config } from './angular/config'; +import SyntaxKind = require('./util/syntaxKind'); + +const InterpolationOpen = Config.interpolation[0]; +const InterpolationClose = Config.interpolation[1]; +const InterpolationNoWhitespaceRe = new RegExp(`${InterpolationOpen}\\S(.*?)\\S${InterpolationClose}`); +const InterpolationExtraWhitespaceRe = + new RegExp(`${InterpolationOpen}\\s\\s(.*?)\\s${InterpolationClose}|${InterpolationOpen}\\s(.*?)\\s\\s${InterpolationClose}`); + +const getReplacements = (text: ast.BoundTextAst, absolutePosition: number) => { + const expr: string = (text.value as any).source; + const trimmed = expr.substring(InterpolationOpen.length, expr.length - InterpolationClose.length).trim(); + return [ + new Lint.Replacement(absolutePosition + text.sourceSpan.start.offset, + expr.length, `${InterpolationOpen} ${trimmed} ${InterpolationClose}`) + ]; +}; + +class WhitespaceTemplateVisitor extends BasicTemplateAstVisitor { + visitBoundText(text: ast.BoundTextAst, context: any): any { + if (ExpTypes.ASTWithSource(text.value)) { + // Note that will not be reliable for different interpolation symbols + let error = null; + const expr: any = (text.value).source; + if (InterpolationNoWhitespaceRe.test(expr)) { + error = 'Missing whitespace in interpolation; expecting {{ expr }}'; + } + if (InterpolationExtraWhitespaceRe.test(expr)) { + error = 'Extra whitespace in interpolation; expecting {{ expr }}'; + } + if (error) { + const absolutePosition = this.getSourcePosition(text.value.span.start); + this.addFailure( + this.createFailure(text.sourceSpan.start.offset, + expr.length, error, getReplacements(text, absolutePosition))); + } + } + } +} + +export class Rule extends Lint.Rules.AbstractRule { + public static metadata: Lint.IRuleMetadata = { + ruleName: 'templates-use-public-rule', + type: 'functionality', + description: `Ensure that properties and methods accessed from the template are public.`, + rationale: `When Angular compiles the templates, it has to access these properties from outside the class.`, + 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: WhitespaceTemplateVisitor, + })); + } +} diff --git a/test/angularWhitespaceRule.spec.ts b/test/angularWhitespaceRule.spec.ts new file mode 100644 index 000000000..e4069ae62 --- /dev/null +++ b/test/angularWhitespaceRule.spec.ts @@ -0,0 +1,105 @@ +import { assertSuccess, assertAnnotated } from './testHelper'; +import { Replacement } from 'tslint'; +import { expect } from 'chai'; + +describe('angular-whitespace', () => { + describe('success', () => { + it('should work with proper style', () => { + let source = ` + @Component({ + template: \` +
{{ foo }}
+ \` + }) + class Bar {} + `; + assertSuccess('angular-whitespace', source); + }); + + it('should work with proper style and complex expressions', () => { + let source = ` + @Component({ + template: \` +
{{ foo + bar | pipe }}
+ \` + }) + class Bar {} + `; + assertSuccess('angular-whitespace', source); + }); + }); + + describe('failure', () => { + it('should not fail when no decorator is set', () => { + let source = ` + @Component({ + template: \` +
{{foo}}
+ ~~~~~~~ + \` + }) + class Bar {} + `; + assertAnnotated({ + ruleName: 'angular-whitespace', + message: 'Missing whitespace in interpolation; expecting {{ expr }}', + source + }); + }); + }); + + describe('replacements', () => { + it('fixes negated pipes', () => { + let source = ` + @Component({ + template: \` +
{{foo}}
+ ~~~~~~~ + \` + }) + class Bar {}`; + const failures = assertAnnotated({ + ruleName: 'angular-whitespace', + message: 'Missing whitespace in interpolation; expecting {{ expr }}', + source + }); + + const res = Replacement.applyAll(source, failures[0].getFix()); + expect(res).to.eq(` + @Component({ + template: \` +
{{ foo }}
+ ~~~~~~~ + \` + }) + class Bar {}`); + }); + + it('should remove extra spaces', () => { + let source = ` + @Component({ + template: \` +
{{ foo }}
+ ~~~~~~~~~~~~~ + \` + }) + class Bar {}`; + const failures = assertAnnotated({ + ruleName: 'angular-whitespace', + message: 'Extra whitespace in interpolation; expecting {{ expr }}', + source + }); + + const res = Replacement.applyAll(source, failures[0].getFix()); + expect(res).to.eq(` + @Component({ + template: \` +
{{ foo }}
+ ~~~~~~~~~~~~~ + \` + }) + class Bar {}`); + }); + }); +}); +