@@ -3,7 +3,6 @@ import type { NewTaskActionFunction } from "../../../types/tasks.js";
3
3
4
4
import { resolveFromRoot } from "@ignored/hardhat-vnext-utils/path" ;
5
5
import chalk from "chalk" ;
6
- import toposort from "toposort" ;
7
6
8
7
import { getHardhatVersion } from "../../utils/package.js" ;
9
8
import { buildDependencyGraph } from "../solidity/build-system/dependency-graph-building.js" ;
@@ -15,14 +14,18 @@ const SPDX_LICENSES_REGEX =
15
14
// Match every group where a pragma directive is defined. The first captured group is the pragma directive.
16
15
const PRAGMA_DIRECTIVES_REGEX =
17
16
/ ^ (?: | \t ) * ( p r a g m a \s * a b i c o d e r \s * v ( 1 | 2 ) | p r a g m a \s * e x p e r i m e n t a l \s * A B I E n c o d e r V 2 ) \s * ; / gim;
17
+ // Match import statements
18
+ const IMPORT_SOLIDITY_REGEX = / ^ \s * i m p o r t ( \s + ) [ \s \S ] * ?; \s * $ / gm;
18
19
19
20
export interface FlattenActionArguments {
20
21
files : string [ ] ;
22
+ logFunction ?: typeof console . log ;
23
+ warnFunction ?: typeof console . warn ;
21
24
}
22
25
23
26
export interface FlattenActionResult {
24
27
flattened : string ;
25
- metadata : FlattenMetadata | null ;
28
+ metadata ? : FlattenMetadata ;
26
29
}
27
30
28
31
export interface FlattenMetadata {
@@ -33,9 +36,12 @@ export interface FlattenMetadata {
33
36
}
34
37
35
38
const flattenAction : NewTaskActionFunction < FlattenActionArguments > = async (
36
- { files } ,
39
+ { files, logFunction , warnFunction } ,
37
40
{ solidity, config } ,
38
41
) : Promise < FlattenActionResult > => {
42
+ const log = logFunction ?? console . log ;
43
+ const warn = warnFunction ?? console . warn ;
44
+
39
45
// Resolve files from arguments or default to all root files
40
46
const rootPaths =
41
47
files . length === 0
@@ -59,7 +65,7 @@ const flattenAction: NewTaskActionFunction<FlattenActionArguments> = async (
59
65
60
66
// Return empty string when no files are resolved
61
67
if ( Array . from ( dependencyGraph . getAllFiles ( ) ) . length === 0 ) {
62
- return { flattened, metadata : null } ;
68
+ return { flattened, metadata : undefined } ;
63
69
}
64
70
65
71
// Write a comment with hardhat version used to flatten
@@ -68,33 +74,33 @@ const flattenAction: NewTaskActionFunction<FlattenActionArguments> = async (
68
74
69
75
const sortedFiles = getSortedFiles ( dependencyGraph ) ;
70
76
71
- const [ licenses , filesWithoutLicenses ] = getLicensesInfo ( sortedFiles ) ;
77
+ const { licenses, filesWithoutLicenses } = getLicensesInfo ( sortedFiles ) ;
72
78
73
- const [
79
+ const {
74
80
pragmaDirective,
75
81
filesWithoutPragmaDirectives,
76
82
filesWithDifferentPragmaDirectives,
77
- ] = getPragmaAbicoderDirectiveInfo ( sortedFiles ) ;
83
+ } = getPragmaAbicoderDirectiveInfo ( sortedFiles ) ;
78
84
79
85
// Write the combined license header and pragma abicoder directive with highest importance
80
86
flattened += getLicensesHeader ( licenses ) ;
81
87
flattened += getPragmaAbicoderDirectiveHeader ( pragmaDirective ) ;
82
88
83
89
for ( const file of sortedFiles ) {
84
90
let normalizedText = getTextWithoutImports ( file ) ;
85
- normalizedText = commentLicenses ( normalizedText ) ;
86
- normalizedText = commentPragmaAbicoderDirectives ( normalizedText ) ;
91
+ normalizedText = commentOutLicenses ( normalizedText ) ;
92
+ normalizedText = commentOutPragmaAbicoderDirectives ( normalizedText ) ;
87
93
88
94
// Write files without imports, with commented licenses and pragma abicoder directives
89
95
flattened += `\n\n// File ${ file . sourceName } \n` ;
90
96
flattened += `\n${ normalizedText } \n` ;
91
97
}
92
98
93
99
// Print the flattened file
94
- console . log ( flattened ) ;
100
+ log ( flattened ) ;
95
101
96
102
if ( filesWithoutLicenses . length > 0 ) {
97
- console . warn (
103
+ warn (
98
104
chalk . yellow (
99
105
`\nThe following file(s) do NOT specify SPDX licenses: ${ filesWithoutLicenses . join (
100
106
", " ,
@@ -104,7 +110,7 @@ const flattenAction: NewTaskActionFunction<FlattenActionArguments> = async (
104
110
}
105
111
106
112
if ( pragmaDirective !== "" && filesWithoutPragmaDirectives . length > 0 ) {
107
- console . warn (
113
+ warn (
108
114
chalk . yellow (
109
115
`\nPragma abicoder directives are defined in some files, but they are not defined in the following ones: ${ filesWithoutPragmaDirectives . join (
110
116
", " ,
@@ -114,7 +120,7 @@ const flattenAction: NewTaskActionFunction<FlattenActionArguments> = async (
114
120
}
115
121
116
122
if ( filesWithDifferentPragmaDirectives . length > 0 ) {
117
- console . warn (
123
+ warn (
118
124
chalk . yellow (
119
125
`\nThe flattened file is using the pragma abicoder directive '${ pragmaDirective } ' but these files have a different pragma abicoder directive: ${ filesWithDifferentPragmaDirectives . join (
120
126
", " ,
@@ -134,11 +140,16 @@ const flattenAction: NewTaskActionFunction<FlattenActionArguments> = async (
134
140
} ;
135
141
} ;
136
142
137
- function getLicensesInfo ( sortedFiles : ResolvedFile [ ] ) : [ string [ ] , string [ ] ] {
143
+ interface LicensesInfo {
144
+ licenses : string [ ] ;
145
+ filesWithoutLicenses : string [ ] ;
146
+ }
147
+
148
+ function getLicensesInfo ( files : ResolvedFile [ ] ) : LicensesInfo {
138
149
const licenses : Set < string > = new Set ( ) ;
139
150
const filesWithoutLicenses : Set < string > = new Set ( ) ;
140
151
141
- for ( const file of sortedFiles ) {
152
+ for ( const file of files ) {
142
153
const matches = [ ...file . content . text . matchAll ( SPDX_LICENSES_REGEX ) ] ;
143
154
144
155
if ( matches . length === 0 ) {
@@ -152,22 +163,31 @@ function getLicensesInfo(sortedFiles: ResolvedFile[]): [string[], string[]] {
152
163
}
153
164
154
165
// 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 [ ] ;
156
176
}
157
177
158
178
function getPragmaAbicoderDirectiveInfo (
159
- sortedFiles : ResolvedFile [ ] ,
160
- ) : [ string , string [ ] , string [ ] ] {
179
+ files : ResolvedFile [ ] ,
180
+ ) : PragmaDirectivesInfo {
161
181
let directive = "" ;
162
182
const directivesByImportance = [
163
183
"pragma abicoder v1" ,
164
184
"pragma experimental ABIEncoderV2" ,
165
185
"pragma abicoder v2" ,
166
186
] ;
167
187
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 > = { } ;
169
189
170
- for ( const file of sortedFiles ) {
190
+ for ( const file of files ) {
171
191
const matches = [ ...file . content . text . matchAll ( PRAGMA_DIRECTIVES_REGEX ) ] ;
172
192
173
193
if ( matches . length === 0 ) {
@@ -177,7 +197,9 @@ function getPragmaAbicoderDirectiveInfo(
177
197
178
198
let fileMostImportantDirective = "" ;
179
199
for ( const groups of matches ) {
180
- const normalizedPragma = removeUnnecessarySpaces ( groups [ 1 ] ) ;
200
+ const normalizedPragma = removeDuplicateAndSurroundingWhitespaces (
201
+ groups [ 1 ] ,
202
+ ) ;
181
203
182
204
// Update the most important pragma directive among all the files
183
205
if (
@@ -196,78 +218,68 @@ function getPragmaAbicoderDirectiveInfo(
196
218
}
197
219
}
198
220
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 ;
204
223
}
205
224
206
225
// Add to the array the files that have a pragma directive which is not the same as the main one that
207
226
// is going to be used in the flatten file
208
- const filesWithDifferentPragmaDirectives = filesWithMostImportantDirective
227
+ const filesWithDifferentPragmaDirectives = Object . entries (
228
+ filesWithMostImportantDirective ,
229
+ )
209
230
. filter ( ( [ , fileDirective ] ) => fileDirective !== directive )
210
- . map ( ( [ fileName ] ) => fileName ) ;
231
+ . map ( ( [ fileName ] ) => fileName )
232
+ . sort ( ) ;
211
233
212
234
// 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
+ } ;
218
242
}
219
243
244
+ // Returns files sorted in topological order
220
245
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
+ } ;
223
254
224
- const walk = ( files : Iterable < ResolvedFile > ) => {
255
+ // Depth-first walking
256
+ const walk = ( files : ResolvedFile [ ] ) => {
225
257
for ( const file of files ) {
226
- if ( visited . has ( file . sourceName ) ) continue ;
258
+ if ( sortedFiles . includes ( file ) ) continue ;
227
259
228
- visited . add ( file . sourceName ) ;
260
+ sortedFiles . push ( file ) ;
229
261
230
- // Sort dependencies in alphabetical order for deterministic results
231
- const dependencies = Array . from (
262
+ const dependencies = sortBySourceName (
232
263
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
+ ) ;
238
265
239
266
walk ( dependencies ) ;
240
267
}
241
268
} ;
242
269
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 ( ) ) ;
247
271
248
272
walk ( roots ) ;
249
273
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 ;
263
275
}
264
276
265
- function removeUnnecessarySpaces ( str : string ) : string {
277
+ function removeDuplicateAndSurroundingWhitespaces ( str : string ) : string {
266
278
return str . replace ( / \s + / g, " " ) . trim ( ) ;
267
279
}
268
280
269
281
function getLicensesHeader ( licenses : string [ ] ) : string {
270
- return licenses . length < = 0
282
+ return licenses . length == = 0
271
283
? ""
272
284
: `\n\n// SPDX-License-Identifier: ${ licenses . join ( " AND " ) } ` ;
273
285
}
@@ -277,21 +289,19 @@ function getPragmaAbicoderDirectiveHeader(pragmaDirective: string): string {
277
289
}
278
290
279
291
function getTextWithoutImports ( resolvedFile : ResolvedFile ) {
280
- const IMPORT_SOLIDITY_REGEX = / ^ \s * i m p o r t ( \s + ) [ \s \S ] * ?; \s * $ / gm;
281
-
282
292
return resolvedFile . content . text . replace ( IMPORT_SOLIDITY_REGEX , "" ) . trim ( ) ;
283
293
}
284
294
285
- function commentLicenses ( file : string ) : string {
295
+ function commentOutLicenses ( file : string ) : string {
286
296
return file . replaceAll (
287
297
SPDX_LICENSES_REGEX ,
288
298
( ...groups ) => `// Original license: SPDX_License_Identifier: ${ groups [ 1 ] } ` ,
289
299
) ;
290
300
}
291
301
292
- function commentPragmaAbicoderDirectives ( file : string ) : string {
302
+ function commentOutPragmaAbicoderDirectives ( file : string ) : string {
293
303
return file . replaceAll ( PRAGMA_DIRECTIVES_REGEX , ( ...groups ) => {
294
- return `// Original pragma directive: ${ removeUnnecessarySpaces (
304
+ return `// Original pragma directive: ${ removeDuplicateAndSurroundingWhitespaces (
295
305
groups [ 1 ] ,
296
306
) } `;
297
307
} ) ;
0 commit comments