Skip to content

Commit 8ae155b

Browse files
authored
Merge pull request #840 from bmish/require-computed-macros-decorator-support
Add `includeNativeGetters` option (default false) to `require-computed-macros` rule
2 parents e20edaa + 375cf67 commit 8ae155b

File tree

3 files changed

+120
-44
lines changed

3 files changed

+120
-44
lines changed

docs/rules/require-computed-macros.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ It is preferred to use Ember's computed property macros as opposed to manually w
1111
* Reduced chance of typos
1212
* Reduced chance of missing dependencies
1313

14+
Note that by default, this rule only applies in classic classes (i.e. `Component.extend({})`) and not in native JavaScript classes with decorators (read more about the `includeNativeGetters` option in the configuration section of this doc).
15+
1416
## Rule Details
1517

1618
This rule requires using Ember's computed property macros when possible.
@@ -113,6 +115,12 @@ export default Component.extend({
113115
});
114116
```
115117

118+
## Configuration
119+
120+
This rule takes an optional object containing:
121+
122+
* `boolean` -- `includeNativeGetters` -- whether the rule should check and autofix computed properties with native getters (i.e. `@computed() get someProp() {}`) to use computed property macros (default `false`). This is off by default because in the Ember Octane world, the better improvement would be to keep the native getter and use tracked properties instead of computed properties.
123+
116124
## References
117125

118126
* [Guide](https://guides.emberjs.com/release/object-model/computed-properties/) for computed properties
@@ -121,9 +129,3 @@ export default Component.extend({
121129
## Related Rules
122130

123131
* [no-incorrect-computed-macros](no-incorrect-computed-macros.md)
124-
125-
## Help Wanted
126-
127-
| Issue | Link |
128-
| :-- | :-- |
129-
| :x: Missing native JavaScript class support | [#560](https://github.com/ember-cli/eslint-plugin-ember/issues/560) |

lib/rules/require-computed-macros.js

Lines changed: 66 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,13 @@ function getThisExpressions(nodeLogicalExpression) {
113113
return arrayOfThisExpressions.reverse();
114114
}
115115

116+
function makeFix(nodeToReplace, macro, macroArgs) {
117+
const isDecoratorUsage = types.isMethodDefinition(nodeToReplace);
118+
const prefix = isDecoratorUsage ? '@' : ''; // Decorator usage has @ symbol prefixing computed()
119+
const suffix = isDecoratorUsage ? ` ${nodeToReplace.key.name}` : ''; // Decorator usage has property name as suffix.
120+
return `${prefix}computed.${macro}(${macroArgs})${suffix}`;
121+
}
122+
116123
module.exports = {
117124
meta: {
118125
type: 'suggestion',
@@ -124,7 +131,18 @@ module.exports = {
124131
'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/require-computed-macros.md',
125132
},
126133
fixable: 'code',
127-
schema: [],
134+
schema: [
135+
{
136+
type: 'object',
137+
properties: {
138+
includeNativeGetters: {
139+
type: 'boolean',
140+
default: false,
141+
},
142+
},
143+
additionalProperties: false,
144+
},
145+
],
128146
},
129147

130148
ERROR_MESSAGE_READS,
@@ -146,7 +164,10 @@ module.exports = {
146164
message: makeErrorMessage(macro),
147165
fix(fixer) {
148166
const text = propertyGetterUtils.nodeToDependentKey(nodeWithThisExpression, context);
149-
return fixer.replaceText(nodeComputedProperty, `computed.${macro}('${text}')`);
167+
return fixer.replaceText(
168+
nodeComputedProperty,
169+
makeFix(nodeComputedProperty, macro, `'${text}'`)
170+
);
150171
},
151172
});
152173
}
@@ -164,7 +185,7 @@ module.exports = {
164185
const textRight = sourceCode.getText(nodeBinaryExpression.right);
165186
return fixer.replaceText(
166187
nodeComputedProperty,
167-
`computed.${macro}('${textLeft}', ${textRight})`
188+
makeFix(nodeComputedProperty, macro, `'${textLeft}', ${textRight}`)
168189
);
169190
},
170191
});
@@ -178,7 +199,10 @@ module.exports = {
178199
const text = getThisExpressions(nodeLogicalExpression)
179200
.map((node) => propertyGetterUtils.nodeToDependentKey(node, context))
180201
.join("', '");
181-
return fixer.replaceText(nodeComputedProperty, `computed.${macro}('${text}')`);
202+
return fixer.replaceText(
203+
nodeComputedProperty,
204+
makeFix(nodeComputedProperty, macro, `'${text}'`)
205+
);
182206
},
183207
});
184208
}
@@ -196,7 +220,7 @@ module.exports = {
196220
const restOfArgs = nodeCallExpression.arguments.map((arg) => sourceCode.getText(arg));
197221
return fixer.replaceText(
198222
nodeComputedProperty,
199-
`computed.${macro}('${arg1}', ${restOfArgs.join(', ')})`
223+
makeFix(nodeComputedProperty, macro, `'${arg1}', ${restOfArgs.join(', ')}`)
200224
);
201225
},
202226
});
@@ -209,24 +233,42 @@ module.exports = {
209233
return;
210234
}
211235

212-
if (node.arguments.length === 0) {
213-
return;
214-
}
236+
// Options:
237+
const includeNativeGetters = context.options[0] && context.options[0].includeNativeGetters;
215238

216-
const lastArg = node.arguments[node.arguments.length - 1];
217-
if (!types.isFunctionExpression(lastArg)) {
239+
let getterBody;
240+
let nodeToReport;
241+
if (
242+
node.arguments.length > 0 &&
243+
types.isFunctionExpression(node.arguments[node.arguments.length - 1])
244+
) {
245+
// Example: computed('dependentKey', function() { return this.x })
246+
getterBody = node.arguments[node.arguments.length - 1].body.body;
247+
nodeToReport = node;
248+
} else if (
249+
includeNativeGetters &&
250+
types.isDecorator(node.parent) &&
251+
types.isMethodDefinition(node.parent.parent) &&
252+
node.parent.parent.decorators.length === 1 &&
253+
node.parent.parent.kind === 'get' &&
254+
types.isFunctionExpression(node.parent.parent.value)
255+
) {
256+
// Example: @computed() get someProp() { return this.x; }
257+
getterBody = node.parent.parent.value.body.body;
258+
nodeToReport = node.parent.parent;
259+
} else {
218260
return;
219261
}
220262

221-
if (lastArg.body.body.length !== 1) {
263+
if (getterBody.length !== 1) {
222264
return;
223265
}
224266

225-
if (!types.isReturnStatement(lastArg.body.body[0])) {
267+
if (!types.isReturnStatement(getterBody[0])) {
226268
return;
227269
}
228270

229-
const statement = lastArg.body.body[0].argument;
271+
const statement = getterBody[0].argument;
230272
if (!statement) {
231273
return;
232274
}
@@ -236,35 +278,35 @@ module.exports = {
236278
statement.operator === '!' &&
237279
propertyGetterUtils.isSimpleThisExpression(statement.argument)
238280
) {
239-
reportSingleArg(node, statement.argument, 'not');
281+
reportSingleArg(nodeToReport, statement.argument, 'not');
240282
} else if (types.isLogicalExpression(statement)) {
241283
if (isSimpleThisExpressionsInsideLogicalExpression(statement, '&&')) {
242-
reportLogicalExpression(node, statement, 'and');
284+
reportLogicalExpression(nodeToReport, statement, 'and');
243285
} else if (isSimpleThisExpressionsInsideLogicalExpression(statement, '||')) {
244-
reportLogicalExpression(node, statement, 'or');
286+
reportLogicalExpression(nodeToReport, statement, 'or');
245287
}
246288
} else if (
247289
types.isBinaryExpression(statement) &&
248290
types.isLiteral(statement.right) &&
249291
propertyGetterUtils.isSimpleThisExpression(statement.left)
250292
) {
251293
if (statement.operator === '===') {
252-
reportBinaryExpression(node, statement, 'equal');
294+
reportBinaryExpression(nodeToReport, statement, 'equal');
253295
} else if (statement.operator === '>') {
254-
reportBinaryExpression(node, statement, 'gt');
296+
reportBinaryExpression(nodeToReport, statement, 'gt');
255297
} else if (statement.operator === '>=') {
256-
reportBinaryExpression(node, statement, 'gte');
298+
reportBinaryExpression(nodeToReport, statement, 'gte');
257299
} else if (statement.operator === '<') {
258-
reportBinaryExpression(node, statement, 'lt');
300+
reportBinaryExpression(nodeToReport, statement, 'lt');
259301
} else if (statement.operator === '<=') {
260-
reportBinaryExpression(node, statement, 'lte');
302+
reportBinaryExpression(nodeToReport, statement, 'lte');
261303
}
262304
} else if (propertyGetterUtils.isSimpleThisExpression(statement)) {
263-
reportSingleArg(node, statement, 'reads');
305+
reportSingleArg(nodeToReport, statement, 'reads');
264306
} else if (isThisPropertyFunctionCall(statement, 'filterBy')) {
265-
reportFunctionCall(node, statement, 'filterBy');
307+
reportFunctionCall(nodeToReport, statement, 'filterBy');
266308
} else if (isThisPropertyFunctionCall(statement, 'mapBy')) {
267-
reportFunctionCall(node, statement, 'mapBy');
309+
reportFunctionCall(nodeToReport, statement, 'mapBy');
268310
}
269311
},
270312
};

tests/lib/rules/require-computed-macros.js

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,14 @@ const {
2323
// Tests
2424
//------------------------------------------------------------------------------
2525

26-
const ruleTester = new RuleTester();
26+
const ruleTester = new RuleTester({
27+
parser: require.resolve('babel-eslint'),
28+
parserOptions: {
29+
ecmaVersion: 6,
30+
sourceType: 'module',
31+
ecmaFeatures: { legacyDecorators: true },
32+
},
33+
});
2734

2835
ruleTester.run('require-computed-macros', rule, {
2936
valid: [
@@ -91,19 +98,11 @@ ruleTester.run('require-computed-macros', rule, {
9198
// MAPBY
9299
"mapBy('children', 'age')",
93100

94-
// Decorator:
95-
{
96-
// TODO: this should be an invalid test case.
97-
// Still missing native class and decorator support: https://github.com/ember-cli/eslint-plugin-ember/issues/560
98-
code: 'class Test { @computed() get someProp() { return this.x; } }',
99-
errors: [{ message: ERROR_MESSAGE_READS, type: 'CallExpression' }],
100-
parser: require.resolve('babel-eslint'),
101-
parserOptions: {
102-
ecmaVersion: 6,
103-
sourceType: 'module',
104-
ecmaFeatures: { legacyDecorators: true },
105-
},
106-
},
101+
// Decorator (these are ignored when the `includeNativeGetters` option is off):
102+
"class Test { @computed('x') get someProp() { return this.x; } }",
103+
'class Test { @computed() get someProp() { return this.x && this.y; } }',
104+
'class Test { @computed() get someProp() { return this.x > 123; } }',
105+
"class Test { @computed() get someProp() { return this.children.mapBy('age'); } }",
107106
],
108107
invalid: [
109108
// READS
@@ -122,6 +121,18 @@ ruleTester.run('require-computed-macros', rule, {
122121
output: "computed.reads('x.y')",
123122
errors: [{ message: ERROR_MESSAGE_READS, type: 'CallExpression' }],
124123
},
124+
{
125+
code: "computed('x', function() { return this.x; })", // With dependent key.
126+
output: "computed.reads('x')",
127+
errors: [{ message: ERROR_MESSAGE_READS, type: 'CallExpression' }],
128+
},
129+
{
130+
// Decorator:
131+
code: "class Test { @computed('x') get someProp() { return this.x; } }",
132+
options: [{ includeNativeGetters: true }],
133+
output: "class Test { @computed.reads('x') someProp }",
134+
errors: [{ message: ERROR_MESSAGE_READS, type: 'MethodDefinition' }],
135+
},
125136

126137
// AND
127138
{
@@ -144,6 +155,13 @@ ruleTester.run('require-computed-macros', rule, {
144155
output: "computed.and('x', 'y.z', 'w')",
145156
errors: [{ message: ERROR_MESSAGE_AND, type: 'CallExpression' }],
146157
},
158+
{
159+
// Decorator:
160+
code: 'class Test { @computed() get someProp() { return this.x && this.y; } }',
161+
options: [{ includeNativeGetters: true }],
162+
output: "class Test { @computed.and('x', 'y') someProp }",
163+
errors: [{ message: ERROR_MESSAGE_AND, type: 'MethodDefinition' }],
164+
},
147165

148166
// OR
149167
{
@@ -158,6 +176,13 @@ ruleTester.run('require-computed-macros', rule, {
158176
output: "computed.gt('x', 123)",
159177
errors: [{ message: ERROR_MESSAGE_GT, type: 'CallExpression' }],
160178
},
179+
{
180+
// Decorator:
181+
code: 'class Test { @computed() get someProp() { return this.x > 123; } }',
182+
options: [{ includeNativeGetters: true }],
183+
output: "class Test { @computed.gt('x', 123) someProp }",
184+
errors: [{ message: ERROR_MESSAGE_GT, type: 'MethodDefinition' }],
185+
},
161186

162187
// GTE
163188
{
@@ -217,5 +242,12 @@ ruleTester.run('require-computed-macros', rule, {
217242
output: "computed.mapBy('nested.children', 'age')",
218243
errors: [{ message: ERROR_MESSAGE_MAP_BY, type: 'CallExpression' }],
219244
},
245+
{
246+
// Decorator:
247+
code: "class Test { @computed() get someProp() { return this.children.mapBy('age'); } }",
248+
options: [{ includeNativeGetters: true }],
249+
output: "class Test { @computed.mapBy('children', 'age') someProp }",
250+
errors: [{ message: ERROR_MESSAGE_MAP_BY, type: 'MethodDefinition' }],
251+
},
220252
],
221253
});

0 commit comments

Comments
 (0)