Validate the emitted constant name in all four emitters - #123
Conversation
`bytesConstantString`, `uint8ConstantString`, `bytes32ConstantString` and `addressConstantString` interpolate `name` verbatim into a generated Solidity declaration with no check, while the contract name reaching the same file is checked. A name carrying a space or a `-` emits a file that does not compile, and a `;` emits one that compiles into a different set of declarations. All four now call the same identifier check the contract name already used. The check is renamed `requireIdentifier` and its error `InvalidIdentifier`, because the rule is the Solidity identifier and it is now applied to constant names as well as contract names. Closes #80 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Warning Review limit reached
Next review available in: 44 minutes Limit details: You’ve used all 1 included review currently available under your plan. You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: 📒 Files selected for processing (12)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
# Conflicts: # test/lib/LibCodeGen.requireIdentifier.t.sol
# Conflicts: # src/lib/LibFs.sol # test/lib/LibCodeGen.requireContractName.t.sol # test/lib/LibCodeGen.requireIdentifier.t.sol # test/src/lib/LibCodeGen.addressConstantString.t.sol # test/src/lib/LibCodeGen.bytes32ConstantString.t.sol # test/src/lib/LibCodeGen.bytesConstantString.t.sol # test/src/lib/LibCodeGen.describedByMetaHashConstantString.t.sol # test/src/lib/LibCodeGen.requireContractName.t.sol # test/src/lib/LibCodeGen.uint8ConstantString.t.sol # test/src/lib/LibFs.buildFileForContract.t.sol
# Conflicts: # test/src/lib/LibCodeGen.addressConstantString.t.sol # test/src/lib/LibCodeGen.bytes32ConstantString.t.sol # test/src/lib/LibCodeGen.bytesConstantString.t.sol # test/src/lib/LibCodeGen.describedByMetaHashConstantString.t.sol # test/src/lib/LibCodeGen.requireIdentifier.t.sol # test/src/lib/LibCodeGen.uint8ConstantString.t.sol
# Conflicts: # test/src/lib/LibFs.buildFileForContract.t.sol
#127 turned the unlink into `while (isPresent(...))` in the same function these parameters run through. Taken as main has it: how many times the path is unlinked is independent of what the header says. #127's new testBuildFileForContractReplacesLiveSymlink merged with no conflict marker and the wrong arity, because neither side touched those two lines. It is threaded here, with the same comment the file's other generating test carries about this repo's own values standing in where nothing reads the header. The compiler is what caught it, not the merge. #123 renamed requireContractName to requireIdentifier and InvalidContractName to InvalidIdentifier, conflicting with the import block this branch had grown. Both sides taken.
Closes #80
What was wrong
bytesConstantString,uint8ConstantString,bytes32ConstantStringandaddressConstantStringinterpolatenameverbatim into a generated Soliditydeclaration and never check it, while the contract name reaching the same
generated file is checked on every path that carries it. A name with a space or
a
-emits a file that does not compile, with the error pointing at generatedcode nobody wrote; a name with a
;emits a file that compiles into adifferent set of declarations than the caller asked for.
What changed
All four emitters now call the identifier check the contract name already used,
so every name this library interpolates — into a path or into a declaration —
is a Solidity identifier or is a revert.
The check is renamed
requireIdentifierand its errorInvalidContractNameisrenamed
InvalidIdentifier. See the decision below.Decision: rename, and why the error had to move with it
The issue offered the rename as a choice — its proposed fix ends
"if the shared name reads badly, rename it
requireIdentifierand keep oneimplementation". Taken, on this evidence:
call sites, and four of them are constant names, not contract names.
script that derives a constant name from data and gets it wrong would have
reverted with
InvalidContractName("MY CONSTANT"), pointing the author at thecontract name — a parameter that was fine. That is a diagnostic defect, not a
cosmetic one, so renaming the function without the error would have been half
a fix.
name are both exactly a Solidity identifier. One implementation, named for the
property it enforces.
Ledger
LibCodeGen.sol,LibFs.soland the suite, andtest/src/lib/LibCodeGen.requireContractName.t.solrenamed totest/src/lib/LibCodeGen.requireIdentifier.t.solso the file still names thefunction under test. 248 insertions, 67 deletions over 12 files, no new files.
requireContractNameandInvalidContractNameareinternal/file-level in a published soldeer package,so a rename is a source break for any consumer that names them. Every
.solfile under
/home/gildlab/code— all local Rain-org clones plus their vendoreddependency trees, including chains as deep as
cyclo.site → ethgild → rain.flare → rain.interpreter → rain.interpreter.interface → rain.sol.codegen— was swept for both symbols. They occur in 20 files andevery one of them is a working clone of
rain.sol.codegenitself. Noconsumer and no vendored copy under any
dependencies/tree names eithersymbol.
pureand runs at build time only.sites. Nothing is layered on top of it.
Does the tightening break a live consumer? No.
Applying the check narrows what a published library accepts, so I went looking
for a name that would now revert rather than reasoning about it. The
nameargument was extracted at every call site of the four emitters across every
.solfile on this box (all local Rain-org clones plus their vendoreddependency trees), 40 distinct
(emitter, name)pairs:identifier:
DEPLOYED_ADDRESS,CREATION_CODE,RUNTIME_CODE,DEPENDENCIES,PARSE_META,PARSE_META_BUILD_DEPTH,LOG_TABLES,LOG_TABLES_SMALL,LOG_TABLES_SMALL_ALT,ANTI_LOG_TABLES,ANTI_LOG_TABLES_SMALL, plus this library's own*_FUNCTION_POINTERS,BYTECODE_HASHandDESCRIBED_BY_META_HASH.namearguments anywhere are in this repo's own testsuite, which is updated here.
st0x.deploy'stagSuffix()— the derived-name case the issue names — doesbuild constant names from a tag, but it concatenates them into
vm.writeLinein its own
BuildPointers.sol, not intoLibCodeGen. It is the shape ofconsumer that would be caught, and it is not currently affected.
Caveat, stated rather than papered over: GitHub code search returned
total_count: 0for terms that certainly exist in this repo, includingrepo:-scoped queries, so it was not usable as a second oracle. The sweep aboveis a local-clone sweep, not a global one.
QA
All numbers below are measured on the merge commit against
mainat89cb0a2,not on the branch as originally written. The QA that was here before this merge
cited
test/lib/*.t.solpaths that #56 has since moved and suite totals from atree that no longer exists, so it has been replaced rather than annotated.
testBytesConstantStringRejectsNonIdentifierName,testBytesConstantStringNameMustBeIdentifier,testUint8ConstantStringRejectsNonIdentifierName,testUint8ConstantStringNameMustBeIdentifier,testBytes32ConstantStringRejectsNonIdentifierName,testBytes32ConstantStringNameMustBeIdentifier,testAddressConstantStringRejectsNonIdentifierName,testAddressConstantStringNameMustBeIdentifier. Each pair is shown below tobe the only thing standing between its emitter's check and a silent pass.
LibCodeGenSlow.isIdentifierSlow,which decides membership against the identifier alphabets written out
character by character, not from the range arithmetic
requireIdentifieruses; the emitted text comes from
LibCodeGenSlow.*ConstantStringSlow, whichbuilds the line and measures it rather than reproducing the library's sum. The
five spelled-out rejected names (
"SOME NAME","SOME;NAME","SOME-NAME","","0LEADING") come from the hazards named in the issue, not from theimplementation.
nameparameter ofbytesConstantString,uint8ConstantString,bytes32ConstantStringandaddressConstantStringtobe validated, and names the shared-name decision; covered all four emitters
plus the decision (rename taken, ledger above), and the pre-existing
describedByMetaHashConstantString/pathForContractcallers of the samecheck are re-verified by mutant 1 rather than assumed.
1. Suite before and after.
nix develop -c forge test:origin/mainatff69c10Ran 23 test suites: 165 tests passed, 0 failed, 0 skipped (165 total tests)main(2801340)Ran 23 test suites: 163 tests passed, 0 failed, 0 skipped (163 total tests)3325514)Ran 23 test suites: 173 tests passed, 0 failed, 0 skipped (173 total tests)The eight new tests are the whole difference against
main, and no pre-existingtest was deleted. Compared by test name rather than by count:
mainatff69c10: exactly eight added — the…NameMustBeIdentifierand…RejectsNonIdentifierNamepair for each of thefour emitters — plus the fourteen
testRequireContractName*renamed 1:1 totestRequireIdentifier*. Nothing removed.2801340: exactly tenadded, every one of them
main's own —#100'stestBuildFileForContractFailedBuildKeepsExistingFileand ninetestBytesToHex*. Nothing removed. So the re-merge dropped nothing fromeither side.
The whole suite was run on the merge commit rather than only the merged files,
because a semantic conflict does not appear between conflict markers.
2. Mutation matrix. Each
requireIdentifier(name);call removed one at atime, the whole suite run each time, then restored. Every run reports
163 total tests, which is what proves the suite compiled and ran rather thanerroring out and reading as "survived".
Measured on this branch at
2801340, when the suite was 163 tests. The laterre-merge of
maindid not disturb it: the ten testsmainadded are#100'sbuildFileForContractordering test and nineLibHexString.bytesToHextests,none of which reach an emitter's
nameparameter, andmainchanged no lineany of these five mutants touches.
163 passed, 0 failed (163 total)requireIdentifier(name)fromdescribedByMetaHashConstantString(the pre-existing call)162 passed, 1 failed (163 total)testDescribedByMetaHashConstantStringRejectsNonIdentifierNamerequireIdentifier(name)frombytesConstantString158 passed, 5 failed (163 total)testBytesConstantStringRejectsNonIdentifierName,testBytesConstantStringNameMustBeIdentifier, + 3 unrelated (see below)requireIdentifier(name)fromuint8ConstantString158 passed, 5 failed (163 total)testUint8ConstantStringRejectsNonIdentifierName,testUint8ConstantStringNameMustBeIdentifier, + the same 3requireIdentifier(name)frombytes32ConstantString158 passed, 5 failed (163 total)testBytes32ConstantStringRejectsNonIdentifierName,testBytes32ConstantStringNameMustBeIdentifier, + the same 3requireIdentifier(name)fromaddressConstantString158 passed, 5 failed (163 total)testAddressConstantStringRejectsNonIdentifierName,testAddressConstantStringNameMustBeIdentifier, + the same 3Every mutant is killed, and each of the four new mutants is killed by exactly
the two tests written for that emitter and by nothing else — so the four call
sites are covered independently rather than by one test that happens to reach
them all.
Mutant 1 is the check that already existed, included to show the new one in
bytes32ConstantStringdid not make it redundant and untested. Without it atraversing name reaches
vm.readFileBinarybefore any constant name is checked,and the test catches the filesystem error in place of the revert:
The 3 unrelated failures, stated rather than filtered out. From mutant 2
onward every run also reported:
These are in
test/src/lib/LibCodeGen.bytecodeHashConstantString.t.sol, whichthis PR does not touch, and none of the mutations reaches that code. A fuzz run
drew the counterexample by seed and
forgethen cached and replayed it, whichis why it appears from mutant 2 onward and in the restore run. It reproduces on
unmodified
origin/mainatc816251with the identical counterexample:8 passed, 3 failedfor that suite alone. It is a pre-existing gap in thatsuite's own
assumeEtchableCode, which excludes0xef01…only when the lengthis not 23 —
forgealso rejects a 23-byte0xef01…whose third byte is not0x00, which is exactly this counterexample. Out of scope here and not fixed;reported so it is not mistaken for something this branch did. The cache was
cleared afterwards and this tree is clean.
3. Formatting and coverage.
nix develop -c forge fmt --checkexits 0 on themerge commit.
forge coverageon the merge commit:4. Merging
origin/main.mainmoved a long way under this branch —#138deleted the worked example (script/Build.sol,src/generated/CodeGennable.sol,.github/workflows/build-pointers.yaml),#112splitLibFsinto adir-taking overload with a privatepathForContractIn,#56moved every.t.solinto thetest/src/mirrortree, and
#100reorderedbuildFileForContractso the content is built beforeany disk mutation.
origin/mainis merged in, not rebased, twice — once at89cb0a2and again atff69c10after#100landed and made this branchconflict. Resolutions:
src/lib/LibFs.sol(content):main's structure taken. The rename now landson
pathForContractIn, which is where the check moved to under Cover the generated directory being created, by making the directory injectable #112, ratherthan on the flat
pathForContractthis branch was written against.src/lib/LibFs.solagain, after LibFs: build the generated file content before touching disk #100 landed: LibFs: build the generated file content before touching disk #100 hoists the generatedcontent into a local before
createDir, the unlink and the write, so abuild that reverts cannot destroy the file already there. That ordering is
taken as-is and is the only shape in the merged function; nothing was moved
above the content computation and the compute-at-write shape was not restored.
This PR's only line in that file remains the
requireContractName→requireIdentifierrename insidepathForContractIn, whichbuildFileForContractcalls before it builds the content, so an invalid name still reverts before
anything on disk is touched. LibFs: build the generated file content before touching disk #100's discriminating test,
testBuildFileForContractFailedBuildKeepsExistingFile, passes on the mergecommit.
LibCodeGen.requireContractName.t.sol(rename/rename): both sides renamed thesame file to different places. Resolved onto
test/src/lib/LibCodeGen.requireIdentifier.t.sol— thetest/src/mirrorMove every .t.sol into the test/src/lib mirror tree #56 requires, under the name this PR gives the function.
main's content istaken whole: its
isIdentifierSloworacle and its alphabet-stated…AlphabetCannotTraversereplace the hand-rolled versions this branchcarried, with only the rename and this PR's own NatSpec applied on top.
*ConstantString.t.solfiles,LibFs.buildFileForContract.t.solandLibFs.t.sol(imports and one doc comment):main's import paths andmain's rewritten doc comments taken, this PR'sInvalidIdentifierand itsnew tests kept. Nothing
mainlanded today in those files was dropped: eachfile's diff against
mainis only the rename and this PR's own additions.test/concrete/CodeGennable.sol: its doc comment names the test file thatasserts on the contract's name, so it follows the rename.
#126(forge-std 1.16.1 → 1.16.2) landed while this was being resolved andrewrote the versioned import prefix in every file. Every file this PR touches
carries
forge-std-1.16.2/after the merge, including the renamed test file,which
gitcould not carrymain's edit onto by itself.forge-std-1.16.1has zero hits anywhere outside
dependencies/,out/andcache/.No reference to
requireContractName,InvalidContractNameorisContractNameSlowsurvives anywhere insrc/ortest/.Where the issue's proposed fix needed correcting
requireContractName(name)to the four emitters andstopped. It did not account for the four existing fuzz tests
(
test*MatchesMeasuredLine) that fuzznameas an arbitrarystring— underthe fix those names are rejected, so every one of them would have reverted.
They now build the name from a fuzzed seed via
LibCodeGenSlow.nameFromSeedSlow, which is the helper the suite already hadfor exactly this, and as a result they now exercise the accepted half of the
domain instead of a domain the library no longer accepts.
function for the reason given above.
Found, not touched
commentis interpolated verbatim into the same generated file with no checkat all, and unlike
namethere is no rule stated anywhere for what a commentmay contain. A comment carrying a bare newline emits lines that are not
comments. Outside this issue; not filed.
assumeEtchableCodegap intest/src/lib/LibCodeGen.bytecodeHashConstantString.t.soldescribed under QA.Pre-existing on
main, outside this issue; not filed.