Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions packages/format/src/types/pointer/pointer.test.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
2 changes: 1 addition & 1 deletion packages/format/src/types/pointer/pointer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
56 changes: 56 additions & 0 deletions packages/format/src/types/type/index.test.ts
Original file line number Diff line number Diff line change
@@ -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", [
Expand Down Expand Up @@ -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,
);
});
});
14 changes: 14 additions & 0 deletions packages/format/src/types/type/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,8 @@ export namespace Type {
class?: "elementary";
kind: "contract";
payable?: boolean;
library?: boolean;
interface?: boolean;
definition?: Definition;
}

Expand All @@ -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 {
Expand Down
Loading