From 5bd0bed033abad2a4c971459fb786aa553aedff2 Mon Sep 17 00:00:00 2001 From: Galmanus Date: Tue, 25 Aug 2026 07:26:35 -0300 Subject: [PATCH] refactor(core): single source for placeholder grammar and expect matchers (#158) - Export CONTRACT_ID_PLACEHOLDER_PATTERN from placeholder-engine and build both the global resolver regex and the anchored validation regex from it, so the placeholder grammar can't drift between resolve-time and validation-time. - Derive EXPECT_MATCHERS from ExpectMatcherSchema.options instead of a hand-kept Set, so adding a matcher only means editing the Zod enum. tsc + affected suites pass. --- .../core/src/config/validate-contract-graph.ts | 5 ++++- packages/core/src/contracts/placeholder-engine.ts | 9 ++++++++- packages/core/src/contracts/verify-expect.ts | 15 ++++----------- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/packages/core/src/config/validate-contract-graph.ts b/packages/core/src/config/validate-contract-graph.ts index 48d4c19a..2398f294 100644 --- a/packages/core/src/config/validate-contract-graph.ts +++ b/packages/core/src/config/validate-contract-graph.ts @@ -1,8 +1,11 @@ import type { ContractConfig } from "./config.schema.js"; import { CaatingaError, CaatingaErrorCode } from "../errors/CaatingaError.js"; import { resolveDeployOrder } from "../contracts/resolve-deploy-order.js"; +import { CONTRACT_ID_PLACEHOLDER_PATTERN } from "../contracts/placeholder-engine.js"; -const CONTRACT_ID_PLACEHOLDER = /^\$\{contracts\.([A-Za-z0-9_-]+)\.contractId\}$/; +// #158: same grammar as the resolver, anchored so a value must be exactly a +// single placeholder. +const CONTRACT_ID_PLACEHOLDER = new RegExp(`^${CONTRACT_ID_PLACEHOLDER_PATTERN}$`); function parseContractIdPlaceholder(value: string): string | undefined { return value.match(CONTRACT_ID_PLACEHOLDER)?.[1]; diff --git a/packages/core/src/contracts/placeholder-engine.ts b/packages/core/src/contracts/placeholder-engine.ts index 5a788677..e979252e 100644 --- a/packages/core/src/contracts/placeholder-engine.ts +++ b/packages/core/src/contracts/placeholder-engine.ts @@ -7,7 +7,14 @@ export type PlaceholderContext = { sourceAddress?: string; }; -const CONTRACT_ID_REGEX = /\$\{contracts\.([A-Za-z0-9_-]+)\.contractId\}/g; +/** + * Grammar of a `${contracts..contractId}` placeholder (#158). Exported as + * a source string so the anchored validation-time check and the global + * resolve-time replacement share one definition and can't drift. + */ +export const CONTRACT_ID_PLACEHOLDER_PATTERN = String.raw`\$\{contracts\.([A-Za-z0-9_-]+)\.contractId\}`; + +const CONTRACT_ID_REGEX = new RegExp(CONTRACT_ID_PLACEHOLDER_PATTERN, "g"); const SOURCE_ADDRESS_REGEX = /\$\{source\.address\}/g; export function resolvePlaceholders(text: string, context: PlaceholderContext): string { diff --git a/packages/core/src/contracts/verify-expect.ts b/packages/core/src/contracts/verify-expect.ts index 2e06a366..ace05acd 100644 --- a/packages/core/src/contracts/verify-expect.ts +++ b/packages/core/src/contracts/verify-expect.ts @@ -1,4 +1,5 @@ import { CaatingaError, CaatingaErrorCode } from "../errors/CaatingaError.js"; +import { ExpectMatcherSchema } from "../config/config.schema.js"; import type { ExpectMatcher, ExpectSpec } from "../config/config.schema.js"; export type VerifyExpectResult = { @@ -14,17 +15,9 @@ export type VerifyExpectFailure = { export type VerifyExpectOutcome = VerifyExpectResult | VerifyExpectFailure; -const EXPECT_MATCHERS: ReadonlySet = new Set([ - "equals", - "reachable", - "isNull", - "isArray", - "minLength", - "maxLength", - "contains", - "matches", - "jsonEquals", -]); +// #158: derive the allowed matchers from the canonical Zod enum so the list +// can't drift from the schema and a new matcher only needs adding in one place. +const EXPECT_MATCHERS: ReadonlySet = new Set(ExpectMatcherSchema.options); function describeExpectSpec(spec: ExpectSpec): string { if (typeof spec === "string") {