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
61 changes: 56 additions & 5 deletions src/lib/LibFs.sol
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,27 @@ library LibFs {
/// @param contractName The name of the contract, interpolated verbatim.
/// @return The file path as a string.
function pathForContract(string memory contractName) internal pure returns (string memory) {
return pathForContractIn(GENERATED_DIR, contractName);
}

/// @notice Constructs the file path for a contract's generated file inside
/// `dir`.
/// @dev `pathForContract` is this function applied to `GENERATED_DIR`, so
/// everything stated there about the name holds here too: the path is a
/// direct child of `dir` for every name that is accepted at all, and an
/// accepted name is interpolated verbatim.
///
/// `dir` is interpolated verbatim and is not checked, so where `dir` itself
/// sits is entirely the caller's, and only `fs_permissions` confines it.
/// That is why this is private rather than internal: the only directory a
/// consumer of this library writes to is `GENERATED_DIR`.
/// @param dir The directory to put the file in, without a trailing
/// separator, interpolated verbatim.
/// @param contractName The name of the contract, interpolated verbatim.
/// @return The file path as a string.
function pathForContractIn(string memory dir, string memory contractName) private pure returns (string memory) {
LibCodeGen.requireContractName(contractName);
return string.concat(GENERATED_DIR, "/", contractName, ".sol");
return string.concat(dir, "/", contractName, ".sol");
}

/// @notice True if anything occupies `path`, including a symlink whose
Expand Down Expand Up @@ -67,8 +86,9 @@ library LibFs {
/// of `GENERATED_DIR` and a rejected name reverts before any cheatcode is
/// reached.
///
/// `GENERATED_DIR` is created if it does not exist, so the first generation
/// in a repo does not need it committed already.
/// `GENERATED_DIR` is created if it does not exist, along with any missing
/// parent of it, so the first generation in a repo does not need it
/// committed already.
///
/// Anything already at the path is unlinked before the write, so a symlink
/// there is replaced by a regular file rather than written through to its
Expand All @@ -87,9 +107,40 @@ library LibFs {
/// @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 {
string memory path = pathForContract(contractName);
buildFileForContract(vm, instance, GENERATED_DIR, contractName, 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
/// that function is this one applied to `GENERATED_DIR`: `dir` is what gets
/// created when it is missing, and what the file is written a direct child
/// of. `dir` is interpolated verbatim and is not checked, so a caller
/// passing something other than a directory it means to own gets whatever
/// `fs_permissions` allows; `contractName` is still required to be a
/// Solidity identifier, so the name can never carry the file out of `dir`.
///
/// This overload exists so that the directory creation is reachable from a
/// test without deleting `GENERATED_DIR`. Every test that generates a file
/// writes under `GENERATED_DIR`, and `forge` runs them in parallel, so
/// removing it to make it missing races all of them.
/// @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 {
string memory path = pathForContractIn(dir, contractName);
//forge-lint: disable-next-line(unsafe-cheatcode)
vm.createDir(GENERATED_DIR, true);
vm.createDir(dir, true);
if (isPresent(vm, path)) {
//forge-lint: disable-next-line(unsafe-cheatcode)
vm.removeFile(path);
Expand Down
25 changes: 25 additions & 0 deletions test/lib/LibFs.buildFileForContract.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,31 @@ contract LibFsBuildFileForContractTest is Test {
}
}

/// The directory is created when it is not there, so a repo generating for
/// the first time does not have to commit it. Two levels of it are missing,
/// so a create that did not also make the missing parent fails here.
///
/// Driven through a directory this test owns rather than through
/// `GENERATED_DIR` itself. Every generating test in this file writes under
/// `GENERATED_DIR` and `forge` runs them in parallel, so removing it to
/// make it missing races all of them. `setUp` above creates `GENERATED_DIR`
/// for exactly that reason, which is also what makes the library's own
/// create unreachable from any test that writes there directly.
function testBuildFileForContractCreatesTheDirectory() external {
string memory root = string.concat(GENERATED_DIR, "/LibFsBuildCreatesDir");
string memory dir = string.concat(root, "/nested");
string memory name = "LibFsBuildCreatesDir";
cleanupPath(root);
assertFalse(vm.exists(root), "dirty precondition");
address instance = address(new CodeGennable());
string memory body = "\n// created\n";

LibFs.buildFileForContract(vm, instance, dir, name, body);

assertEq(vm.readFile(string.concat(dir, "/", name, ".sol")), expectedFile(instance, body));
cleanupPath(root);
}

/// A name that is not a Solidity identifier gets no path from
/// `pathForContract`, and `buildFileForContract` asks for the path before it
/// reaches a cheatcode, so the refusal arrives before anything is written.
Expand Down
Loading