Skip to content

Commit 25667f9

Browse files
authored
Merge pull request #356 from wKoza/whitespaceAfterSemiColon
Ensure whitespaces after semicolon in structural dir
2 parents 2f52d77 + 3abd245 commit 25667f9

File tree

3 files changed

+368
-156
lines changed

3 files changed

+368
-156
lines changed

src/angular/ngWalker.ts

-6
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,6 @@ export class NgWalker extends Lint.RuleWalker {
4747
cssVisitorCtrl: BasicCssAstVisitor
4848
}, this._config || {});
4949

50-
this._config = Object.assign({
51-
templateVisitorCtrl: BasicTemplateAstVisitor,
52-
expressionVisitorCtrl: RecursiveAngularExpressionVisitor,
53-
cssVisitorCtrl: BasicCssAstVisitor
54-
}, this._config || {});
55-
// this._config = ngWalkerFactoryUtils.normalizeConfig(this._config);
5650
}
5751

5852
visitClassDeclaration(declaration: ts.ClassDeclaration) {

src/angularWhitespaceRule.ts

+88-17
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,20 @@ import * as Lint from 'tslint';
22
import * as ts from 'typescript';
33
import {NgWalker} from './angular/ngWalker';
44
import * as ast from '@angular/compiler';
5-
import { BasicTemplateAstVisitor } from './angular/templates/basicTemplateAstVisitor';
6-
import { ExpTypes } from './angular/expressionTypes';
7-
import { Config } from './angular/config';
8-
import { RecursiveAngularExpressionVisitor } from './angular/templates/recursiveAngularExpressionVisitor';
5+
import {BasicTemplateAstVisitor} from './angular/templates/basicTemplateAstVisitor';
6+
import {ExpTypes} from './angular/expressionTypes';
7+
import {Config} from './angular/config';
8+
import {RecursiveAngularExpressionVisitor} from './angular/templates/recursiveAngularExpressionVisitor';
99

1010
const InterpolationOpen = Config.interpolation[0];
1111
const InterpolationClose = Config.interpolation[1];
1212
const InterpolationNoWhitespaceRe = new RegExp(`${InterpolationOpen}\\S(.*?)\\S${InterpolationClose}|${InterpolationOpen}` +
1313
`\\s(.*?)\\S${InterpolationClose}|${InterpolationOpen}\\S(.*?)\\s${InterpolationClose}`);
1414
const InterpolationExtraWhitespaceRe =
1515
new RegExp(`${InterpolationOpen}\\s\\s(.*?)\\s${InterpolationClose}|${InterpolationOpen}\\s(.*?)\\s\\s${InterpolationClose}`);
16+
const SemicolonNoWhitespaceNotInSimpleQuoteRe = new RegExp(/;\S(?![^']*')/);
17+
const SemicolonNoWhitespaceNotInDoubleQuoteRe = new RegExp(/;\S(?![^"]*")/);
18+
1619

1720
const getReplacements = (text: ast.BoundTextAst, absolutePosition: number) => {
1821
const expr: string = (text.value as any).source;
@@ -27,7 +30,16 @@ const getReplacements = (text: ast.BoundTextAst, absolutePosition: number) => {
2730
];
2831
};
2932

30-
type Option = 'check-interpolation' | 'check-pipe';
33+
34+
const getSemicolonReplacements = (text: ast.BoundDirectivePropertyAst, absolutePosition: number) => {
35+
36+
return [
37+
new Lint.Replacement(absolutePosition, 1, '; ')
38+
];
39+
40+
};
41+
42+
type Option = 'check-interpolation' | 'check-pipe' | 'check-semicolon';
3143

3244
interface ConfigurableVisitor {
3345
getOption(): Option;
@@ -66,9 +78,52 @@ class InterpolationWhitespaceVisitor extends BasicTemplateAstVisitor implements
6678
}
6779
}
6880

81+
class SemicolonTemplateVisitor extends BasicTemplateAstVisitor implements ConfigurableVisitor {
82+
83+
visitDirectiveProperty(prop: ast.BoundDirectivePropertyAst, context: BasicTemplateAstVisitor): any {
84+
85+
86+
if (prop.sourceSpan) {
87+
const directive = (<any>prop.sourceSpan).toString();
88+
const rawExpression = directive.split('=')[1].trim();
89+
const expr = rawExpression.substring(1, rawExpression.length - 1).trim();
90+
91+
const doubleQuote = rawExpression.substring(0, 1).indexOf('\"') === 0;
92+
93+
// Note that will not be reliable for different interpolation symbols
94+
let error = null;
95+
96+
if (doubleQuote && SemicolonNoWhitespaceNotInSimpleQuoteRe.test(expr)) {
97+
error = 'Missing whitespace after semicolon; expecting \'; expr\'';
98+
const internalStart = expr.search(SemicolonNoWhitespaceNotInSimpleQuoteRe) + 1;
99+
const start = prop.sourceSpan.start.offset + internalStart + directive.length - directive.split('=')[1].trim().length + 1;
100+
const absolutePosition = context.getSourcePosition(start - 1);
101+
return context.addFailure(context.createFailure(start, 2,
102+
error, getSemicolonReplacements(prop, absolutePosition))
103+
);
104+
} else if (!doubleQuote && SemicolonNoWhitespaceNotInDoubleQuoteRe.test(expr)) {
105+
error = 'Missing whitespace after semicolon; expecting \'; expr\'';
106+
const internalStart = expr.search(SemicolonNoWhitespaceNotInDoubleQuoteRe) + 1;
107+
const start = prop.sourceSpan.start.offset + internalStart + directive.length - directive.split('=')[1].trim().length + 1;
108+
const absolutePosition = context.getSourcePosition(start - 1);
109+
return context.addFailure(context.createFailure(start, 2,
110+
error, getSemicolonReplacements(prop, absolutePosition))
111+
);
112+
}
113+
}
114+
}
115+
116+
getOption(): Option {
117+
return 'check-semicolon';
118+
}
119+
120+
}
121+
122+
69123
class WhitespaceTemplateVisitor extends BasicTemplateAstVisitor {
70124
private visitors: (BasicTemplateAstVisitor & ConfigurableVisitor)[] = [
71-
new InterpolationWhitespaceVisitor(this.getSourceFile(), this.getOptions(), this.context, this.templateStart)
125+
new InterpolationWhitespaceVisitor(this.getSourceFile(), this.getOptions(), this.context, this.templateStart),
126+
new SemicolonTemplateVisitor(this.getSourceFile(), this.getOptions(), this.context, this.templateStart)
72127
];
73128

74129
visitBoundText(text: ast.BoundTextAst, context: any): any {
@@ -80,8 +135,21 @@ class WhitespaceTemplateVisitor extends BasicTemplateAstVisitor {
80135
.forEach(f => this.addFailure(f));
81136
super.visitBoundText(text, context);
82137
}
138+
139+
visitDirectiveProperty(prop: ast.BoundDirectivePropertyAst, context: any): any {
140+
const options = this.getOptions();
141+
this.visitors
142+
.filter(v => options.indexOf(v.getOption()) >= 0)
143+
.map(v => v.visitDirectiveProperty(prop, this))
144+
.filter(f => !!f)
145+
.forEach(f => this.addFailure(f));
146+
super.visitDirectiveProperty(prop, context);
147+
}
148+
149+
83150
}
84151

152+
85153
/* Expression visitors */
86154

87155
class PipeWhitespaceVisitor extends RecursiveAngularExpressionVisitor implements ConfigurableVisitor {
@@ -123,8 +191,8 @@ class PipeWhitespaceVisitor extends RecursiveAngularExpressionVisitor implements
123191
if (replacements.length) {
124192
context.addFailure(
125193
context.createFailure(ast.exp.span.end - 1, 3,
126-
'The pipe operator should be surrounded by one space on each side, i.e. " | ".',
127-
replacements)
194+
'The pipe operator should be surrounded by one space on each side, i.e. " | ".',
195+
replacements)
128196
);
129197
}
130198
super.visitPipe(ast, context);
@@ -163,27 +231,30 @@ export class Rule extends Lint.Rules.AbstractRule {
163231
description: `Ensures the proper formatting of Angular expressions.`,
164232
rationale: `Having whitespace in the right places in an Angular expression makes the template more readable.`,
165233
optionsDescription: Lint.Utils.dedent`
166-
One argument may be optionally provided:
167-
* \`"check-interpolation"\` checks for whitespace before and after the interpolation characters`,
234+
Arguments may be optionally provided:
235+
* \`"check-interpolation"\` checks for whitespace before and after the interpolation characters
236+
* \`"check-pipe"\` checks for whitespace before and after a pipe
237+
* \`"check-semicolon"\` checks for whitespace after semicolon`,
238+
168239
options: {
169240
type: 'array',
170241
items: {
171242
type: 'string',
172-
enum: ['check-interpolation'],
243+
enum: ['check-interpolation', 'check-pipe', 'check-semicolon'],
173244
},
174245
minLength: 0,
175-
maxLength: 1,
246+
maxLength: 3,
176247
},
177248
optionExamples: ['[true, "check-interpolation"]'],
178249
typescriptOnly: true,
179250
};
180251

181252
public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
182253
return this.applyWithWalker(
183-
new NgWalker(sourceFile,
184-
this.getOptions(), {
185-
templateVisitorCtrl: WhitespaceTemplateVisitor,
186-
expressionVisitorCtrl: TemplateExpressionVisitor
187-
}));
254+
new NgWalker(sourceFile,
255+
this.getOptions(), {
256+
templateVisitorCtrl: WhitespaceTemplateVisitor,
257+
expressionVisitorCtrl: TemplateExpressionVisitor,
258+
}));
188259
}
189260
}

0 commit comments

Comments
 (0)