Skip to content

Commit

Permalink
feat: angular specific whitespace rule
Browse files Browse the repository at this point in the history
1. Implement whitespace check for interpolation (fix #320).
  • Loading branch information
mgechev committed Jun 16, 2017
1 parent f95b2d5 commit 335776f
Show file tree
Hide file tree
Showing 2 changed files with 176 additions and 0 deletions.
71 changes: 71 additions & 0 deletions 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 = (<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,
}));
}
}
105 changes: 105 additions & 0 deletions 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: \`
<div>{{ foo }}</div>
\`
})
class Bar {}
`;
assertSuccess('angular-whitespace', source);
});

it('should work with proper style and complex expressions', () => {
let source = `
@Component({
template: \`
<div>{{ foo + bar | pipe }}</div>
\`
})
class Bar {}
`;
assertSuccess('angular-whitespace', source);
});
});

describe('failure', () => {
it('should not fail when no decorator is set', () => {
let source = `
@Component({
template: \`
<div>{{foo}}</div>
~~~~~~~
\`
})
class Bar {}
`;
assertAnnotated({
ruleName: 'angular-whitespace',
message: 'Missing whitespace in interpolation; expecting {{ expr }}',
source
});
});
});

describe('replacements', () => {
it('fixes negated pipes', () => {
let source = `
@Component({
template: \`
<div>{{foo}}</div>
~~~~~~~
\`
})
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: \`
<div>{{ foo }}</div>
~~~~~~~
\`
})
class Bar {}`);
});

it('should remove extra spaces', () => {
let source = `
@Component({
template: \`
<div>{{ foo }}</div>
~~~~~~~~~~~~~
\`
})
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: \`
<div>{{ foo }}</div>
~~~~~~~~~~~~~
\`
})
class Bar {}`);
});
});
});

0 comments on commit 335776f

Please sign in to comment.