eslint-plugin-mocha
Version:
Eslint rules for mocha.
259 lines • 10.7 kB
JavaScript
import { createMochaVisitors } from "../ast/mocha-visitors.js";
import { isFunction } from "../ast/node-types.js";
import { asRuleNode } from "../ast/rule-node.js";
import { hasUnhandledReturnPath } from "../done-callback-paths.js";
import { getIdentifierCallbackParameter } from "../mocha/callback-parameter.js";
import { getMemberExpressionBindingAndProperty, getTrackedBinding } from "../tracked-callback-reference-state.js";
function pushOperation(operationsBySegmentId, segmentId, operation) {
const operations = operationsBySegmentId.get(segmentId);
if (operations === undefined) {
return new Map([
...operationsBySegmentId,
[segmentId, [operation]]
]);
}
return new Map([
...operationsBySegmentId,
[segmentId, [...operations, operation]]
]);
}
function createTrackedCallbackFunction(trackedCallbackFunctionContext) {
const { sourceCode, trackedCallbackNodes, inheritedCallbackBinding, includeInheritedCallbackBinding, codePath, node } = trackedCallbackFunctionContext;
if (!isFunction(node)) {
return undefined;
}
const callbackParameter = trackedCallbackNodes.has(node) ? getIdentifierCallbackParameter(node) : undefined;
if (callbackParameter !== undefined) {
return {
callbackBinding: getTrackedBinding(sourceCode, callbackParameter),
callbackName: callbackParameter.name,
callbackNode: asRuleNode(callbackParameter),
codePath,
currentSegments: new Set(),
operationsBySegmentId: new Map()
};
}
if (!includeInheritedCallbackBinding || inheritedCallbackBinding === undefined) {
return undefined;
}
return {
callbackBinding: inheritedCallbackBinding,
callbackName: undefined,
callbackNode: undefined,
codePath,
currentSegments: new Set(),
operationsBySegmentId: new Map()
};
}
function recordMemberPropertyAssignment(context, recordOperation, node, memberExpression) {
const bindingAndProperty = getMemberExpressionBindingAndProperty(context.sourceCode, memberExpression);
if (bindingAndProperty === undefined) {
return;
}
recordOperation({
node,
propertyName: bindingAndProperty.propertyName,
source: null,
target: bindingAndProperty.binding,
type: 'containerPropertyAssignment'
});
}
function isDirectTrackedCallbackFunction(trackedFunction) {
return trackedFunction.callbackName !== undefined;
}
function isInheritedTrackedCallbackFunction(trackedFunction) {
return trackedFunction.callbackName === undefined;
}
function reportTrackedFunction(callbackTrackingOptions, trackedFunction) {
if (callbackTrackingOptions.includeInheritedCallbackBinding) {
const { onTrackedFunctionEnd } = callbackTrackingOptions;
onTrackedFunctionEnd(trackedFunction);
return;
}
if (isDirectTrackedCallbackFunction(trackedFunction)) {
const { onTrackedFunctionEnd } = callbackTrackingOptions;
onTrackedFunctionEnd(trackedFunction);
}
}
function isHandledCallbackObjectArgument(node, handledCallbackNodes) {
return node.type === 'ObjectExpression' && node.properties.some(function (property) {
return property.type === 'Property' && handledCallbackNodes.has(asRuleNode(property.value));
});
}
export function createTrackedCallbackVisitors(context, callbackTrackingOptions) {
const trackedCallbackNodes = new WeakSet();
const handledCallbackNodes = new WeakSet();
let currentFunction = null;
function recordOperation(operation) {
const trackedFunction = currentFunction?.tracked;
if (trackedFunction === undefined) {
return;
}
const { operationsBySegmentId: initialOperationsBySegmentId } = trackedFunction;
let operationsBySegmentId = initialOperationsBySegmentId;
for (const segment of trackedFunction.currentSegments) {
operationsBySegmentId = pushOperation(operationsBySegmentId, segment.id, operation);
}
if (currentFunction !== null) {
currentFunction = {
node: currentFunction.node,
tracked: {
...trackedFunction,
operationsBySegmentId
},
upper: currentFunction.upper
};
}
}
function updateCurrentTrackedFunction(updateTrackedFunction) {
const trackedFunction = currentFunction?.tracked;
if (currentFunction !== null && trackedFunction !== undefined) {
currentFunction = {
...currentFunction,
tracked: updateTrackedFunction(trackedFunction)
};
}
}
function trackMochaCallback(node) {
trackedCallbackNodes.add(node);
}
function trackHandledCallbackNode(node, trackedFunction) {
if (callbackTrackingOptions.trackHandledCallbackArguments &&
isInheritedTrackedCallbackFunction(trackedFunction) &&
!hasUnhandledReturnPath({
callbackBinding: trackedFunction.callbackBinding,
codePath: trackedFunction.codePath,
operationsBySegmentId: trackedFunction.operationsBySegmentId,
sourceCode: context.sourceCode
})) {
handledCallbackNodes.add(asRuleNode(node));
}
}
function hasHandledCallbackArgument(node) {
return node.arguments.some(function (argument) {
const candidate = argument.type === 'SpreadElement' ? argument.argument : argument;
if (handledCallbackNodes.has(asRuleNode(candidate))) {
return true;
}
return isHandledCallbackObjectArgument(asRuleNode(candidate), handledCallbackNodes);
});
}
return createMochaVisitors(context, {
testCaseCallback(visitorContext) {
if (visitorContext.modifier === 'pending' && callbackTrackingOptions.ignorePending) {
return;
}
trackMochaCallback(visitorContext.node);
},
hookCallback(visitorContext) {
trackMochaCallback(visitorContext.node);
},
onCodePathStart(codePath, node) {
currentFunction = {
node,
tracked: createTrackedCallbackFunction({
sourceCode: context.sourceCode,
trackedCallbackNodes,
inheritedCallbackBinding: currentFunction?.tracked?.callbackBinding,
includeInheritedCallbackBinding: callbackTrackingOptions.includeInheritedCallbackBinding,
codePath,
node
}),
upper: currentFunction
};
},
onCodePathEnd() {
const endedFunction = currentFunction;
const trackedFunction = currentFunction?.tracked;
if (trackedFunction !== undefined) {
if (endedFunction !== null) {
trackHandledCallbackNode(endedFunction.node, trackedFunction);
}
reportTrackedFunction(callbackTrackingOptions, trackedFunction);
}
currentFunction = currentFunction?.upper ?? null;
},
onCodePathSegmentStart(segment) {
updateCurrentTrackedFunction(function (trackedFunction) {
return {
...trackedFunction,
currentSegments: new Set([...trackedFunction.currentSegments, segment])
};
});
},
onCodePathSegmentEnd(segment) {
updateCurrentTrackedFunction(function (trackedFunction) {
return {
...trackedFunction,
currentSegments: new Set(Array.from(trackedFunction.currentSegments).filter(function (currentSegment) {
return currentSegment !== segment;
}))
};
});
},
'CallExpression:exit'(node) {
if (callbackTrackingOptions.trackHandledCallbackArguments && hasHandledCallbackArgument(node)) {
recordOperation({ node, type: 'callbackHandoff' });
}
recordOperation({ node, type: 'call' });
},
'VariableDeclarator:exit'(node) {
if (node.id.type !== 'Identifier') {
return;
}
recordOperation({
node,
source: node.init === null ? null : asRuleNode(node.init),
target: getTrackedBinding(context.sourceCode, node.id),
type: 'bindingAssignment'
});
},
'AssignmentExpression:exit'(node) {
if (node.left.type === 'Identifier') {
recordOperation({
node,
source: asRuleNode(node.right),
target: getTrackedBinding(context.sourceCode, node.left),
type: 'bindingAssignment'
});
return;
}
if (node.left.type !== 'MemberExpression') {
return;
}
const bindingAndProperty = getMemberExpressionBindingAndProperty(context.sourceCode, node.left);
if (bindingAndProperty === undefined) {
return;
}
recordOperation({
node,
propertyName: bindingAndProperty.propertyName,
source: asRuleNode(node.right),
target: bindingAndProperty.binding,
type: 'containerPropertyAssignment'
});
},
'UpdateExpression:exit'(node) {
if (node.argument.type === 'Identifier') {
recordOperation({
node,
source: null,
target: getTrackedBinding(context.sourceCode, node.argument),
type: 'bindingAssignment'
});
return;
}
if (node.argument.type !== 'MemberExpression') {
return;
}
recordMemberPropertyAssignment(context, recordOperation, node, node.argument);
},
'UnaryExpression:exit'(node) {
if (node.operator !== 'delete' || node.argument.type !== 'MemberExpression') {
return;
}
recordMemberPropertyAssignment(context, recordOperation, node, node.argument);
}
});
}
//# sourceMappingURL=callback-tracking.js.map