Skip to content

Commit e3eb5da

Browse files
committed
Addressing feedback on flatten-v3 task
1 parent c55bb75 commit e3eb5da

File tree

23 files changed

+153
-157
lines changed

23 files changed

+153
-157
lines changed

pnpm-lock.yaml

-8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

v-next/hardhat/package.json

-1
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,6 @@
100100
"micro-eth-signer": "^0.13.0",
101101
"p-map": "^7.0.2",
102102
"semver": "^7.6.3",
103-
"toposort": "^2.0.2",
104103
"tsx": "^4.11.0",
105104
"ws": "^8.18.0",
106105
"zod": "^3.23.8"

v-next/hardhat/src/internal/builtin-plugins/flatten/task-action.ts

+79-69
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ import type { NewTaskActionFunction } from "../../../types/tasks.js";
33

44
import { resolveFromRoot } from "@ignored/hardhat-vnext-utils/path";
55
import chalk from "chalk";
6-
import toposort from "toposort";
76

87
import { getHardhatVersion } from "../../utils/package.js";
98
import { buildDependencyGraph } from "../solidity/build-system/dependency-graph-building.js";
@@ -15,14 +14,18 @@ const SPDX_LICENSES_REGEX =
1514
// Match every group where a pragma directive is defined. The first captured group is the pragma directive.
1615
const PRAGMA_DIRECTIVES_REGEX =
1716
/^(?: |\t)*(pragma\s*abicoder\s*v(1|2)|pragma\s*experimental\s*ABIEncoderV2)\s*;/gim;
17+
// Match import statements
18+
const IMPORT_SOLIDITY_REGEX = /^\s*import(\s+)[\s\S]*?;\s*$/gm;
1819

1920
export interface FlattenActionArguments {
2021
files: string[];
22+
logFunction?: typeof console.log;
23+
warnFunction?: typeof console.warn;
2124
}
2225

2326
export interface FlattenActionResult {
2427
flattened: string;
25-
metadata: FlattenMetadata | null;
28+
metadata?: FlattenMetadata;
2629
}
2730

2831
export interface FlattenMetadata {
@@ -33,9 +36,12 @@ export interface FlattenMetadata {
3336
}
3437

3538
const flattenAction: NewTaskActionFunction<FlattenActionArguments> = async (
36-
{ files },
39+
{ files, logFunction, warnFunction },
3740
{ solidity, config },
3841
): Promise<FlattenActionResult> => {
42+
const log = logFunction ?? console.log;
43+
const warn = warnFunction ?? console.warn;
44+
3945
// Resolve files from arguments or default to all root files
4046
const rootPaths =
4147
files.length === 0
@@ -59,7 +65,7 @@ const flattenAction: NewTaskActionFunction<FlattenActionArguments> = async (
5965

6066
// Return empty string when no files are resolved
6167
if (Array.from(dependencyGraph.getAllFiles()).length === 0) {
62-
return { flattened, metadata: null };
68+
return { flattened, metadata: undefined };
6369
}
6470

6571
// Write a comment with hardhat version used to flatten
@@ -68,33 +74,33 @@ const flattenAction: NewTaskActionFunction<FlattenActionArguments> = async (
6874

6975
const sortedFiles = getSortedFiles(dependencyGraph);
7076

71-
const [licenses, filesWithoutLicenses] = getLicensesInfo(sortedFiles);
77+
const { licenses, filesWithoutLicenses } = getLicensesInfo(sortedFiles);
7278

73-
const [
79+
const {
7480
pragmaDirective,
7581
filesWithoutPragmaDirectives,
7682
filesWithDifferentPragmaDirectives,
77-
] = getPragmaAbicoderDirectiveInfo(sortedFiles);
83+
} = getPragmaAbicoderDirectiveInfo(sortedFiles);
7884

7985
// Write the combined license header and pragma abicoder directive with highest importance
8086
flattened += getLicensesHeader(licenses);
8187
flattened += getPragmaAbicoderDirectiveHeader(pragmaDirective);
8288

8389
for (const file of sortedFiles) {
8490
let normalizedText = getTextWithoutImports(file);
85-
normalizedText = commentLicenses(normalizedText);
86-
normalizedText = commentPragmaAbicoderDirectives(normalizedText);
91+
normalizedText = commentOutLicenses(normalizedText);
92+
normalizedText = commentOutPragmaAbicoderDirectives(normalizedText);
8793

8894
// Write files without imports, with commented licenses and pragma abicoder directives
8995
flattened += `\n\n// File ${file.sourceName}\n`;
9096
flattened += `\n${normalizedText}\n`;
9197
}
9298

9399
// Print the flattened file
94-
console.log(flattened);
100+
log(flattened);
95101

96102
if (filesWithoutLicenses.length > 0) {
97-
console.warn(
103+
warn(
98104
chalk.yellow(
99105
`\nThe following file(s) do NOT specify SPDX licenses: ${filesWithoutLicenses.join(
100106
", ",
@@ -104,7 +110,7 @@ const flattenAction: NewTaskActionFunction<FlattenActionArguments> = async (
104110
}
105111

106112
if (pragmaDirective !== "" && filesWithoutPragmaDirectives.length > 0) {
107-
console.warn(
113+
warn(
108114
chalk.yellow(
109115
`\nPragma abicoder directives are defined in some files, but they are not defined in the following ones: ${filesWithoutPragmaDirectives.join(
110116
", ",
@@ -114,7 +120,7 @@ const flattenAction: NewTaskActionFunction<FlattenActionArguments> = async (
114120
}
115121

116122
if (filesWithDifferentPragmaDirectives.length > 0) {
117-
console.warn(
123+
warn(
118124
chalk.yellow(
119125
`\nThe flattened file is using the pragma abicoder directive '${pragmaDirective}' but these files have a different pragma abicoder directive: ${filesWithDifferentPragmaDirectives.join(
120126
", ",
@@ -134,11 +140,16 @@ const flattenAction: NewTaskActionFunction<FlattenActionArguments> = async (
134140
};
135141
};
136142

137-
function getLicensesInfo(sortedFiles: ResolvedFile[]): [string[], string[]] {
143+
interface LicensesInfo {
144+
licenses: string[];
145+
filesWithoutLicenses: string[];
146+
}
147+
148+
function getLicensesInfo(files: ResolvedFile[]): LicensesInfo {
138149
const licenses: Set<string> = new Set();
139150
const filesWithoutLicenses: Set<string> = new Set();
140151

141-
for (const file of sortedFiles) {
152+
for (const file of files) {
142153
const matches = [...file.content.text.matchAll(SPDX_LICENSES_REGEX)];
143154

144155
if (matches.length === 0) {
@@ -152,22 +163,31 @@ function getLicensesInfo(sortedFiles: ResolvedFile[]): [string[], string[]] {
152163
}
153164

154165
// Sort alphabetically
155-
return [Array.from(licenses).sort(), Array.from(filesWithoutLicenses).sort()];
166+
return {
167+
licenses: Array.from(licenses).sort(),
168+
filesWithoutLicenses: Array.from(filesWithoutLicenses).sort(),
169+
};
170+
}
171+
172+
interface PragmaDirectivesInfo {
173+
pragmaDirective: string;
174+
filesWithoutPragmaDirectives: string[];
175+
filesWithDifferentPragmaDirectives: string[];
156176
}
157177

158178
function getPragmaAbicoderDirectiveInfo(
159-
sortedFiles: ResolvedFile[],
160-
): [string, string[], string[]] {
179+
files: ResolvedFile[],
180+
): PragmaDirectivesInfo {
161181
let directive = "";
162182
const directivesByImportance = [
163183
"pragma abicoder v1",
164184
"pragma experimental ABIEncoderV2",
165185
"pragma abicoder v2",
166186
];
167187
const filesWithoutPragmaDirectives: Set<string> = new Set();
168-
const filesWithMostImportantDirective: Array<[string, string]> = []; // Every array element has the structure: [ fileName, fileMostImportantDirective ]
188+
const filesWithMostImportantDirective: Record<string, string> = {};
169189

170-
for (const file of sortedFiles) {
190+
for (const file of files) {
171191
const matches = [...file.content.text.matchAll(PRAGMA_DIRECTIVES_REGEX)];
172192

173193
if (matches.length === 0) {
@@ -177,7 +197,9 @@ function getPragmaAbicoderDirectiveInfo(
177197

178198
let fileMostImportantDirective = "";
179199
for (const groups of matches) {
180-
const normalizedPragma = removeUnnecessarySpaces(groups[1]);
200+
const normalizedPragma = removeDuplicateAndSurroundingWhitespaces(
201+
groups[1],
202+
);
181203

182204
// Update the most important pragma directive among all the files
183205
if (
@@ -196,78 +218,68 @@ function getPragmaAbicoderDirectiveInfo(
196218
}
197219
}
198220

199-
// Add in the array the most important directive for the current file
200-
filesWithMostImportantDirective.push([
201-
file.sourceName,
202-
fileMostImportantDirective,
203-
]);
221+
filesWithMostImportantDirective[file.sourceName] =
222+
fileMostImportantDirective;
204223
}
205224

206225
// Add to the array the files that have a pragma directive which is not the same as the main one that
207226
// is going to be used in the flatten file
208-
const filesWithDifferentPragmaDirectives = filesWithMostImportantDirective
227+
const filesWithDifferentPragmaDirectives = Object.entries(
228+
filesWithMostImportantDirective,
229+
)
209230
.filter(([, fileDirective]) => fileDirective !== directive)
210-
.map(([fileName]) => fileName);
231+
.map(([fileName]) => fileName)
232+
.sort();
211233

212234
// Sort alphabetically
213-
return [
214-
directive,
215-
Array.from(filesWithoutPragmaDirectives).sort(),
216-
filesWithDifferentPragmaDirectives.sort(),
217-
];
235+
return {
236+
pragmaDirective: directive,
237+
filesWithoutPragmaDirectives: Array.from(
238+
filesWithoutPragmaDirectives,
239+
).sort(),
240+
filesWithDifferentPragmaDirectives,
241+
};
218242
}
219243

244+
// Returns files sorted in topological order
220245
function getSortedFiles(dependencyGraph: DependencyGraph): ResolvedFile[] {
221-
const sortingGraph: Array<[string, string]> = [];
222-
const visited = new Set<string>();
246+
const sortedFiles: ResolvedFile[] = [];
247+
248+
// Helper function for sorting files by sourceName, for deterministic results
249+
const sortBySourceName = (files: Iterable<ResolvedFile>) => {
250+
return Array.from(files).sort((f1, f2) =>
251+
f1.sourceName.localeCompare(f2.sourceName),
252+
);
253+
};
223254

224-
const walk = (files: Iterable<ResolvedFile>) => {
255+
// Depth-first walking
256+
const walk = (files: ResolvedFile[]) => {
225257
for (const file of files) {
226-
if (visited.has(file.sourceName)) continue;
258+
if (sortedFiles.includes(file)) continue;
227259

228-
visited.add(file.sourceName);
260+
sortedFiles.push(file);
229261

230-
// Sort dependencies in alphabetical order for deterministic results
231-
const dependencies = Array.from(
262+
const dependencies = sortBySourceName(
232263
dependencyGraph.getDependencies(file),
233-
).sort((f1, f2) => f1.sourceName.localeCompare(f2.sourceName));
234-
235-
for (const dependency of dependencies) {
236-
sortingGraph.push([dependency.sourceName, file.sourceName]);
237-
}
264+
);
238265

239266
walk(dependencies);
240267
}
241268
};
242269

243-
// Sort roots in alphabetical order for deterministic results
244-
const roots = Array.from(dependencyGraph.getRoots().values()).sort((f1, f2) =>
245-
f1.sourceName.localeCompare(f2.sourceName),
246-
);
270+
const roots = sortBySourceName(dependencyGraph.getRoots().values());
247271

248272
walk(roots);
249273

250-
// Get all nodes so the graph includes files with no dependencies
251-
const allSourceNames = Array.from(dependencyGraph.getAllFiles()).map(
252-
(f) => f.sourceName,
253-
);
254-
255-
// Get source names sorted in topological order
256-
const sortedSourceNames = toposort.array(allSourceNames, sortingGraph);
257-
258-
const sortedFiles = sortedSourceNames.map((sourceName) =>
259-
dependencyGraph.getFileBySourceName(sourceName),
260-
);
261-
262-
return sortedFiles.filter((f) => f !== undefined);
274+
return sortedFiles;
263275
}
264276

265-
function removeUnnecessarySpaces(str: string): string {
277+
function removeDuplicateAndSurroundingWhitespaces(str: string): string {
266278
return str.replace(/\s+/g, " ").trim();
267279
}
268280

269281
function getLicensesHeader(licenses: string[]): string {
270-
return licenses.length <= 0
282+
return licenses.length === 0
271283
? ""
272284
: `\n\n// SPDX-License-Identifier: ${licenses.join(" AND ")}`;
273285
}
@@ -277,21 +289,19 @@ function getPragmaAbicoderDirectiveHeader(pragmaDirective: string): string {
277289
}
278290

279291
function getTextWithoutImports(resolvedFile: ResolvedFile) {
280-
const IMPORT_SOLIDITY_REGEX = /^\s*import(\s+)[\s\S]*?;\s*$/gm;
281-
282292
return resolvedFile.content.text.replace(IMPORT_SOLIDITY_REGEX, "").trim();
283293
}
284294

285-
function commentLicenses(file: string): string {
295+
function commentOutLicenses(file: string): string {
286296
return file.replaceAll(
287297
SPDX_LICENSES_REGEX,
288298
(...groups) => `// Original license: SPDX_License_Identifier: ${groups[1]}`,
289299
);
290300
}
291301

292-
function commentPragmaAbicoderDirectives(file: string): string {
302+
function commentOutPragmaAbicoderDirectives(file: string): string {
293303
return file.replaceAll(PRAGMA_DIRECTIVES_REGEX, (...groups) => {
294-
return `// Original pragma directive: ${removeUnnecessarySpaces(
304+
return `// Original pragma directive: ${removeDuplicateAndSurroundingWhitespaces(
295305
groups[1],
296306
)}`;
297307
});
Original file line numberDiff line numberDiff line change
@@ -1,3 +1 @@
1-
artifacts/
2-
cache/
31
!node_modules

v-next/hardhat/test/fixture-projects/flatten-task/consistent-build-info-names/hardhat.config.js

-7
This file was deleted.

v-next/hardhat/test/fixture-projects/flatten-task/contracts-nameclash-project/hardhat.config.ts

-1
This file was deleted.

v-next/hardhat/test/fixture-projects/flatten-task/contracts-no-spdx-no-pragma/hardhat.config.ts

-1
This file was deleted.

v-next/hardhat/test/fixture-projects/flatten-task/contracts-pragma-different-directives/hardhat.config.ts

-1
This file was deleted.

v-next/hardhat/test/fixture-projects/flatten-task/contracts-pragma-multiple-directives/hardhat.config.ts

-1
This file was deleted.

v-next/hardhat/test/fixture-projects/flatten-task/contracts-pragma-same-directives/hardhat.config.ts

-1
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
// Sources flattened with hardhat v{HARDHAT_VERSION} https://hardhat.org
22

3-
// File contracts/C.sol
3+
// File contracts/A.sol
44

55
pragma solidity ^0.5.1;
6-
contract C {}
6+
7+
contract A {}
78

89

910
// File contracts/B.sol
@@ -13,8 +14,7 @@ pragma solidity ^0.5.1;
1314
contract B {}
1415

1516

16-
// File contracts/A.sol
17+
// File contracts/C.sol
1718

1819
pragma solidity ^0.5.1;
19-
20-
contract A {}
20+
contract C {}

v-next/hardhat/test/fixture-projects/flatten-task/contracts-project/hardhat.config.ts

-1
This file was deleted.

0 commit comments

Comments
 (0)