Skip to content

Commit 126d16a

Browse files
hjdivadscalvert
authored andcommitted
Add missing tests (#91)
* Add missing tests This adds two failing tests to highlight bugs and a passing but previously missing test. The missing test added is for calling `set` which was destructured from `import Ember from 'ember'` One failing test is to handle the case where `Ember.computed` is called from an import that isn't named `Ember`, eg ``` import E from 'ember'; E.computed() ``` Another failing test illustrates that ember import bindings are clobbered if followed by any other default import, as in: ``` import Ember from 'ember'; import AnythingElse from 'anywhere'; ``` * Clean up no-side-effect-cp rule. Fixes a number of the errors previously identified, as well as a few additional ones (tests added in this commit).
1 parent 7c0a4e7 commit 126d16a

File tree

3 files changed

+113
-23
lines changed

3 files changed

+113
-23
lines changed

lib/rules/no-side-effect-cp.js

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,28 @@
22
* @fileOverview Disallow use of computed properties that include side-effect producing calls.
33
*/
44

5+
const { get } = require('../utils/get');
56
const { getCaller, cleanCaller } = require('../utils/caller');
67
const isCPGetter = require('../utils/computed-property');
78
const { getEmberImportBinding, collectImportBindings } = require('../utils/imports');
89
const { collectObjectPatternBindings } = require('../utils/destructed-binding');
910

10-
let SIDE_EFFECTS = ['this.send', 'this.sendAction', 'this.sendEvent', 'Em.sendEvent', 'Ember.sendEvent', 'this.trigger', 'this.set', 'Ember.set', 'Em.set', 'this.setProperties', 'Ember.setProperties', 'Em.setProperties'];
11+
function sideEffectsForEmber(importName) {
12+
return [
13+
`${importName}.sendEvent`,
14+
`${importName}.set`,
15+
`${importName}.setProperties`
16+
];
17+
}
18+
19+
const SIDE_EFFECTS = Object.freeze([].concat(
20+
'this.send',
21+
'this.sendAction',
22+
'this.trigger',
23+
sideEffectsForEmber('this'),
24+
sideEffectsForEmber('Em'),
25+
sideEffectsForEmber('Ember')
26+
));
1127

1228
const MESSAGE = 'Do not send events or actions in Computed Properties. This will cause data flow issues in the application, where the accessing of a property causes some side-effect. You should only send actions on behalf of user initiated events. Please see the following guide for more information: https://github.com/ember-best-practices/eslint-plugin-ember-best-practices/blob/master/guides/rules/no-side-effect-cp.md';
1329

@@ -21,45 +37,54 @@ module.exports = {
2137
message: MESSAGE
2238
},
2339
create(context) {
24-
let inCPGettter = false;
25-
let bindings = [];
26-
let emberImportBinding;
40+
let inCPGetter = false;
41+
let importedSideEffectBindings = [];
42+
let emberImportBinding, importedCPBinding;
43+
let sideEffects = SIDE_EFFECTS.slice();
2744

2845
return {
2946
ImportDeclaration(node) {
30-
bindings = collectImportBindings(node, {
47+
importedSideEffectBindings = importedSideEffectBindings.concat(collectImportBindings(node, {
3148
'@ember/object': ['set', 'setProperties'],
3249
'@ember/object/events': ['sendEvent']
33-
});
50+
}));
3451
},
3552

3653
ObjectPattern(node) {
3754
if (emberImportBinding) {
38-
SIDE_EFFECTS = SIDE_EFFECTS.concat(collectObjectPatternBindings(node, {
55+
sideEffects = sideEffects.concat(collectObjectPatternBindings(node, {
3956
[emberImportBinding]: ['set', 'setProperties', 'sendEvent']
4057
}));
4158
}
4259
},
4360

4461
ImportDefaultSpecifier(node) {
45-
emberImportBinding = getEmberImportBinding(node);
62+
let localEmberImportBinding = getEmberImportBinding(node);
63+
if (localEmberImportBinding) {
64+
emberImportBinding = localEmberImportBinding;
65+
sideEffects = sideEffects.concat(sideEffectsForEmber(emberImportBinding));
66+
}
67+
68+
if (get(node, 'parent.source.value') === '@ember/computed') {
69+
importedCPBinding = node.local.name;
70+
}
4671
},
4772

4873
FunctionExpression(node) {
49-
if (isCPGetter(node)) {
50-
inCPGettter = true;
74+
if (isCPGetter(node, emberImportBinding, importedCPBinding)) {
75+
inCPGetter = node;
5176
}
5277
},
5378
'FunctionExpression:exit'(node) {
54-
if (isCPGetter(node)) {
55-
inCPGettter = false;
79+
if (node === inCPGetter) {
80+
inCPGetter = null;
5681
}
5782
},
5883

5984
CallExpression(node) {
60-
if (inCPGettter) {
85+
if (inCPGetter) {
6186
let caller = cleanCaller(getCaller(node));
62-
let hasSideEffect = SIDE_EFFECTS.includes(caller) || bindings.includes(caller);
87+
let hasSideEffect = sideEffects.includes(caller) || importedSideEffectBindings.includes(caller);
6388
if (hasSideEffect) {
6489
context.report(node, MESSAGE);
6590
}

lib/utils/computed-property.js

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
const { get } = require('./get');
22
const { getCaller } = require('./caller');
33

4-
function isCPDesc(node, method) {
4+
function isCPDesc(node, importedEmberName, importedCPName) {
55
if (node.type !== 'FunctionExpression') {
66
return false;
77
}
88

9-
if (get(node, 'parent.key.name') !== method) {
9+
if (get(node, 'parent.key.name') !== 'get') {
1010
return false;
1111
}
1212

@@ -22,12 +22,12 @@ function isCPDesc(node, method) {
2222
return false;
2323
}
2424
let callee = greatGrandParent.callee;
25-
if (greatGrandParent.type === 'Identifier' && callee.name === 'computed') {
25+
if (greatGrandParent.type === 'Identifier' && (callee.name === 'computed' || callee.name === importedCPName)) {
2626
return true;
2727
}
2828
if (callee.type === 'MemberExpression') {
2929
let caller = getCaller(callee);
30-
return caller === 'Ember.computed' || caller === 'Em.computed';
30+
return caller === 'Ember.computed' || caller === 'Em.computed' || caller === `${importedEmberName}.computed`;
3131
}
3232
return false; // don't know how you could get here
3333
}
@@ -40,7 +40,7 @@ function isPrototypeExtCP(node) {
4040
return get(node, 'parent.property.name');
4141
}
4242

43-
function isCPAccessor(node) {
43+
function isCPAccessor(node, importedEmberName, importedCPName) {
4444
if (node.type !== 'FunctionExpression') {
4545
return false;
4646
}
@@ -51,18 +51,20 @@ function isCPAccessor(node) {
5151
}
5252

5353
let callee = parent.callee;
54-
if (callee.type === 'Identifier' && callee.name === 'computed') {
54+
if (callee.type === 'Identifier' && (callee.name === 'computed' || callee.name === importedCPName)) {
5555
return true;
5656
}
5757

5858
if (callee.type === 'MemberExpression') {
5959
let caller = getCaller(callee);
60-
return caller === 'Ember.computed' || caller === 'Em.computed';
60+
return caller === 'Ember.computed' || caller === 'Em.computed' || caller === `${importedEmberName}.computed`;
6161
}
6262
return false;
6363
}
6464

65-
module.exports = function isCPGetter(node) {
66-
return isCPDesc(node, 'get') || isCPAccessor(node) || isPrototypeExtCP(node);
65+
module.exports = function isCPGetter(node, importedEmberName, importedCPName) {
66+
return isCPDesc(node, importedEmberName, importedCPName)
67+
|| isCPAccessor(node, importedEmberName, importedCPName)
68+
|| isPrototypeExtCP(node);
6769
};
6870

tests/lib/rules/no-side-effect-cp.js

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,22 @@ ruleTester.run('no-side-efffect-cp', rule, {
2626
code: `
2727
import EmberObject from '@ember/object';
2828
export default EmberObject();`
29+
},
30+
{
31+
code: `
32+
import lodash from 'lodash';
33+
import Ember from 'ember';
34+
const { set } = lodash;
35+
export default Ember.Component({
36+
foo: 'bar',
37+
baz: false,
38+
bar: Ember.computed('foo', function() {
39+
set('baz', 'wat');
40+
})
41+
});`
2942
}
3043
],
44+
3145
invalid: [
3246
{
3347
code: `
@@ -228,6 +242,55 @@ ruleTester.run('no-side-efffect-cp', rule, {
228242
errors: [{
229243
message: MESSAGE
230244
}]
245+
},
246+
// This test must come before any others that `import Ember from 'ember'`
247+
// to illustrate the problem of the import binding being saved in module
248+
// scope.
249+
// see #91
250+
{
251+
code: `
252+
import Ember from 'ember';
253+
import SomethingElse from 'something-else';
254+
const { set } = Ember;
255+
export default Ember.Component({
256+
foo: 'bar',
257+
baz: false,
258+
bar: Ember.computed('foo', function() {
259+
set('baz', 'wat');
260+
})
261+
});`,
262+
errors: [{
263+
message: MESSAGE
264+
}]
265+
},
266+
{
267+
code: `
268+
import Ember from 'ember';
269+
const { set } = Ember;
270+
export default Ember.Component({
271+
foo: 'bar',
272+
baz: false,
273+
bar: Ember.computed('foo', function() {
274+
set('baz', 'wat');
275+
})
276+
});`,
277+
errors: [{
278+
message: MESSAGE
279+
}]
280+
},
281+
{
282+
code: `
283+
import E from 'ember';
284+
export default E.Component({
285+
foo: 'bar',
286+
baz: false,
287+
bar: E.computed('foo', function() {
288+
E.set('baz', 'wat');
289+
})
290+
});`,
291+
errors: [{
292+
message: MESSAGE
293+
}]
231294
}
232295
]
233296
});

0 commit comments

Comments
 (0)