From d16d8a0de217b20e472a781466437d83ebff3fbc Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Thu, 23 Jul 2026 21:57:49 -0400 Subject: [PATCH 1/2] format: align isIdentifier with the identifier schema pattern The `isIdentifier` type guard copied the identifier schema's regex with its YAML escaping intact. In the schema, `\\-` inside the character classes decodes to an escaped hyphen; carried verbatim into a JavaScript regex literal, `\\-` instead matches a literal backslash. The guard therefore accepted identifiers containing backslashes (e.g. "foo\bar") that fail the schema. Drop the extra backslash so the character classes match the schema exactly, and cover the guard with the identifier schema examples plus a regression corpus that asserts guard/schema agreement (backslash strings rejected, valid identifiers accepted). --- .../format/src/types/pointer/pointer.test.ts | 48 +++++++++++++++++++ packages/format/src/types/pointer/pointer.ts | 2 +- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/packages/format/src/types/pointer/pointer.test.ts b/packages/format/src/types/pointer/pointer.test.ts index eb2457a20e..583178acdb 100644 --- a/packages/format/src/types/pointer/pointer.test.ts +++ b/packages/format/src/types/pointer/pointer.test.ts @@ -1,9 +1,57 @@ +import { describe, it, expect } from "vitest"; + import { testSchemaGuards } from "#test/guards"; +import { describeSchema } from "#describe"; import { Pointer, isPointer } from "./pointer.js"; +const identifierSchema = "schema:ethdebug/format/pointer/identifier"; + +describe("Pointer.isIdentifier", () => { + const valid = ["a", "a0", "-$", "__init__", "_x", "A", "foo-bar", "x$y"]; + + const invalid = [ + "", // empty + "0abc", // leading digit + "$x", // leading $ + "a b", // space + "foo\\bar", // embedded backslash — accepted before the fix + "\\", // lone backslash + "foo\tbar", // tab + "café", // non-ASCII letter + ]; + + it.each(valid)("accepts %j", (value) => { + expect(Pointer.isIdentifier(value)).toBe(true); + }); + + it.each(invalid)("rejects %j", (value) => { + expect(Pointer.isIdentifier(value)).toBe(false); + }); + + it("agrees with the identifier schema pattern over a corpus", () => { + const { schema } = describeSchema({ schema: identifierSchema }); + const { pattern } = schema as { pattern: string }; + const schemaRegex = new RegExp(pattern); + + for (const value of [...valid, ...invalid]) { + expect(Pointer.isIdentifier(value)).toBe(schemaRegex.test(value)); + } + }); + + it("rejects non-string inputs", () => { + for (const value of [undefined, null, 42, {}, ["a"]]) { + expect(Pointer.isIdentifier(value)).toBe(false); + } + }); +}); + const expressionSchema = "schema:ethdebug/format/pointer/expression"; testSchemaGuards("ethdebug/format/pointer", [ + { + schema: identifierSchema, + guard: Pointer.isIdentifier, + }, { schema: expressionSchema, guard: Pointer.isExpression, diff --git a/packages/format/src/types/pointer/pointer.ts b/packages/format/src/types/pointer/pointer.ts index 1b4930ec8c..11f38de8b4 100644 --- a/packages/format/src/types/pointer/pointer.ts +++ b/packages/format/src/types/pointer/pointer.ts @@ -6,7 +6,7 @@ export const isPointer = (value: unknown): value is Pointer => export namespace Pointer { export type Identifier = string; export const isIdentifier = (value: unknown): value is Identifier => - typeof value === "string" && /^[a-zA-Z_\\-]+[a-zA-Z0-9$_\\-]*$/.test(value); + typeof value === "string" && /^[a-zA-Z_-]+[a-zA-Z0-9$_-]*$/.test(value); export type Region = | Region.Stack From a2754b76b2e1eeb1be25b67719c5ca3bfd6015de Mon Sep 17 00:00:00 2001 From: "g. nicholas d'andrea" Date: Thu, 23 Jul 2026 22:09:36 -0400 Subject: [PATCH 2/2] format: carry contract library/interface flags in isContract MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The contract type schema uses a oneOf to distinguish normal, library, and interface contract types through the `library` and `interface` boolean flags, but the `Contract` interface and `isContract` guard dropped both flags. The guard accepted a contract that sets both flags true — a shape the schema rejects because it satisfies two oneOf branches at once — and the interface lost the three-way distinction. Add the flags to the interface and the guard, validating each as a boolean when present and rejecting the both-true shape, matching the current schema semantics. Cover the three legal shapes and the both-true rejection, asserting the guard and the schema agree on each. --- packages/format/src/types/type/index.test.ts | 56 ++++++++++++++++++++ packages/format/src/types/type/index.ts | 14 +++++ 2 files changed, 70 insertions(+) diff --git a/packages/format/src/types/type/index.test.ts b/packages/format/src/types/type/index.test.ts index cc461540ae..7747395dc7 100644 --- a/packages/format/src/types/type/index.test.ts +++ b/packages/format/src/types/type/index.test.ts @@ -1,4 +1,9 @@ +import { describe, it, expect } from "vitest"; + import { testSchemaGuards } from "#test/guards"; +// loads schemas into the global hyperjump validator + `toValidate` matcher +import "#test/hyperjump"; + import { Type, isType } from "./index.js"; testSchemaGuards("ethdebug/format/type", [ @@ -96,3 +101,54 @@ testSchemaGuards("ethdebug/format/type", [ guard: Type.isWrapper, }, ] as const); + +// The contract type's schema uses a oneOf to distinguish normal / +// library / interface contracts via the `library` and `interface` +// flags. The guard must carry those flags and agree with the schema — +// in particular it must reject a contract that sets both flags true +// (which matches two oneOf branches at once). +describe("Type.Elementary.isContract flag semantics", () => { + const contractSchema = "schema:ethdebug/format/type/elementary/contract"; + + const legal = [ + { title: "normal (flags omitted)", value: { kind: "contract" } }, + { + title: "normal (flags explicitly false)", + value: { kind: "contract", library: false, interface: false }, + }, + { title: "library", value: { kind: "contract", library: true } }, + { title: "interface", value: { kind: "contract", interface: true } }, + ] as const; + + const illegal = [ + { + title: "both library and interface true", + value: { kind: "contract", library: true, interface: true }, + }, + ] as const; + + for (const { title, value } of legal) { + it(`accepts the ${title} contract shape`, async () => { + expect(Type.Elementary.isContract(value)).toBe(true); + // guard agrees with the schema + await expect(value).toValidate({ schema: contractSchema }); + }); + } + + for (const { title, value } of illegal) { + it(`rejects a contract with ${title}`, async () => { + expect(Type.Elementary.isContract(value)).toBe(false); + // guard agrees with the schema + await expect(value).not.toValidate({ schema: contractSchema }); + }); + } + + it("rejects non-boolean flag values", () => { + expect( + Type.Elementary.isContract({ kind: "contract", library: "true" }), + ).toBe(false); + expect(Type.Elementary.isContract({ kind: "contract", interface: 1 })).toBe( + false, + ); + }); +}); diff --git a/packages/format/src/types/type/index.ts b/packages/format/src/types/type/index.ts index 7d06497496..32c8bee84e 100644 --- a/packages/format/src/types/type/index.ts +++ b/packages/format/src/types/type/index.ts @@ -237,6 +237,8 @@ export namespace Type { class?: "elementary"; kind: "contract"; payable?: boolean; + library?: boolean; + interface?: boolean; definition?: Definition; } @@ -246,6 +248,18 @@ export namespace Type { mayHaveClass(value, "elementary") && hasKind(value, "contract") && (!("payable" in value) || typeof value.payable === "boolean") && + (!("library" in value) || typeof value.library === "boolean") && + (!("interface" in value) || typeof value.interface === "boolean") && + // The schema's oneOf splits contract types three ways (normal / + // library / interface). `library` and `interface` cannot both be + // true: that shape satisfies two branches at once, which the + // oneOf rejects. + !( + "library" in value && + value.library === true && + "interface" in value && + value.interface === true + ) && (!("definition" in value) || isDefinition(value.definition)); export interface Enum {