Skip to content

Commit 116717f

Browse files
Merge remote-tracking branch 'origin/main' into 2026-08-16-issue-78
`main` gave `buildFileForContract` the caller's licence and copyright and renamed the identifier rule, neither of which a textual merge reaches: the tagged overload delegates to a signature that no longer exists. It carries both through to the delegate, and the paragraph restating the single unlink is replaced by what the loop main landed actually does. The two sides each added a symlink test to `LibFsIsPresentTest`; both stay, one for the tagged path and one for a live link. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2 parents 8b3fec3 + 1c81613 commit 116717f

19 files changed

Lines changed: 861 additions & 170 deletions

foundry.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "rain-sol-codegen"
3-
version = "0.1.30"
3+
version = "0.1.32"
44

55
[profile.default]
66
src = 'src'

src/lib/LibCodeGen.sol

Lines changed: 85 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,27 @@ uint256 constant MAX_LINE_LENGTH = 120;
1717
/// needs to match what formatters expect.
1818
string constant NEWLINE_DUE_TO_MAX_LENGTH = "\n ";
1919

20-
/// Thrown when a contract name is not a Solidity identifier. Such a name cannot
21-
/// be interpolated into a file path or a constant declaration.
20+
/// Thrown when a name is not a Solidity identifier. Such a name cannot be
21+
/// interpolated into a file path or a constant declaration.
2222
/// @param name The rejected name.
23-
error InvalidContractName(string name);
23+
error InvalidIdentifier(string name);
2424

2525
/// Thrown when a bytecode hash is asked for at an address that holds no code.
2626
/// @param instance The address that holds no code.
2727
error CodelessInstance(address instance);
2828

29+
/// Thrown when an SPDX licence identifier is not a non-empty single line. An
30+
/// empty identifier names no licence on a tag that a presence check accepts, and
31+
/// solc refuses the file it heads with "Invalid SPDX license identifier". A line
32+
/// break ends the tag's line so that everything after it lands as source.
33+
/// @param spdxLicenseIdentifier The rejected identifier.
34+
error InvalidSpdxLicenseIdentifier(string spdxLicenseIdentifier);
35+
36+
/// Thrown when a copyright text is not a non-empty single line, for the same
37+
/// reasons the licence identifier has to be one.
38+
/// @param copyrightText The rejected text.
39+
error InvalidCopyrightText(string copyrightText);
40+
2941
/// @title LibCodeGen
3042
/// @notice Library for generating Solidity code snippets for contract function
3143
/// pointers, code hashes, associated comments, etc. All snippets are returned
@@ -34,22 +46,27 @@ error CodelessInstance(address instance);
3446
library LibCodeGen {
3547
/// Reverts unless `name` is a Solidity identifier: at least one character,
3648
/// drawn from ASCII letters, digits, `_` and `$`, and not starting with a
37-
/// digit. A contract name is such an identifier, and restricting it to one
38-
/// is also what makes it safe to interpolate into a path: no identifier
39-
/// contains a path separator, and none of them is `.` or `..`.
49+
/// digit. Every name this library interpolates verbatim into generated
50+
/// source or into a path is one, and being an identifier is what makes that
51+
/// interpolation safe. A declaration named by an identifier is the
52+
/// declaration the caller asked for and no other, because no identifier
53+
/// carries a space, a `;` or any other character that ends a declaration or
54+
/// starts another. A path built from an identifier stays a direct child of
55+
/// the directory it is joined to, because no identifier contains a path
56+
/// separator and none of them is `.` or `..`.
4057
/// @param name The name to check.
41-
function requireContractName(string memory name) internal pure {
58+
function requireIdentifier(string memory name) internal pure {
4259
bytes memory nameBytes = bytes(name);
4360
if (nameBytes.length == 0) {
44-
revert InvalidContractName(name);
61+
revert InvalidIdentifier(name);
4562
}
4663
for (uint256 i = 0; i < nameBytes.length; i++) {
4764
bytes1 char = nameBytes[i];
4865
bool isLetter = (char >= 0x41 && char <= 0x5A) || (char >= 0x61 && char <= 0x7A);
4966
bool isDigit = char >= 0x30 && char <= 0x39;
5067
bool isUnderscoreOrDollar = char == 0x5F || char == 0x24;
5168
if (!(isLetter || isUnderscoreOrDollar || (isDigit && i > 0))) {
52-
revert InvalidContractName(name);
69+
revert InvalidIdentifier(name);
5370
}
5471
}
5572
}
@@ -66,16 +83,58 @@ library LibCodeGen {
6683
return bytes(comment).length == 0 ? "\n" : string.concat("\n", comment, "\n");
6784
}
6885

86+
/// True when `text` can be interpolated into a header line as itself: at
87+
/// least one byte, and no byte that ends a line. Solidity ends a `//`
88+
/// comment at either a line feed or a carriage return, so a value carrying
89+
/// one would close the tag's line and continue as source.
90+
/// @param text The text to check.
91+
/// @return Whether the text is a non-empty single line.
92+
function isSingleLine(string memory text) internal pure returns (bool) {
93+
bytes memory textBytes = bytes(text);
94+
if (textBytes.length == 0) {
95+
return false;
96+
}
97+
for (uint256 i = 0; i < textBytes.length; i++) {
98+
if (textBytes[i] == 0x0A || textBytes[i] == 0x0D) {
99+
return false;
100+
}
101+
}
102+
return true;
103+
}
104+
69105
/// The file prefix for autogenerated files outlines the license, pragma,
70106
/// and a note about the file being autogenerated. The pragma is ^ as the
71107
/// generated code is expected to be imported into some concrete contract
72108
/// with pragma = version.
73-
function filePrefix() internal pure returns (string memory) {
109+
///
110+
/// The generated file lands in the calling project's repo, so the licence it
111+
/// is under and the copyright holder it names are the calling project's to
112+
/// state and are taken from the caller. Both are interpolated verbatim into
113+
/// their tags, and both have to be a non-empty single line for the tag they
114+
/// land on to say what it appears to.
115+
/// @param spdxLicenseIdentifier The SPDX licence identifier for the
116+
/// generated file, interpolated verbatim.
117+
/// @param copyrightText The copyright text for the generated file,
118+
/// interpolated verbatim.
119+
/// @return The text that heads the generated file.
120+
function filePrefix(string memory spdxLicenseIdentifier, string memory copyrightText)
121+
internal
122+
pure
123+
returns (string memory)
124+
{
125+
if (!isSingleLine(spdxLicenseIdentifier)) {
126+
revert InvalidSpdxLicenseIdentifier(spdxLicenseIdentifier);
127+
}
128+
if (!isSingleLine(copyrightText)) {
129+
revert InvalidCopyrightText(copyrightText);
130+
}
74131
//REUSE-IgnoreStart
75132
return string.concat(
76-
"// SPDX-License-Identifier: LicenseRef-DCL-1.0\n"
77-
"// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd\n"
78-
"pragma solidity ^0.8.25;\n\n",
133+
"// SPDX-License-Identifier: ",
134+
spdxLicenseIdentifier,
135+
"\n" "// SPDX-FileCopyrightText: ",
136+
copyrightText,
137+
"\n" "pragma solidity ^0.8.25;\n\n",
79138
"// THIS FILE IS AUTOGENERATED BY THE BUILD SCRIPT. DO NOT EDIT BY HAND.\n"
80139
);
81140
//REUSE-IgnoreEnd
@@ -253,7 +312,7 @@ library LibCodeGen {
253312
/// @return A string containing the Solidity code for the described by meta
254313
/// hash constant.
255314
function describedByMetaHashConstantString(Vm vm, string memory name) internal view returns (string memory) {
256-
requireContractName(name);
315+
requireIdentifier(name);
257316
bytes memory describedByMeta = vm.readFileBinary(string.concat("meta/", name, ".rain.meta"));
258317
return bytes32ConstantString(
259318
vm,
@@ -269,14 +328,16 @@ library LibCodeGen {
269328
/// @param vm The Vm instance used to format values as strings.
270329
/// @param comment The comment to include above the constant declaration.
271330
/// An empty comment emits no comment line.
272-
/// @param name The name of the constant.
331+
/// @param name The name of the constant, interpolated verbatim. Has to be a
332+
/// Solidity identifier.
273333
/// @param data The bytes data for the constant.
274334
/// @return A string containing the Solidity code for the bytes constant.
275335
function bytesConstantString(Vm vm, string memory comment, string memory name, bytes memory data)
276336
internal
277337
pure
278338
returns (string memory)
279339
{
340+
requireIdentifier(name);
280341
string memory hexData = LibHexString.bytesToHex(vm, data);
281342
return string.concat(
282343
commentPrefix(comment),
@@ -297,14 +358,16 @@ library LibCodeGen {
297358
/// @param vm The Vm instance used to format values as strings.
298359
/// @param comment The comment to include above the constant declaration.
299360
/// An empty comment emits no comment line.
300-
/// @param name The name of the constant.
361+
/// @param name The name of the constant, interpolated verbatim. Has to be a
362+
/// Solidity identifier.
301363
/// @param data The uint8 data for the constant.
302364
/// @return A string containing the Solidity code for the uint8 constant.
303365
function uint8ConstantString(Vm vm, string memory comment, string memory name, uint8 data)
304366
internal
305367
pure
306368
returns (string memory)
307369
{
370+
requireIdentifier(name);
308371
string memory intString = vm.toString(data);
309372
return string.concat(
310373
commentPrefix(comment),
@@ -325,14 +388,16 @@ library LibCodeGen {
325388
/// @param vm The Vm instance used to format values as strings.
326389
/// @param comment The comment to include above the constant declaration.
327390
/// An empty comment emits no comment line.
328-
/// @param name The name of the constant.
391+
/// @param name The name of the constant, interpolated verbatim. Has to be a
392+
/// Solidity identifier.
329393
/// @param data The bytes32 value for the constant.
330394
/// @return A string containing the Solidity code for the bytes32 constant.
331395
function bytes32ConstantString(Vm vm, string memory comment, string memory name, bytes32 data)
332396
internal
333397
pure
334398
returns (string memory)
335399
{
400+
requireIdentifier(name);
336401
string memory hexString = vm.toString(data);
337402
return string.concat(
338403
commentPrefix(comment),
@@ -354,14 +419,16 @@ library LibCodeGen {
354419
/// @param vm The Vm instance used to format values as strings.
355420
/// @param comment The comment to include above the constant declaration.
356421
/// An empty comment emits no comment line.
357-
/// @param name The name of the constant.
422+
/// @param name The name of the constant, interpolated verbatim. Has to be a
423+
/// Solidity identifier.
358424
/// @param data The address for the constant.
359425
/// @return A string containing the Solidity code for the address constant.
360426
function addressConstantString(Vm vm, string memory comment, string memory name, address data)
361427
internal
362428
pure
363429
returns (string memory)
364430
{
431+
requireIdentifier(name);
365432
string memory addressString = vm.toString(data);
366433
return string.concat(
367434
commentPrefix(comment),

src/lib/LibFs.sol

Lines changed: 64 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ library LibFs {
102102
/// @param contractName The name of the contract, interpolated verbatim.
103103
/// @return The file path as a string.
104104
function pathForContractIn(string memory dir, string memory contractName) private pure returns (string memory) {
105-
LibCodeGen.requireContractName(contractName);
105+
LibCodeGen.requireIdentifier(contractName);
106106
return string.concat(dir, "/", contractName, ".sol");
107107
}
108108

@@ -171,30 +171,51 @@ library LibFs {
171171
/// committed already.
172172
///
173173
/// The whole file content is built before anything on disk is touched, and
174-
/// building it reverts for an `instance` that holds no code. A revert does
175-
/// not roll back cheatcode filesystem effects, so ordering the build first
176-
/// is what keeps a failed generation from leaving the directory worse than
177-
/// it found it: nothing is created, unlinked or written unless there is
178-
/// content to write.
174+
/// building it reverts for an `instance` that holds no code and for a
175+
/// licence or copyright `filePrefix` refuses. A revert does not roll back
176+
/// cheatcode filesystem effects, so ordering the build first is what keeps
177+
/// a failed generation from leaving the directory worse than it found it:
178+
/// nothing is created, unlinked or written unless there is content to
179+
/// write.
179180
///
180-
/// Anything already at the path is unlinked before the write, so a symlink
181+
/// The path is unlinked until it holds nothing, then written, so a symlink
181182
/// there is replaced by a regular file rather than written through to its
182-
/// target, including a symlink whose target does not exist, and the path
183-
/// does not exist between the unlink and the write.
184-
/// Any manual changes to the generated file, or any other existing file at
185-
/// that path, are lost.
183+
/// target, whether or not that target exists, and the path does not exist
184+
/// between the last unlink and the write. Taking a live symlink off the
185+
/// path takes what it resolves to with it, because that is what the unlink
186+
/// acts on first.
187+
/// Any manual changes to the generated file, any other existing file at
188+
/// that path, and whatever a symlink at that path resolves to, are lost.
189+
///
190+
/// A directory at the path, and a symlink at the path that resolves to a
191+
/// directory, are the cases this cannot unlink, and both revert rather than
192+
/// being written into or through.
186193
///
187194
/// The whole file is written on every call, so the same arguments always
188195
/// produce the same bytes. The prefix and bytecode hash constant are always
189196
/// included, further content is provided in the body parameter, which is
190197
/// expected to be generated by `LibCodeGen` by the caller.
198+
///
199+
/// The file lands in the calling project's repo, so the licence it is under
200+
/// and the copyright holder it names come from the caller and are subject to
201+
/// `LibCodeGen.filePrefix`'s rule for them.
191202
/// @param vm The Vm instance for file operations.
192203
/// @param instance The contract instance whose bytecode hash is to be
193204
/// included.
194205
/// @param contractName The name of the contract.
206+
/// @param spdxLicenseIdentifier The SPDX licence identifier the written file
207+
/// declares.
208+
/// @param copyrightText The copyright text the written file declares.
195209
/// @param body The body of the contract file to be written.
196-
function buildFileForContract(Vm vm, address instance, string memory contractName, string memory body) internal {
197-
buildFileForContract(vm, instance, GENERATED_DIR, contractName, body);
210+
function buildFileForContract(
211+
Vm vm,
212+
address instance,
213+
string memory contractName,
214+
string memory spdxLicenseIdentifier,
215+
string memory copyrightText,
216+
string memory body
217+
) internal {
218+
buildFileForContract(vm, instance, GENERATED_DIR, contractName, spdxLicenseIdentifier, copyrightText, body);
198219
}
199220

200221
/// @notice Builds a file for a generated contract inside `dir` rather than
@@ -217,20 +238,32 @@ library LibFs {
217238
/// @param dir The directory to put the file in, without a trailing
218239
/// separator, interpolated verbatim.
219240
/// @param contractName The name of the contract.
241+
/// @param spdxLicenseIdentifier The SPDX licence identifier the written file
242+
/// declares.
243+
/// @param copyrightText The copyright text the written file declares.
220244
/// @param body The body of the contract file to be written.
221245
function buildFileForContract(
222246
Vm vm,
223247
address instance,
224248
string memory dir,
225249
string memory contractName,
250+
string memory spdxLicenseIdentifier,
251+
string memory copyrightText,
226252
string memory body
227253
) internal {
228254
string memory path = pathForContractIn(dir, contractName);
229-
string memory content =
230-
string.concat(LibCodeGen.filePrefix(), LibCodeGen.bytecodeHashConstantString(vm, instance), body);
255+
string memory content = string.concat(
256+
LibCodeGen.filePrefix(spdxLicenseIdentifier, copyrightText),
257+
LibCodeGen.bytecodeHashConstantString(vm, instance),
258+
body
259+
);
231260
//forge-lint: disable-next-line(unsafe-cheatcode)
232261
vm.createDir(dir, true);
233-
if (isPresent(vm, path)) {
262+
// `vm.removeFile` resolves the path before it acts, so on a live symlink
263+
// it takes what the link points at and leaves the link, now dangling.
264+
// Every pass removes something the next one no longer finds, so this
265+
// ends with the path holding nothing.
266+
while (isPresent(vm, path)) {
234267
//forge-lint: disable-next-line(unsafe-cheatcode)
235268
vm.removeFile(path);
236269
}
@@ -251,17 +284,21 @@ library LibFs {
251284
/// `GENERATED_DIR` itself, so the first generation for a tag does not need
252285
/// it committed already.
253286
///
254-
/// Anything already at the path is unlinked before the write, so a symlink
287+
/// The path is unlinked until it holds nothing, then written, so a symlink
255288
/// there is replaced by a regular file rather than written through to its
256-
/// target, including a symlink whose target does not exist, and the path
257-
/// does not exist between the unlink and the write.
258-
/// Any manual changes to the generated file, or any other existing file at
259-
/// that path, are lost.
289+
/// target, whether or not that target exists, and the path does not exist
290+
/// between the last unlink and the write.
291+
/// Any manual changes to the generated file, any other existing file at
292+
/// that path, and whatever a symlink at that path resolves to, are lost.
260293
///
261294
/// The whole file is written on every call, so the same arguments always
262295
/// produce the same bytes. The prefix and bytecode hash constant are always
263296
/// included, further content is provided in the body parameter, which is
264297
/// expected to be generated by `LibCodeGen` by the caller.
298+
///
299+
/// The file lands in the calling project's repo, so the licence it is under
300+
/// and the copyright holder it names come from the caller and are subject to
301+
/// `LibCodeGen.filePrefix`'s rule for them.
265302
/// @dev This is the `dir` overload of `buildFileForContract` applied to
266303
/// `dirForTag(tag)`, so everything that overload states holds here, and the
267304
/// only thing this function adds is that the directory is not the caller's
@@ -272,14 +309,19 @@ library LibFs {
272309
/// included.
273310
/// @param tag The tag whose directory the file lives in.
274311
/// @param contractName The name of the contract.
312+
/// @param spdxLicenseIdentifier The SPDX licence identifier the written file
313+
/// declares.
314+
/// @param copyrightText The copyright text the written file declares.
275315
/// @param body The body of the contract file to be written.
276316
function buildFileForTaggedContract(
277317
Vm vm,
278318
address instance,
279319
string memory tag,
280320
string memory contractName,
321+
string memory spdxLicenseIdentifier,
322+
string memory copyrightText,
281323
string memory body
282324
) internal {
283-
buildFileForContract(vm, instance, dirForTag(tag), contractName, body);
325+
buildFileForContract(vm, instance, dirForTag(tag), contractName, spdxLicenseIdentifier, copyrightText, body);
284326
}
285327
}

test/concrete/CodeGennable.sol

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ pragma solidity =0.8.25;
77
/// code. It carries no behaviour of its own: what the tests want from it is a
88
/// stable, non-zero `codehash` to feed to `bytecodeHashConstantString` and
99
/// `buildFileForContract`. The name is asserted on in
10-
/// `LibCodeGen.requireContractName.t.sol` and used as a contract name in
10+
/// `LibCodeGen.requireIdentifier.t.sol` and used as a contract name in
1111
/// `LibCodeGen.describedByMetaHashConstantString.t.sol`, so it is not free to
1212
/// change.
1313
contract CodeGennable {}

0 commit comments

Comments
 (0)