diff --git a/src/render.test.ts b/src/render.test.ts index c639f0af..c2da5a6d 100644 --- a/src/render.test.ts +++ b/src/render.test.ts @@ -4,6 +4,7 @@ import { buildSite, PageStructure, SiteConfig } from './site'; import { itemPartialName, render } from './render'; import { NodeType } from 'solidity-ast/node'; import { Templates } from './templates'; +import * as defaultProperties from './common/properties'; interface TestSpec extends Templates { collapseNewlines?: boolean; @@ -22,7 +23,7 @@ function testRender(title: string, file: string, spec: TestSpec, expected: strin }; test(title, t => { - const site = buildSite(t.context.build, cfg); + const site = buildSite(t.context.build, cfg, spec.properties ?? {}); const rendered = render(site, spec, spec.collapseNewlines); t.is(rendered.length, 1); t.is(rendered[0]!.contents, expected); @@ -62,3 +63,17 @@ testRender('item partial', }, 'A, B, ', ); + +// Solidity identifiers may contain '$'. ERC-7201 namespaced storage names its struct +// pointer '$', so `@return $ ...` has to resolve to that return parameter rather than +// throwing, and `@param a$b ...` has to attach to `a$b` rather than silently missing it. +testRender('dollar in parameter and return names', + 'S08_Dollar', + { + properties: { ...defaultProperties }, + partials: { + page: () => '{{#each items}}{{#each items}}{{#each params}}({{name}}:{{natspec}}){{/each}}{{#each returns}}[{{name}}:{{natspec}}]{{/each}}{{/each}}{{/each}}', + }, + }, + '[$:the storage struct pointer](a$b:the first operand)($c:the second operand)[$d:the sum]', +); diff --git a/src/utils/natspec.ts b/src/utils/natspec.ts index 40f752aa..e126eba4 100644 --- a/src/utils/natspec.ts +++ b/src/utils/natspec.ts @@ -8,6 +8,11 @@ import { ItemError } from './ItemError'; import { readItemDocs } from './read-item-docs'; import { getContractsInScope } from './scope'; +// Solidity identifiers may contain '$' and may start with '$' or '_', which \w does not +// cover. ERC-7201 namespaced storage conventionally names its struct pointer '$', so a +// tag like `@return $ the storage struct` has to match here. +const IDENTIFIER = String.raw`[A-Za-z_$][A-Za-z0-9_$]*`; + export interface NatSpec { title?: string; notice?: string; @@ -59,7 +64,7 @@ export function parseNatspec(item: DocItemWithContext): NatSpec { } if (tag === 'param') { - const paramMatches = content.match(/(\w+) ([^]*)/); + const paramMatches = content.match(new RegExp(`(${IDENTIFIER}) ([^]*)`)); if (paramMatches) { const [, name, description] = paramMatches as [string, string, string]; res.params ??= []; @@ -80,7 +85,7 @@ export function parseNatspec(item: DocItemWithContext): NatSpec { if (!p.name) { res.returns.push({ description: content.trim() }); } else { - const paramMatches = content.match(/(\w+)( ([^]*))?/); + const paramMatches = content.match(new RegExp(`(${IDENTIFIER})( ([^]*))?`)); if (!paramMatches || paramMatches[1] !== p.name) { throw new ItemError(`Expected @return tag to start with name '${p.name}'`, item); } diff --git a/test-contracts/S08_Dollar.sol b/test-contracts/S08_Dollar.sol new file mode 100644 index 00000000..5477ad34 --- /dev/null +++ b/test-contracts/S08_Dollar.sol @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.11; + +/// Identifiers may contain '$', as ERC-7201 namespaced storage does for its struct pointer. +contract Dollar { + struct Storage { + uint256 value; + } + + /** + * @notice Loads the storage struct from its slot. + * @return $ the storage struct pointer + */ + function loadStorage() internal pure returns (Storage storage $) { + assembly { + $.slot := 0 + } + } + + /** + * @notice Takes a parameter whose name contains '$'. + * @param a$b the first operand + * @param $c the second operand + * @return $d the sum + */ + function add(uint256 a$b, uint256 $c) external pure returns (uint256 $d) { + $d = a$b + $c; + } +}