hardhat
Version:
Hardhat is an extensible developer tool that helps smart contract developers increase productivity by reliably bringing together the tools they want.
226 lines • 9.89 kB
JavaScript
import { isObject } from "@nomicfoundation/hardhat-utils/lang";
/**
* Walks every AST in the build and indexes every `UserDefinedValueTypeDefinition`
* by its node `id`, mapping to its `underlyingType` (an `ElementaryTypeName`).
*
* User-defined value types can sit at file scope or inside a contract, and a
* struct in one source may reference a user-defined value type defined in
* another, so this must run across every AST in the build — not just the ones
* matched by the user's include globs.
*/
export function buildUserDefinedValueTypeIndex(asts) {
const index = new Map();
for (const ast of asts) {
if (!isObject(ast) || ast.nodeType !== "SourceUnit") {
continue;
}
const topLevelNodes = Array.isArray(ast.nodes) ? ast.nodes : [];
for (const node of topLevelNodes) {
if (!isObject(node)) {
continue;
}
if (node.nodeType === "UserDefinedValueTypeDefinition") {
recordUserDefinedValueType(node, index);
}
else if (node.nodeType === "ContractDefinition") {
const members = Array.isArray(node.nodes) ? node.nodes : [];
for (const member of members) {
if (isObject(member) &&
member.nodeType === "UserDefinedValueTypeDefinition") {
recordUserDefinedValueType(member, index);
}
}
}
}
}
return index;
}
function recordUserDefinedValueType(node, index) {
if (typeof node.id === "number" &&
isObject(node.underlyingType) &&
node.underlyingType.nodeType === "ElementaryTypeName") {
index.set(node.id, node.underlyingType);
}
}
/**
* Returns every struct definition reachable from a solc source AST (Abstract Syntax Tree),
* including structs nested inside contracts.
*/
export function extractStructsFromAst(ast, sourcePath, userDefinedValueTypeI = new Map()) {
if (!isObject(ast) || ast.nodeType !== "SourceUnit") {
return [];
}
const results = [];
const topLevelNodes = Array.isArray(ast.nodes) ? ast.nodes : [];
for (const node of topLevelNodes) {
if (!isObject(node)) {
continue;
}
if (node.nodeType === "StructDefinition") {
const collected = collectStruct(node, sourcePath, userDefinedValueTypeI);
if (collected !== undefined) {
results.push(collected);
}
}
else if (node.nodeType === "ContractDefinition") {
const members = Array.isArray(node.nodes) ? node.nodes : [];
for (const member of members) {
if (isObject(member) && member.nodeType === "StructDefinition") {
const collected = collectStruct(member, sourcePath, userDefinedValueTypeI);
if (collected !== undefined) {
results.push(collected);
}
}
}
}
}
return results;
}
function collectStruct(node, sourcePath, userDefinedValueTypeI) {
if (typeof node.name !== "string") {
return undefined;
}
const memberNodes = Array.isArray(node.members)
? node.members
: [];
const members = [];
for (const memberNode of memberNodes) {
if (!isObject(memberNode) ||
memberNode.nodeType !== "VariableDeclaration") {
continue;
}
if (typeof memberNode.name !== "string") {
continue;
}
members.push({
name: memberNode.name,
type: encodeMemberType(memberNode.typeName, userDefinedValueTypeI),
});
}
return {
name: node.name,
members,
sourcePath,
};
}
/**
* Converts a solc `typeName` AST node into its EIP-712 type string, following
* the same conventions as `forge bind-json`:
*
* - elementary types pass through (`address`, `uint256`, `string`, ...)
* - enums → `uint8`
* - contracts / interfaces → `address`
* - structs → bare name (`Wallet.Person` → `Person`)
* - user-defined value types → underlying elementary type (`type Foo is bytes32` → `bytes32`)
* - arrays → `T[]` (dynamic) or `T[N]` (fixed)
* - mappings / functions → `undefined` (not EIP-712 encodable)
*/
export function encodeMemberType(typeName, userDefinedValueTypeI = new Map()) {
if (!isObject(typeName)) {
return undefined;
}
switch (typeName.nodeType) {
case "ElementaryTypeName": {
// Prefer `typeDescriptions.typeString` over `name`: solc emits the
// unresolved alias in `name` (`uint`, `int`, `byte`), but the canonical
// EIP-712 type is in `typeString` (`uint256`, `int256`, `bytes1`).
// `address payable` is the exception — `typeString` is `"address payable"`
// while the canonical EIP-712 type is just `address`.
const desc = isObject(typeName.typeDescriptions)
? typeName.typeDescriptions
: undefined;
const typeString = typeof desc?.typeString === "string" ? desc.typeString : undefined;
if (typeString !== undefined) {
return typeString.endsWith(" payable")
? typeString.slice(0, -" payable".length)
: typeString;
}
return typeof typeName.name === "string" ? typeName.name : undefined;
}
case "UserDefinedTypeName": {
// `typeDescriptions.typeString` is the only reliable way to tell what
// kind of user-defined type this is — e.g. "struct Foo", "enum Bar",
// "contract Token". The AST node itself doesn't say. Note that solc
// emits the "contract " prefix for interface references as well.
const desc = isObject(typeName.typeDescriptions)
? typeName.typeDescriptions
: undefined;
const typeString = typeof desc?.typeString === "string" ? desc.typeString : "";
if (typeString.startsWith("enum ")) {
return "uint8";
}
if (typeString.startsWith("contract ")) {
return "address";
}
if (typeString.startsWith("struct ")) {
// EIP-712 references structs by their bare name, so strip both the
// "struct " prefix, any storage-location suffix solc may append
// ("memory", "storage", ...), and any qualifier ("Wallet.Person").
const remainder = typeString.slice("struct ".length).trim();
const namePart = remainder.split(/\s+/)[0];
const segments = namePart.split(".");
return segments[segments.length - 1];
}
// User-defined value types (`type Foo is bytes32;`, solc 0.8.8+).
// Resolve via `referencedDeclaration` against the build-wide
// user-defined value type index
// and recurse on the underlying elementary type — matching forge's
// `Resolver::resolve_type` so `Foo h` encodes as `bytes32 h`, not `Foo h`.
const refId = typeof typeName.referencedDeclaration === "number"
? typeName.referencedDeclaration
: isObject(typeName.pathNode) &&
typeof typeName.pathNode.referencedDeclaration === "number"
? typeName.pathNode.referencedDeclaration
: undefined;
if (refId !== undefined) {
const underlying = userDefinedValueTypeI.get(refId);
if (underlying !== undefined) {
return encodeMemberType(underlying, userDefinedValueTypeI);
}
}
// Fallback when the reference can't be resolved (missing
// `referencedDeclaration`, or its definition wasn't in the build).
// Emitting the name lets the downstream encoder produce a clear error
// rather than failing silently here.
if (typeof typeName.name === "string") {
return typeName.name;
}
if (isObject(typeName.pathNode) &&
typeof typeName.pathNode.name === "string") {
const segments = typeName.pathNode.name.split(".");
return segments[segments.length - 1];
}
return undefined;
}
case "ArrayTypeName": {
const base = encodeMemberType(typeName.baseType, userDefinedValueTypeI);
if (base === undefined) {
return undefined;
}
// solc omits `length` entirely for dynamic arrays; it isn't emitted as `null`.
const length = typeName.length;
if (length === null || length === undefined) {
return `${base}[]`;
}
// Always read from typeString: `Literal.value` preserves source text
// (`0x100`, `1_000`) but typeString canonicalizes to decimal.
const desc = isObject(typeName.typeDescriptions)
? typeName.typeDescriptions
: undefined;
const typeString = typeof desc?.typeString === "string" ? desc.typeString : "";
const match = /\[(\d+)\]$/.exec(typeString);
if (match !== null) {
return `${base}[${match[1]}]`;
}
return undefined;
}
case "Mapping":
return undefined;
case "FunctionTypeName":
// EIP-712 can't encode function types.
return undefined;
default:
return undefined;
}
}
//# sourceMappingURL=ast-walker.js.map