Skip to content

Commit 3a2ca0f

Browse files
committed
fix: avoid false positive involving this keyword in filterBy/mapBy value in require-computed-macros rule
1 parent 26821f0 commit 3a2ca0f

File tree

4 files changed

+79
-2
lines changed

4 files changed

+79
-2
lines changed

lib/rules/require-computed-macros.js

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const types = require('../utils/types');
44
const emberUtils = require('../utils/ember');
55
const propertyGetterUtils = require('../utils/property-getter');
66
const assert = require('assert');
7+
const scopeReferencesThis = require('../utils/scope-references-this');
78

89
//------------------------------------------------------------------------------
910
// Rule Definition
@@ -303,9 +304,15 @@ module.exports = {
303304
}
304305
} else if (propertyGetterUtils.isSimpleThisExpression(statement)) {
305306
reportSingleArg(nodeToReport, statement, 'reads');
306-
} else if (isThisPropertyFunctionCall(statement, 'filterBy')) {
307+
} else if (
308+
isThisPropertyFunctionCall(statement, 'filterBy') &&
309+
!statement.arguments.some(scopeReferencesThis)
310+
) {
307311
reportFunctionCall(nodeToReport, statement, 'filterBy');
308-
} else if (isThisPropertyFunctionCall(statement, 'mapBy')) {
312+
} else if (
313+
isThisPropertyFunctionCall(statement, 'mapBy') &&
314+
!statement.arguments.some(scopeReferencesThis)
315+
) {
309316
reportFunctionCall(nodeToReport, statement, 'mapBy');
310317
}
311318
},

lib/utils/scope-references-this.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
'use strict';
2+
3+
const Traverser = require('../utils/traverser');
4+
5+
/**
6+
* Determines whether this AST node uses the `this` of the surrounding context.
7+
*
8+
* @param {ASTNode} node
9+
* @returns {boolean}
10+
*/
11+
function scopeReferencesThis(node) {
12+
let result = false;
13+
14+
new Traverser().traverse(node, {
15+
enter(node) {
16+
switch (node.type) {
17+
case 'FunctionDeclaration':
18+
case 'FunctionExpression':
19+
this.skip();
20+
break;
21+
22+
case 'ThisExpression':
23+
result = true;
24+
this.break();
25+
break;
26+
27+
default:
28+
// Ignored.
29+
break;
30+
}
31+
},
32+
});
33+
34+
return result;
35+
}
36+
37+
module.exports = scopeReferencesThis;

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ ruleTester.run('require-computed-macros', rule, {
6868
// GT
6969
"gt('x', 123)",
7070
'computed(function() { return SOME_VAR > OTHER_VAR; })',
71+
'computed(function() { return this.x > this.y; })',
7172

7273
// GTE
7374
"gte('x', 123)",
@@ -91,14 +92,19 @@ ruleTester.run('require-computed-macros', rule, {
9192
'computed(function() { return SOME_VAR === "Hello"; })',
9293
'computed(function() { return this.prop === MY_VAR; })',
9394
"computed(function() { return this.get('prop') === MY_VAR; })",
95+
'computed(function() { return this.prop === this.otherProp; })',
9496

9597
// FILTERBY
9698
"filterBy('chores', 'done', true)",
99+
'computed(function() { return this.chores.filterBy(this.otherProp, true); })', // Ignored because value depends on function's `this`.
100+
"computed(function() { return this.chores.filterBy('done', this.otherProp); })", // Ignored because value depends on function's `this`.
97101

98102
// MAPBY
99103
"mapBy('children', 'age')",
100104
"computed(function() { return this.children?.mapBy('age'); })", // Ignored because function might not exist.
101105
"computed(function() { return this.nested?.children.mapBy('age'); })", // Ignored because function might not exist.
106+
'computed(function() { return this.children.mapBy(this.otherProp); })', // Ignored because value depends on function's `this`.
107+
'computed(function() { return this.children.mapBy(someFunction(this.otherProp)); })', // Ignored because value depends on function's `this`.
102108

103109
// Decorator (these are ignored when the `includeNativeGetters` option is off):
104110
"class Test { @computed('x') get someProp() { return this.x; } }",
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
'use strict';
2+
3+
const babelEslint = require('babel-eslint');
4+
const scopeReferencesThis = require('../../../lib/utils/scope-references-this');
5+
6+
function parse(code) {
7+
return babelEslint.parse(code).body[0];
8+
}
9+
10+
describe('scopeReferencesThis', function () {
11+
it('recognizes simple cases`', function () {
12+
expect(scopeReferencesThis(parse('this'))).toBeTruthy(); // `this` uses `this`
13+
expect(scopeReferencesThis(parse('"this"'))).toBeFalsy(); // the string "this" does not use this
14+
});
15+
16+
it('can find nested `this`', function () {
17+
expect(scopeReferencesThis(parse('if (a) { this } else { null }'))).toBeTruthy(); // if statement uses this
18+
expect(scopeReferencesThis(parse('() => this'))).toBeTruthy(); // arrow function uses outer this
19+
});
20+
21+
it('does not consider `this` within non-arrow functions', function () {
22+
expect(scopeReferencesThis(parse('function foo() { return this; }'))).toBeFalsy(); // function uses different this
23+
expect(scopeReferencesThis(parse('function foo() { return () => this; }'))).toBeFalsy(); // inner arrow function uses different this'
24+
expect(scopeReferencesThis(parse('() => function() { return this; }'))).toBeFalsy(); // inner function uses different this
25+
expect(scopeReferencesThis(parse('({ a() { this } })'))).toBeFalsy(); // object method uses different this
26+
});
27+
});

0 commit comments

Comments
 (0)