diff --git a/.changeset/sixty-rabbits-open.md b/.changeset/sixty-rabbits-open.md new file mode 100644 index 00000000000..8d5bd6125af --- /dev/null +++ b/.changeset/sixty-rabbits-open.md @@ -0,0 +1,5 @@ +--- +"hardhat": patch +--- + +Improved HHE909 error message to show incompatible files diff --git a/v-next/example-project/hardhat.config.ts.save b/v-next/example-project/hardhat.config.ts.save new file mode 100644 index 00000000000..350b2a6ade1 --- /dev/null +++ b/v-next/example-project/hardhat.config.ts.save @@ -0,0 +1,176 @@ +import { HardhatPluginError } from "hardhat/plugins"; + +import util from "node:util"; +import { task, emptyTask, globalOption, configVariable } from "hardhat/config"; +import HardhatNodeTestRunner from "@nomicfoundation/hardhat-node-test-runner"; +import HardhatMochaTestRunner from "@nomicfoundation/hardhat-mocha"; +import HardhatKeystore from "@nomicfoundation/hardhat-keystore"; +import HardhatViem from "@nomicfoundation/hardhat-viem"; +import HardhatViemAssertions import hardhatNetworkHelpersPlugin from "@nomicfoundation/hardhat-network-helpers"; +import hardhatEthersPlugin from "@nomicfoundation/hardhat-ethers"; +import hardhatChaiMatchersPlugin from "@nomicfoundation/hardhat-ethers-chai-matchers"; +import hardhatTypechain from "@nomicfoundation/hardhat-typechain"; +import hardhatIgnitionViem from "@nomicfoundation/hardhat-ignition-viem"; +import hardhatVerify from "@nomicfoundation/hardhat-verify"; +import hardhatLedger from "@nomicfoundation/hardhat-ledger"; +import { ArgumentType } from "hardhat/types/arguments"; + +util.inspect.defaultOptions.depth = null; + +const exampleEmptyTask = emptyTask("empty", "An example empty task").build(); + +const exampleEmptySubtask = task(["empty", "task"]) + .setDescription("An example empty subtask task") + .setAction(async () => ({ + default: async (_, _hre) => { + console.log("empty task"); + }, + })) + .build(); + +const exampleTaskOverride = task("example2") + .setAction(async () => ({ + default: async (_, _hre) => { + console.log("from an override"); + }, + })) + .setDescription("An example task") + .addVariadicArgument({ + name: "testFiles", + description: "An optional list of files to test", + defaultValue: [], + }) + .addFlag({ + name: "noCompile", + description: "Don't compile before running this task", + }) + .addFlag({ + name: "parallel", + description: "Run tests in parallel", + }) + .addFlag({ + name: "bail", + description: "Stop running tests after the first test failure", + }) + .addOption({ + name: "grep", + description: "Only run tests matching the given string or regexp", + type: ArgumentType.STRING_WITHOUT_DEFAULT, + defaultValue: undefined, + }) + .build(); + +const greeting = task("hello", "Print a greeting") + .addOption({ + name: "greeting", + description: "The greeting to print", + defaultValue: "Hello, World!", + }) + .addOption({ + name: "programmatic", + description: "An example to show a hidden option", + type: ArgumentType.BOOLEAN, + defaultValue: false, + hidden: true, + }) + .setAction(async () => ({ + default: async ({ greeting }, _) => { + console.log(greeting); + }, + })) + .build(); + +const printConfig = task("config", "Print the config") + .setAction(async () => ({ + default: async ({}, hre) => { + console.log(util.inspect(hre.config, { colors: true, depth: null })); + }, + })) + .build(); + +const printAccounts = task("accounts", "Print the accounts") + .setAction(async () => ({ + default: async ({}, hre) => { + const { provider } = await hre.network.connect(); + console.log(await provider.request({ method: "eth_accounts" })); + }, + })) + .build(); + +const pluginExample = { + id: "community-plugin", + tasks: [ + task("plugin-hello", "Print a greeting from community-plugin") + .addOption({ + name: "greeting", + description: "The greeting to print", + defaultValue: "Hello, World from community-plugin!", + }) + .setAction(async () => ({ + default: async ({ greeting }, _) => { + console.log(greeting); + + if (greeting === "") { + throw new HardhatPluginError( + "community-plugin", + "Greeting cannot be empty", + ); + } + }, + })) + .build(), + ], + globalOptions: [ + globalOption({ + name: "myGlobalOption", + description: "A global option", + defaultValue: "default", + }), + ], +}; + +export default defineConfig({ + networks: { + op: { + type: "http", + chainType: "op", + url: "https://mainnet.optimism.io/", + accounts: [configVariable("OP_SENDER")], + }, + edrOp: { + type: "edr-simulated", + chainType: "op", + chainId: 10, + forking: { + url: "https://mainnet.optimism.io", + }, + ledgerAccounts: [ + // Set your ledger address here + // "0x070Da0697e6B82F0ab3f5D0FD9210EAdF2Ba1516", + ], + }, + opSepolia: { + type: "http", + chainType: "op", + url: "https://sepolia.optimism.io", + accounts: [configVariable("OP_SEPOLIA_SENDER")], + }, + edrOpSepolia: { + type: "edr-simulated", + chainType: "op", + forking: { + url: "https://sepolia.optimism.io", + }, + }, + }, + tasks: [ + exampleTaskOverride, + exampleEmptyTask, + exampleEmptySubtask, + greeting, + printConfig, + printAccounts, + ], + plugins: [ + pluginExample, + diff --git a/v-next/hardhat/src/internal/builtin-plugins/solidity-test/config.ts b/v-next/hardhat/src/internal/builtin-plugins/solidity-test/config.ts index f9c02cf9438..d773831326b 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/solidity-test/config.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/solidity-test/config.ts @@ -60,7 +60,7 @@ const solidityTestUserConfigType = z.object({ forking: z .object({ url: z.optional(sensitiveUrlSchema), - blockNumber: z.bigint().optional(), + blockNumber: z.number().or(z.bigint()).optional(), rpcEndpoints: z.record(sensitiveStringSchema).optional(), }) .optional(), diff --git a/v-next/hardhat/src/internal/builtin-plugins/solidity/build-system/solc-config-selection.ts b/v-next/hardhat/src/internal/builtin-plugins/solidity/build-system/solc-config-selection.ts index 95a2250a887..6f259ce012f 100644 --- a/v-next/hardhat/src/internal/builtin-plugins/solidity/build-system/solc-config-selection.ts +++ b/v-next/hardhat/src/internal/builtin-plugins/solidity/build-system/solc-config-selection.ts @@ -154,12 +154,66 @@ export class SolcConfigSelector { } } + // Collect incompatible files information + const incompatibleFiles: Array<{ path: string; pragma: string }> = []; + + // check root file + if (maxSatisfying(compilerVersions, rootVersionRange) === null) { + incompatibleFiles.push({ + path: shortenPath(root.fsPath), + pragma: root.content.versionPragmas.join(" "), + }); + } + + // Check all dependencies + for (const transitiveDependency of this.#getTransitiveDependencies( + root, + dependencyGraph, + )) { + const depVersionRange = transitiveDependency.versionPragmasPath + .map((pragmas) => pragmas.join(" ")) + .join(" "); + if (maxSatisfying(compilerVersions, depVersionRange) === null) { + incompatibleFiles.push({ + path: shortenPath(transitiveDependency.dependency.fsPath), + pragma: + transitiveDependency.dependency.content.versionPragmas.join(" "), + }); + } + } + + // Bulid detailed error message + let detailedMessage = `No solc version enabled in this profile is compatible with this file and all of its dependencies.`; + + if (incompatibleFiles.length > 0) { + detailedMessage += ` + +The following file(s) have incompatible version requirements:`; + + const maxFilesToShow = 5; + const filesToShow = incompatibleFiles.slice(0, maxFilesToShow); + + for (const file of filesToShow) { + detailedMessage += ` + - ${file.path}: requires ${file.pragma}`; + } + + if (incompatibleFiles.length > maxFilesToShow) { + detailedMessage += ` + ... and ${incompatibleFiles.length - maxFilesToShow} more file(s)`; + } + + detailedMessage += ` + +Available compiler versions: ${compilerVersions.join(", ")}`; + } + return { reason: CompilationJobCreationErrorReason.NO_COMPATIBLE_SOLC_VERSION_FOUND, rootFilePath: root.fsPath, buildProfile: this.#buildProfileName, - formattedReason: `No solc version enabled in this profile is compatible with this file and all of its dependencies.`, + formattedReason: detailedMessage, }; }