Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,32 @@ one of them either, because a tag carries no `.`. `buildFileForTaggedContract`
writes into one of those directories, and reads that directory rather than
`src/generated/`, so each is checked against its own contents.

## Generated file header

Every generated file opens with a licence tag, a copyright tag,
`pragma solidity ^0.8.25;` and an autogenerated notice.

The file lands in the consuming repo, so the licence it is under and the
copyright holder it names are that repo's statement about its own source, and
`LibCodeGen.filePrefix`, `LibFs.buildFileForContract` and
`LibFs.buildFileForTaggedContract` all take them as arguments. Each value has to
be a non-empty single line: an empty one leaves a tag that `reuse lint` accepts
while it names nothing and `solc` refuses, and a line break ends the tag's line
so that whatever follows lands as source.

Each of those three also has an overload taking neither, which declares
`RAIN_SPDX_LICENSE_IDENTIFIER` (`LicenseRef-DCL-1.0`) and `RAIN_COPYRIGHT_TEXT`
(`Copyright (c) 2020 Rain Open Source Software Ltd`) — the values every repo in
this org declares for its own source. A Rain repo generates through those and
passes nothing. Both constants are exported from `LibCodeGen`, so a consumer
that threads the header down through its own build library names them rather
than restating the strings.

A repo this org does not own passes its own two values. The defaulting overloads
would stamp Rain's licence and Rain's copyright holder into its files, and
`reuse lint` there would still pass, because it checks that the tag is present
rather than that it is right.

## Formatter requirements

`LibCodeGen` wraps the declarations it emits itself, deciding against
Expand Down
37 changes: 37 additions & 0 deletions src/lib/LibCodeGen.sol
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,18 @@ uint256 constant MAX_LINE_LENGTH = 120;
/// needs to match what formatters expect.
string constant NEWLINE_DUE_TO_MAX_LENGTH = "\n ";

/// @dev The SPDX licence identifier every repo in this org declares for its own
/// source, and so the identifier that heads a file generated into one of them
/// when the caller names none. Exported rather than inlined into the defaulting
/// overload, so a consumer that threads the header through its own build can
/// name the value instead of restating the string.
string constant RAIN_SPDX_LICENSE_IDENTIFIER = "LicenseRef-DCL-1.0";

/// @dev The copyright text every repo in this org declares for its own source,
/// and so the text that heads a file generated into one of them when the caller
/// names none. Exported for the same reason.
string constant RAIN_COPYRIGHT_TEXT = "Copyright (c) 2020 Rain Open Source Software Ltd";

/// Thrown when a name is not a Solidity identifier. Such a name cannot be
/// interpolated into a file path or a constant declaration.
/// @param name The rejected name.
Expand Down Expand Up @@ -112,6 +124,11 @@ library LibCodeGen {
/// state and are taken from the caller. Both are interpolated verbatim into
/// their tags, and both have to be a non-empty single line for the tag they
/// land on to say what it appears to.
///
/// The overload taking no arguments makes that statement out of this org's
/// own values, which is a statement only a repo this org owns can make. A
/// consumer in another org calls this two argument overload and passes its
/// own values.
/// @param spdxLicenseIdentifier The SPDX licence identifier for the
/// generated file, interpolated verbatim.
/// @param copyrightText The copyright text for the generated file,
Expand Down Expand Up @@ -140,6 +157,26 @@ library LibCodeGen {
//REUSE-IgnoreEnd
}

/// The file prefix headed by the licence and the copyright text this org's
/// repos declare for their own source.
///
/// A generated file lands in the calling project's repo, so what it says
/// about its own licensing is that project's statement to make. This
/// overload makes it on the caller's behalf out of
/// `RAIN_SPDX_LICENSE_IDENTIFIER` and `RAIN_COPYRIGHT_TEXT`, which is right
/// for a repo this org owns and wrong for every other repo. A consumer
/// elsewhere calls the overload that takes the two values, and passes its
/// own.
///
/// This is that overload applied to those two constants and nothing else,
/// so a header written by defaulting is one the caller could have written
/// out, and every rule that overload puts on either value holds here rather
/// than being skipped because the values came from the library.
/// @return The text that heads the generated file.
function filePrefix() internal pure returns (string memory) {
return filePrefix(RAIN_SPDX_LICENSE_IDENTIFIER, RAIN_COPYRIGHT_TEXT);
}

/// Puts the hash of the bytecode of some contract instance into a constant
/// string. Often used to ensure that the deployed bytecode matches the
/// expected bytecode.
Expand Down
82 changes: 81 additions & 1 deletion src/lib/LibFs.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
pragma solidity ^0.8.25;

import {Vm, VmSafe} from "forge-std-1.16.2/src/Vm.sol";
import {LibCodeGen} from "./LibCodeGen.sol";
import {LibCodeGen, RAIN_SPDX_LICENSE_IDENTIFIER, RAIN_COPYRIGHT_TEXT} from "./LibCodeGen.sol";

/// @dev The directory that generated contract files are written to, relative to
/// the project root. Consumers commit this directory and import from it by
Expand Down Expand Up @@ -438,6 +438,27 @@ library LibFs {
buildFileForContract(vm, instance, GENERATED_DIR, contractName, spdxLicenseIdentifier, copyrightText, body);
}

/// @notice Builds a file for a generated contract, declaring the licence and
/// the copyright text this org's repos declare for their own source.
/// @dev This is `buildFileForContract` applied to
/// `RAIN_SPDX_LICENSE_IDENTIFIER` and `RAIN_COPYRIGHT_TEXT`, so everything
/// that function states holds here, including the rule those two values are
/// subject to like any other.
///
/// The file lands in the calling project's repo, so the header is a
/// statement that project makes about its own source. This overload makes it
/// out of this org's values, which is right for a repo this org owns and
/// wrong for every other repo; a consumer elsewhere calls the overload that
/// takes the two values, and passes its own.
/// @param vm The Vm instance for file operations.
/// @param instance The contract instance whose bytecode hash is to be
/// included.
/// @param contractName The name of the contract.
/// @param body The body of the contract file to be written.
function buildFileForContract(Vm vm, address instance, string memory contractName, string memory body) internal {
buildFileForContract(vm, instance, contractName, RAIN_SPDX_LICENSE_IDENTIFIER, RAIN_COPYRIGHT_TEXT, body);
}

/// @notice Builds a file for a generated contract inside `dir` rather than
/// inside `GENERATED_DIR`.
/// @dev Identical to `buildFileForContract` in every other respect, and
Expand Down Expand Up @@ -508,6 +529,35 @@ library LibFs {
vm.writeFile(path, content);
}

/// @notice Builds a file for a generated contract inside `dir`, declaring
/// the licence and the copyright text this org's repos declare for their own
/// source.
/// @dev This is the `dir` overload of `buildFileForContract` applied to
/// `RAIN_SPDX_LICENSE_IDENTIFIER` and `RAIN_COPYRIGHT_TEXT`, so everything
/// that overload states holds here. Choosing the directory and stating the
/// licence are independent of each other, so a caller that does the first
/// does not thereby have to do the second.
///
/// The header is this org's, which is right for a repo this org owns and
/// wrong for every other repo; a consumer elsewhere calls the overload that
/// takes the two values, and passes its own.
/// @param vm The Vm instance for file operations.
/// @param instance The contract instance whose bytecode hash is to be
/// included.
/// @param dir The directory to put the file in, without a trailing
/// separator, interpolated verbatim.
/// @param contractName The name of the contract.
/// @param body The body of the contract file to be written.
function buildFileForContract(
Vm vm,
address instance,
string memory dir,
string memory contractName,
string memory body
) internal {
buildFileForContract(vm, instance, dir, contractName, RAIN_SPDX_LICENSE_IDENTIFIER, RAIN_COPYRIGHT_TEXT, body);
}

/// @notice Builds a file for a generated contract at
/// `pathForTaggedContract(tag, contractName)`.
///
Expand Down Expand Up @@ -573,4 +623,34 @@ library LibFs {
) internal {
buildFileForContract(vm, instance, dirForTag(tag), contractName, spdxLicenseIdentifier, copyrightText, body);
}

/// @notice Builds a file for a generated contract at
/// `pathForTaggedContract(tag, contractName)`, declaring the licence and the
/// copyright text this org's repos declare for their own source.
/// @dev This is `buildFileForTaggedContract` applied to
/// `RAIN_SPDX_LICENSE_IDENTIFIER` and `RAIN_COPYRIGHT_TEXT`, so everything
/// that function states holds here.
///
/// The header is this org's, which is right for a repo this org owns and
/// wrong for every other repo; a consumer elsewhere calls the overload that
/// takes the two values, and passes its own. A tagged file is a per release
/// snapshot that the org's release convention freezes once written, so the
/// header it lands with is the header it keeps.
/// @param vm The Vm instance for file operations.
/// @param instance The contract instance whose bytecode hash is to be
/// included.
/// @param tag The tag whose directory the file lives in.
/// @param contractName The name of the contract.
/// @param body The body of the contract file to be written.
function buildFileForTaggedContract(
Vm vm,
address instance,
string memory tag,
string memory contractName,
string memory body
) internal {
buildFileForTaggedContract(
vm, instance, tag, contractName, RAIN_SPDX_LICENSE_IDENTIFIER, RAIN_COPYRIGHT_TEXT, body
);
}
}
47 changes: 46 additions & 1 deletion test/src/lib/LibCodeGen.filePrefix.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,13 @@
pragma solidity =0.8.25;

import {Test} from "forge-std-1.16.2/src/Test.sol";
import {LibCodeGen, InvalidSpdxLicenseIdentifier, InvalidCopyrightText} from "src/lib/LibCodeGen.sol";
import {
LibCodeGen,
InvalidSpdxLicenseIdentifier,
InvalidCopyrightText,
RAIN_SPDX_LICENSE_IDENTIFIER,
RAIN_COPYRIGHT_TEXT
} from "src/lib/LibCodeGen.sol";

// The subject of this whole file is emitted licence text, so SPDX tag prefixes
// appear throughout it without a value attached. The file's own header is above
Expand Down Expand Up @@ -187,5 +193,44 @@ contract LibCodeGenFilePrefixTest is Test {
vm.expectRevert(abi.encodeWithSelector(InvalidSpdxLicenseIdentifier.selector, ""));
this.callFilePrefix("", "");
}

/// The two exported defaults are this repo's own licence and copyright,
/// pinned as literals rather than read back out of the header they build.
/// They are what every Rain repo's generated files declare, so a change to
/// either rewrites committed source org wide and has to land as a
/// deliberate diff here.
function testRainDefaultsExact() external pure {
assertEq(RAIN_SPDX_LICENSE_IDENTIFIER, "LicenseRef-DCL-1.0");
assertEq(RAIN_COPYRIGHT_TEXT, "Copyright (c) 2020 Rain Open Source Software Ltd");
}

/// A Rain repo gets Rain's licence and copyright without passing them, and
/// what it gets is byte for byte what a caller naming the two exported
/// defaults gets. The short overload is the long one applied to those two
/// constants and nothing else, so there is no second spelling of the header
/// that can drift from the first.
function testFilePrefixDefaultEqualsExplicitRainValues() external pure {
assertEq(LibCodeGen.filePrefix(), LibCodeGen.filePrefix(RAIN_SPDX_LICENSE_IDENTIFIER, RAIN_COPYRIGHT_TEXT));
}

/// The defaulted header, pinned to the literal. The equality above is
/// satisfied by both sides being wrong in the same way; this is what says
/// which bytes they both have to be.
function testFilePrefixDefaultExact() external pure {
assertEq(
LibCodeGen.filePrefix(),
"// SPDX-License-Identifier: LicenseRef-DCL-1.0\n"
"// SPDX-FileCopyrightText: Copyright (c) 2020 Rain Open Source Software Ltd\n"
"pragma solidity ^0.8.25;\n\n" "// THIS FILE IS AUTOGENERATED BY THE BUILD SCRIPT. DO NOT EDIT BY HAND.\n"
);
}

/// The defaults are values the long overload accepts, so the short one is
/// reachable rather than a call that always reverts. Nothing about the two
/// constants is exempt from the rule the header puts on any other value.
function testFilePrefixDefaultsAreAcceptedValues() external pure {
assertTrue(LibCodeGen.isSingleLine(RAIN_SPDX_LICENSE_IDENTIFIER), "default licence is not a single line");
assertTrue(LibCodeGen.isSingleLine(RAIN_COPYRIGHT_TEXT), "default copyright is not a single line");
}
}
//REUSE-IgnoreEnd
56 changes: 55 additions & 1 deletion test/src/lib/LibFs.buildFileForContract.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ import {
InvalidIdentifier,
InvalidSpdxLicenseIdentifier,
InvalidCopyrightText,
CodelessInstance
CodelessInstance,
RAIN_SPDX_LICENSE_IDENTIFIER,
RAIN_COPYRIGHT_TEXT
} from "src/lib/LibCodeGen.sol";
import {CodeGennable} from "test/concrete/CodeGennable.sol";
import {RemovalVm, WriteReached, RemoveFileReached, FfiReached} from "test/concrete/RemovalVm.sol";
Expand Down Expand Up @@ -803,4 +805,56 @@ contract LibFsBuildFileForContractTest is Test {
cleanupPath(dir);
assertEq(written, expectedFile(instance, SPDX_LICENSE_IDENTIFIER, COPYRIGHT_TEXT, body));
}

/// A Rain repo writes without naming a licence or a copyright holder, and
/// the file that lands is byte for byte the file a caller naming the two
/// exported defaults gets. Both writes are for the same instance, so the
/// bytecode hash constant is identical in both and the header is the only
/// thing that could differ.
///
/// Asserted against the literal as well, so the two spellings agreeing with
/// each other is not what makes this pass. A default that drifted from this
/// repo's own licence would move both files together, and only the literal
/// is outside the library to catch it.
function testBuildFileForContractDefaultHeaderIsRainsExplicitHeader() external {
string memory defaulted = "LibFsBuildDefaultHeader";
string memory named = "LibFsBuildNamedHeader";
cleanup(defaulted);
cleanup(named);
address instance = address(new CodeGennable());
string memory body = "\n// default header\n";

LibFs.buildFileForContract(vm, instance, defaulted, body);
LibFs.buildFileForContract(vm, instance, named, RAIN_SPDX_LICENSE_IDENTIFIER, RAIN_COPYRIGHT_TEXT, body);

string memory defaultedFile = vm.readFile(LibFs.pathForContract(defaulted));
string memory namedFile = vm.readFile(LibFs.pathForContract(named));
cleanup(defaulted);
cleanup(named);

assertEq(defaultedFile, namedFile, "the defaulted write is not the write that names the defaults");
assertEq(defaultedFile, expectedFile(instance, SPDX_LICENSE_IDENTIFIER, COPYRIGHT_TEXT, body));
}

/// The `dir` overload defaults the header the same way. A caller that
/// chooses the directory is not thereby a caller that has to state the
/// licence, so the two arguments the overloads differ by are independent.
function testBuildFileForContractInDirDefaultHeaderIsRainsExplicitHeader() external {
string memory dir = string.concat(GENERATED_DIR, "/LibFsBuildDirDefaultHeader");
string memory defaulted = "LibFsBuildDirDefaultHeader";
string memory named = "LibFsBuildDirNamedHeader";
cleanupPath(dir);
address instance = address(new CodeGennable());
string memory body = "\n// dir default header\n";

LibFs.buildFileForContract(vm, instance, dir, defaulted, body);
LibFs.buildFileForContract(vm, instance, dir, named, RAIN_SPDX_LICENSE_IDENTIFIER, RAIN_COPYRIGHT_TEXT, body);

string memory defaultedFile = vm.readFile(string.concat(dir, "/", defaulted, ".sol"));
string memory namedFile = vm.readFile(string.concat(dir, "/", named, ".sol"));
cleanupPath(dir);

assertEq(defaultedFile, namedFile, "the defaulted write is not the write that names the defaults");
assertEq(defaultedFile, expectedFile(instance, SPDX_LICENSE_IDENTIFIER, COPYRIGHT_TEXT, body));
}
}
31 changes: 30 additions & 1 deletion test/src/lib/LibFs.buildFileForTaggedContract.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pragma solidity =0.8.25;

import {Test} from "forge-std-1.16.2/src/Test.sol";
import {LibFs, GENERATED_DIR, InvalidTag} from "src/lib/LibFs.sol";
import {InvalidIdentifier} from "src/lib/LibCodeGen.sol";
import {InvalidIdentifier, RAIN_SPDX_LICENSE_IDENTIFIER, RAIN_COPYRIGHT_TEXT} from "src/lib/LibCodeGen.sol";
import {CodeGennable} from "test/concrete/CodeGennable.sol";
import {LibFsExternal} from "test/concrete/LibFsExternal.sol";
import {LibCodeGenSlow} from "test/lib/LibCodeGenSlow.sol";
Expand Down Expand Up @@ -440,4 +440,33 @@ contract LibFsBuildFileForTaggedContractTest is Test {
vm.assume(!LibCodeGenSlow.isIdentifierSlow(contractName));
assertNameRejected("0_1_1$taggedFuzzName", contractName);
}

/// A Rain repo writes a snapshot without naming a licence or a copyright
/// holder, and the file that lands is byte for byte the file a caller
/// naming the two exported defaults gets. Both writes are for the same
/// instance and into the same tag, so the header is the only thing that
/// could differ.
///
/// Asserted against the literal as well, through `expectedFile`, which
/// spells this repo's licence and copyright out rather than reading them
/// back out of the library. A snapshot is frozen once written, so a header
/// that drifted is permanent in the release record.
function testBuildFileForTaggedContractDefaultHeaderIsRainsExplicitHeader() external {
string memory tag = "0_1_1$taggedDefaultHeader";
cleanup(tag);
address instance = address(new CodeGennable());
string memory body = "\n// default header\n";

LibFs.buildFileForTaggedContract(vm, instance, tag, "LibFsTaggedDefaultHeader", body);
LibFs.buildFileForTaggedContract(
vm, instance, tag, "LibFsTaggedNamedHeader", RAIN_SPDX_LICENSE_IDENTIFIER, RAIN_COPYRIGHT_TEXT, body
);

string memory defaultedFile = vm.readFile(LibFs.pathForTaggedContract(tag, "LibFsTaggedDefaultHeader"));
string memory namedFile = vm.readFile(LibFs.pathForTaggedContract(tag, "LibFsTaggedNamedHeader"));
cleanup(tag);

assertEq(defaultedFile, namedFile, "the defaulted write is not the write that names the defaults");
assertEq(defaultedFile, expectedFile(instance, body));
}
}
Loading