fix: tighten tagged error linting

This commit is contained in:
Aiden Cline 2026-06-23 17:01:06 -05:00
commit bf24cf423c
4 changed files with 140 additions and 49 deletions

View file

@ -32,23 +32,44 @@ export namespace Example { export class Error extends Schema.TaggedErrorClass<Er
},
{
name: "spread fields",
errors: 0,
errors: 1,
source: `import { Schema } from "effect"
const fields = { message: Schema.String }
class Example extends Schema.TaggedErrorClass<Example>()("Example", { ...fields }) {}`,
},
{
name: "computed fields",
errors: 0,
errors: 1,
source: `import { Schema } from "effect"
const key = "message"
class Example extends Schema.TaggedErrorClass<Example>()("Example", { [key]: Schema.String }) {}`,
},
{
name: "optional message",
errors: 1,
source: `import { Schema } from "effect"
class Example extends Schema.TaggedErrorClass<Example>()("Example", { message: Schema.optional(Schema.String) }) {}`,
},
{
name: "piped optional message",
errors: 1,
source: `import { Schema } from "effect"
class Example extends Schema.TaggedErrorClass<Example>()("Example", { message: Schema.String.pipe(Schema.optional) }) {}`,
},
{
name: "unrelated Schema binding",
errors: 0,
source: `const Schema = getSchema()
class Example extends Schema.TaggedErrorClass()("Example", {}) {}`,
},
{
name: "shadowed Schema binding",
errors: 0,
source: `import { Schema } from "effect"
function example() {
const Schema = getSchema()
return class extends Schema.TaggedErrorClass()("Example", {}) {}
}`,
},
{
name: "missing message",
@ -67,6 +88,24 @@ class Example extends Schema.TaggedErrorClass<Example>()("Example", {}) { static
errors: 1,
source: `import { Schema } from "effect"
class Example extends Schema.TaggedErrorClass<Example>()("Example", {}) { declare message: string }`,
},
{
name: "undefined property",
errors: 1,
source: `import { Schema } from "effect"
class Example extends Schema.TaggedErrorClass<Example>()("Example", {}) { override message = undefined }`,
},
{
name: "empty getter",
errors: 1,
source: `import { Schema } from "effect"
class Example extends Schema.TaggedErrorClass<Example>()("Example", {}) { override get message() { return "" } }`,
},
{
name: "non-string message schema",
errors: 1,
source: `import { Schema } from "effect"
class Example extends Schema.TaggedErrorClass<Example>()("Example", { message: Schema.Unknown }) {}`,
},
{
name: "documented disable",
@ -88,28 +127,31 @@ try {
rules: { "opencode/tagged-error-message": "error" },
}),
)
for (const [index, fixture] of cases.entries()) {
const file = path.join(directory, `${index}.ts`)
await Bun.write(file, fixture.source)
const result = Bun.spawnSync([
path.join(import.meta.dir, "../../node_modules/.bin/oxlint"),
"--config",
config,
"--format",
"json",
file,
])
if (!result.stdout.length) throw new Error(`${fixture.name}: ${result.stderr.toString()}`)
const diagnostics: unknown = JSON.parse(result.stdout.toString())
if (!hasDiagnostics(diagnostics)) throw new Error(`${fixture.name}: invalid Oxlint output`)
const errors = diagnostics.diagnostics.filter(
(item) => typeof item === "object" && item !== null && "code" in item && item.code === "opencode(tagged-error-message)",
).length
if (errors !== fixture.errors)
throw new Error(
`${fixture.name}: expected ${fixture.errors} errors, received ${errors}\n${result.stdout.toString()}`,
)
}
await Promise.all(
cases.map(async (fixture, index) => {
const file = path.join(directory, `${index}.ts`)
await Bun.write(file, fixture.source)
const result = Bun.spawnSync([
path.join(import.meta.dir, "../../node_modules/.bin/oxlint"),
"--config",
config,
"--format",
"json",
file,
])
if (!result.stdout.length) throw new Error(`${fixture.name}: ${result.stderr.toString()}`)
const diagnostics: unknown = JSON.parse(result.stdout.toString())
if (!hasDiagnostics(diagnostics)) throw new Error(`${fixture.name}: invalid Oxlint output`)
const errors = diagnostics.diagnostics.filter(isErrorDiagnostic)
if (errors.length !== fixture.errors || !errors.every(isRuleDiagnostic))
throw new Error(
`${fixture.name}: expected ${fixture.errors} errors, received ${errors.length}\n${result.stdout.toString()}`,
)
const exitCode = fixture.errors === 0 ? 0 : 1
if (result.exitCode !== exitCode || result.signalCode)
throw new Error(`${fixture.name}: expected exit ${exitCode}, received ${result.exitCode}`)
}),
)
} finally {
await rm(directory, { recursive: true, force: true })
}
@ -119,3 +161,16 @@ console.log(`Validated ${cases.length} tagged error lint fixtures`)
function hasDiagnostics(value: unknown): value is { diagnostics: unknown[] } {
return typeof value === "object" && value !== null && "diagnostics" in value && Array.isArray(value.diagnostics)
}
function isRuleDiagnostic(value: unknown) {
return (
typeof value === "object" &&
value !== null &&
"code" in value &&
value.code === "opencode(tagged-error-message)"
)
}
function isErrorDiagnostic(value: unknown) {
return typeof value === "object" && value !== null && "severity" in value && value.severity === "error"
}