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
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,12 @@
// 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.

// We could also throw from this file, but if it gets imported by an ESM module
// you don't get an import-stack-trace, so you loose the possibility of figuring
// out which plugin is triggering the error.
108 changes: 108 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,108 @@
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 a Hardhat 2 plugin calling an API that was removed in Hardhat 3: ${chalk.bold(callerPath)}

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.

Check the stack trace below to identify which plugin is causing this.

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

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

/**
* Returns the relative path of the file that called a deprecated Hardhat
* plugin API, based on the stack trace. This helps identify which plugin
* file is triggering usage of Hardhat 2 APIs in a Hardhat 3 project.
*
* @param {number} [depth=5] The stack trace depth to locate the caller's
* source file. By default, depth 5 is used because:
* 0 = message
* 1 = getCallerRelativePath
* 2 = UsingHardhat2PluginError constructor
* 3 = throwUsingHardhat2PluginError
* 4 = deprecated function
* 5 = actual caller (the plugin file)
*
* @returns {string|undefined} The shortened relative path of the caller file,
* or undefined if not found.
*
* @example
* If the stack trace is:
* // Error
* // at getCallerRelativePath (src/internal/using-hardhat2-plugin-errors.ts:34:15)
* // at UsingHardhat2PluginError.constructor (src/internal/using-hardhat2-plugin-errors.ts:7:3)
* // at throwUsingHardhat2PluginError (src/internal/using-hardhat2-plugin-errors.ts:90:3)
* // at deprecatedFunction (plugins/example-plugin/deprecated.js:50:10)
* // at main (plugins/example-plugin/index.js:100:5)
* Calling getCallerRelativePath() returns 'plugins/example-plugin/index.js'
*/
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;
}

const lines = stack.split("\n");
const callerLine = lines[depth];
if (callerLine === undefined) {
return undefined;
}

/**
* Matches a single stack trace line:
*
* at FunctionName (path/to/file.ts:10:5)
* at path/to/file.ts:10:5
*
* Captures:
* - group 1: file location (without line/column)
*/
const STACK_TRACE_LINE_REGEX =
/^at (?:.+? \()?([^\(].*?)(?::\d+)?(?::\d+)?\)?$/;

const match = callerLine.trim().match(STACK_TRACE_LINE_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