Skip to content

Commit 4fa35f6

Browse files
committed
feat: allow multiple selector types
Allow multiple selector types (fix #290) and refactor the selector-related rules to depend on less state.
1 parent 54081b0 commit 4fa35f6

6 files changed

+115
-120
lines changed

src/componentSelectorRule.ts

+9-4
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,13 @@ export class Rule extends SelectorRule {
6060
};
6161

6262
public handleType = 'Component';
63-
public getTypeFailure():any { return 'The selector of the component "%s" should be used as %s ($$05-03$$)'; }
64-
public getNameFailure():any { return 'The selector of the component "%s" should be named %s ($$05-02$$)'; }
65-
getSinglePrefixFailure():any { return 'The selector of the component "%s" should have prefix "%s" ($$02-07$$)'; }
66-
getManyPrefixFailure():any { return 'The selector of the component "%s" should have one of the prefixes: %s ($$02-07$$)'; }
63+
public getTypeFailure(): any { return 'The selector of the component "%s" should be used as %s ($$05-03$$)'; }
64+
public getStyleFailure(): any { return 'The selector of the component "%s" should be named %s ($$05-02$$)'; }
65+
getPrefixFailure(prefixes: string[]): any {
66+
if (prefixes.length === 1) {
67+
return 'The selector of the component "%s" should have prefix "%s" ($$02-07$$)';
68+
} else {
69+
return 'The selector of the component "%s" should have one of the prefixes "%s" ($$02-07$$)';
70+
}
71+
}
6772
}

src/directiveSelectorRule.ts

+9-4
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,15 @@ export class Rule extends SelectorRule {
5757
};
5858

5959
public handleType = 'Directive';
60-
public getTypeFailure():any { return 'The selector of the directive "%s" should be used as %s ($$02-06$$)'; }
61-
public getNameFailure():any { return 'The selector of the directive "%s" should be named %s ($$02-06$$)'; }
62-
getSinglePrefixFailure():any { return 'The selector of the directive "%s" should have prefix "%s" ($$02-08$$)'; }
63-
getManyPrefixFailure():any { return 'The selector of the directive "%s" should have one of the prefixes: %s ($$02-08$$)'; }
60+
public getTypeFailure(): string { return 'The selector of the directive "%s" should be used as %s ($$02-06$$)'; }
61+
public getStyleFailure(): string { return 'The selector of the directive "%s" should be named %s ($$02-06$$)'; }
62+
getPrefixFailure(prefixes: string[]): string {
63+
if (prefixes.length === 1) {
64+
return 'The selector of the directive "%s" should have prefix "%s" ($$02-08$$)';
65+
} else {
66+
return 'The selector of the directive "%s" should have one of the prefixes "%s" ($$02-08$$)';
67+
}
68+
}
6469

6570
}
6671

src/selectorNameBase.ts

+70-103
Original file line numberDiff line numberDiff line change
@@ -1,115 +1,91 @@
11
import * as Lint from 'tslint';
2-
import {SelectorValidator} from './util/selectorValidator';
2+
import { SelectorValidator } from './util/selectorValidator';
33
import * as ts from 'typescript';
44
import {sprintf} from 'sprintf-js';
55
import * as compiler from '@angular/compiler';
66
import { IOptions } from 'tslint';
77
import SyntaxKind = require('./util/syntaxKind');
88

9-
export abstract class SelectorRule extends Lint.Rules.AbstractRule {
10-
11-
public isMultiPrefix:boolean;
12-
public prefixArguments:string;
13-
public cssSelectorProperty:string;
9+
export type SelectorType = 'element' | 'attribute';
10+
export type SelectorTypeInternal = 'element' | 'attrs';
11+
export type SelectorStyle = 'kebab-case' | 'camelCase';
1412

15-
public handleType: string;
16-
17-
private typeValidator:Function;
18-
private prefixValidator:Function;
19-
private nameValidator:Function;
20-
private FAILURE_PREFIX;
21-
private isMultiSelectors:boolean;
13+
export abstract class SelectorRule extends Lint.Rules.AbstractRule {
14+
handleType: string;
15+
prefixes: string[];
16+
types: SelectorTypeInternal[];
17+
style: SelectorStyle[];
2218

2319
constructor(options: IOptions) {
24-
const args = options.ruleArguments;
25-
let type = args[1];
26-
let prefix = args[2] || [];
27-
let name = args[3];
2820
super(options);
29-
this.setMultiPrefix(prefix);
30-
this.setPrefixArguments(prefix);
31-
this.setPrefixValidator(prefix, name);
32-
this.setPrefixFailure();
33-
this.setTypeValidator(type);
34-
this.setNameValidator(name);
35-
}
36-
37-
public getPrefixFailure():string {
38-
return this.FAILURE_PREFIX;
39-
}
40-
41-
public validateType(selector:string):boolean {
42-
return this.typeValidator(selector);
43-
}
21+
const args = options.ruleArguments;
4422

45-
public validateName(selector:any):boolean {
46-
if(this.isMultiSelectors) {
47-
return selector.some((a) => this.nameValidator(a));
48-
} else {
49-
return this.nameValidator(selector);
23+
let type: SelectorType[] = args[1] || ['element', 'attribute'];
24+
if (!(type instanceof Array)) {
25+
type = [type];
5026
}
51-
}
52-
53-
public validatePrefix(selector:any):boolean {
54-
if(this.isMultiSelectors) {
55-
return selector.some((a) => this.prefixValidator(a));
56-
} else {
57-
return this.prefixValidator(selector);
27+
let internal: SelectorTypeInternal[] = [];
28+
if (type.indexOf('element') >= 0) {
29+
internal.push('element');
5830
}
59-
}
60-
61-
public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
62-
return this.applyWithWalker(
63-
new SelectorValidatorWalker(
64-
sourceFile,
65-
this));
66-
}
31+
if (type.indexOf('attribute') >= 0) {
32+
internal.push('attrs');
33+
}
34+
this.types = internal;
6735

68-
public abstract getTypeFailure(): any;
69-
public abstract getNameFailure(): any;
70-
protected abstract getSinglePrefixFailure(): any;
71-
protected abstract getManyPrefixFailure(): any;
36+
let prefix = args[2] || [];
37+
if (!(prefix instanceof Array)) {
38+
prefix = [prefix];
39+
}
40+
this.prefixes = prefix;
7241

73-
private setNameValidator(name:string) {
74-
if (name === 'camelCase') {
75-
this.nameValidator = SelectorValidator.camelCase;
76-
} else if(name === 'kebab-case') {
77-
this.nameValidator = SelectorValidator.kebabCase;
42+
let style = args[3];
43+
if (!(style instanceof Array)) {
44+
style = [style];
7845
}
46+
this.style = style;
7947
}
8048

81-
private setMultiPrefix(prefix:string) {
82-
this.isMultiPrefix = typeof prefix ==='string';
49+
public validateType(selectors: compiler.CssSelector[]): boolean {
50+
return this.getValidSelectors(selectors).length > 0;
8351
}
8452

85-
private setPrefixArguments(prefix:any) {
86-
this.prefixArguments = this.isMultiPrefix?prefix:prefix.join(',');
53+
public validateStyle(selectors: compiler.CssSelector[]): boolean {
54+
return this.getValidSelectors(selectors).some(selector => {
55+
return this.style.some(style => {
56+
let validator = SelectorValidator.camelCase;
57+
if (style === 'kebab-case') {
58+
validator = SelectorValidator.kebabCase;
59+
}
60+
return validator(selector);
61+
});
62+
});
8763
}
8864

89-
private setPrefixValidator(prefix: any, name: string) {
90-
if (!this.isMultiPrefix) {
91-
prefix = (prefix||[]).sort((a: string, b: string) => {
92-
return a.length < b.length ? 1 : -1;
93-
});
94-
}
95-
let prefixExpression: string = this.isMultiPrefix?prefix:(prefix||[]).join('|');
96-
this.prefixValidator = SelectorValidator.prefix(prefixExpression, name);
65+
public validatePrefix(selectors: compiler.CssSelector[]): boolean {
66+
return this.getValidSelectors(selectors)
67+
.some(selector => !this.prefixes.length || this.prefixes.some(p =>
68+
this.style.some(s => SelectorValidator.prefix(p, s)(selector))));
9769
}
9870

99-
private setPrefixFailure() {
100-
this.FAILURE_PREFIX = this.isMultiPrefix?this.getSinglePrefixFailure():this.getManyPrefixFailure();
71+
public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
72+
return this.applyWithWalker(new SelectorValidatorWalker(sourceFile, this));
10173
}
10274

103-
private setTypeValidator(type:string) {
104-
if (type === 'element') {
105-
this.typeValidator = SelectorValidator.element;
106-
this.isMultiSelectors = false;
107-
this.cssSelectorProperty = 'element';
108-
} else if (type === 'attribute') {
109-
this.typeValidator = SelectorValidator.attribute;
110-
this.isMultiSelectors = true;
111-
this.cssSelectorProperty = 'attrs';
112-
}
75+
abstract getTypeFailure(): string;
76+
abstract getStyleFailure(): string;
77+
abstract getPrefixFailure(prefixes: string[]): string;
78+
79+
private getValidSelectors(selectors: compiler.CssSelector[]) {
80+
return [].concat.apply([], selectors.map(selector => {
81+
return [].concat.apply([], this.types.map(t => {
82+
let prop = selector[t];
83+
if (prop && !(prop instanceof Array)) {
84+
prop = [prop];
85+
}
86+
return prop;
87+
}).filter(s => !!s));
88+
}));
11389
}
11490
}
11591

@@ -140,30 +116,21 @@ export class SelectorValidatorWalker extends Lint.RuleWalker {
140116
private validateSelector(className: string, arg: ts.Node) {
141117
if (arg.kind === SyntaxKind.current().ObjectLiteralExpression) {
142118
(<ts.ObjectLiteralExpression>arg).properties.filter(prop => this.validateProperty(prop))
143-
.map(prop=>(<any>prop).initializer)
119+
.map(prop => (<any>prop).initializer)
144120
.forEach(i => {
145-
const selectors: any = this.extractMainSelector(i);
146-
const validateSelectors = (cb: any) => {
147-
// If all selectors fail, this will return true which means that the selector is invalid
148-
// according to the validation callback.
149-
// Since the method is called validateSelector, it's suppose to return true if the
150-
// selector is valid, so we're taking the result with "!".
151-
return !selectors.every((selector: any) => {
152-
return !cb(selector[this.rule.cssSelectorProperty]);
153-
});
154-
};
155-
if (!validateSelectors(this.rule.validateType.bind(this.rule))) {
121+
const selectors: compiler.CssSelector[] = this.extractMainSelector(i);
122+
if (!this.rule.validateType(selectors)) {
156123
let error = sprintf(this.rule.getTypeFailure(), className, this.rule.getOptions().ruleArguments[1]);
157-
this.addFailure(this.createFailure(i.getStart(), i.getWidth(),error));
158-
} else if (!validateSelectors(this.rule.validateName.bind(this.rule))) {
124+
this.addFailure(this.createFailure(i.getStart(), i.getWidth(), error));
125+
} else if (!this.rule.validateStyle(selectors)) {
159126
let name = this.rule.getOptions().ruleArguments[3];
160127
if (name === 'kebab-case') {
161128
name += ' and include dash';
162129
}
163-
let error = sprintf(this.rule.getNameFailure(), className, name);
130+
let error = sprintf(this.rule.getStyleFailure(), className, name);
164131
this.addFailure(this.createFailure(i.getStart(), i.getWidth(), error));
165-
} else if (!validateSelectors(this.rule.validatePrefix.bind(this.rule))) {
166-
let error = sprintf(this.rule.getPrefixFailure(),className,this.rule.prefixArguments);
132+
} else if (!this.rule.validatePrefix(selectors)) {
133+
let error = sprintf(this.rule.getPrefixFailure(this.rule.prefixes), className, this.rule.prefixes.join(', '));
167134
this.addFailure(this.createFailure(i.getStart(), i.getWidth(), error));
168135
}
169136
});
@@ -179,7 +146,7 @@ export class SelectorValidatorWalker extends Lint.RuleWalker {
179146
return [current.StringLiteral, current.NoSubstitutionTemplateLiteral].some(kindType => kindType === kind);
180147
}
181148

182-
private extractMainSelector(i:any) {
149+
private extractMainSelector(i: any) {
183150
return compiler.CssSelector.parse(i.text);
184151
}
185152
}

test/componentClassSuffixRule.spec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ describe('component-class-suffix', () => {
88
selector: 'sg-foo-bar'
99
})
1010
class Test {}
11-
~~~~
11+
~~~~
1212
`;
1313
assertAnnotated({
1414
ruleName: 'component-class-suffix',

test/componentSelectorRule.spec.ts

+15-6
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {assertSuccess, assertAnnotated, assertMultipleAnnotated} from './testHelper';
1+
import { assertSuccess, assertAnnotated, assertMultipleAnnotated } from './testHelper';
22

33
describe('component-selector-prefix', () => {
44
describe('invalid component selectors', () => {
@@ -26,7 +26,7 @@ describe('component-selector-prefix', () => {
2626
class Test {}`;
2727
assertAnnotated({
2828
ruleName: 'component-selector',
29-
message: 'The selector of the component "Test" should have one of the prefixes: sg,mg,ng ($$02-07$$)',
29+
message: 'The selector of the component "Test" should have one of the prefixes "sg, mg, ng" ($$02-07$$)',
3030
source,
3131
options: ['element', ['sg','mg','ng'], 'kebab-case']
3232
});
@@ -40,7 +40,7 @@ describe('component-selector-prefix', () => {
4040
class Test {}`;
4141
assertAnnotated({
4242
ruleName: 'component-selector',
43-
message: 'The selector of the component "Test" should have one of the prefixes: sg,mg,ng ($$02-07$$)',
43+
message: 'The selector of the component "Test" should have one of the prefixes "sg, mg, ng" ($$02-07$$)',
4444
source,
4545
options: ['element', ['sg','mg','ng'], 'kebab-case']
4646
});
@@ -49,16 +49,16 @@ describe('component-selector-prefix', () => {
4949

5050
it('should fail when component used longer prefix', () => {
5151
let source = `
52-
@Component({selector: 'foo-bar'}) class TestOne {}
52+
@Component({selector: 'foo-bar'}) class TestOne {}
5353
~~~~~~~~~
5454
@Component({selector: 'ngg-bar'}) class TestTwo {}
5555
^^^^^^^^^
5656
`;
5757
assertMultipleAnnotated({
5858
ruleName: 'component-selector',
5959
failures: [
60-
{ char: '~', msg: 'The selector of the component "TestOne" should have one of the prefixes: fo,mg,ng ($$02-07$$)'},
61-
{ char: '^', msg: 'The selector of the component "TestTwo" should have one of the prefixes: fo,mg,ng ($$02-07$$)'},
60+
{ char: '~', msg: 'The selector of the component "TestOne" should have one of the prefixes "fo, mg, ng" ($$02-07$$)'},
61+
{ char: '^', msg: 'The selector of the component "TestTwo" should have one of the prefixes "fo, mg, ng" ($$02-07$$)'},
6262
],
6363
source,
6464
options: ['element', ['fo','mg','ng'], 'kebab-case']
@@ -152,6 +152,15 @@ describe('component-selector-type', () => {
152152
options: ['element', ['sg','ng'], 'kebab-case']
153153
});
154154
});
155+
156+
it('should accept several selector types', () => {
157+
let source = `
158+
@Component({
159+
selector: \`[fooBar]\`
160+
})
161+
class Test {}`;
162+
assertSuccess('component-selector', source, [['element', 'attribute'], ['foo','ng'], 'camelCase']);
163+
});
155164
});
156165

157166
describe('valid component selector', () => {

test/directiveSelectorRule.spec.ts

+11-2
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ describe('directive-selector-prefix', () => {
9595
class Test {}`;
9696
assertAnnotated({
9797
ruleName: 'directive-selector',
98-
message: 'The selector of the directive "Test" should have one of the prefixes: sg,ng,mg ($$02-08$$)',
98+
message: 'The selector of the directive "Test" should have one of the prefixes "sg, ng, mg" ($$02-08$$)',
9999
source,
100100
options: ['attribute',['sg', 'ng', 'mg'],'camelCase']
101101
});
@@ -109,7 +109,7 @@ describe('directive-selector-prefix', () => {
109109
class Test {}`;
110110
assertAnnotated({
111111
ruleName: 'directive-selector',
112-
message: 'The selector of the directive "Test" should have one of the prefixes: sg,ng,mg ($$02-08$$)',
112+
message: 'The selector of the directive "Test" should have one of the prefixes "sg, ng, mg" ($$02-08$$)',
113113
source,
114114
options: ['attribute',['sg', 'ng', 'mg'],'camelCase']
115115
});
@@ -125,6 +125,15 @@ describe('directive-selector-prefix', () => {
125125
assertSuccess('directive-selector', source, ['attribute','sg','camelCase']);
126126
});
127127

128+
it('should succeed when set valid selector in @Directive', () => {
129+
let source = `
130+
@Directive({
131+
selector: 'sgBarFoo'
132+
})
133+
class Test {}`;
134+
assertSuccess('directive-selector', source, [['attribute', 'element'],'sg','camelCase']);
135+
});
136+
128137
it('should succeed when set valid selector in @Directive using multiple prefixes', () => {
129138
let source = `
130139
@Directive({

0 commit comments

Comments
 (0)