Skip to content

Commit 1267a6c

Browse files
committed
feat: add decorator support to no-side-effects rule
This is a major rewrite/refactor/simplification of the rule in order to support decorators and other future improvements.
1 parent 9895554 commit 1267a6c

File tree

5 files changed

+142
-47
lines changed

5 files changed

+142
-47
lines changed

docs/rules/no-side-effects.md

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,14 @@
22

33
:white_check_mark: The `"extends": "plugin:ember/recommended"` property in a configuration file enables this rule.
44

5-
Don't introduce side-effects in computed properties.
5+
When using computed properties, do not introduce side effects. Side effects make it much more difficult to reason about the origin of changes.
66

7-
When using computed properties do not introduce side effects. It will make reasoning about the origin of the change much harder.
7+
This rule currently disallows the following side-effect-causing statements inside computed properties:
8+
9+
* `this.set('x', 123);`
10+
* `this.setProperties({ x: 123 });`
11+
12+
Note that other side effects like network requests should be avoided as well.
813

914
## Examples
1015

@@ -35,9 +40,3 @@ export default Component.extend({
3540
})
3641
});
3742
```
38-
39-
## Help Wanted
40-
41-
| Issue | Link |
42-
| :-- | :-- |
43-
| :x: Missing native JavaScript class support | [#560](https://github.com/ember-cli/eslint-plugin-ember/issues/560) |

lib/rules/no-side-effects.js

Lines changed: 43 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22

33
const types = require('../utils/types');
44
const ember = require('../utils/ember');
5+
const computedPropertyUtils = require('../utils/computed-properties');
6+
const emberUtils = require('../utils/ember');
7+
const Traverser = require('../utils/traverser');
58

69
//------------------------------------------------------------------------------
710
// General rule - Don't introduce side-effects in computed properties
@@ -11,6 +14,40 @@ function isUnallowedMethod(name) {
1114
return ['set', 'setProperties'].includes(name);
1215
}
1316

17+
function isEmberSet(node, emberImportAliasName) {
18+
const callee = node.callee;
19+
return (
20+
(types.isIdentifier(callee) && isUnallowedMethod(callee.name)) ||
21+
(types.isMemberExpression(callee) &&
22+
(types.isThisExpression(callee.object) ||
23+
callee.object.name === 'Ember' ||
24+
callee.object.name === emberImportAliasName) &&
25+
types.isIdentifier(callee.property) &&
26+
isUnallowedMethod(callee.property.name))
27+
);
28+
}
29+
30+
/**
31+
* Recursively finds calls that could be side effects in a computed property function body.
32+
*
33+
* @param {ASTNode} computedPropertyBody body of computed property to search
34+
* @param {string} emberImportAliasName
35+
* @returns {Array<ASTNode>}
36+
*/
37+
function findSideEffects(computedPropertyBody, emberImportAliasName) {
38+
const results = [];
39+
40+
new Traverser().traverse(computedPropertyBody, {
41+
enter(child) {
42+
if (isEmberSet(child, emberImportAliasName)) {
43+
results.push(child);
44+
}
45+
},
46+
});
47+
48+
return results;
49+
}
50+
1451
const ERROR_MESSAGE = "Don't introduce side-effects in computed properties";
1552

1653
module.exports = {
@@ -44,33 +81,13 @@ module.exports = {
4481
},
4582

4683
CallExpression(node) {
47-
const callee = node.callee;
48-
const isEmberSet =
49-
(types.isIdentifier(callee) && isUnallowedMethod(callee.name)) ||
50-
(types.isMemberExpression(callee) &&
51-
(types.isThisExpression(callee.object) ||
52-
callee.object.name === 'Ember' ||
53-
callee.object.name === emberImportAliasName) &&
54-
types.isIdentifier(callee.property) &&
55-
isUnallowedMethod(callee.property.name));
56-
57-
if (isEmberSet) {
58-
const ancestors = context.getAncestors();
59-
const computedIndex = ancestors.findIndex(ember.isComputedProp);
60-
const setPropertyFunctionIndex = ancestors.findIndex(
61-
(ancestor) =>
62-
ancestor.type === 'Property' &&
63-
ancestor.key.name === 'set' &&
64-
types.isFunctionExpression(ancestor.value)
65-
);
66-
67-
if (
68-
computedIndex > -1 &&
69-
(setPropertyFunctionIndex === -1 || setPropertyFunctionIndex < computedIndex)
70-
) {
71-
report(node);
72-
}
84+
if (!emberUtils.isComputedProp(node)) {
85+
return;
7386
}
87+
88+
const computedPropertyBody = computedPropertyUtils.getComputedPropertyFunctionBody(node);
89+
90+
findSideEffects(computedPropertyBody, emberImportAliasName).forEach(report);
7491
},
7592
};
7693
},

lib/rules/require-computed-property-dependencies.js

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,6 @@
11
'use strict';
22

3-
function getTraverser() {
4-
let traverser;
5-
try {
6-
// eslint-disable-next-line node/no-unpublished-require
7-
traverser = require('eslint/lib/shared/traverser'); // eslint >= 6
8-
} catch (error) {
9-
// eslint-disable-next-line node/no-unpublished-require, node/no-missing-require
10-
traverser = require('eslint/lib/util/traverser'); // eslint < 6
11-
}
12-
return traverser;
13-
}
14-
15-
const Traverser = getTraverser();
3+
const Traverser = require('../utils/traverser');
164
const emberUtils = require('../utils/ember');
175
const computedPropertyUtils = require('../utils/computed-properties');
186
const types = require('../utils/types');

lib/utils/traverser.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
'use strict';
2+
3+
module.exports = getTraverser;
4+
5+
function getTraverser() {
6+
let traverser;
7+
try {
8+
// eslint-disable-next-line node/no-unpublished-require
9+
traverser = require('eslint/lib/shared/traverser'); // eslint >= 6
10+
} catch (error) {
11+
// eslint-disable-next-line node/no-unpublished-require, node/no-missing-require
12+
traverser = require('eslint/lib/util/traverser'); // eslint < 6
13+
}
14+
return traverser;
15+
}

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

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,32 @@ eslintTester.run('no-side-effects', rule, {
2525
'import Ember from "ember"; import Foo from "some-other-thing"; let foo = computed("test", function() { Foo.set(this, "testAmount", test.length); return ""; });',
2626

2727
'import Ember from "ember"; import Foo from "some-other-thing"; let foo = computed("test", function() { Foo.setProperties(); return ""; });',
28+
29+
// Decorators:
30+
{
31+
code: `
32+
class Test {
33+
@computed('first', 'last')
34+
get fullName() { return this.first + ' ' + this.last; }
35+
}
36+
`,
37+
parser: require.resolve('babel-eslint'),
38+
parserOptions: {
39+
ecmaVersion: 6,
40+
sourceType: 'module',
41+
ecmaFeatures: { legacyDecorators: true },
42+
},
43+
},
44+
45+
// No computed property function body;
46+
'computed()',
47+
'computed("test")',
48+
'computed("test", function() {})',
49+
50+
// Not in a computed property:
51+
"this.set('x', 123);",
52+
'this.setProperties({ x: 123 });',
53+
'this.x = 123;',
2854
],
2955
invalid: [
3056
{
@@ -180,5 +206,55 @@ eslintTester.run('no-side-effects', rule, {
180206
},
181207
],
182208
},
209+
210+
// Decorator with getter inside object parameter:
211+
{
212+
code: `
213+
class Test {
214+
@computed('key', {
215+
get() {
216+
this.set('x', 123);
217+
},
218+
set(key, value) {}
219+
})
220+
someProp
221+
}
222+
`,
223+
output: null,
224+
errors: [
225+
{
226+
message: ERROR_MESSAGE,
227+
type: 'CallExpression',
228+
},
229+
],
230+
parser: require.resolve('babel-eslint'),
231+
parserOptions: {
232+
ecmaVersion: 6,
233+
sourceType: 'module',
234+
ecmaFeatures: { legacyDecorators: true },
235+
},
236+
},
237+
// Decorator with getter function:
238+
{
239+
code: `
240+
class Test {
241+
@computed()
242+
get someProp() { this.set('x', 123); }
243+
}
244+
`,
245+
output: null,
246+
errors: [
247+
{
248+
message: ERROR_MESSAGE,
249+
type: 'CallExpression',
250+
},
251+
],
252+
parser: require.resolve('babel-eslint'),
253+
parserOptions: {
254+
ecmaVersion: 6,
255+
sourceType: 'module',
256+
ecmaFeatures: { legacyDecorators: true },
257+
},
258+
},
183259
],
184260
});

0 commit comments

Comments
 (0)