diff --git a/package.json b/package.json index f26e62d7..1bbc2c4c 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,6 @@ "devDependencies": { "@hyperjump/json-schema": "1.6.7", "@types/jest": "^29.5.14", - "chalk": "^5.4.1", "concurrently": "^8.2.2", "jest": "^29.7.0", "lerna": "^8.0.2", diff --git a/packages/format/jest.config.ts b/packages/format/jest.config.ts new file mode 100644 index 00000000..45d68f11 --- /dev/null +++ b/packages/format/jest.config.ts @@ -0,0 +1,18 @@ +import type { Config } from "jest"; + +const config: Config = { + displayName: "@ethdebug/format", + preset: "ts-jest", + testEnvironment: "node", + moduleFileExtensions: ["ts", "js"], + moduleNameMapper: { + '^(\\.{1,2}/.*)\\.js$': '$1', + }, + modulePathIgnorePatterns: ["/dist/"], + setupFilesAfterEnv: ["/jest.setup.ts"], + transform: { + '^.+\\.tsx?$': "ts-jest" + }, +}; + +export default config; diff --git a/jest.d.ts b/packages/format/jest.d.ts similarity index 100% rename from jest.d.ts rename to packages/format/jest.d.ts diff --git a/jest.setup.ts b/packages/format/jest.setup.ts similarity index 100% rename from jest.setup.ts rename to packages/format/jest.setup.ts diff --git a/packages/format/package.json b/packages/format/package.json index 578dff92..43ad0aee 100644 --- a/packages/format/package.json +++ b/packages/format/package.json @@ -12,6 +12,7 @@ "prepare:yamls": "node ./bin/generate-schema-yamls.js > yamls.ts", "prepare": "yarn prepare:yamls && tsc", "clean": "rm -rf dist && rm yamls.ts", + "test": "node --experimental-vm-modules $(yarn bin jest)", "watch:typescript": "tsc --watch", "watch:schemas": "nodemon --watch ../../schemas -e 'yaml' --exec 'yarn prepare:yamls'", "watch": "concurrently --names=tsc,schemas \"yarn watch:typescript\" \"yarn watch:schemas\"" @@ -21,8 +22,14 @@ "yaml": "^2.3.4" }, "devDependencies": { + "@jest/globals": "^29.7.0", + "chalk": "^4.1.0", "concurrently": "^8.2.2", - "nodemon": "^3.0.2" + "jest": "^29.7.0", + "nodemon": "^3.0.2", + "ts-jest": "^29.1.1", + "ts-node": "^10.9.2", + "typescript": "^5.3.3" }, "publishConfig": { "access": "public" diff --git a/packages/format/src/describe.ts b/packages/format/src/describe.ts index f12eb326..94e743b2 100644 --- a/packages/format/src/describe.ts +++ b/packages/format/src/describe.ts @@ -35,10 +35,10 @@ export function describeSchema({ } return referencesId(schema) - ? describeSchemaById({ schema, pointer }) + ? describeSchemaById({ schema, ...(pointer ? { pointer } : {}) }) : referencesYaml(schema) - ? describeSchemaByYaml({ schema, pointer }) - : describeSchemaByObject({ schema, pointer }); + ? describeSchemaByYaml({ schema, ...(pointer ? { pointer } : {}) }) + : describeSchemaByObject({ schema, ...(pointer ? { pointer } : {}) }); } function describeSchemaById({ @@ -65,7 +65,7 @@ function describeSchemaById({ return { id, - pointer, + ...(pointer ? { pointer } : {}), yaml, schema, rootSchema @@ -86,14 +86,14 @@ function describeSchemaByYaml({ if (id) { return { id, - pointer, + ...(pointer ? { pointer } : {}), yaml, schema, rootSchema } } else { return { - pointer, + ...(pointer ? { pointer } : {}), yaml, schema, rootSchema @@ -116,14 +116,14 @@ function describeSchemaByObject({ if (id) { return { id, - pointer, + ...(pointer ? { pointer } : {}), yaml, schema, rootSchema } } else { return { - pointer, + ...(pointer ? { pointer } : {}), yaml, schema, rootSchema diff --git a/packages/format/src/index.ts b/packages/format/src/index.ts index 2d1d8ff1..983b437c 100644 --- a/packages/format/src/index.ts +++ b/packages/format/src/index.ts @@ -1,2 +1,4 @@ export * from "./describe"; export { schemas, schemaIds, type Schema } from "./schemas"; + +export * from "./types"; diff --git a/packages/format/src/types/data/index.test.ts b/packages/format/src/types/data/index.test.ts new file mode 100644 index 00000000..f849476e --- /dev/null +++ b/packages/format/src/types/data/index.test.ts @@ -0,0 +1,37 @@ +import { expect, describe, it } from "@jest/globals"; + +import { describeSchema } from "../../describe"; + +import { Data } from "./index.js"; + +describe("type guards", () => { + const schemaGuards = [ + { + schema: { + id: "schema:ethdebug/format/data/value" + }, + guard: Data.isValue + }, + { + schema: { + id: "schema:ethdebug/format/data/unsigned" + }, + guard: Data.isUnsigned + }, + { + schema: { + id: "schema:ethdebug/format/data/hex" + }, + guard: Data.isHex + }, + ] as const; + + it.each(schemaGuards)("matches its examples", ({ + guard, + ...describeSchemaOptions + }) => { + const { schema: { examples = [] } } = describeSchema(describeSchemaOptions); + + expect(guard).toSatisfyAll(examples); + }); +}); diff --git a/packages/format/src/types/data/index.ts b/packages/format/src/types/data/index.ts new file mode 100644 index 00000000..69e27d28 --- /dev/null +++ b/packages/format/src/types/data/index.ts @@ -0,0 +1,20 @@ +export namespace Data { + export type Value = + | Unsigned + | Hex; + + export const isValue = (value: unknown): value is Value => + [isUnsigned, isHex].some(guard => guard(value)); + + export type Unsigned = number; + + export const isUnsigned = (value: unknown): value is Unsigned => + typeof value === "number" && value >= 0; + + export type Hex = string; + + const hexPattern = new RegExp(/^0x[0-9a-fA-F]{1,}$/); + + export const isHex = (value: unknown): value is Hex => + typeof value === "string" && hexPattern.test(value); +} diff --git a/packages/format/src/types/index.ts b/packages/format/src/types/index.ts new file mode 100644 index 00000000..de2fda8e --- /dev/null +++ b/packages/format/src/types/index.ts @@ -0,0 +1,5 @@ +export * from "./data"; +export * from "./materials"; +export * from "./type"; +export * from "./pointer"; +export * from "./program"; diff --git a/packages/format/src/types/materials/index.test.ts b/packages/format/src/types/materials/index.test.ts new file mode 100644 index 00000000..42cd19fd --- /dev/null +++ b/packages/format/src/types/materials/index.test.ts @@ -0,0 +1,55 @@ +import { expect, describe, it } from "@jest/globals"; + +import { describeSchema } from "../../describe"; + +import { Materials } from "./index"; + +describe("type guards", () => { + const schemaGuards = [ + { + schema: { + id: "schema:ethdebug/format/materials/id" + }, + guard: Materials.isId + }, + { + schema: { + id: "schema:ethdebug/format/materials/reference" + }, + guard: Materials.isReference + }, + { + schema: { + id: "schema:ethdebug/format/materials/compilation" + }, + guard: Materials.isCompilation + }, + { + schema: { + id: "schema:ethdebug/format/materials/source" + }, + guard: Materials.isSource + }, + { + schema: { + id: "schema:ethdebug/format/materials/source-range" + }, + guard: Materials.isSourceRange + }, + ] as const; + + for (const { guard, ...describeSchemaOptions } of schemaGuards) { + const { schema } = describeSchemaOptions; + describe(schema.id.slice("schema:".length), () => { + it("matches its examples", () => { + const { + schema: { + examples = [] + } + } = describeSchema(describeSchemaOptions); + + expect(guard).toSatisfyAll(examples); + }); + }); + } +}); diff --git a/packages/format/src/types/materials/index.ts b/packages/format/src/types/materials/index.ts new file mode 100644 index 00000000..899e44e2 --- /dev/null +++ b/packages/format/src/types/materials/index.ts @@ -0,0 +1,126 @@ +import { Data } from "../data"; + +export namespace Materials { + export type Id = + | number + | string; + + export const isId = (value: unknown): value is Id => + ["number", "string"].includes(typeof value); + + // this and `toReference` are a bit janky to ensure Reference can't + // be assigned to Reference unless X can be assigned to Y + export type Reference = OmitNever< + & OmitNever<{ + [K in keyof O]-?: K extends "id" ? O[K] : never; + }> + & ( + Reference.Type extends string + ? { type?: Reference.Type; } + : { type?: string;} + ) + >; + + export const isReference = ( + value: unknown + ): value is Reference => + typeof value === "object" && !!value && + "id" in value && isId(value.id); + + export function toReference(object: O): Reference { + return { + id: object.id, + ...( + ([ + [isCompilation, "compilation"], + [isSource, "source"] + ] as const) + .filter(([guard]) => guard(object)) + .map(([_, type]) => ({ type })) + [0] || {} + ) + } as unknown as Reference; + } + + + type OmitNever = { + [K in keyof T as T[K] extends never ? never : K]: T[K] + }; + + + export namespace Reference { + export type Type = { + [T in keyof Types]: O extends Types[T] ? T : never; + }[keyof Types]; + + + type Types = { + "compilation": Compilation, + "source": Source + }; + + } + + export interface Compilation { + id: Id; + compiler: { + name: string; + version: string; + }; + settings?: any; + sources: Source[]; + } + + export const isCompilation = (value: unknown): value is Compilation => + typeof value === "object" && !!value && + "id" in value && isId(value.id) && + "compiler" in value && typeof value.compiler === "object" && !!value.compiler && + "name" in value.compiler && typeof value.compiler.name === "string" && + "version" in value.compiler && typeof value.compiler.version === "string" && + "sources" in value && value.sources instanceof Array && + value.sources.every(isSource); + + export interface Source { + id: Id; + path: string; + contents: string; + encoding?: string; + language: string; + } + + export const isSource = (value: unknown): value is Source => + typeof value === "object" && !!value && + "id" in value && isId(value.id) && + "path" in value && typeof value.path === "string" && + "contents" in value && typeof value.contents === "string" && + "language" in value && typeof value.language === "string" && + ( + !("encoding" in value) || typeof value.encoding === "string" + ); + + + export interface SourceRange { + compilation?: Reference; + source: Reference; + range?: { + offset: Data.Value; + length: Data.Value; + }; + } + + export const isSourceRange = (value: unknown): value is SourceRange => + typeof value === "object" && !!value && + "source" in value && isReference(value.source) && + ( + !("range" in value) || + ( + typeof value.range === "object" && !!value.range && + "offset" in value.range && Data.isValue(value.range.offset) && + "length" in value.range && Data.isValue(value.range.length) + ) + ) && + ( + !("compilation" in value) || isReference(value.compilation) + ); + +} diff --git a/packages/format/src/types/pointer/index.ts b/packages/format/src/types/pointer/index.ts new file mode 100644 index 00000000..a5db72c0 --- /dev/null +++ b/packages/format/src/types/pointer/index.ts @@ -0,0 +1 @@ +export { Pointer, isPointer } from "./pointer"; diff --git a/packages/pointers/src/pointer.test.ts b/packages/format/src/types/pointer/pointer.test.ts similarity index 94% rename from packages/pointers/src/pointer.test.ts rename to packages/format/src/types/pointer/pointer.test.ts index 0561846a..6f5e435f 100644 --- a/packages/pointers/src/pointer.test.ts +++ b/packages/format/src/types/pointer/pointer.test.ts @@ -1,11 +1,8 @@ -/// import { expect, describe, it } from "@jest/globals"; -import chalk from "chalk"; -import { describeSchema } from "@ethdebug/format"; - -import { Pointer, isPointer } from "./index.js"; +import { describeSchema } from "../../describe"; +import { Pointer, isPointer } from "./pointer"; describe("type guards", () => { const expressionSchema = { diff --git a/packages/pointers/src/pointer.ts b/packages/format/src/types/pointer/pointer.ts similarity index 100% rename from packages/pointers/src/pointer.ts rename to packages/format/src/types/pointer/pointer.ts diff --git a/packages/format/src/types/program/context.test.ts b/packages/format/src/types/program/context.test.ts new file mode 100644 index 00000000..9b4a1f39 --- /dev/null +++ b/packages/format/src/types/program/context.test.ts @@ -0,0 +1,49 @@ +import { expect, describe, it } from "@jest/globals"; + +import { describeSchema } from "../../describe"; + +import { Context, isContext } from "./context"; + +describe("type guards", () => { + const schemaGuards = [ + { + schema: { + id: "schema:ethdebug/format/program/context" + }, + guard: isContext + }, + { + schema: { + id: "schema:ethdebug/format/program/context/code" + }, + guard: Context.isCode + }, + { + schema: { + id: "schema:ethdebug/format/program/context/variables" + }, + guard: Context.isVariables + }, + { + schema: { + id: "schema:ethdebug/format/program/context/remark" + }, + guard: Context.isRemark + }, + ] as const; + + for (const { guard, ...describeSchemaOptions } of schemaGuards) { + const { schema } = describeSchemaOptions; + describe(schema.id.slice("schema:".length), () => { + it("matches its examples", () => { + const { + schema: { + examples = [] + } + } = describeSchema(describeSchemaOptions); + + expect(guard).toSatisfyAll(examples); + }); + }); + } +}); diff --git a/packages/format/src/types/program/context.ts b/packages/format/src/types/program/context.ts new file mode 100644 index 00000000..8fd027cc --- /dev/null +++ b/packages/format/src/types/program/context.ts @@ -0,0 +1,79 @@ +import { Materials } from "../materials"; +import { Type, isType } from "../type"; +import { Pointer, isPointer } from "../pointer"; + +export type Context = + | Context.Code + | Context.Variables + | Context.Remark; + +export const isContext = (value: unknown): value is Context => [ + Context.isCode, + Context.isVariables, + Context.isRemark, +].some(guard => guard(value)); + +export namespace Context { + export interface Code { + code: Materials.SourceRange; + } + + export const isCode = (value: unknown): value is Code => + typeof value === "object" && !!value && + "code" in value && Materials.isSourceRange(value.code); + + export interface Variables { + variables: Variables.Variable[] + } + + export const isVariables = (value: unknown): value is Variables => + typeof value === "object" && !!value && + "variables" in value && value.variables instanceof Array && + value.variables.length > 0 && + value.variables.every(Variables.isVariable); + + export namespace Variables { + export interface Variable { + identifier?: string; + declaration?: Materials.SourceRange; + type?: Type; + pointer?: Pointer; + } + + const allowedKeys = new Set([ + "identifier", + "declaration", + "type", + "pointer" + ]); + + export const isVariable = (value: unknown): value is Variable => + typeof value === "object" && !!value && + Object.keys(value).length > 0 && + Object.keys(value).every(key => allowedKeys.has(key)) && + ( + !("identifier" in value) || + typeof value.identifier === "string" + ) && + ( + !("declaration" in value) || + Materials.isSourceRange(value.declaration) + ) && + ( + !("type" in value) || + isType(value.type) + ) && + ( + !("pointer" in value) || + isPointer(value.pointer) + ); + } + + export interface Remark { + remark: string; + } + + export const isRemark = (value: unknown): value is Remark => + typeof value === "object" && !!value && + "remark" in value && typeof value.remark === "string"; +} diff --git a/packages/format/src/types/program/index.ts b/packages/format/src/types/program/index.ts new file mode 100644 index 00000000..e5f765d5 --- /dev/null +++ b/packages/format/src/types/program/index.ts @@ -0,0 +1 @@ +export { Program, isProgram } from "./program"; diff --git a/packages/format/src/types/program/instruction.test.ts b/packages/format/src/types/program/instruction.test.ts new file mode 100644 index 00000000..1be5f348 --- /dev/null +++ b/packages/format/src/types/program/instruction.test.ts @@ -0,0 +1,31 @@ +import { expect, describe, it } from "@jest/globals"; + +import { describeSchema } from "../../describe"; + +import { Instruction, isInstruction } from "./instruction"; + +describe("type guards", () => { + const schemaGuards = [ + { + schema: { + id: "schema:ethdebug/format/program/instruction" + }, + guard: isInstruction + }, + ] as const; + + for (const { guard, ...describeSchemaOptions } of schemaGuards) { + const { schema } = describeSchemaOptions; + describe(schema.id.slice("schema:".length), () => { + it("matches its examples", () => { + const { + schema: { + examples = [] + } + } = describeSchema(describeSchemaOptions); + + expect(guard).toSatisfyAll(examples); + }); + }); + } +}); diff --git a/packages/format/src/types/program/instruction.ts b/packages/format/src/types/program/instruction.ts new file mode 100644 index 00000000..6edc36a2 --- /dev/null +++ b/packages/format/src/types/program/instruction.ts @@ -0,0 +1,33 @@ +import { Data } from "../data"; + +import { Context, isContext } from "./context"; + +export interface Instruction { + offset: Data.Value; + context?: Context; + operation?: Instruction.Operation; +} + +export const isInstruction = (value: unknown): value is Instruction => + typeof value === "object" && !!value && + "offset" in value && Data.isValue(value.offset) && + (!("context" in value) || isContext(value.context)) && + (!("operation" in value) || Instruction.isOperation(value.operation)); + + +export namespace Instruction { + export interface Operation { + mnemonic: string; + arguments?: Data.Value[]; + } + + export const isOperation = (value: unknown): value is Operation => + typeof value === "object" && !!value && + "mnemonic" in value && typeof value.mnemonic === "string" && + ( + !("arguments" in value) || ( + value.arguments instanceof Array && + value.arguments.every(Data.isValue) + ) + ) +} diff --git a/packages/format/src/types/program/program.test.ts b/packages/format/src/types/program/program.test.ts new file mode 100644 index 00000000..dcd46e31 --- /dev/null +++ b/packages/format/src/types/program/program.test.ts @@ -0,0 +1,31 @@ +import { expect, describe, it } from "@jest/globals"; + +import { describeSchema } from "../../describe"; + +import { Program, isProgram } from "./program"; + +describe("type guards", () => { + const schemaGuards = [ + { + schema: { + id: "schema:ethdebug/format/program" + }, + guard: isProgram + }, + ] as const; + + for (const { guard, ...describeSchemaOptions } of schemaGuards) { + const { schema } = describeSchemaOptions; + describe(schema.id.slice("schema:".length), () => { + it("matches its examples", () => { + const { + schema: { + examples = [] + } + } = describeSchema(describeSchemaOptions); + + expect(guard).toSatisfyAll(examples); + }); + }); + } +}); diff --git a/packages/format/src/types/program/program.ts b/packages/format/src/types/program/program.ts new file mode 100644 index 00000000..663adc20 --- /dev/null +++ b/packages/format/src/types/program/program.ts @@ -0,0 +1,58 @@ +import { Materials } from "../materials"; + +import { + Context as _Context, + isContext as _isContext +} from "./context"; + +import { + Instruction as _Instruction, + isInstruction as _isInstruction +} from "./instruction"; + +export interface Program { + compilation?: Materials.Reference; + contract: Program.Contract; + environment: Program.Environment; + context?: Program.Context; + instructions: Program.Instruction[]; +} + +export const isProgram = (value: unknown): value is Program => + typeof value === "object" && !!value && + "contract" in value && Program.isContract(value.contract) && + "environment" in value && Program.isEnvironment(value.environment) && + "instructions" in value && value.instructions instanceof Array && + value.instructions.every(Program.isInstruction) && + ( + !("compilation" in value) || + Materials.isReference(value.compilation) + ) && + ( + !("context" in value) || + Program.isContext(value.context) + ); + +export namespace Program { + export import Context = _Context; + export const isContext = _isContext; + export import Instruction = _Instruction; + export const isInstruction = _isInstruction; + export type Environment = + | "call" + | "create"; + + export const isEnvironment = (value: unknown): value is Environment => + typeof value === "string" && + ["call", "create"].includes(value); + + export interface Contract { + name?: string; + definition: Materials.SourceRange; + } + + export const isContract = (value: unknown): value is Contract => + typeof value === "object" && !!value && + "definition" in value && Materials.isSourceRange(value.definition) && + (!("name" in value) || typeof value.name === "string"); +} diff --git a/packages/format/src/types/type/base.test.ts b/packages/format/src/types/type/base.test.ts new file mode 100644 index 00000000..4da13357 --- /dev/null +++ b/packages/format/src/types/type/base.test.ts @@ -0,0 +1,48 @@ +import { expect, describe, it } from "@jest/globals"; + +import { describeSchema } from "../../describe"; + +import * as Base from "./base"; + +describe("type guards", () => { + const schemaGuards = [ + { + schema: { + id: "schema:ethdebug/format/type/base" + }, + pointer: "#/$defs/ElementaryType", + guard: Base.isElementary + }, + { + schema: { + id: "schema:ethdebug/format/type/base" + }, + pointer: "#/$defs/ComplexType", + guard: Base.isComplex + }, + { + schema: { + id: "schema:ethdebug/format/type/base" + }, + pointer: "#/$defs/TypeWrapper", + guard: Base.isWrapper + }, + ] as const; + + for (const { guard, ...describeSchemaOptions } of schemaGuards) { + const { schema, pointer } = describeSchemaOptions; + describe( + `${schema.id.slice("schema:".length)}${pointer}`, + () => { + it("matches its examples", () => { + const { + schema: { + examples = [] + } + } = describeSchema(describeSchemaOptions); + + expect(guard).toSatisfyAll(examples); + }); + }); + } +}); diff --git a/packages/format/src/types/type/base.ts b/packages/format/src/types/type/base.ts new file mode 100644 index 00000000..ecf72f52 --- /dev/null +++ b/packages/format/src/types/type/base.ts @@ -0,0 +1,67 @@ +export type Type = + | Elementary + | Complex; + +export const isType = (value: unknown): value is Type => + [ + isElementary, + isComplex + ].some(guard => guard(value)); + +export interface Elementary { + class?: "elementary"; + kind: string; +} + +export const isElementary = (value: unknown): value is Elementary => + typeof value === "object" && !!value && + "kind" in value && typeof value.kind === "string" && + ( + !("class" in value) || value.class === "elementary" + ) && + !("contains" in value); + +export interface Complex { + class?: "complex"; + kind: string; + contains: + | Wrapper + | Wrapper[] + | { + [key: string]: Wrapper + }; +} + +export const isComplex = (value: unknown): value is Complex => + typeof value === "object" && !!value && + "kind" in value && typeof value.kind === "string" && + ( + !("class" in value) || value.class === "complex" + ) && + "contains" in value && !!value.contains && ( + isWrapper(value.contains) || + ( + value.contains instanceof Array && + value.contains.every(isWrapper) + ) || + ( + typeof value.contains === "object" && + Object.values(value.contains).every(isWrapper) + ) + ); + +export interface Wrapper { + type: + | Type + | { id: any; }; +} + +export const isWrapper = (value: unknown): value is Wrapper => + typeof value === "object" && !!value && + "type" in value && ( + isType(value.type) || + ( + typeof value.type === "object" && !!value.type && + "id" in value.type + ) + ); diff --git a/packages/format/src/types/type/index.test.ts b/packages/format/src/types/type/index.test.ts new file mode 100644 index 00000000..73c7e8a5 --- /dev/null +++ b/packages/format/src/types/type/index.test.ts @@ -0,0 +1,139 @@ +import { expect, describe, it } from "@jest/globals"; + +import { describeSchema } from "../../describe"; + +import { Type, isType } from "./index"; + +describe("type guards", () => { + const schemaGuards = [ + { + schema: { + id: "schema:ethdebug/format/type" + }, + guard: isType + }, + + // elementary types + + { + schema: { + id: "schema:ethdebug/format/type/elementary" + }, + guard: Type.isElementary + }, + { + schema: { + id: "schema:ethdebug/format/type/elementary/uint" + }, + guard: Type.Elementary.isUint + }, + { + schema: { + id: "schema:ethdebug/format/type/elementary/int" + }, + guard: Type.Elementary.isInt + }, + { + schema: { + id: "schema:ethdebug/format/type/elementary/ufixed" + }, + guard: Type.Elementary.isUfixed + }, + { + schema: { + id: "schema:ethdebug/format/type/elementary/fixed" + }, + guard: Type.Elementary.isFixed + }, + { + schema: { + id: "schema:ethdebug/format/type/elementary/bool" + }, + guard: Type.Elementary.isBool + }, + { + schema: { + id: "schema:ethdebug/format/type/elementary/bytes" + }, + guard: Type.Elementary.isBytes + }, + { + schema: { + id: "schema:ethdebug/format/type/elementary/string" + }, + guard: Type.Elementary.isString + }, + { + schema: { + id: "schema:ethdebug/format/type/elementary/address" + }, + guard: Type.Elementary.isAddress + }, + { + schema: { + id: "schema:ethdebug/format/type/elementary/contract" + }, + guard: Type.Elementary.isContract + }, + { + schema: { + id: "schema:ethdebug/format/type/elementary/enum" + }, + guard: Type.Elementary.isEnum + }, + + // complex types + + { + schema: { + id: "schema:ethdebug/format/type/complex" + }, + guard: Type.isComplex + }, + { + schema: { + id: "schema:ethdebug/format/type/complex/alias" + }, + guard: Type.Complex.isAlias + }, + { + schema: { + id: "schema:ethdebug/format/type/complex/tuple" + }, + guard: Type.Complex.isTuple + }, + { + schema: { + id: "schema:ethdebug/format/type/complex/array" + }, + guard: Type.Complex.isArray + }, + { + schema: { + id: "schema:ethdebug/format/type/complex/mapping" + }, + guard: Type.Complex.isMapping + }, + { + schema: { + id: "schema:ethdebug/format/type/complex/struct" + }, + guard: Type.Complex.isStruct + }, + ] as const; + + for (const { guard, ...describeSchemaOptions } of schemaGuards) { + const { schema } = describeSchemaOptions; + describe(schema.id.slice("schema:".length), () => { + it("matches its examples", () => { + const { + schema: { + examples = [] + } + } = describeSchema(describeSchemaOptions); + + expect(guard).toSatisfyAll(examples); + }); + }); + } +}); diff --git a/packages/format/src/types/type/index.ts b/packages/format/src/types/type/index.ts new file mode 100644 index 00000000..0df86d2b --- /dev/null +++ b/packages/format/src/types/type/index.ts @@ -0,0 +1,411 @@ +import { Data } from "../data"; +import { Materials } from "../materials"; + +import * as _Base from "./base"; + +export type Type = + | Type.Known + | Type.Unknown; + +export const isType = (value: unknown): value is Type => + (Type.hasElementaryKind(value) || Type.hasComplexKind(value)) + ? Type.isKnown(value) + : Type.isUnknown(value); + +export namespace Type { + export import Base = _Base; + + export type Known = + | Elementary + | Complex; + + export const isKnown = (value: unknown): value is Known => + [ + isElementary, + isComplex + ].some(guard => guard(value)); + + export type Unknown = + & Base.Type + & { class: Exclude }; + + export const isUnknown = (value: unknown): value is Unknown => + Base.isType(value) && + "class" in value && + ( + !("contains" in value) || Type.isWrapper(value.contains) || + ( + value.contains instanceof Array && + value.contains.every(Type.isWrapper) + ) || + ( + typeof value.contains === "object" && + Object.values(value.contains).every(Type.isWrapper) + ) + ); + + export interface Wrapper { + type: + | Type + | { id: any; } + } + + export const isWrapper = (value: unknown): value is Wrapper => + typeof value === "object" && !!value && + "type" in value && ( + isType(value.type) || + ( + typeof value.type === "object" && !!value.type && + "id" in value.type + ) + ); + + export type Elementary = + | Elementary.Uint + | Elementary.Int + | Elementary.Ufixed + | Elementary.Fixed + | Elementary.Bool + | Elementary.Bytes + | Elementary.String + | Elementary.Address + | Elementary.Contract + | Elementary.Enum; + + export const hasElementaryKind = (value: unknown): value is { + kind: Elementary["kind"] + } => + typeof value === "object" && !!value && + "kind" in value && typeof value.kind === "string" && + [ + "uint", + "int", + "ufixed", + "fixed", + "bool", + "bytes", + "string", + "address", + "contract", + "enum" + ].includes(value.kind); + + export const isElementary = (value: unknown): value is Elementary => + [ + Elementary.isUint, + Elementary.isInt, + Elementary.isUfixed, + Elementary.isFixed, + Elementary.isBool, + Elementary.isBytes, + Elementary.isString, + Elementary.isAddress, + Elementary.isContract, + Elementary.isEnum + ].some(guard => guard(value)); + + export namespace Elementary { + export interface Uint { + class?: "elementary"; + kind: "uint"; + bits: number; + } + + export const isUint = (value: unknown): value is Uint => + typeof value === "object" && !!value && + mayHaveClass(value, "elementary") && + hasKind(value, "uint") && + "bits" in value && typeof value.bits === "number" && + value.bits >= 8 && value.bits <= 256 && value.bits % 8 === 0; + + export interface Int { + class?: "elementary"; + kind: "int"; + bits: number; + } + + export const isInt = (value: unknown): value is Int => + typeof value === "object" && !!value && + mayHaveClass(value, "elementary") && + hasKind(value, "int") && + "bits" in value && typeof value.bits === "number" && + value.bits >= 8 && value.bits <= 256 && value.bits % 8 === 0; + + export interface Ufixed { + class?: "elementary"; + kind: "ufixed"; + bits: number; + places: number; + } + + export const isUfixed = (value: unknown): value is Ufixed => + typeof value === "object" && !!value && + mayHaveClass(value, "elementary") && + hasKind(value, "ufixed") && + "bits" in value && typeof value.bits === "number" && + value.bits >= 8 && value.bits <= 256 && value.bits % 8 === 0 && + "places" in value && typeof value.places === "number" && + value.places >= 1 && value.places <= 80; + + export interface Fixed { + class?: "elementary"; + kind: "fixed"; + bits: number; + places: number; + } + + export const isFixed = (value: unknown): value is Fixed => + typeof value === "object" && !!value && + mayHaveClass(value, "elementary") && + hasKind(value, "fixed") && + "bits" in value && typeof value.bits === "number" && + value.bits >= 8 && value.bits <= 256 && value.bits % 8 === 0 && + "places" in value && typeof value.places === "number" && + value.places >= 1 && value.places <= 80; + + export interface Bool { + class?: "elementary"; + kind: "bool"; + } + + export const isBool = (value: unknown): value is Bool => + typeof value === "object" && !!value && + mayHaveClass(value, "elementary") && + hasKind(value, "bool"); + + export interface Bytes { + class?: "elementary"; + kind: "bytes"; + size?: Data.Unsigned; + } + + export const isBytes = (value: unknown): value is Bytes => + typeof value === "object" && !!value && + mayHaveClass(value, "elementary") && + hasKind(value, "bytes") && + ( + !("size" in value) || Data.isUnsigned(value.size) + ); + + export interface String { + class?: "elementary"; + kind: "string"; + encoding?: string; + } + + export const isString = (value: unknown): value is String => + typeof value === "object" && !!value && + mayHaveClass(value, "elementary") && + hasKind(value, "string") && + ( + !("encoding" in value) || typeof value.encoding === "string" + ); + + export interface Address { + class?: "elementary"; + kind: "address"; + payable?: boolean; + } + + export const isAddress = (value: unknown): value is Address => + typeof value === "object" && !!value && + mayHaveClass(value, "elementary") && + hasKind(value, "address") && + ( + !("payable" in value) || typeof value.payable === "boolean" + ); + + export interface Contract { + class?: "elementary"; + kind: "contract"; + payable?: boolean; + definition?: Definition; + } + + export const isContract = (value: unknown): value is Contract => + typeof value === "object" && !!value && + mayHaveClass(value, "elementary") && + hasKind(value, "contract") && + ( + !("payable" in value) || typeof value.payable === "boolean" + ) && + ( + !("definition" in value) || isDefinition(value.definition) + ); + + export interface Enum { + class?: "elementary"; + kind: "enum"; + values: any[]; + definition?: Definition; + } + + export const isEnum = (value: unknown): value is Enum => + typeof value === "object" && !!value && + mayHaveClass(value, "elementary") && + hasKind(value, "enum") && + "values" in value && value.values instanceof Array && + ( + !("definition" in value) || isDefinition(value.definition) + ); + } + + export type Complex = + | Complex.Alias + | Complex.Tuple + | Complex.Array + | Complex.Mapping + | Complex.Struct + /* currently unsupported: | Complex.Function */; + + export const hasComplexKind = (value: unknown): value is { + kind: Complex["kind"] + } => + typeof value === "object" && !!value && + "kind" in value && typeof value.kind === "string" && + [ + "alias", + "tuple", + "array", + "mapping", + "struct", + // "function" + ].includes(value.kind); + + export const isComplex = (value: unknown): value is Complex => + [ + Complex.isAlias, + Complex.isTuple, + Complex.isArray, + Complex.isMapping, + Complex.isStruct, + ].some(guard => guard(value)); + + export namespace Complex { + export interface Alias { + class?: "complex"; + kind: "alias"; + contains: Wrapper; + definition?: Definition; + } + + export const isAlias = (value: unknown): value is Alias => + typeof value === "object" && !!value && + mayHaveClass(value, "complex") && + hasKind(value, "alias") && + "contains" in value && isWrapper(value.contains) && + ( + !("definition" in value) || isDefinition(value.definition) + ); + + export interface Tuple { + class?: "complex"; + kind: "tuple"; + contains: ( + & Wrapper + & { name?: string } + )[]; + } + + export const isTuple = (value: unknown): value is Tuple => + typeof value === "object" && !!value && + mayHaveClass(value, "complex") && + hasKind(value, "tuple") && + "contains" in value && value.contains instanceof Array && + value.contains.every( + (element) => + isWrapper(element) && + ( + !("name" in element) || typeof element.name === "string" + ) + ); + + export interface Array { + class?: "complex"; + kind: "array"; + contains: Wrapper; + } + + export const isArray = (value: unknown): value is Array => + typeof value === "object" && !!value && + mayHaveClass(value, "complex") && + hasKind(value, "array") && + "contains" in value && isWrapper(value.contains); + + export interface Mapping { + class?: "complex"; + kind: "mapping"; + contains: { + key: Wrapper + value: Wrapper + }; + } + + export const isMapping = (value: unknown): value is Mapping => + typeof value === "object" && !!value && + mayHaveClass(value, "complex") && + hasKind(value, "mapping") && + "contains" in value && + typeof value.contains === "object" && !!value.contains && + "key" in value.contains && isWrapper(value.contains.key) && + "value" in value.contains && isWrapper(value.contains.value); + + export interface Struct { + class?: "complex"; + kind: "struct"; + contains: ( + & Wrapper + & { name?: string } + )[]; + definition?: Definition; + } + + export const isStruct = (value: unknown): value is Struct => + typeof value === "object" && !!value && + mayHaveClass(value, "complex") && + hasKind(value, "struct") && + "contains" in value && value.contains instanceof Array && + value.contains.every( + (field) => + isWrapper(field) && + ( + !("name" in field) || typeof field.name === "string" + ) + ) && + ( + !("definition" in value) || isDefinition(value.definition) + ); + } + + export interface Definition { + name?: string; + location?: Materials.SourceRange; + } + + export const isDefinition = (value: unknown): value is Definition => + typeof value === "object" && !!value && + ( + !("name" in value) || typeof value.name === "string" + ) && + ( + !("location" in value) || Materials.isSourceRange(value.location) + ) && + ( + Object.keys(value).includes("name") || + Object.keys(value).includes("location") + ); + +} + +export const mayHaveClass = ( + object: object, + class_: Class +): object is { class: Class } => + !("class" in object) || object.class === class_; + + +export const hasKind = ( + object: object, + kind: Kind +): object is { kind: Kind } => + "kind" in object && object.kind === kind; diff --git a/packages/format/tsconfig.json b/packages/format/tsconfig.json index 37ec4da4..bf855362 100644 --- a/packages/format/tsconfig.json +++ b/packages/format/tsconfig.json @@ -1,14 +1,16 @@ { "extends": "../../tsconfig.base.json", "compilerOptions": { - "target": "es2016", + "target": "es2017", "module": "commonjs", "moduleResolution": "node10", + "exactOptionalPropertyTypes": true, "rootDir": "./", "outDir": "./dist/", }, "include": [ "./src/**/*.ts", + "jest.d.ts", "yamls.ts" ] } diff --git a/packages/pointers/jest.config.ts b/packages/pointers/jest.config.ts index 6e86c2c6..2d060258 100644 --- a/packages/pointers/jest.config.ts +++ b/packages/pointers/jest.config.ts @@ -10,7 +10,6 @@ const config: Config = { '^(\\.{1,2}/.*)\\.js$': '$1', }, modulePathIgnorePatterns: ["/dist/"], - setupFilesAfterEnv: ["/../../jest.setup.ts"], transform: { // '^.+\\.[tj]sx?$' to process js/ts with `ts-jest` // '^.+\\.m?[tj]sx?$' to process js/ts/mjs/mts with `ts-jest` diff --git a/packages/pointers/src/cursor.ts b/packages/pointers/src/cursor.ts index 4655b82b..61b5ac85 100644 --- a/packages/pointers/src/cursor.ts +++ b/packages/pointers/src/cursor.ts @@ -1,5 +1,5 @@ +import type { Pointer } from "@ethdebug/format"; import type { Machine } from "./machine.js"; -import type { Pointer } from "./pointer.js"; import type { Data } from "./data.js"; /** diff --git a/packages/pointers/src/dereference/generate.ts b/packages/pointers/src/dereference/generate.ts index 9cc97ff2..06c25e70 100644 --- a/packages/pointers/src/dereference/generate.ts +++ b/packages/pointers/src/dereference/generate.ts @@ -1,7 +1,7 @@ +import type { Pointer } from "@ethdebug/format"; import type { Machine } from "../machine.js"; import type { Cursor } from "../cursor.js"; import type { Data } from "../data.js"; -import type { Pointer } from "../pointer.js"; import { Memo } from "./memo.js"; import { processPointer, type ProcessOptions } from "./process.js"; diff --git a/packages/pointers/src/dereference/index.test.ts b/packages/pointers/src/dereference/index.test.ts index 894c40ae..0ffd8bd2 100644 --- a/packages/pointers/src/dereference/index.test.ts +++ b/packages/pointers/src/dereference/index.test.ts @@ -1,7 +1,7 @@ import { jest, expect, describe, it, beforeEach } from "@jest/globals"; +import { Pointer } from "@ethdebug/format"; import { Machine } from "../machine.js"; import { Data } from "../data.js"; -import { Pointer } from "../pointer.js"; import { dereference } from "./index.js"; describe("dereference", () => { diff --git a/packages/pointers/src/dereference/index.ts b/packages/pointers/src/dereference/index.ts index 15172245..793b34c5 100644 --- a/packages/pointers/src/dereference/index.ts +++ b/packages/pointers/src/dereference/index.ts @@ -1,4 +1,4 @@ -import type { Pointer } from "../pointer.js"; +import type { Pointer } from "@ethdebug/format"; import type { Machine } from "../machine.js"; import type { Cursor } from "../cursor.js"; diff --git a/packages/pointers/src/dereference/memo.ts b/packages/pointers/src/dereference/memo.ts index d44eaadd..c3be3d67 100644 --- a/packages/pointers/src/dereference/memo.ts +++ b/packages/pointers/src/dereference/memo.ts @@ -1,4 +1,4 @@ -import type { Pointer } from "../pointer.js"; +import type { Pointer } from "@ethdebug/format"; import type { Cursor } from "../cursor.js"; import type { Data } from "../data.js"; diff --git a/packages/pointers/src/dereference/process.ts b/packages/pointers/src/dereference/process.ts index 61a866cb..8e960cc8 100644 --- a/packages/pointers/src/dereference/process.ts +++ b/packages/pointers/src/dereference/process.ts @@ -1,7 +1,7 @@ +import { Pointer } from "@ethdebug/format"; import type { Machine } from "../machine.js"; import type { Cursor } from "../cursor.js"; import { Data } from "../data.js"; -import { Pointer } from "../pointer.js"; import { evaluate } from "../evaluate.js"; import { Memo } from "./memo.js"; diff --git a/packages/pointers/src/dereference/region.ts b/packages/pointers/src/dereference/region.ts index 3f8262cf..672ae837 100644 --- a/packages/pointers/src/dereference/region.ts +++ b/packages/pointers/src/dereference/region.ts @@ -1,6 +1,6 @@ +import { Pointer } from "@ethdebug/format"; import type { Cursor } from "../cursor.js"; import type { Data } from "../data.js"; -import { Pointer } from "../pointer.js"; import { evaluate, type EvaluateOptions } from "../evaluate.js"; /** diff --git a/packages/pointers/src/evaluate.test.ts b/packages/pointers/src/evaluate.test.ts index ee256b0c..783f0e12 100644 --- a/packages/pointers/src/evaluate.test.ts +++ b/packages/pointers/src/evaluate.test.ts @@ -1,10 +1,13 @@ import { expect, describe, it, beforeEach } from "@jest/globals"; + import { keccak256 } from "ethereum-cryptography/keccak"; import { toHex } from "ethereum-cryptography/utils"; + +import { Pointer } from "@ethdebug/format"; + import { Machine } from "./machine.js"; import { Data } from "./data.js"; import { Cursor } from "./cursor.js"; -import { Pointer } from "./pointer.js"; import { evaluate, type EvaluateOptions } from "./evaluate.js"; // Create a stub for the Machine.State interface diff --git a/packages/pointers/src/evaluate.ts b/packages/pointers/src/evaluate.ts index 039d33d9..4d39b237 100644 --- a/packages/pointers/src/evaluate.ts +++ b/packages/pointers/src/evaluate.ts @@ -1,4 +1,4 @@ -import { Pointer } from "./pointer.js"; +import { Pointer } from "@ethdebug/format"; import { Machine } from "./machine.js"; import { Data } from "./data.js"; import type { Cursor } from "./cursor.js"; diff --git a/packages/pointers/src/index.ts b/packages/pointers/src/index.ts index d396b964..78924bea 100644 --- a/packages/pointers/src/index.ts +++ b/packages/pointers/src/index.ts @@ -1,4 +1,3 @@ -export { Pointer, isPointer } from "./pointer.js"; export { dereference, DereferenceOptions } from "./dereference/index.js"; export { Cursor } from "./cursor.js"; diff --git a/packages/pointers/src/read.test.ts b/packages/pointers/src/read.test.ts index 9c3e2273..1487bfd2 100644 --- a/packages/pointers/src/read.test.ts +++ b/packages/pointers/src/read.test.ts @@ -1,7 +1,9 @@ import { jest, expect, describe, it, beforeEach } from "@jest/globals"; + +import type { Pointer } from "@ethdebug/format"; + import { Machine } from "./machine.js"; import { Data } from "./data.js"; -import type { Pointer } from "./pointer.js"; import { read, type ReadOptions } from "./read.js"; import { Cursor } from "./cursor.js"; diff --git a/packages/pointers/src/read.ts b/packages/pointers/src/read.ts index 57bb692a..0a920bdc 100644 --- a/packages/pointers/src/read.ts +++ b/packages/pointers/src/read.ts @@ -1,4 +1,4 @@ -import { Pointer } from "./pointer.js"; +import { Pointer } from "@ethdebug/format"; import { Machine } from "./machine.js"; import { Data } from "./data.js"; import type { Cursor } from "./cursor.js"; diff --git a/packages/pointers/test/examples.ts b/packages/pointers/test/examples.ts index 1c4d7663..7c14cf67 100644 --- a/packages/pointers/test/examples.ts +++ b/packages/pointers/test/examples.ts @@ -1,6 +1,5 @@ -import { describeSchema } from "@ethdebug/format"; +import { type Pointer, describeSchema } from "@ethdebug/format"; -import type { Pointer } from "../src/pointer.js"; import type { CompileOptions } from "./solc.js"; export const findExamplePointer = (() => { diff --git a/packages/pointers/test/observe.ts b/packages/pointers/test/observe.ts index 2b6b5277..03b14fa2 100644 --- a/packages/pointers/test/observe.ts +++ b/packages/pointers/test/observe.ts @@ -1,4 +1,6 @@ -import { type Machine, type Pointer, type Cursor, dereference } from "../src/index.js"; +import type { Pointer } from "@ethdebug/format"; + +import { type Machine, type Cursor, dereference } from "../src/index.js"; import { loadGanache, machineForProvider } from "./ganache.js"; import { compileCreateBytecode, type CompileOptions } from "./solc.js"; diff --git a/packages/web/docs/implementation-guides/pointers/types/pointer-types.mdx b/packages/web/docs/implementation-guides/pointers/types/pointer-types.mdx index afe03cfc..bd3652da 100644 --- a/packages/web/docs/implementation-guides/pointers/types/pointer-types.mdx +++ b/packages/web/docs/implementation-guides/pointers/types/pointer-types.mdx @@ -8,21 +8,21 @@ import CodeListing from "@site/src/components/CodeListing"; ## Types and type guards for all kinds of pointers -This package provides the root `Pointer` type and accompanying `Pointer` -namespace, which contains TypeScript type definitions and type guards for -working with **ethdebug/format/pointer** objects. +The **@ethdebug/format** package provides the root `Pointer` type and +accompanying `Pointer` namespace, which contains TypeScript type definitions +and type predicates for working with **ethdebug/format/pointer** objects. The `Pointer` namespace is organized itself into namespaces in a nested manner, roughly to correspond to the JSON-Schema organization itself. -Types and type guards are available for all pointer schemas, i.e., for every -different kind of region and collection. +Types and type predicates are available for all pointer schemas, i.e., for +every different kind of region and collection. A [full source listing](#code-listing) follows below, but see example usage to get a sense for how these types are organized: ```typescript title="Usage example" -import { Pointer, isPointer } from "@ethdebug/pointers"; +import { Pointer, isPointer } from "@ethdebug/format"; const region: Pointer.Region = { location: "stack", slot: 0 }; const group: Pointer.Collection.Group = { group: [region] }; @@ -43,7 +43,7 @@ nested, also roughly to correspond to the root JSON-Schema. See these quick examples to get a sense for this part of the type hierarchy: ```typescript title="Usage example" -import { Pointer } from "@ethdebug/pointers"; +import { Pointer } from "@ethdebug/format"; const expression: Pointer.Expression = { $sum: [0, 1] @@ -57,5 +57,6 @@ Pointer.Expression.Arithmetic.isSum(expression); // true ## Code listing + packageName="@ethdebug/format" + includePackageNameInTitle + sourcePath="src/types/pointer/pointer.ts" /> diff --git a/packages/web/docusaurus.config.ts b/packages/web/docusaurus.config.ts index c10cecc2..1c3ac964 100644 --- a/packages/web/docusaurus.config.ts +++ b/packages/web/docusaurus.config.ts @@ -63,6 +63,10 @@ const config: Config = { "./plugins/project-code-plugin.ts", { packages: { + "@ethdebug/format": { + tsConfigFilePath: + path.resolve(__dirname, "../format/tsconfig.json") + }, "@ethdebug/pointers": { tsConfigFilePath: path.resolve(__dirname, "../pointers/tsconfig.json") diff --git a/packages/web/src/components/CodeListing.tsx b/packages/web/src/components/CodeListing.tsx index 0e89374e..16ce7bf4 100644 --- a/packages/web/src/components/CodeListing.tsx +++ b/packages/web/src/components/CodeListing.tsx @@ -9,6 +9,7 @@ export interface CodeListingProps { packageName: string; sourcePath: string; + includePackageNameInTitle?: boolean; extract?: ( sourceFile: SourceFile, project: Project @@ -19,6 +20,7 @@ export interface CodeListingProps export default function CodeListing({ packageName, sourcePath, + includePackageNameInTitle = false, extract, links = {}, ...codeBlockProps @@ -36,6 +38,9 @@ export default function CodeListing({ // bit of a HACK const listingFullSource = !extract; + const title = includePackageNameInTitle + ? <>{packageName} {sourcePath} + : sourcePath; if (Object.keys(links).length > 0) { return ( @@ -49,12 +54,13 @@ export default function CodeListing({ } return ( + // @ts-ignore element seems to work even though title says it wants string