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
2 changes: 1 addition & 1 deletion packages/hardhat-typechain/.prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@
CHANGELOG.md
/test/fixture-projects/**/artifacts
/test/fixture-projects/**/cache
/test/fixture-projects/**/types
/test/fixture-projects/custom-out-dir/custom-types
test/fixture-projects/generate-types/types
64 changes: 61 additions & 3 deletions packages/hardhat-typechain/src/internal/hook-handlers/solidity.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import type { HookContext, SolidityHooks } from "hardhat/types/hooks";
import type {
BuildOptions,
BuildScope,
CompilationJobCreationError,
FileBuildResult,
} from "hardhat/types/solidity";

import path from "node:path";

import { generateTypes } from "../generate-types.js";

export default async (): Promise<Partial<SolidityHooks>> => {
Expand Down Expand Up @@ -34,14 +37,25 @@ export default async (): Promise<Partial<SolidityHooks>> => {
// Clear cache to ensure fresh data after compilation
await context.artifacts.clearCache();

// Get all artifact paths and generate types
const allArtifactPaths = await context.artifacts.getAllArtifactPaths();
let artifactPaths: string[];

if (context.config.solidity.splitTestsCompilation) {
artifactPaths = Array.from(
await context.artifacts.getAllArtifactPaths(),
);
} else {
// Contracts and tests share the artifacts folder.
// Filter out test artifacts using each artifact's sourceName (derived
// from its fully qualified name), which is the project-relative or npm
// source identifier.
artifactPaths = await getContractArtifactPaths(context);
}

await generateTypes(
context.config.paths.root,
context.config.typechain,
context.globalOptions.noTypechain,
Array.from(allArtifactPaths),
artifactPaths,
);

return result;
Expand All @@ -50,3 +64,47 @@ export default async (): Promise<Partial<SolidityHooks>> => {

return handlers;
};

async function getContractArtifactPaths(
context: HookContext,
): Promise<string[]> {
const fqns = await context.artifacts.getAllFullyQualifiedNames();
const projectRoot = context.config.paths.root;

const scopeBySource = new Map<string, BuildScope>();
const contractFqns: string[] = [];

for (const fqn of fqns) {
const sourceName = fqn.slice(0, fqn.lastIndexOf(":"));

let scope = scopeBySource.get(sourceName);
if (scope === undefined) {
const fsPath = path.resolve(projectRoot, sourceName);

// npm files will be classified as "contracts" because their sourceName is
// not an existing file, and "contracts" is the default.
//
// If the package name clashed with
// ```ts
// path.relative(
// context.config.paths.root,
// context.config.paths.tests.solidity
// )
// ```
//
// They could be misclassified as test files. This is highly improbable,
// so we don't check it. You could read the artifact and see if the
// inputSourceName starts with `npm/` to rule this out.
scope = await context.solidity.getScope(fsPath);
scopeBySource.set(sourceName, scope);
Comment thread
alcuadrado marked this conversation as resolved.
Comment thread
schaable marked this conversation as resolved.
}

if (scope === "contracts") {
contractFqns.push(fqn);
}
}

return Promise.all(
contractFqns.map((fqn) => context.artifacts.getArtifactPath(fqn)),
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract A {
function getMessage() external pure returns (string memory) {
return "Hello from A contract!";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export default {};

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract A {
function getMessage() external pure returns (string memory) {
return "Hello from A contract!";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import {A} from "./A.sol";

contract ATest {
A a;

function setUp() public {
a = new A();
}

function test_Assertion() public view {
require(1 == 1, "test assertion");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { HardhatUserConfig } from "hardhat/config";

// eslint-disable-next-line import/no-relative-packages -- allow in fixture projects
import hardhatTypechain from "../../../src/index.js";

const config: HardhatUserConfig = {
solidity: {
version: "0.8.28",
splitTestsCompilation: false,
},
plugins: [hardhatTypechain],
};

export default config;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"name": "hardhat-project",
"private": true,
"type": "module"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract BTest {
function test_Assertion() public view {
require(1 == 1, "test assertion");
}
}
159 changes: 158 additions & 1 deletion packages/hardhat-typechain/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -355,7 +355,14 @@ describe("hardhat-typechain", () => {
`./fixture-projects/${projectFolder}/hardhat.config.js`
);

const hre = await createHardhatRuntimeEnvironment(hardhatConfig.default);
// scope: "tests" requires splitTestsCompilation: true
const hre = await createHardhatRuntimeEnvironment({
...hardhatConfig.default,
solidity: {
...hardhatConfig.default.solidity,
splitTestsCompilation: true,
},
});

await hre.tasks.getTask("clean").run();

Expand All @@ -371,6 +378,156 @@ describe("hardhat-typechain", () => {
});
});

describe("types are not generated for test artifacts when splitTestsCompilation is false", () => {
const projectFolder = "unified-mode";

useFixtureProject(projectFolder);

before(async () => {
await remove(`${process.cwd()}/types`);

const hardhatConfig = await import(
`./fixture-projects/${projectFolder}/hardhat.config.js`
);

const hre = await createHardhatRuntimeEnvironment(hardhatConfig.default);

await hre.tasks.getTask("clean").run();
await hre.tasks.getTask("build").run();
});

it("should generate types for contract artifacts", async () => {
assert.equal(await exists(`${process.cwd()}/types`), true);

// Contract A should have types generated
assert.equal(
await exists(
path.join(process.cwd(), "types", "ethers-contracts", "A.ts"),
),
true,
);
});

it("should not generate types for .t.sol test artifacts", async () => {
// ATest from contracts/A.t.sol should NOT have types
assert.equal(
await exists(
path.join(process.cwd(), "types", "ethers-contracts", "ATest.ts"),
),
false,
);
});

it("should not generate types for test directory artifacts", async () => {
// BTest from test/BTest.sol should NOT have types
assert.equal(
await exists(
path.join(process.cwd(), "types", "ethers-contracts", "BTest.ts"),
),
false,
);
});

it("should classify artifacts using getScope, not artifact-path heuristics", async () => {
// The A.t.sol test file imports A.sol, so A.sol's contract artifact
// appears under contracts/A.sol/ (a contract path). The filter must
// use getScope() on the source file, not a path-based heuristic.
// A.sol is a contract, so its type should be generated.
assert.equal(
await exists(
path.join(
process.cwd(),
"types",
"ethers-contracts",
"factories",
"A__factory.ts",
),
),
true,
);

// ATest is a test (from .t.sol), so its factory should NOT exist
assert.equal(
await exists(
path.join(
process.cwd(),
"types",
"ethers-contracts",
"factories",
"ATest__factory.ts",
),
),
false,
);
});
});

describe("npm-dependency artifacts are classified as contracts in unified mode", () => {
useFixtureProject("unified-mode-npm");

before(async () => {
await remove(`${process.cwd()}/types`);

const hre = await createHardhatRuntimeEnvironment({
solidity: {
version: "0.8.28",
splitTestsCompilation: false,
npmFilesToBuild: ["@fake/lib/Token.sol", "test-lib/Helper.sol"],
},
plugins: [hardhatTypechain],
});

await hre.tasks.getTask("clean").run();
await hre.tasks.getTask("build").run();
});

it("should generate types for the npm-dependency artifact", async () => {
assert.equal(
await exists(
path.join(
process.cwd(),
"types",
"ethers-contracts",
"@fake",
"lib",
"Token.ts",
),
),
true,
);
});

it("should generate types for an unscoped npm package whose name starts with the tests dir name", async () => {
assert.equal(
await exists(
path.join(
process.cwd(),
"types",
"ethers-contracts",
"test-lib",
"Helper.ts",
),
),
true,
);
});

it("should generate types for the local contract artifact", async () => {
assert.equal(
await exists(
path.join(
process.cwd(),
"types",
"ethers-contracts",
"contracts",
"A.ts",
),
),
true,
);
});
});

describe("clean hook removes the types folder", () => {
describe("with default outDir", () => {
const projectFolder = "generate-types";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -191,7 +191,7 @@ export class ArtifactManagerImplementation implements ArtifactManager {
HardhatError.ERRORS.CORE.ARTIFACTS.MULTIPLE_FOUND,
{
contractName: contractNameOrFullyQualifiedName,
candidates: Array.from(fqns).join(EOL),
candidates: Array.from(fqns).sort().join(EOL),
},
);
}
Expand Down
Loading
Loading