import { getCallName, hasObjectProperty, isConfigOrTooling, isDeclarationFile, unwrapExpression,} from "../utils.js"; const readOnlyMutations = new Set([ "probeMcpEndpoint", "startMcpOAuth", "probeGoogleDiscovery", "startGoogleDiscoveryOAuth", "previewOpenApiSpec", "startOpenApiOAuth", "startOAuth", "resolveSecret", "detectSource", "getDomainVerificationLink", // Health-check probes: they hit the live upstream and return a result, but // persist nothing, so there is no cached query for them to invalidate. "validateConnection", "checkConnectionHealth",]); export default { meta: { type: "problem", docs: { description: "Require write mutation calls to pass reactivityKeys.", }, }, create(context) { if (isConfigOrTooling(context.filename) || isDeclarationFile(context.filename)) return {}; const mutationCalls = new Map(); return { VariableDeclarator(node) { const binding = node.id?.type === "Identifier" ? node.id.name : undefined; if (!binding) return; const call = unwrapExpression(node.init); if (call?.type !== "CallExpression" || getCallName(call.callee) !== "useAtomSet") return; if (!isPromiseMode(call.arguments[1])) return; const mutationName = getMutationName(call.arguments[0]); if (!mutationName || readOnlyMutations.has(mutationName)) return; mutationCalls.set(binding, { mutationName, node }); }, AwaitExpression(node) { const call = unwrapExpression(node.argument); if (call?.type !== "CallExpression") return; const binding = getCallName(call.callee); const mutation = mutationCalls.get(binding); if (!mutation) return; if (hasObjectProperty(call.arguments[0], "reactivityKeys")) return; context.report({ node: call, message: `Mutation ${mutation.mutationName} must pass reactivityKeys at the call site. Skill: wrdn-effect-atom-reactivity-keys.`, }); }, }; },}; function isPromiseMode(options) { const object = unwrapExpression(options); if (object?.type !== "ObjectExpression") return false; return object.properties.some((property) => { if (property.type === "SpreadElement") return false; if (property.key?.type !== "Identifier" || property.key.name !== "mode") return false; const mode = unwrapExpression(property.value); return ( (mode?.type === "Literal" || mode?.type === "StringLiteral") && (mode.value === "promise" || mode.value === "promiseExit") ); });} function getMutationName(atomExpression) { const expression = unwrapExpression(atomExpression); if (expression?.type === "Identifier") return expression.name; if (expression?.type === "CallExpression") return getCallName(expression.callee); return undefined;}
import {
getCallName,
hasObjectProperty,
isConfigOrTooling,
isDeclarationFile,
unwrapExpression,
} from "../utils.js";
const readOnlyMutations = new Set([
"probeMcpEndpoint",
"startMcpOAuth",
"probeGoogleDiscovery",
"startGoogleDiscoveryOAuth",
"previewOpenApiSpec",
"startOpenApiOAuth",
"startOAuth",
"resolveSecret",
"detectSource",
"getDomainVerificationLink",
// Health-check probes: they hit the live upstream and return a result, but
// persist nothing, so there is no cached query for them to invalidate.
"validateConnection",
"checkConnectionHealth",
]);
export default {
meta: {
type: "problem",
docs: {
description: "Require write mutation calls to pass reactivityKeys.",
},
},
create(context) {
if (isConfigOrTooling(context.filename) || isDeclarationFile(context.filename)) return {};
const mutationCalls = new Map();
return {
VariableDeclarator(node) {
const binding = node.id?.type === "Identifier" ? node.id.name : undefined;
if (!binding) return;
const call = unwrapExpression(node.init);
if (call?.type !== "CallExpression" || getCallName(call.callee) !== "useAtomSet") return;
if (!isPromiseMode(call.arguments[1])) return;
const mutationName = getMutationName(call.arguments[0]);
if (!mutationName || readOnlyMutations.has(mutationName)) return;
mutationCalls.set(binding, { mutationName, node });
},
AwaitExpression(node) {
const call = unwrapExpression(node.argument);
if (call?.type !== "CallExpression") return;
const binding = getCallName(call.callee);
const mutation = mutationCalls.get(binding);
if (!mutation) return;
if (hasObjectProperty(call.arguments[0], "reactivityKeys")) return;
context.report({
node: call,
message: `Mutation ${mutation.mutationName} must pass reactivityKeys at the call site. Skill: wrdn-effect-atom-reactivity-keys.`,
});
},
};
},
};
function isPromiseMode(options) {
const object = unwrapExpression(options);
if (object?.type !== "ObjectExpression") return false;
return object.properties.some((property) => {
if (property.type === "SpreadElement") return false;
if (property.key?.type !== "Identifier" || property.key.name !== "mode") return false;
const mode = unwrapExpression(property.value);
return (
(mode?.type === "Literal" || mode?.type === "StringLiteral") &&
(mode.value === "promise" || mode.value === "promiseExit")
);
});
}
function getMutationName(atomExpression) {
const expression = unwrapExpression(atomExpression);
if (expression?.type === "Identifier") return expression.name;
if (expression?.type === "CallExpression") return getCallName(expression.callee);
return undefined;
}