mirror of
https://github.com/anomalyco/opencode.git
synced 2026-07-23 18:55:37 -04:00
114 lines
4.5 KiB
JavaScript
114 lines
4.5 KiB
JavaScript
const taggedErrorMessage = {
|
|
meta: {
|
|
type: "problem",
|
|
docs: {
|
|
description: "require Effect tagged errors to expose a message",
|
|
},
|
|
messages: {
|
|
missing: "Schema.TaggedErrorClass must define a message schema field or instance message implementation.",
|
|
},
|
|
schema: [],
|
|
},
|
|
create(context) {
|
|
function visitClass(node) {
|
|
const fields = taggedErrorFields(context, node.superClass)
|
|
if (!fields) return
|
|
if (fields.properties.some(hasRequiredMessage)) return
|
|
if (node.body.body.some(hasInstanceMessage)) return
|
|
context.report({ node, messageId: "missing" })
|
|
}
|
|
|
|
return {
|
|
ClassDeclaration: visitClass,
|
|
ClassExpression: visitClass,
|
|
}
|
|
},
|
|
}
|
|
|
|
function taggedErrorFields(context, superClass) {
|
|
if (superClass?.type !== "CallExpression" || superClass.arguments.length < 2) return null
|
|
if (superClass.arguments[1].type !== "ObjectExpression") return null
|
|
const factory = superClass.callee
|
|
if (factory.type !== "CallExpression" || factory.arguments.length !== 0) return null
|
|
const taggedError = factory.callee
|
|
if (taggedError.type !== "MemberExpression" || taggedError.computed) return null
|
|
if (taggedError.object.type !== "Identifier" || !isEffectSchema(context, taggedError.object)) return null
|
|
if (taggedError.property.type !== "Identifier" || taggedError.property.name !== "TaggedErrorClass") return null
|
|
return superClass.arguments[1]
|
|
}
|
|
|
|
function isEffectSchema(context, identifier) {
|
|
const variable = findVariable(context.sourceCode.getScope(identifier), identifier.name)
|
|
if (!variable || variable.defs.length !== 1 || variable.defs[0].type !== "ImportBinding") return false
|
|
const definition = variable.defs[0]
|
|
if (definition.parent.source.value === "effect")
|
|
return definition.node.type === "ImportSpecifier" && definition.node.imported.name === "Schema"
|
|
return definition.parent.source.value === "effect/Schema" && definition.node.type === "ImportNamespaceSpecifier"
|
|
}
|
|
|
|
function findVariable(scope, name) {
|
|
return scope.set.get(name) ?? (scope.upper ? findVariable(scope.upper, name) : null)
|
|
}
|
|
|
|
function hasRequiredMessage(property) {
|
|
if (propertyName(property) !== "message" || property.type !== "Property") return false
|
|
return isStringSchema(property.value) && !isOptional(property.value)
|
|
}
|
|
|
|
function isStringSchema(node) {
|
|
if (node.type === "MemberExpression")
|
|
return (
|
|
node.object.type === "Identifier" &&
|
|
node.object.name === "Schema" &&
|
|
node.property.type === "Identifier" &&
|
|
node.property.name === "String"
|
|
)
|
|
if (node.type !== "CallExpression" || node.callee.type !== "MemberExpression") return false
|
|
return isStringSchema(node.callee.object)
|
|
}
|
|
|
|
function isOptional(node) {
|
|
if (node.type !== "CallExpression" || node.callee.type !== "MemberExpression") return false
|
|
if (node.callee.object.type === "Identifier" && node.callee.object.name === "Schema")
|
|
return node.callee.property.type === "Identifier" && node.callee.property.name === "optional"
|
|
if (node.callee.property.type !== "Identifier" || node.callee.property.name !== "pipe") return false
|
|
return node.arguments.some(
|
|
(argument) =>
|
|
argument.type === "MemberExpression" &&
|
|
argument.object.type === "Identifier" &&
|
|
argument.object.name === "Schema" &&
|
|
argument.property.type === "Identifier" &&
|
|
argument.property.name === "optional",
|
|
)
|
|
}
|
|
|
|
function propertyName(property) {
|
|
if (property.type !== "Property" && property.type !== "PropertyDefinition" && property.type !== "MethodDefinition")
|
|
return null
|
|
if (property.computed) return null
|
|
if (property.key.type === "Identifier" || property.key.type === "Literal") return property.key.name ?? property.key.value
|
|
return null
|
|
}
|
|
|
|
function hasInstanceMessage(member) {
|
|
if (member.static || propertyName(member) !== "message") return false
|
|
if (member.type === "PropertyDefinition") return member.value !== null && !isEmptyMessage(member.value)
|
|
if (member.type !== "MethodDefinition" || member.kind !== "get") return false
|
|
if (member.value.body.body.length === 0) return false
|
|
const direct = member.value.body.body.length === 1 ? member.value.body.body[0] : null
|
|
return direct?.type !== "ReturnStatement" || (direct.argument !== null && !isEmptyMessage(direct.argument))
|
|
}
|
|
|
|
function isEmptyMessage(node) {
|
|
return (node.type === "Identifier" && node.name === "undefined") || (node.type === "Literal" && !node.value)
|
|
}
|
|
|
|
export default {
|
|
meta: {
|
|
name: "opencode",
|
|
},
|
|
rules: {
|
|
"tagged-error-message": taggedErrorMessage,
|
|
},
|
|
}
|