Files

219 lines
7.4 KiB
JavaScript

import { findBinding } from "../bindings.mjs";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { collectToolCatalogSources } from "./tool-catalog-sources.mjs";
import { resolveImportSource } from "./import-source.mjs";
const root = fileURLToPath(new URL("../../../", import.meta.url));
const catalogs = collectToolCatalogSources(
resolve(root, "src/contracts/toolContracts.ts"),
);
const unwrap = (node) => {
while (
node &&
[
"TSAsExpression",
"TSSatisfiesExpression",
"TSNonNullExpression",
"ChainExpression",
].includes(node.type)
)
node = node.expression;
return node;
};
const numeric = (node) =>
(node?.type === "Literal" &&
(typeof node.value === "number" ||
(typeof node.value === "string" &&
/^(?:0|[1-9][0-9]*)$/u.test(node.value)))) ||
(node?.type === "UnaryExpression" &&
["+", "-"].includes(node.operator) &&
node.argument.type === "Literal" &&
typeof node.argument.value === "number");
export default {
meta: {
type: "problem",
docs: {
description:
"Select canonical tool contracts by name, never catalog position.",
},
schema: [],
messages: {
positional:
"Select a named tool contract; catalog ordering must not select an operation or schema.",
},
},
create(context) {
const sourceCode = context.sourceCode;
const filename = context.physicalFilename ?? context.filename;
const variable = (node) => findBinding(sourceCode, node);
const imported = (node) => {
if (node?.type !== "Identifier") return undefined;
const definition = variable(node)?.defs[0];
if (definition?.type !== "ImportBinding") return undefined;
const declaration = definition.parent ?? definition.node.parent;
const specifier = declaration.source?.value;
const path = resolveImportSource(filename, specifier)?.replace(
/\.(?:[cm]?[jt]s)$/u,
"",
);
const expected = catalogs.get(path);
if (expected === undefined) return undefined;
return { definition, expected };
};
const isCatalog = (raw, seen = new Set()) => {
const node = unwrap(raw);
if (!node) return false;
if (node.type === "Identifier") {
const binding = variable(node);
if (!binding || seen.has(binding)) return false;
seen.add(binding);
const identity = imported(node);
if (identity) {
const specifier = identity.definition.node;
return (
specifier.type === "ImportSpecifier" &&
identity.expected.has(
specifier.imported.name ?? specifier.imported.value,
)
);
}
const definition = binding.defs[0];
return (
definition?.type === "Variable" &&
definition.parent.kind === "const" &&
definition.node.id.type === "Identifier" &&
isCatalog(definition.node.init, seen)
);
}
if (node.type === "MemberExpression" || node.type === "TSQualifiedName") {
const object =
node.type === "MemberExpression" ? node.object : node.left;
const identity = imported(object);
if (identity?.definition.node.type !== "ImportNamespaceSpecifier")
return false;
const property =
node.type === "MemberExpression" ? node.property : node.right;
const name = node.computed ? property.value : property.name;
return identity.expected.has(name);
}
return false;
};
const selectedContract = (raw, seen = new Set()) => {
const node = unwrap(raw);
if (!node) return undefined;
if (
node.type === "MemberExpression" &&
node.computed &&
numeric(node.property) &&
isCatalog(node.object)
)
return node;
if (node.type === "CallExpression") {
const callee = unwrap(node.callee);
if (
callee?.type === "MemberExpression" &&
(callee.computed ? callee.property.value : callee.property.name) ===
"at" &&
numeric(node.arguments[0]) &&
isCatalog(callee.object)
)
return node;
}
if (node.type !== "Identifier") return undefined;
const binding = variable(node);
if (!binding || seen.has(binding)) return undefined;
seen.add(binding);
const definition = binding.defs[0];
if (definition?.type !== "Variable" || definition.parent.kind !== "const")
return undefined;
if (definition.node.id.type === "Identifier")
return selectedContract(definition.node.init, seen);
if (
definition.node.id.type === "ArrayPattern" &&
definition.node.id.elements.some(
(element) =>
element?.type === "Identifier" && element.name === node.name,
) &&
isCatalog(definition.node.init)
)
return definition.node;
return undefined;
};
const bindingFields = new Set(["name", "inputSchema", "outputSchema"]);
const isRegistrationOptions = (node) => {
if (node?.type !== "Identifier") return false;
const definition = variable(node)?.defs[0];
if (
definition?.type !== "ImportBinding" ||
definition.node.type !== "ImportSpecifier"
)
return false;
const declaration = definition.parent ?? definition.node.parent;
const specifier = declaration.source?.value;
const path = resolveImportSource(filename, specifier)?.replace(
/\.(?:[cm]?[jt]s)$/u,
"",
);
const name =
definition.node.imported.name ?? definition.node.imported.value;
return (
path === resolve(root, "src/server/toolRegistrationOptions") &&
[
"toolRegistrationOptions",
"applicationToolRegistrationOptions",
].includes(name)
);
};
const reported = new Set();
const reportSelection = (raw) => {
const node = selectedContract(raw);
if (node && !reported.has(node)) {
reported.add(node);
context.report({ node, messageId: "positional" });
}
};
return {
MemberExpression(node) {
const name = node.computed ? node.property.value : node.property.name;
const parent = node.parent;
const writesProperty =
(parent?.type === "AssignmentExpression" && parent.left === node) ||
parent?.type === "UpdateExpression" ||
(parent?.type === "UnaryExpression" && parent.operator === "delete");
if (bindingFields.has(name) && !writesProperty)
reportSelection(node.object);
},
CallExpression(node) {
if (isRegistrationOptions(unwrap(node.callee)))
reportSelection(node.arguments[0]);
},
VariableDeclarator(node) {
if (
node.id.type === "ObjectPattern" &&
node.id.properties.some(
(property) =>
property.type === "Property" &&
bindingFields.has(
property.computed ? property.key.value : property.key.name,
),
)
)
reportSelection(node.init);
},
TSIndexedAccessType(node) {
if (
node.objectType.type === "TSTypeQuery" &&
node.indexType.type === "TSLiteralType" &&
numeric(node.indexType.literal) &&
isCatalog(node.objectType.exprName)
)
context.report({ node, messageId: "positional" });
},
};
},
};