Skip to content

Commit 809456f

Browse files
authored
Merge pull request #916 from bmish/no-side-effects-event-functions
Add `catchEvents` option (default false) to `no-side-effects` rule
2 parents d9107e1 + 157bb0b commit 809456f

File tree

3 files changed

+165
-11
lines changed

3 files changed

+165
-11
lines changed

docs/rules/no-side-effects.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,3 +41,9 @@ export default Component.extend({
4141
})
4242
});
4343
```
44+
45+
## Configuration
46+
47+
This rule takes an optional object containing:
48+
49+
* `boolean` -- `catchEvents` -- whether the rule should catch function calls that send actions or events (default `false`, TODO: enable in next major release)

lib/rules/no-side-effects.js

Lines changed: 74 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ function isEmberSetThis(node, importedEmberName) {
2121
types.isIdentifier(node.callee.property) &&
2222
['set', 'setProperties'].includes(node.callee.property.name) &&
2323
node.arguments.length > 0 &&
24-
memberExpressionBeginWithThis(node.arguments[0])
24+
memberExpressionBeginsWithThis(node.arguments[0])
2525
);
2626
}
2727

@@ -32,7 +32,7 @@ function isImportedSetThis(node, importedSetName, importedSetPropertiesName) {
3232
types.isIdentifier(node.callee) &&
3333
[importedSetName, importedSetPropertiesName].includes(node.callee.name) &&
3434
node.arguments.length > 0 &&
35-
memberExpressionBeginWithThis(node.arguments[0])
35+
memberExpressionBeginsWithThis(node.arguments[0])
3636
);
3737
}
3838

@@ -44,15 +44,50 @@ function isThisSet(node) {
4444
types.isMemberExpression(node.callee) &&
4545
types.isIdentifier(node.callee.property) &&
4646
['set', 'setProperties'].includes(node.callee.property.name) &&
47-
memberExpressionBeginWithThis(node.callee.object)
47+
memberExpressionBeginsWithThis(node.callee.object)
4848
);
4949
}
5050

51-
function memberExpressionBeginWithThis(node) {
51+
// import { sendEvent } from "@ember/object/events"
52+
// Ember.sendEvent
53+
54+
// Looks for variations like:
55+
// - this.send(...)
56+
// - Ember.send(...)
57+
const DISALLOWED_FUNCTION_CALLS = new Set(['send', 'sendAction', 'sendEvent', 'trigger']);
58+
function isDisallowedFunctionCall(node, importedEmberName) {
59+
return (
60+
types.isCallExpression(node) &&
61+
types.isMemberExpression(node.callee) &&
62+
(types.isThisExpression(node.callee.object) ||
63+
(types.isIdentifier(node.callee.object) && node.callee.object.name === importedEmberName)) &&
64+
types.isIdentifier(node.callee.property) &&
65+
DISALLOWED_FUNCTION_CALLS.has(node.callee.property.name)
66+
);
67+
}
68+
69+
// sendEvent(...)
70+
function isImportedSendEventCall(node, importedSendEventName) {
71+
return (
72+
types.isCallExpression(node) &&
73+
types.isIdentifier(node.callee) &&
74+
node.callee.name === importedSendEventName
75+
);
76+
}
77+
78+
/**
79+
* Finds:
80+
* - this
81+
* - this.foo
82+
* - this.foo.bar
83+
* - this?.foo?.bar
84+
* @param {node} node
85+
*/
86+
function memberExpressionBeginsWithThis(node) {
5287
if (types.isThisExpression(node)) {
5388
return true;
54-
} else if (types.isMemberExpression(node)) {
55-
return memberExpressionBeginWithThis(node.object);
89+
} else if (types.isMemberExpression(node) || types.isOptionalMemberExpression(node)) {
90+
return memberExpressionBeginsWithThis(node.object);
5691
}
5792
return false;
5893
}
@@ -61,16 +96,20 @@ function memberExpressionBeginWithThis(node) {
6196
* Recursively finds calls that could be side effects in a computed property function body.
6297
*
6398
* @param {ASTNode} computedPropertyBody body of computed property to search
99+
* @param {boolean} catchEvents
64100
* @param {string} importedEmberName
65101
* @param {string} importedSetName
66102
* @param {string} importedSetPropertiesName
103+
* @param {string} importedSendEventName
67104
* @returns {Array<ASTNode>}
68105
*/
69106
function findSideEffects(
70107
computedPropertyBody,
108+
catchEvents,
71109
importedEmberName,
72110
importedSetName,
73-
importedSetPropertiesName
111+
importedSetPropertiesName,
112+
importedSendEventName
74113
) {
75114
const results = [];
76115

@@ -80,7 +119,9 @@ function findSideEffects(
80119
isEmberSetThis(child, importedEmberName) || // Ember.set(this, 'foo', 123)
81120
isImportedSetThis(child, importedSetName, importedSetPropertiesName) || // set(this, 'foo', 123)
82121
isThisSet(child) || // this.set('foo', 123)
83-
propertySetterUtils.isThisSet(child) // this.foo = 123;
122+
propertySetterUtils.isThisSet(child) || // this.foo = 123;
123+
(catchEvents && isDisallowedFunctionCall(child, importedEmberName)) || // this.send('done')
124+
(catchEvents && isImportedSendEventCall(child, importedSendEventName)) // sendEvent(...)
84125
) {
85126
results.push(child);
86127
}
@@ -103,16 +144,30 @@ module.exports = {
103144
'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/no-side-effects.md',
104145
},
105146
fixable: null,
106-
schema: [],
147+
schema: [
148+
{
149+
type: 'object',
150+
properties: {
151+
catchEvents: {
152+
type: 'boolean',
153+
default: false,
154+
},
155+
},
156+
},
157+
],
107158
},
108159

109160
ERROR_MESSAGE,
110161

111162
create(context) {
163+
// Options:
164+
const catchEvents = context.options[0] && context.options[0].catchEvents;
165+
112166
let importedEmberName;
113167
let importedComputedName;
114168
let importedSetName;
115169
let importedSetPropertiesName;
170+
let importedSendEventName;
116171

117172
const report = function (node) {
118173
context.report(node, ERROR_MESSAGE);
@@ -131,6 +186,10 @@ module.exports = {
131186
importedSetPropertiesName ||
132187
getImportIdentifier(node, '@ember/object', 'setProperties');
133188
}
189+
if (node.source.value === '@ember/object/events') {
190+
importedSendEventName =
191+
importedSendEventName || getImportIdentifier(node, '@ember/object/events', 'sendEvent');
192+
}
134193
},
135194

136195
CallExpression(node) {
@@ -142,9 +201,11 @@ module.exports = {
142201

143202
findSideEffects(
144203
computedPropertyBody,
204+
catchEvents,
145205
importedEmberName,
146206
importedSetName,
147-
importedSetPropertiesName
207+
importedSetPropertiesName,
208+
importedSendEventName
148209
).forEach(report);
149210
},
150211

@@ -157,9 +218,11 @@ module.exports = {
157218

158219
findSideEffects(
159220
computedPropertyBody,
221+
catchEvents,
160222
importedEmberName,
161223
importedSetName,
162-
importedSetPropertiesName
224+
importedSetPropertiesName,
225+
importedSendEventName
163226
).forEach(report);
164227
},
165228
};

tests/lib/rules/no-side-effects.js

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ const { ERROR_MESSAGE } = rule;
1313
// ------------------------------------------------------------------------------
1414

1515
const eslintTester = new RuleTester({
16+
parser: require.resolve('babel-eslint'),
1617
parserOptions: { ecmaVersion: 6, sourceType: 'module' },
1718
});
1819

@@ -83,6 +84,23 @@ eslintTester.run('no-side-effects', rule, {
8384
"import Ember from 'ember'; Ember.setProperties(this, 'x', 123);",
8485
'this.x = 123;',
8586
'this.x.y = 123;',
87+
88+
// Events (but `catchEvents` option off):
89+
'computed(function() { this.send(); })',
90+
'computed(function() { this.sendAction(); })',
91+
'computed(function() { this.sendEvent(); })',
92+
'computed(function() { this.trigger(); })',
93+
'import { sendEvent } from "@ember/object/events"; computed(function() { sendEvent(); })',
94+
95+
// Not in a computed property (events):
96+
{ code: 'this.send()', options: [{ catchEvents: true }] },
97+
{ code: 'this.sendAction()', options: [{ catchEvents: true }] },
98+
{ code: 'this.sendEvent()', options: [{ catchEvents: true }] },
99+
{ code: 'this.trigger()', options: [{ catchEvents: true }] },
100+
{
101+
code: 'import { sendEvent } from "@ember/object/events"; sendEvent();',
102+
options: [{ catchEvents: true }],
103+
},
86104
].map(addComputedImport),
87105
invalid: [
88106
// this.set
@@ -134,6 +152,12 @@ eslintTester.run('no-side-effects', rule, {
134152
output: null,
135153
errors: [{ message: ERROR_MESSAGE, type: 'CallExpression' }],
136154
},
155+
{
156+
code:
157+
'import Ember from "ember"; computed(function() { Ember.set(this.foo?.bar, "testAmount", test.length); return ""; })',
158+
output: null,
159+
errors: [{ message: ERROR_MESSAGE, type: 'CallExpression' }],
160+
},
137161
{
138162
code:
139163
'import Ember from "ember"; computed(function() { Ember.setProperties(this, "testAmount", test.length); return ""; })',
@@ -225,5 +249,66 @@ eslintTester.run('no-side-effects', rule, {
225249
output: null,
226250
errors: [{ message: ERROR_MESSAGE, type: 'AssignmentExpression' }],
227251
},
252+
253+
// Events (from this):
254+
{
255+
code: 'computed(function() { this.send(); })',
256+
options: [{ catchEvents: true }],
257+
output: null,
258+
errors: [{ message: ERROR_MESSAGE, type: 'CallExpression' }],
259+
},
260+
{
261+
code: 'computed(function() { this.sendAction(); })',
262+
options: [{ catchEvents: true }],
263+
output: null,
264+
errors: [{ message: ERROR_MESSAGE, type: 'CallExpression' }],
265+
},
266+
{
267+
code: 'computed(function() { this.sendEvent(); })',
268+
options: [{ catchEvents: true }],
269+
output: null,
270+
errors: [{ message: ERROR_MESSAGE, type: 'CallExpression' }],
271+
},
272+
{
273+
code: 'computed(function() { this.trigger(); })',
274+
options: [{ catchEvents: true }],
275+
output: null,
276+
errors: [{ message: ERROR_MESSAGE, type: 'CallExpression' }],
277+
},
278+
279+
// Events (from Ember):
280+
{
281+
code: 'import Ember from "ember"; computed(function() { Ember.send(); })',
282+
options: [{ catchEvents: true }],
283+
output: null,
284+
errors: [{ message: ERROR_MESSAGE, type: 'CallExpression' }],
285+
},
286+
{
287+
code: 'import Ember from "ember"; computed(function() { Ember.sendAction(); })',
288+
options: [{ catchEvents: true }],
289+
output: null,
290+
errors: [{ message: ERROR_MESSAGE, type: 'CallExpression' }],
291+
},
292+
{
293+
code: 'import Ember from "ember"; computed(function() { Ember.sendEvent(); })',
294+
options: [{ catchEvents: true }],
295+
output: null,
296+
errors: [{ message: ERROR_MESSAGE, type: 'CallExpression' }],
297+
},
298+
{
299+
code: 'import Ember from "ember"; computed(function() { Ember.trigger(); })',
300+
options: [{ catchEvents: true }],
301+
output: null,
302+
errors: [{ message: ERROR_MESSAGE, type: 'CallExpression' }],
303+
},
304+
305+
{
306+
// Imported sendEvent function:
307+
code:
308+
'import { sendEvent as se } from "@ember/object/events"; computed(function() { se(); })',
309+
options: [{ catchEvents: true }],
310+
output: null,
311+
errors: [{ message: ERROR_MESSAGE, type: 'CallExpression' }],
312+
},
228313
].map(addComputedImport),
229314
});

0 commit comments

Comments
 (0)