Skip to content

Commit 701279c

Browse files
gurgundayscagoodbrettz9
authored
feat(always-return): add ignoreAssignmentVariable option (#518)
Co-authored-by: Sebastian Good <2230835+scagood@users.noreply.github.com> Co-authored-by: Brett Zamir <brettz9@yahoo.com>
1 parent e545254 commit 701279c

File tree

3 files changed

+135
-3
lines changed

3 files changed

+135
-3
lines changed

__tests__/always-return.js

+38
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,16 @@ ruleTester.run('always-return', rule, {
105105
.finally(() => console.error('end'))`,
106106
options: [{ ignoreLastCallback: true }],
107107
},
108+
`hey.then(x => { globalThis = x })`,
109+
`hey.then(x => { globalThis[a] = x })`,
110+
`hey.then(x => { globalThis.a = x })`,
111+
`hey.then(x => { globalThis.a.n = x })`,
112+
`hey.then(x => { globalThis[12] = x })`,
113+
`hey.then(x => { globalThis['12']["test"] = x })`,
114+
{
115+
code: `hey.then(x => { window['x'] = x })`,
116+
options: [{ ignoreAssignmentVariable: ['globalThis', 'window'] }],
117+
},
108118
],
109119

110120
invalid: [
@@ -230,5 +240,33 @@ ruleTester.run('always-return', rule, {
230240
options: [{ ignoreLastCallback: true }],
231241
errors: [{ message }],
232242
},
243+
{
244+
code: `hey.then(x => { invalid = x })`,
245+
errors: [{ message }],
246+
},
247+
{
248+
code: `hey.then(x => { invalid['x'] = x })`,
249+
errors: [{ message }],
250+
},
251+
{
252+
code: `hey.then(x => { windo[x] = x })`,
253+
options: [{ ignoreAssignmentVariable: ['window'] }],
254+
errors: [{ message }],
255+
},
256+
{
257+
code: `hey.then(x => { windo['x'] = x })`,
258+
options: [{ ignoreAssignmentVariable: ['window'] }],
259+
errors: [{ message }],
260+
},
261+
{
262+
code: `hey.then(x => { windows['x'] = x })`,
263+
options: [{ ignoreAssignmentVariable: ['window'] }],
264+
errors: [{ message }],
265+
},
266+
{
267+
code: `hey.then(x => { x() })`,
268+
options: [{ ignoreAssignmentVariable: ['window'] }],
269+
errors: [{ message }],
270+
},
233271
],
234272
})

docs/rules/always-return.md

+43-3
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,9 @@ myPromise.then((b) => {
4949

5050
##### `ignoreLastCallback`
5151

52-
You can pass an `{ ignoreLastCallback: true }` as an option to this rule to the
53-
last `then()` callback in a promise chain does not warn if it does not have a
54-
`return`. Default is `false`.
52+
You can pass an `{ ignoreLastCallback: true }` as an option to this rule so that
53+
the last `then()` callback in a promise chain does not warn if it does not have
54+
a `return`. Default is `false`.
5555

5656
```js
5757
// OK
@@ -92,3 +92,43 @@ function foo() {
9292
})
9393
}
9494
```
95+
96+
##### `ignoreAssignmentVariable`
97+
98+
You can pass an `{ ignoreAssignmentVariable: [] }` as an option to this rule
99+
with a list of variable names so that the last `then()` callback in a promise
100+
chain does not warn if it does an assignment to a global variable. Default is
101+
`["globalThis"]`.
102+
103+
```js
104+
/* eslint promise/always-return: ["error", { ignoreAssignmentVariable: ["globalThis"] }] */
105+
106+
// OK
107+
promise.then((x) => {
108+
globalThis = x
109+
})
110+
111+
promise.then((x) => {
112+
globalThis.x = x
113+
})
114+
115+
// OK
116+
promise.then((x) => {
117+
globalThis.x.y = x
118+
})
119+
120+
// NG
121+
promise.then((x) => {
122+
anyOtherVariable = x
123+
})
124+
125+
// NG
126+
promise.then((x) => {
127+
anyOtherVariable.x = x
128+
})
129+
130+
// NG
131+
promise.then((x) => {
132+
x()
133+
})
134+
```

rules/always-return.js

+54
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,36 @@ function peek(arr) {
124124
return arr[arr.length - 1]
125125
}
126126

127+
/**
128+
* Gets the root object name for a MemberExpression or Identifier.
129+
* @param {Node} node
130+
* @returns {string | undefined}
131+
*/
132+
function getRootObjectName(node) {
133+
if (node.type === 'Identifier') {
134+
return node.name
135+
}
136+
// istanbul ignore else (fallback)
137+
if (node.type === 'MemberExpression') {
138+
return getRootObjectName(node.object)
139+
}
140+
}
141+
142+
/**
143+
* Checks if the node is an assignment to an ignored variable.
144+
* @param {Node} node
145+
* @param {string[]} ignoredVars
146+
* @returns {boolean}
147+
*/
148+
function isIgnoredAssignment(node, ignoredVars) {
149+
if (node.type !== 'ExpressionStatement') return false
150+
const expr = node.expression
151+
if (expr.type !== 'AssignmentExpression') return false
152+
const left = expr.left
153+
const rootName = getRootObjectName(left)
154+
return ignoredVars.includes(rootName)
155+
}
156+
127157
module.exports = {
128158
meta: {
129159
type: 'problem',
@@ -139,6 +169,14 @@ module.exports = {
139169
ignoreLastCallback: {
140170
type: 'boolean',
141171
},
172+
ignoreAssignmentVariable: {
173+
type: 'array',
174+
items: {
175+
type: 'string',
176+
pattern: '^[\\w$]+$',
177+
},
178+
uniqueItems: true,
179+
},
142180
},
143181
additionalProperties: false,
144182
},
@@ -150,6 +188,10 @@ module.exports = {
150188
create(context) {
151189
const options = context.options[0] || {}
152190
const ignoreLastCallback = !!options.ignoreLastCallback
191+
const ignoreAssignmentVariable = options.ignoreAssignmentVariable || [
192+
'globalThis',
193+
]
194+
153195
/**
154196
* @typedef {object} FuncInfo
155197
* @property {string[]} branchIDStack This is a stack representing the currently
@@ -244,6 +286,18 @@ module.exports = {
244286
return
245287
}
246288

289+
if (
290+
ignoreAssignmentVariable.length &&
291+
isLastCallback(node) &&
292+
node.body?.type === 'BlockStatement'
293+
) {
294+
for (const statement of node.body.body) {
295+
if (isIgnoredAssignment(statement, ignoreAssignmentVariable)) {
296+
return
297+
}
298+
}
299+
}
300+
247301
path.finalSegments.forEach((segment) => {
248302
const id = segment.id
249303
const branch = funcInfo.branchInfoMap[id]

0 commit comments

Comments
 (0)