Skip to content

Commit 92ade8f

Browse files
authored
feat(rules): add ion-radio-slot-required rule (#25)
1 parent 7c0ee7f commit 92ade8f

File tree

3 files changed

+226
-3
lines changed

3 files changed

+226
-3
lines changed

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -373,12 +373,14 @@ We are looking for contributors to help build these rules out! See [`CONTRIBUTIN
373373
<th rowspan="2">
374374
<a href="https://github.com/ionic-team/ionic/blob/master/angular/BREAKING.md#radio">Radio</a>
375375
</th>
376-
<td></td>
377-
<td>:white_large_square:</td>
376+
<td>:wrench:</td>
377+
<td>:white_check_mark:</td>
378378
<td>
379379
<code>ion-radio-slot-required</code>
380380
</td>
381-
<td></td>
381+
<td>
382+
<a href="https://github.com/dwieeb">@dwieeb</a>
383+
</td>
382384
</tr>
383385
<tr>
384386
<td></td>

src/ionRadioSlotRequiredRule.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import * as ast from '@angular/compiler';
2+
import { NgWalker } from 'codelyzer/angular/ngWalker';
3+
import { BasicTemplateAstVisitor } from 'codelyzer/angular/templates/basicTemplateAstVisitor';
4+
import * as Lint from 'tslint';
5+
import * as ts from 'typescript';
6+
7+
export const ruleName = 'ion-radio-slot-required';
8+
9+
class TemplateVisitor extends BasicTemplateAstVisitor {
10+
visitElement(element: ast.ElementAst, context: any): any {
11+
if (element.name && element.name === 'ion-radio') {
12+
const attributeFound = element.attrs.find(attr => attr.name === 'slot');
13+
14+
if (!attributeFound) {
15+
const start = element.sourceSpan.start.offset;
16+
const length = element.name.length;
17+
const position = this.getSourcePosition(start) + length + 1;
18+
19+
this.addFailureAt(start + 1, length, 'The slot attribute of ion-radio is required. Use slot="start".', [
20+
Lint.Replacement.replaceFromTo(position, position, ' slot="start"')
21+
]);
22+
}
23+
}
24+
25+
super.visitElement(element, context);
26+
}
27+
}
28+
29+
export class Rule extends Lint.Rules.AbstractRule {
30+
public static metadata: Lint.IRuleMetadata = {
31+
ruleName: ruleName,
32+
type: 'functionality',
33+
description: 'The slot attribute of ion-radio is now required.',
34+
options: null,
35+
optionsDescription: 'Not configurable.',
36+
typescriptOnly: false,
37+
hasFix: true
38+
};
39+
40+
public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
41+
return this.applyWithWalker(
42+
new NgWalker(sourceFile, this.getOptions(), {
43+
templateVisitorCtrl: TemplateVisitor
44+
})
45+
);
46+
}
47+
}

test/ionRadioSlotRequired.spec.ts

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import { expect } from 'chai';
2+
import { Replacement, Utils } from 'tslint';
3+
import { ruleName } from '../src/ionRadioSlotRequiredRule';
4+
import { assertAnnotated, assertFailure, assertFailures, assertMultipleAnnotated, assertSuccess } from './testHelper';
5+
6+
describe(ruleName, () => {
7+
describe('success', () => {
8+
it('should work with proper style', () => {
9+
let source = `
10+
@Component({
11+
template: \`<ion-radio slot="start"></ion-radio>\`
12+
})
13+
class Bar{}
14+
`;
15+
assertSuccess(ruleName, source);
16+
});
17+
});
18+
19+
describe('failure', () => {
20+
it('should fail with no attributes', () => {
21+
let source = `
22+
@Component({
23+
template: \`
24+
<ion-radio></ion-radio>\`
25+
~~~~~~~~~
26+
})
27+
class Bar{}
28+
`;
29+
30+
assertAnnotated({
31+
ruleName,
32+
message: 'The slot attribute of ion-radio is required. Use slot="start".',
33+
source
34+
});
35+
});
36+
37+
it('should fail without slot attribute', () => {
38+
let source = `
39+
@Component({
40+
template: \`
41+
<ion-radio value="grape" checked disabled></ion-radio>\`
42+
~~~~~~~~~
43+
})
44+
class Bar{}
45+
`;
46+
47+
assertAnnotated({
48+
ruleName,
49+
message: 'The slot attribute of ion-radio is required. Use slot="start".',
50+
source
51+
});
52+
});
53+
});
54+
55+
describe('replacements', () => {
56+
it('should add slot="start" to ion-radio without attributes', () => {
57+
let source = `
58+
@Component({
59+
template: \`<ion-radio></ion-radio>
60+
\`
61+
})
62+
class Bar {}
63+
`;
64+
65+
const fail = {
66+
message: 'The slot attribute of ion-radio is required. Use slot="start".',
67+
startPosition: {
68+
line: 2,
69+
character: 22
70+
},
71+
endPosition: {
72+
line: 2,
73+
character: 31
74+
}
75+
};
76+
77+
const failures = assertFailure(ruleName, source, fail);
78+
const fixes = failures.map(f => f.getFix());
79+
const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify));
80+
81+
let expected = `
82+
@Component({
83+
template: \`<ion-radio slot="start"></ion-radio>
84+
\`
85+
})
86+
class Bar {}
87+
`;
88+
89+
expect(res).to.eq(expected);
90+
});
91+
92+
it('should add slot="start" to ion-radio without slot', () => {
93+
let source = `
94+
@Component({
95+
template: \`
96+
<ion-radio value="grape" checked disabled></ion-radio>
97+
\`
98+
})
99+
class Bar {}
100+
`;
101+
102+
const fail = {
103+
message: 'The slot attribute of ion-radio is required. Use slot="start".',
104+
startPosition: {
105+
line: 3,
106+
character: 13
107+
},
108+
endPosition: {
109+
line: 3,
110+
character: 22
111+
}
112+
};
113+
114+
const failures = assertFailure(ruleName, source, fail);
115+
const fixes = failures.map(f => f.getFix());
116+
const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify));
117+
118+
let expected = `
119+
@Component({
120+
template: \`
121+
<ion-radio slot="start" value="grape" checked disabled></ion-radio>
122+
\`
123+
})
124+
class Bar {}
125+
`;
126+
127+
expect(res).to.eq(expected);
128+
});
129+
130+
it('should add slot="start" to ion-radio without slot on multiline', () => {
131+
let source = `
132+
@Component({
133+
template: \`
134+
<ion-radio
135+
value="grape"
136+
checked
137+
disabled></ion-radio>
138+
\`
139+
})
140+
class Bar {}
141+
`;
142+
143+
const fail = {
144+
message: 'The slot attribute of ion-radio is required. Use slot="start".',
145+
startPosition: {
146+
line: 3,
147+
character: 13
148+
},
149+
endPosition: {
150+
line: 3,
151+
character: 22
152+
}
153+
};
154+
155+
const failures = assertFailure(ruleName, source, fail);
156+
const fixes = failures.map(f => f.getFix());
157+
const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify));
158+
159+
let expected = `
160+
@Component({
161+
template: \`
162+
<ion-radio slot="start"
163+
value="grape"
164+
checked
165+
disabled></ion-radio>
166+
\`
167+
})
168+
class Bar {}
169+
`;
170+
171+
expect(res).to.eq(expected);
172+
});
173+
});
174+
});

0 commit comments

Comments
 (0)