Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
5 changes: 5 additions & 0 deletions .changeset/healthy-ears-knock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"hardhat": patch
---

Throw better error messages when trying to use a Hardhat 2 plugin with Hardhat 3 [#7991](https://github.com/NomicFoundation/hardhat/pull/7991).
6 changes: 5 additions & 1 deletion v-next/hardhat/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"./hre": "./dist/src/hre.js",
"./plugins": "./dist/src/plugins.js",
"./internal/lsp-helpers": "./dist/src/lsp-helpers.js",
"./types": "./dist/src/types/index.js",
"./types/arguments": "./dist/src/types/arguments.js",
"./types/artifacts": "./dist/src/types/artifacts.js",
"./types/config": "./dist/src/types/config.js",
Expand All @@ -38,7 +39,10 @@
"./console.sol": "./console.sol",
"./internal/coverage": "./dist/src/internal/builtin-plugins/coverage/exports.js",
"./internal/gas-analytics": "./dist/src/internal/builtin-plugins/gas-analytics/exports.js",
"./utils/contract-names": "./dist/src/utils/contract-names.js"
"./utils/contract-names": "./dist/src/utils/contract-names.js",
"./types/runtime": "./dist/src/internal/deprecated-module-imported-from-hardhat2-plugin.js",
"./builtin-tasks/task-names": "./dist/src/internal/deprecated-module-imported-from-hardhat2-plugin.js",
"./internal/artifacts": "./dist/src/internal/deprecated-module-imported-from-hardhat2-plugin.js"
},
"keywords": [
"ethereum",
Expand Down
37 changes: 37 additions & 0 deletions v-next/hardhat/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ export type { HardhatUserConfig } from "./types/config.js";
import "./internal/builtin-plugins/index.js";
import type { HardhatUserConfig } from "./types/config.js";

import { throwUsingHardhat2PluginError } from "./internal/using-hardhat2-plugin-errors.js";

/**
* Defines a Hardhat user config.
*
Expand Down Expand Up @@ -38,3 +40,38 @@ export function defineConfig(config: HardhatUserConfig): HardhatUserConfig {
// use it and have a better user experience.
return config;
}

/**
* @deprecated This function is part of the Hardhat 2 plugin API.
*/
export function extendConfig(..._args: any): any {
throwUsingHardhat2PluginError();
}

/**
* @deprecated This function is part of the Hardhat 2 plugin API.
*/
export function extendEnvironment(..._args: any): any {
throwUsingHardhat2PluginError();
}

/**
* @deprecated This function is part of the Hardhat 2 plugin API.
*/
export function extendProvider(..._args: any): any {
throwUsingHardhat2PluginError();
}

/**
* @deprecated This function is part of the Hardhat 2 plugin API.
*/
export function scope(..._args: any): any {
throwUsingHardhat2PluginError();
}

/**
* @deprecated This function is part of the Hardhat 2 plugin API.
*/
export function subtask(..._args: any): any {
throwUsingHardhat2PluginError();
}
18 changes: 18 additions & 0 deletions v-next/hardhat/src/internal/cli/error-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
import chalk from "chalk";

import { HARDHAT_NAME, HARDHAT_WEBSITE_URL } from "../constants.js";
import { UsingHardhat2PluginError } from "../using-hardhat2-plugin-errors.js";

/**
* The different categories of errors that can be handled by hardhat cli.
Expand Down Expand Up @@ -69,6 +70,11 @@ export function printErrorMessages(
shouldShowStackTraces: boolean = false,
print: (message: string | Error) => void = console.error,
): void {
if (error instanceof UsingHardhat2PluginError) {
printUsingHardhat2Error(error, print);
return;
}

const showStackTraces =
shouldShowStackTraces ||
getErrorWithCategory(error).category === ErrorCategory.OTHER;
Expand Down Expand Up @@ -146,3 +152,15 @@ function getErrorMessages(error: Error): ErrorMessages {
};
}
}
function printUsingHardhat2Error(
error: UsingHardhat2PluginError,
print: (message: string | Error) => void = console.error,
): void {
print(chalk.red.bold(`Hardhat 3 installation error:`));
print("");
if (error.callerRelativePath !== undefined) {
print(error.message);
} else {
print(error.stack);
Comment thread
schaable marked this conversation as resolved.
}
}
5 changes: 5 additions & 0 deletions v-next/hardhat/src/internal/cli/telemetry/sentry/reporter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
ProviderError,
UnknownError,
} from "../../../builtin-plugins/network-manager/provider-errors.js";
import { UsingHardhat2PluginError } from "../../../using-hardhat2-plugin-errors.js";
import { getHardhatVersion } from "../../../utils/package.js";
import { isTelemetryAllowed } from "../telemetry-permissions.js";

Expand Down Expand Up @@ -134,6 +135,10 @@ class Reporter {
return false;
}

if (error instanceof UsingHardhat2PluginError) {
return false;
}

if (HardhatPluginError.isHardhatPluginError(error)) {
// Don't log errors from third-party plugins
return false;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// This is an empty module that is used to exported it with a subpath that's

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤯 this is very smart!

// commonly used by Hardhat 2 plugins. This is to avoid the plugins breaking
// when the `require` it, so that they have an opportunity to run a function
// that throws a better error message.

// The reason this module can be empty is that Hardhat 2 plugins are CJS modules
// so they can destructure the require and get `undefined` values, instead of
// a load-time error.
94 changes: 94 additions & 0 deletions v-next/hardhat/src/internal/using-hardhat2-plugin-errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { fileURLToPath } from "node:url";

import { CustomError } from "@nomicfoundation/hardhat-utils/error";
import { shortenPath } from "@nomicfoundation/hardhat-utils/path";
import chalk from "chalk";

export class UsingHardhat2PluginError extends CustomError {
public readonly callerRelativePath: string | undefined;
constructor() {
const callerPath = getCallerRelativePath();

let message: string;
if (callerPath !== undefined) {
message = `You are trying to use a Hardhat 2 plugin in a Hardhat 3 project.

This file is part of the Hardhat 2 plugin calling an API that was removed in Hardhat 3: ${chalk.bold(callerPath)}
Comment thread
alcuadrado marked this conversation as resolved.
Outdated

Please read https://hardhat.org/migrate-from-hardhat2 to learn how to migrate your project to Hardhat 3.
`;
} else {
message = `You are trying to use a Hardhat 2 plugin in a Hardhat 3 project.

Take a look at this stack trace to find the which plugin it is.
Comment thread
alcuadrado marked this conversation as resolved.
Outdated

Please read https://hardhat.org/migrate-from-hardhat2 to learn how to migrate your project to Hardhat 3.
`;
}

super(message);
this.callerRelativePath = callerPath;
}
}

export function getCallerRelativePath(depth: number = 5): string | undefined {
Comment thread
alcuadrado marked this conversation as resolved.
try {
const stack = new Error().stack;
if (stack === undefined) {
return undefined;
}

// Default depth=5 assumes: 0=message, 1=getCallerRelativePath, 2=Hardhat2PluginError constructor, 3=throwHardhat2PluginError, 4=deprecated fn, 5=actual caller
const lines = stack.split("\n");
const callerLine = lines[depth];
if (callerLine === undefined) {
return undefined;
}

const regex = new RegExp(
Comment thread
schaable marked this conversation as resolved.
Outdated
[
"^", // Matches the beginning of the line
"at ", // Matches the string "at "
"(?:", // Opens a non-capturing group
".+?", // Lazily matches the context
" \\(", // Matches the string " ("
")?", // Closes the non-capturing group and makes it optional
"(?:", // Opens a non-capturing group
"([^\\(].*?)", // Lazily captures the location as 1 or more characters not starting with "("
"(?:", // Opens a non-capturing group
":", // Matches the string ":"
"\\d+", // Matches the line number
")?", // Closes the non-capturing group and makes it optional
"(?:", // Opens a non-capturing group
":", // Matches the string ":"
"\\d+", // Matches the column number
")?", // Closes the non-capturing group and makes it optional
")", // Closes the non-capturing group
"\\)?", // Optionally matches the string ")"
"$", // Matches the end of the line
].join(""),
);

const match = callerLine.trim().match(regex);
if (match === null || match[1] === undefined) {
return undefined;
}

let filePath = match[1];

// Handle file:// URLs from ESM stack traces
if (filePath.startsWith("file://")) {
filePath = fileURLToPath(filePath);
}

return shortenPath(filePath);
} catch {
return undefined;
}
}

export function throwUsingHardhat2PluginError(): never {
/* eslint-disable-next-line no-restricted-syntax -- Intentionally throwing a
custom error here so that we always print the stack trace */
throw new UsingHardhat2PluginError();
}
16 changes: 16 additions & 0 deletions v-next/hardhat/src/plugins.ts
Original file line number Diff line number Diff line change
@@ -1 +1,17 @@
export { HardhatPluginError } from "@nomicfoundation/hardhat-errors";

import { throwUsingHardhat2PluginError } from "./internal/using-hardhat2-plugin-errors.js";

/**
* @deprecated This function is part of the Hardhat 2 plugin API.
*/
export function lazyFunction(..._args: any): any {
throwUsingHardhat2PluginError();
}

/**
* @deprecated This function is part of the Hardhat 2 plugin API.
*/
export function lazyObject(..._args: any): any {
throwUsingHardhat2PluginError();
}
14 changes: 14 additions & 0 deletions v-next/hardhat/src/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export * from "./arguments.js";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

whoops, did we forget to add this file earlier?

export * from "./artifacts.js";
export * from "./config.js";
export * from "./global-options.js";
export * from "./hooks.js";
export * from "./hre.js";
export * from "./network.js";
export * from "./plugins.js";
export * from "./providers.js";
export * from "./solidity.js";
export * from "./tasks.js";
export * from "./test.js";
export * from "./user-interruptions.js";
export * from "./utils.js";
106 changes: 106 additions & 0 deletions v-next/hardhat/test/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import assert from "node:assert/strict";
import path from "node:path";
import { describe, it } from "node:test";

import { assertThrows } from "@nomicfoundation/hardhat-test-utils";

import {
extendConfig,
extendEnvironment,
extendProvider,
scope,
subtask,
} from "../src/config.js";
import { UsingHardhat2PluginError } from "../src/internal/using-hardhat2-plugin-errors.js";

describe("Hardhat 2 plugin compatibility", () => {
it("should throw when calling extendConfig", () => {
assertThrows(
() => extendConfig(),
(error) => {
assert.ok(
error instanceof UsingHardhat2PluginError,
"Should be a UsingHardhat2PluginError",
);
assert.ok(
error.callerRelativePath?.includes(path.join("test", "config.ts")) ===
true,
"Should have the caller path",
);
return true;
},
);
});

it("should throw when calling extendEnvironment", () => {
assertThrows(
() => extendEnvironment(),
(error) => {
assert.ok(
error instanceof UsingHardhat2PluginError,
"Should be a UsingHardhat2PluginError",
);
assert.ok(
error.callerRelativePath?.includes(path.join("test", "config.ts")) ===
true,
"Should have the caller path",
);
return true;
},
);
});

it("should throw when calling extendProvider", () => {
assertThrows(
() => extendProvider(),
(error) => {
assert.ok(
error instanceof UsingHardhat2PluginError,
"Should be a UsingHardhat2PluginError",
);
assert.ok(
error.callerRelativePath?.includes(path.join("test", "config.ts")) ===
true,
"Should have the caller path",
);
return true;
},
);
});

it("should throw when calling scope", () => {
assertThrows(
() => scope(),
(error) => {
assert.ok(
error instanceof UsingHardhat2PluginError,
"Should be a UsingHardhat2PluginError",
);
assert.ok(
error.callerRelativePath?.includes(path.join("test", "config.ts")) ===
true,
"Should have the caller path",
);
return true;
},
);
});

it("should throw when calling subtask", () => {
assertThrows(
() => subtask(),
(error) => {
assert.ok(
error instanceof UsingHardhat2PluginError,
"Should be a UsingHardhat2PluginError",
);
assert.ok(
error.callerRelativePath?.includes(path.join("test", "config.ts")) ===
true,
"Should have the caller path",
);
return true;
},
);
});
});
Loading