Skip to content

Commit 6ee87d3

Browse files
authored
Merge pull request #883 from bmish/no-assignment-of-untracked-properties-used-in-tracking-contexts-macros
Handle computed property macros in `no-assignment-of-untracked-properties-used-in-tracking-contexts` rule
2 parents ae38932 + c4fcda2 commit 6ee87d3

10 files changed

+629
-94
lines changed

lib/rules/no-assignment-of-untracked-properties-used-in-tracking-contexts.js

Lines changed: 32 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -3,93 +3,34 @@
33
const emberUtils = require('../utils/ember');
44
const types = require('../utils/types');
55
const decoratorUtils = require('../utils/decorators');
6-
const javascriptUtils = require('../utils/javascript');
76
const propertySetterUtils = require('../utils/property-setter');
8-
const assert = require('assert');
97
const { getImportIdentifier } = require('../utils/import');
8+
const { getMacros } = require('../utils/computed-property-macros');
109
const {
11-
expandKeys,
10+
findComputedPropertyDependentKeys,
1211
keyExistsAsPrefixInList,
1312
} = require('../utils/computed-property-dependent-keys');
1413

1514
const ERROR_MESSAGE =
1615
"Use `set(this, 'propertyName', 'value')` instead of assignment for untracked properties that are used as computed property dependencies (or convert to using tracked properties).";
1716

1817
/**
19-
* Gets the list of string dependent keys from a computed property.
20-
*
21-
* @param {Node} node - the computed property node
22-
* @returns {String[]} - the list of string dependent keys from this computed property
23-
*/
24-
function getComputedPropertyDependentKeys(node) {
25-
if (!node.arguments) {
26-
return [];
27-
}
28-
29-
return expandKeys(
30-
node.arguments
31-
.filter((arg) => arg.type === 'Literal' && typeof arg.value === 'string')
32-
.map((node) => node.value)
33-
);
34-
}
35-
36-
/**
37-
* Gets a list of computed property dependency keys used inside a class.
18+
* Gets a set of tracked properties used inside a class.
3819
*
3920
* @param {Node} nodeClass - Node for the class
40-
* @returns {String[]} - list of dependent keys used inside the class
41-
*/
42-
function findComputedPropertyDependentKeys(nodeClass, computedImportName) {
43-
if (types.isClassDeclaration(nodeClass)) {
44-
// Native JS class.
45-
return javascriptUtils.flatMap(nodeClass.body.body, (node) => {
46-
const computedDecorator = decoratorUtils.findDecorator(node, computedImportName);
47-
if (computedDecorator) {
48-
return getComputedPropertyDependentKeys(computedDecorator.expression);
49-
} else {
50-
return [];
51-
}
52-
});
53-
} else if (types.isCallExpression(nodeClass)) {
54-
// Classic class.
55-
return javascriptUtils.flatMap(
56-
nodeClass.arguments.filter(types.isObjectExpression),
57-
(classObject) => {
58-
return javascriptUtils.flatMap(classObject.properties, (node) => {
59-
if (
60-
types.isProperty(node) &&
61-
emberUtils.isComputedProp(node.value) &&
62-
node.value.arguments
63-
) {
64-
return getComputedPropertyDependentKeys(node.value);
65-
} else {
66-
return [];
67-
}
68-
});
69-
}
70-
);
71-
} else {
72-
assert(false, 'Unexpected node type for a class.');
73-
}
74-
75-
return [];
76-
}
77-
78-
/**
79-
* Gets a list of tracked properties used inside a class.
80-
*
81-
* @param {Node} nodeClass - Node for the class
82-
* @returns {String[]} - list of tracked properties used inside the class
21+
* @returns {Set<string>} - set of tracked properties used inside the class
8322
*/
8423
function findTrackedProperties(nodeClassDeclaration, trackedImportName) {
85-
return nodeClassDeclaration.body.body
86-
.filter(
87-
(node) =>
88-
types.isClassProperty(node) &&
89-
decoratorUtils.hasDecorator(node, trackedImportName) &&
90-
types.isIdentifier(node.key)
91-
)
92-
.map((node) => node.key.name);
24+
return new Set(
25+
nodeClassDeclaration.body.body
26+
.filter(
27+
(node) =>
28+
types.isClassProperty(node) &&
29+
decoratorUtils.hasDecorator(node, trackedImportName) &&
30+
types.isIdentifier(node.key)
31+
)
32+
.map((node) => node.key.name)
33+
);
9334
}
9435

9536
class Stack {
@@ -137,6 +78,7 @@ module.exports = {
13778
let computedImportName = undefined;
13879
let trackedImportName = undefined;
13980
let setImportName = undefined;
81+
let macroImportNames = new Map();
14082

14183
// State being tracked for the current class we're inside.
14284
const classStack = new Stack();
@@ -147,6 +89,14 @@ module.exports = {
14789
computedImportName =
14890
computedImportName || getImportIdentifier(node, '@ember/object', 'computed');
14991
setImportName = setImportName || getImportIdentifier(node, '@ember/object', 'set');
92+
} else if (node.source.value === '@ember/object/computed') {
93+
macroImportNames = new Map(
94+
getMacros().map((macro) => [
95+
macro,
96+
macroImportNames.get(macro) ||
97+
getImportIdentifier(node, '@ember/object/computed', macro),
98+
])
99+
);
150100
} else if (node.source.value === '@glimmer/tracking') {
151101
trackedImportName =
152102
trackedImportName || getImportIdentifier(node, '@glimmer/tracking', 'tracked');
@@ -156,12 +106,14 @@ module.exports = {
156106
// Native JS class:
157107
ClassDeclaration(node) {
158108
// Gather computed property dependent keys from this class.
159-
const computedPropertyDependentKeys = new Set(
160-
findComputedPropertyDependentKeys(node, computedImportName)
109+
const computedPropertyDependentKeys = findComputedPropertyDependentKeys(
110+
node,
111+
computedImportName,
112+
macroImportNames
161113
);
162114

163115
// Gather tracked properties from this class.
164-
const trackedProperties = new Set(findTrackedProperties(node, trackedImportName));
116+
const trackedProperties = findTrackedProperties(node, trackedImportName);
165117

166118
// Keep track of whether we're inside a Glimmer component.
167119
const isGlimmerComponent = emberUtils.isGlimmerComponent(context, node);
@@ -178,8 +130,10 @@ module.exports = {
178130
// Classic class:
179131
if (emberUtils.isAnyEmberCoreModule(context, node)) {
180132
// Gather computed property dependent keys from this class.
181-
const computedPropertyDependentKeys = new Set(
182-
findComputedPropertyDependentKeys(node, computedImportName)
133+
const computedPropertyDependentKeys = findComputedPropertyDependentKeys(
134+
node,
135+
computedImportName,
136+
macroImportNames
183137
);
184138

185139
// No tracked properties in classic classes.

lib/utils/computed-property-dependent-keys.js

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,22 @@
11
'use strict';
22

33
const javascriptUtils = require('./javascript');
4+
const { getName } = require('../utils/utils');
5+
const types = require('../utils/types');
6+
const decoratorUtils = require('../utils/decorators');
7+
const assert = require('assert');
8+
const {
9+
getMacrosFromImportNames,
10+
getTrackedArgumentCount,
11+
macroToCanonicalName,
12+
} = require('../utils/computed-property-macros');
413

514
module.exports = {
615
collapseKeys,
716
expandKeys,
817
expandKey,
18+
findComputedPropertyDependentKeys,
19+
getComputedPropertyDependentKeys,
920
computedPropertyDependencyMatchesKeyPath,
1021
keyExistsAsPrefixInList,
1122
};
@@ -161,3 +172,99 @@ function computedPropertyDependencyMatchesKeyPath(dependency, keyPath) {
161172
function keyExistsAsPrefixInList(keys, key) {
162173
return keys.some((currentKey) => computedPropertyDependencyMatchesKeyPath(currentKey, key));
163174
}
175+
176+
/**
177+
* Gets the set of computed property dependency keys used inside a class.
178+
*
179+
* @param {Node} nodeClass - Node for the class
180+
* @returns {Set<string>} - set of dependent keys used inside the class
181+
*/
182+
function findComputedPropertyDependentKeys(nodeClass, computedImportName, macroImportNames) {
183+
if (types.isClassDeclaration(nodeClass)) {
184+
// Native JS class.
185+
return new Set(
186+
javascriptUtils.flatMap(nodeClass.body.body, (node) => {
187+
// Check for `computed` itself.
188+
const computedDecorator = decoratorUtils.findDecorator(node, computedImportName);
189+
if (computedDecorator) {
190+
return getComputedPropertyDependentKeys(computedDecorator.expression);
191+
}
192+
193+
// Check for a computed macro.
194+
const macroNames = getMacrosFromImportNames(computedImportName, macroImportNames);
195+
const macroDecorator = decoratorUtils.findDecoratorByNameCallback(node, (decoratorName) =>
196+
macroNames.has(decoratorName)
197+
);
198+
if (macroDecorator) {
199+
return getComputedPropertyDependentKeys(
200+
macroDecorator.expression,
201+
getTrackedArgumentCount(
202+
macroToCanonicalName(
203+
decoratorUtils.getDecoratorName(macroDecorator),
204+
macroImportNames
205+
)
206+
)
207+
);
208+
}
209+
210+
return [];
211+
})
212+
);
213+
} else if (types.isCallExpression(nodeClass)) {
214+
// Classic class.
215+
return new Set(
216+
javascriptUtils.flatMap(
217+
nodeClass.arguments.filter(types.isObjectExpression),
218+
(classObject) => {
219+
return javascriptUtils.flatMap(classObject.properties, (node) => {
220+
if (types.isProperty(node)) {
221+
const name = getName(node.value);
222+
if (!name) {
223+
return [];
224+
}
225+
226+
// Check for `computed` itself.
227+
if (name === computedImportName) {
228+
return getComputedPropertyDependentKeys(node.value);
229+
}
230+
231+
// Check for a computed macro.
232+
const macroNames = getMacrosFromImportNames(computedImportName, macroImportNames);
233+
if (macroNames.has(name)) {
234+
return getComputedPropertyDependentKeys(
235+
node.value,
236+
getTrackedArgumentCount(macroToCanonicalName(name, macroImportNames))
237+
);
238+
}
239+
}
240+
return [];
241+
});
242+
}
243+
)
244+
);
245+
} else {
246+
assert(false, 'Unexpected node type for a class.');
247+
}
248+
249+
return new Set();
250+
}
251+
252+
/**
253+
* Gets the list of string dependent keys from a computed property.
254+
*
255+
* @param {Node} node - the computed property node
256+
* @param {number} dependentKeyArgumentCount - number of arguments to check for dependent keys
257+
* @returns {String[]} - the list of string dependent keys from this computed property
258+
*/
259+
function getComputedPropertyDependentKeys(node, dependentKeyArgumentCount = Number.MAX_VALUE) {
260+
if (!node.arguments) {
261+
return [];
262+
}
263+
264+
return expandKeys(
265+
node.arguments
266+
.slice(0, dependentKeyArgumentCount)
267+
.filter((arg) => arg.type === 'Literal' && typeof arg.value === 'string')
268+
.map((node) => node.value)
269+
);
270+
}

lib/utils/computed-property-macros.js

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
const assert = require('assert');
2+
3+
module.exports = {
4+
getMacros,
5+
getMacrosFromImportNames,
6+
getTrackedArgumentCount,
7+
macroToCanonicalName,
8+
};
9+
10+
/**
11+
* Example macros:
12+
* - and('x', 'y', 'z') can have any number of tracked dependent key arguments
13+
* - mapBy('chores', 'done', true) only has a single tracked dependent key argument
14+
*/
15+
const MACROS_TO_TRACKED_ARGUMENT_COUNT = {
16+
alias: 1,
17+
and: Number.MAX_VALUE,
18+
bool: 1,
19+
collect: Number.MAX_VALUE,
20+
deprecatingAlias: 1,
21+
empty: 1,
22+
equal: 1,
23+
filter: 1,
24+
filterBy: 1,
25+
gt: 1,
26+
gte: 1,
27+
intersect: Number.MAX_VALUE,
28+
lt: 1,
29+
lte: 1,
30+
map: 1,
31+
mapBy: 1,
32+
match: 1,
33+
max: 1,
34+
min: 1,
35+
none: 1,
36+
not: 1,
37+
notEmpty: 1,
38+
oneWay: 1,
39+
or: Number.MAX_VALUE,
40+
readOnly: 1,
41+
reads: 1,
42+
setDiff: 2,
43+
sort: 1,
44+
sum: Number.MAX_VALUE,
45+
union: Number.MAX_VALUE,
46+
uniq: 1,
47+
uniqBy: 1,
48+
};
49+
50+
/**
51+
* @returns {string[]} - a list of all macros.
52+
*/
53+
function getMacros() {
54+
return Object.keys(MACROS_TO_TRACKED_ARGUMENT_COUNT);
55+
}
56+
57+
/**
58+
* Example of return value: ['and', 'readOnly', 'computed.and', 'computed.readOnly', ...]
59+
*
60+
* @param {string} computedImportName - name that computed is imported under
61+
* @param {set<string>} macroImportNames
62+
* @returns {set<string>} - a set containing all possible macro names to check for.
63+
*/
64+
function getMacrosFromImportNames(computedImportName, macroImportNames) {
65+
return new Set([
66+
...macroImportNames.values(),
67+
...(computedImportName ? getMacros().map((macro) => `${computedImportName}.${macro}`) : []),
68+
]);
69+
}
70+
71+
/**
72+
* @param {string} macro
73+
* @returns {number} - the number arguments that are tracked dependent keys for a given macro.
74+
*/
75+
function getTrackedArgumentCount(macro) {
76+
assert(typeof macro === 'string', 'macro parameter should be a string');
77+
78+
return MACROS_TO_TRACKED_ARGUMENT_COUNT[macro];
79+
}
80+
81+
/**
82+
* @param {string} macro - imported macro name
83+
* @param {map<string,string>} macroImportNames
84+
* @returns {string} the original name of the imported macro
85+
*/
86+
function macroToCanonicalName(macro, macroImportNames) {
87+
assert(typeof macro === 'string', 'macro parameter should be a string');
88+
89+
return macro.includes('.')
90+
? macro.slice(macro.lastIndexOf('.') + 1) // Removes the leading `computed.`
91+
: [...macroImportNames.keys()].find(
92+
(canonicalName) => macroImportNames.get(canonicalName) === macro
93+
);
94+
}

0 commit comments

Comments
 (0)