Skip to content

Commit 0057bfd

Browse files
mhartingtonimhoffd
authored andcommitted
feat(rules): add ion-menu-events-renamed rule (#91)
1 parent e66fc7a commit 0057bfd

File tree

4 files changed

+254
-0
lines changed

4 files changed

+254
-0
lines changed

README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ We are looking for contributors to help build these rules out! See [`CONTRIBUTIN
4646
"ion-label-attributes-renamed": true,
4747
"ion-list-header-ion-label-required": true,
4848
"ion-loading-method-create-parameters-renamed": true,
49+
"ion-menu-events-renamed": true,
4950
"ion-menu-toggle-is-now-an-element": true,
5051
"ion-navbar-is-now-ion-toolbar": true,
5152
"ion-option-is-now-ion-select-option": true,
@@ -59,6 +60,7 @@ We are looking for contributors to help build these rules out! See [`CONTRIBUTIN
5960
"ion-spinner-attribute-values-renamed": true,
6061
"ion-tab-attributes-renamed": true,
6162
"ion-text-is-now-an-element": true
63+
6264
}
6365
}
6466
```
@@ -392,6 +394,21 @@ We are looking for contributors to help build these rules out! See [`CONTRIBUTIN
392394
<a href="https://github.com/dwieeb">@dwieeb</a>
393395
</td>
394396
</tr>
397+
398+
<tr>
399+
<th>
400+
<a href="https://github.com/ionic-team/ionic/blob/master/angular/BREAKING.md#menu">Menu</a>
401+
</th>
402+
<td>:wrench:</td>
403+
<td>:white_check_mark:</td>
404+
<td>
405+
<code>ion-menu-events-renamed</code>
406+
</td>
407+
<td>
408+
<a href="https://github.com/mhartington">@mhartington</a>
409+
</td>
410+
</tr>
411+
395412
<tr>
396413
<th>
397414
<a href="https://github.com/ionic-team/ionic/blob/master/angular/BREAKING.md#menu-toggle">Menu Toggle</a>

src/helpers/attributeEventsRenamed.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import * as ast from '@angular/compiler';
2+
import * as Lint from 'tslint';
3+
import { BasicTemplateAstVisitor } from 'codelyzer/angular/templates/basicTemplateAstVisitor';
4+
5+
function generateErrorMessage(elementName: string, attrName: string, replacement: string) {
6+
return `The ${attrName} event of ${elementName} has been renamed. Use ${replacement} instead.`;
7+
}
8+
9+
export function createAttributesRenamedTemplateVisitorClass(elementNames: string[] | undefined, replacementMap: Map<string, string>) {
10+
return class extends BasicTemplateAstVisitor {
11+
visitElement(element: ast.ElementAst, context: any): any {
12+
if (!elementNames || elementNames.includes(element.name)) {
13+
this.checkAttributesForReplacements(element);
14+
}
15+
16+
super.visitElement(element, context);
17+
}
18+
19+
private checkAttributesForReplacements(element: ast.ElementAst) {
20+
for (const output of element.outputs) {
21+
const replacement = replacementMap.get(output.name);
22+
23+
if (replacement) {
24+
const start = output.sourceSpan.start.offset + 1;
25+
const length = output.name.length;
26+
const position = this.getSourcePosition(start);
27+
28+
this.addFailureAt(start, length, generateErrorMessage(element.name, output.name, replacement), [
29+
Lint.Replacement.replaceFromTo(position, position + length, replacement)
30+
]);
31+
}
32+
}
33+
}
34+
};
35+
}

src/ionMenuEventsRenamedRule.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { NgWalker } from 'codelyzer/angular/ngWalker';
2+
import * as Lint from 'tslint';
3+
import * as ts from 'typescript';
4+
5+
import { createAttributesRenamedTemplateVisitorClass } from './helpers/attributeEventsRenamed';
6+
7+
export const ruleName = 'ion-menu-events-renamed';
8+
9+
const replacementMap = new Map([['ionOpen', 'ionDidOpen'], ['ionClose', 'ionDidClose']]);
10+
11+
const TemplateVisitor = createAttributesRenamedTemplateVisitorClass(undefined, replacementMap);
12+
13+
export class Rule extends Lint.Rules.AbstractRule {
14+
public static metadata: Lint.IRuleMetadata = {
15+
ruleName: ruleName,
16+
type: 'functionality',
17+
description: 'Events for ion-menu have changed',
18+
options: null,
19+
optionsDescription: 'Not configurable.',
20+
typescriptOnly: false,
21+
hasFix: true
22+
};
23+
24+
public apply(sourceFile: ts.SourceFile): Lint.RuleFailure[] {
25+
return this.applyWithWalker(
26+
new NgWalker(sourceFile, this.getOptions(), {
27+
templateVisitorCtrl: TemplateVisitor
28+
})
29+
);
30+
}
31+
}

test/ionMenuEventsRenamed.spec.ts

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
import { expect } from 'chai';
2+
import { Replacement, Utils } from 'tslint';
3+
import { ruleName } from '../src/ionMenuEventsRenamedRule';
4+
import { assertAnnotated, assertFailure, assertFailures, assertMultipleAnnotated, assertSuccess } from './testHelper';
5+
6+
describe(ruleName, () => {
7+
describe('success', () => {
8+
it('events should be ionDidClose and ionDidOpen', () => {
9+
let source = `
10+
@Component({
11+
template: \`
12+
<ion-menu side="start" (ionDidClose)="logEvent($event)" (ionDidOpen)="logEvent($event)">
13+
</ion-menu>
14+
\`
15+
})
16+
class Bar{}
17+
`;
18+
assertSuccess(ruleName, source);
19+
});
20+
});
21+
22+
describe('failure', () => {
23+
it('should fail when ionOpen is used', () => {
24+
let source = `
25+
@Component({
26+
template: \`
27+
<ion-menu side="start" (ionOpen)="logEvent($event)" >
28+
~~~~~~~
29+
</ion-menu>
30+
\`
31+
})
32+
class Bar{}
33+
`;
34+
35+
assertAnnotated({
36+
ruleName,
37+
message: 'The ionOpen event of ion-menu has been renamed. Use ionDidOpen instead.',
38+
source
39+
});
40+
});
41+
42+
it('should fail when ionClose is used', () => {
43+
let source = `
44+
@Component({
45+
template: \`
46+
<ion-menu side="start" (ionClose)="logEvent($event)" >
47+
~~~~~~~~
48+
</ion-menu>
49+
\`
50+
})
51+
class Bar{}
52+
`;
53+
54+
assertAnnotated({
55+
ruleName,
56+
message: 'The ionClose event of ion-menu has been renamed. Use ionDidClose instead.',
57+
source
58+
});
59+
});
60+
61+
it('should fail when ionClose and ionOpen are used', () => {
62+
let source = `
63+
@Component({
64+
template: \`
65+
<ion-menu side="start" (ionClose)="logEvent($event)" (ionOpen)="logEvent($event)" >
66+
~~~~~~~~ ^^^^^^^
67+
</ion-menu>
68+
\`
69+
})
70+
class Bar{}
71+
`;
72+
73+
assertMultipleAnnotated({
74+
ruleName,
75+
source,
76+
failures: [
77+
{
78+
char: '~',
79+
msg: 'The ionClose event of ion-menu has been renamed. Use ionDidClose instead.'
80+
},
81+
{
82+
char: '^',
83+
msg: 'The ionOpen event of ion-menu has been renamed. Use ionDidOpen instead.'
84+
}
85+
]
86+
});
87+
});
88+
});
89+
//
90+
describe('replacements', () => {
91+
it('should replace ionOpen with ionDidOpen', () => {
92+
let source = `
93+
@Component({
94+
template: \`
95+
<ion-menu side="start" (ionOpen)="logEvent($event)" >
96+
</ion-menu>
97+
\`
98+
})
99+
class Bar {}
100+
`;
101+
102+
const fail = {
103+
message: 'The ionOpen event of ion-menu has been renamed. Use ionDidOpen instead.',
104+
startPosition: {
105+
line: 3,
106+
character: 36
107+
},
108+
endPosition: {
109+
line: 3,
110+
character: 43
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-menu side="start" (ionDidOpen)="logEvent($event)" >
122+
</ion-menu>
123+
\`
124+
})
125+
class Bar {}
126+
`;
127+
128+
expect(res).to.eq(expected);
129+
});
130+
131+
it('should replace ionClose with ionDidClose', () => {
132+
let source = `
133+
@Component({
134+
template: \`
135+
<ion-menu side="start" (ionClose)="logEvent($event)" >
136+
</ion-menu>
137+
\`
138+
})
139+
class Bar {}
140+
`;
141+
142+
const fail = {
143+
message: 'The ionClose event of ion-menu has been renamed. Use ionDidClose instead.',
144+
startPosition: {
145+
line: 3,
146+
character: 36
147+
},
148+
endPosition: {
149+
line: 3,
150+
character: 44
151+
}
152+
};
153+
154+
const failures = assertFailure(ruleName, source, fail);
155+
const fixes = failures.map(f => f.getFix());
156+
const res = Replacement.applyAll(source, Utils.flatMap(fixes, Utils.arrayify));
157+
158+
let expected = `
159+
@Component({
160+
template: \`
161+
<ion-menu side="start" (ionDidClose)="logEvent($event)" >
162+
</ion-menu>
163+
\`
164+
})
165+
class Bar {}
166+
`;
167+
168+
expect(res).to.eq(expected);
169+
});
170+
});
171+
});

0 commit comments

Comments
 (0)