You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
293 lines
9.0 KiB
293 lines
9.0 KiB
"use strict"; |
|
|
|
Object.defineProperty(exports, "__esModule", { |
|
value: true |
|
}); |
|
exports.default = void 0; |
|
|
|
var _experimentalUtils = require("@typescript-eslint/experimental-utils"); |
|
|
|
var _utils = require("./utils"); |
|
|
|
/* |
|
* This implementation is ported from from eslint-plugin-jasmine. |
|
* MIT license, Tom Vincent. |
|
*/ |
|
|
|
/** |
|
* Async assertions might be called in Promise |
|
* methods like `Promise.x(expect1)` or `Promise.x([expect1, expect2])`. |
|
* If that's the case, Promise node have to be awaited or returned. |
|
* |
|
* @Returns CallExpressionNode |
|
*/ |
|
const getPromiseCallExpressionNode = node => { |
|
if (node.type === _experimentalUtils.AST_NODE_TYPES.ArrayExpression && node.parent && node.parent.type === _experimentalUtils.AST_NODE_TYPES.CallExpression) { |
|
node = node.parent; |
|
} |
|
|
|
if (node.type === _experimentalUtils.AST_NODE_TYPES.CallExpression && node.callee.type === _experimentalUtils.AST_NODE_TYPES.MemberExpression && (0, _utils.isSupportedAccessor)(node.callee.object) && (0, _utils.getAccessorValue)(node.callee.object) === 'Promise' && node.parent) { |
|
return node; |
|
} |
|
|
|
return null; |
|
}; |
|
|
|
const findPromiseCallExpressionNode = node => { |
|
var _node$parent; |
|
|
|
return (_node$parent = node.parent) !== null && _node$parent !== void 0 && _node$parent.parent && [_experimentalUtils.AST_NODE_TYPES.CallExpression, _experimentalUtils.AST_NODE_TYPES.ArrayExpression].includes(node.parent.type) ? getPromiseCallExpressionNode(node.parent) : null; |
|
}; |
|
|
|
const getParentIfThenified = node => { |
|
var _node$parent2; |
|
|
|
const grandParentNode = (_node$parent2 = node.parent) === null || _node$parent2 === void 0 ? void 0 : _node$parent2.parent; |
|
|
|
if (grandParentNode && grandParentNode.type === _experimentalUtils.AST_NODE_TYPES.CallExpression && (0, _utils.isExpectMember)(grandParentNode.callee) && ['then', 'catch'].includes((0, _utils.getAccessorValue)(grandParentNode.callee.property)) && grandParentNode.parent) { |
|
// Just in case `then`s are chained look one above. |
|
return getParentIfThenified(grandParentNode); |
|
} |
|
|
|
return node; |
|
}; |
|
|
|
const isAcceptableReturnNode = (node, allowReturn) => { |
|
if (allowReturn && node.type === _experimentalUtils.AST_NODE_TYPES.ReturnStatement) { |
|
return true; |
|
} |
|
|
|
if (node.type === _experimentalUtils.AST_NODE_TYPES.ConditionalExpression && node.parent) { |
|
return isAcceptableReturnNode(node.parent, allowReturn); |
|
} |
|
|
|
return [_experimentalUtils.AST_NODE_TYPES.ArrowFunctionExpression, _experimentalUtils.AST_NODE_TYPES.AwaitExpression].includes(node.type); |
|
}; |
|
|
|
const isNoAssertionsParentNode = node => node.type === _experimentalUtils.AST_NODE_TYPES.ExpressionStatement || node.type === _experimentalUtils.AST_NODE_TYPES.AwaitExpression && node.parent !== undefined && node.parent.type === _experimentalUtils.AST_NODE_TYPES.ExpressionStatement; |
|
|
|
const promiseArrayExceptionKey = ({ |
|
start, |
|
end |
|
}) => `${start.line}:${start.column}-${end.line}:${end.column}`; |
|
|
|
const defaultAsyncMatchers = ['toReject', 'toResolve']; |
|
|
|
var _default = (0, _utils.createRule)({ |
|
name: __filename, |
|
meta: { |
|
docs: { |
|
category: 'Best Practices', |
|
description: 'Enforce valid `expect()` usage', |
|
recommended: 'error' |
|
}, |
|
messages: { |
|
tooManyArgs: 'Expect takes at most {{ amount }} argument{{ s }}.', |
|
notEnoughArgs: 'Expect requires at least {{ amount }} argument{{ s }}.', |
|
modifierUnknown: 'Expect has no modifier named "{{ modifierName }}".', |
|
matcherNotFound: 'Expect must have a corresponding matcher call.', |
|
matcherNotCalled: 'Matchers must be called to assert.', |
|
asyncMustBeAwaited: 'Async assertions must be awaited{{ orReturned }}.', |
|
promisesWithAsyncAssertionsMustBeAwaited: 'Promises which return async assertions must be awaited{{ orReturned }}.' |
|
}, |
|
type: 'suggestion', |
|
schema: [{ |
|
type: 'object', |
|
properties: { |
|
alwaysAwait: { |
|
type: 'boolean', |
|
default: false |
|
}, |
|
asyncMatchers: { |
|
type: 'array', |
|
items: { |
|
type: 'string' |
|
} |
|
}, |
|
minArgs: { |
|
type: 'number', |
|
minimum: 1 |
|
}, |
|
maxArgs: { |
|
type: 'number', |
|
minimum: 1 |
|
} |
|
}, |
|
additionalProperties: false |
|
}] |
|
}, |
|
defaultOptions: [{ |
|
alwaysAwait: false, |
|
asyncMatchers: defaultAsyncMatchers, |
|
minArgs: 1, |
|
maxArgs: 1 |
|
}], |
|
|
|
create(context, [{ |
|
alwaysAwait, |
|
asyncMatchers = defaultAsyncMatchers, |
|
minArgs = 1, |
|
maxArgs = 1 |
|
}]) { |
|
// Context state |
|
const arrayExceptions = new Set(); |
|
|
|
const pushPromiseArrayException = loc => arrayExceptions.add(promiseArrayExceptionKey(loc)); |
|
/** |
|
* Promise method that accepts an array of promises, |
|
* (eg. Promise.all), will throw warnings for the each |
|
* unawaited or non-returned promise. To avoid throwing |
|
* multiple warnings, we check if there is a warning in |
|
* the given location. |
|
*/ |
|
|
|
|
|
const promiseArrayExceptionExists = loc => arrayExceptions.has(promiseArrayExceptionKey(loc)); |
|
|
|
return { |
|
CallExpression(node) { |
|
if (!(0, _utils.isExpectCall)(node)) { |
|
return; |
|
} |
|
|
|
const { |
|
expect, |
|
modifier, |
|
matcher |
|
} = (0, _utils.parseExpectCall)(node); |
|
|
|
if (expect.arguments.length < minArgs) { |
|
const expectLength = (0, _utils.getAccessorValue)(expect.callee).length; |
|
const loc = { |
|
start: { |
|
column: node.loc.start.column + expectLength, |
|
line: node.loc.start.line |
|
}, |
|
end: { |
|
column: node.loc.start.column + expectLength + 1, |
|
line: node.loc.start.line |
|
} |
|
}; |
|
context.report({ |
|
messageId: 'notEnoughArgs', |
|
data: { |
|
amount: minArgs, |
|
s: minArgs === 1 ? '' : 's' |
|
}, |
|
node, |
|
loc |
|
}); |
|
} |
|
|
|
if (expect.arguments.length > maxArgs) { |
|
const { |
|
start |
|
} = expect.arguments[maxArgs].loc; |
|
const { |
|
end |
|
} = expect.arguments[node.arguments.length - 1].loc; |
|
const loc = { |
|
start, |
|
end: { |
|
column: end.column - 1, |
|
line: end.line |
|
} |
|
}; |
|
context.report({ |
|
messageId: 'tooManyArgs', |
|
data: { |
|
amount: maxArgs, |
|
s: maxArgs === 1 ? '' : 's' |
|
}, |
|
node, |
|
loc |
|
}); |
|
} // something was called on `expect()` |
|
|
|
|
|
if (!matcher) { |
|
if (modifier) { |
|
context.report({ |
|
messageId: 'matcherNotFound', |
|
node: modifier.node.property |
|
}); |
|
} |
|
|
|
return; |
|
} |
|
|
|
if ((0, _utils.isExpectMember)(matcher.node.parent)) { |
|
context.report({ |
|
messageId: 'modifierUnknown', |
|
data: { |
|
modifierName: matcher.name |
|
}, |
|
node: matcher.node.property |
|
}); |
|
return; |
|
} |
|
|
|
if (!matcher.arguments) { |
|
context.report({ |
|
messageId: 'matcherNotCalled', |
|
node: matcher.node.property |
|
}); |
|
} |
|
|
|
const parentNode = matcher.node.parent; |
|
const shouldBeAwaited = modifier && modifier.name !== _utils.ModifierName.not || asyncMatchers.includes(matcher.name); |
|
|
|
if (!parentNode.parent || !shouldBeAwaited) { |
|
return; |
|
} |
|
/** |
|
* If parent node is an array expression, we'll report the warning, |
|
* for the array object, not for each individual assertion. |
|
*/ |
|
|
|
|
|
const isParentArrayExpression = parentNode.parent.type === _experimentalUtils.AST_NODE_TYPES.ArrayExpression; |
|
const orReturned = alwaysAwait ? '' : ' or returned'; |
|
/** |
|
* An async assertion can be chained with `then` or `catch` statements. |
|
* In that case our target CallExpression node is the one with |
|
* the last `then` or `catch` statement. |
|
*/ |
|
|
|
const targetNode = getParentIfThenified(parentNode); |
|
const finalNode = findPromiseCallExpressionNode(targetNode) || targetNode; |
|
|
|
if (finalNode.parent && // If node is not awaited or returned |
|
!isAcceptableReturnNode(finalNode.parent, !alwaysAwait) && // if we didn't warn user already |
|
!promiseArrayExceptionExists(finalNode.loc)) { |
|
context.report({ |
|
loc: finalNode.loc, |
|
data: { |
|
orReturned |
|
}, |
|
messageId: finalNode === targetNode ? 'asyncMustBeAwaited' : 'promisesWithAsyncAssertionsMustBeAwaited', |
|
node |
|
}); |
|
|
|
if (isParentArrayExpression) { |
|
pushPromiseArrayException(finalNode.loc); |
|
} |
|
} |
|
}, |
|
|
|
// nothing called on "expect()" |
|
'CallExpression:exit'(node) { |
|
if ((0, _utils.isExpectCall)(node) && isNoAssertionsParentNode(node.parent)) { |
|
context.report({ |
|
messageId: 'matcherNotFound', |
|
node |
|
}); |
|
} |
|
} |
|
|
|
}; |
|
} |
|
|
|
}); |
|
|
|
exports.default = _default; |