@@ -7,8 +7,8 @@ use once_cell::sync::Lazy;
7
7
8
8
use regex:: Regex ;
9
9
use solang_parser:: pt:: {
10
- ContractPart , FunctionAttribute , FunctionDefinition , SourceUnitPart , VariableAttribute ,
11
- VariableDefinition , Visibility ,
10
+ ContractPart , FunctionAttribute , FunctionDefinition , FunctionTy , SourceUnitPart ,
11
+ VariableAttribute , VariableDefinition , Visibility ,
12
12
} ;
13
13
use std:: { error:: Error , ffi:: OsStr , fmt, fs, process} ;
14
14
use walkdir:: { DirEntry , WalkDir } ;
@@ -170,24 +170,32 @@ enum Validator {
170
170
}
171
171
172
172
struct InvalidItem {
173
- // TODO Map solang `File` info to line number.
174
173
kind : Validator ,
175
174
file : String , // File name.
176
- text : String , // Incorrectly named item.
175
+ text : String , // Details to show about the invalid item.
176
+ line : usize , // Line number.
177
177
}
178
178
179
179
impl InvalidItem {
180
180
fn description ( & self ) -> String {
181
181
match self . kind {
182
182
Validator :: Test => {
183
- format ! ( "Invalid test name in {}: {}" , self . file, self . text)
183
+ format ! ( "Invalid test name in {} on line {} : {}" , self . file, self . line , self . text)
184
184
}
185
185
Validator :: Constant => {
186
- format ! ( "Invalid constant or immutable name in {}: {}" , self . file, self . text)
186
+ format ! (
187
+ "Invalid constant or immutable name in {} on line {}: {}" ,
188
+ self . file, self . line, self . text
189
+ )
190
+ }
191
+ Validator :: Script => {
192
+ format ! ( "Invalid script interface in {}: {}" , self . file, self . text)
187
193
}
188
- Validator :: Script => format ! ( "Invalid script interface in {}" , self . file) ,
189
194
Validator :: Src => {
190
- format ! ( "Invalid src method name in {}: {}" , self . file, self . text)
195
+ format ! (
196
+ "Invalid src method name in {} on line {}: {}" ,
197
+ self . file, self . line, self . text
198
+ )
191
199
}
192
200
}
193
201
}
@@ -214,11 +222,15 @@ impl ValidationResults {
214
222
}
215
223
216
224
trait Validate {
217
- fn validate ( & self , dent : & DirEntry ) -> Vec < InvalidItem > ;
225
+ fn validate ( & self , content : & str , dent : & DirEntry ) -> Vec < InvalidItem > ;
226
+ }
227
+
228
+ trait Name {
229
+ fn name ( & self ) -> String ;
218
230
}
219
231
220
232
impl Validate for VariableDefinition {
221
- fn validate ( & self , dent : & DirEntry ) -> Vec < InvalidItem > {
233
+ fn validate ( & self , content : & str , dent : & DirEntry ) -> Vec < InvalidItem > {
222
234
let mut invalid_items = Vec :: new ( ) ;
223
235
let name = & self . name . name ;
224
236
@@ -232,24 +244,37 @@ impl Validate for VariableDefinition {
232
244
kind : Validator :: Constant ,
233
245
file : dent. path ( ) . display ( ) . to_string ( ) ,
234
246
text : name. clone ( ) ,
247
+ line : offset_to_line ( content, self . loc . start ( ) ) ,
235
248
} ) ;
236
249
}
237
250
238
251
invalid_items
239
252
}
240
253
}
241
254
255
+ impl Name for FunctionDefinition {
256
+ fn name ( & self ) -> String {
257
+ match self . ty {
258
+ FunctionTy :: Constructor => "constructor" . to_string ( ) ,
259
+ FunctionTy :: Fallback => "fallback" . to_string ( ) ,
260
+ FunctionTy :: Receive => "receive" . to_string ( ) ,
261
+ FunctionTy :: Function | FunctionTy :: Modifier => self . name . as_ref ( ) . unwrap ( ) . name . clone ( ) ,
262
+ }
263
+ }
264
+ }
265
+
242
266
impl Validate for FunctionDefinition {
243
- fn validate ( & self , dent : & DirEntry ) -> Vec < InvalidItem > {
267
+ fn validate ( & self , content : & str , dent : & DirEntry ) -> Vec < InvalidItem > {
244
268
let mut invalid_items = Vec :: new ( ) ;
245
- let name = & self . name . as_ref ( ) . unwrap ( ) . name ;
269
+ let name = & self . name ( ) ;
246
270
247
271
// Validate test names match the required pattern.
248
272
if dent. path ( ) . starts_with ( "./test" ) && !is_valid_test_name ( name) {
249
273
invalid_items. push ( InvalidItem {
250
274
kind : Validator :: Test ,
251
275
file : dent. path ( ) . display ( ) . to_string ( ) ,
252
- text : name. clone ( ) ,
276
+ text : name. to_string ( ) ,
277
+ line : offset_to_line ( content, self . loc . start ( ) ) ,
253
278
} ) ;
254
279
}
255
280
@@ -265,7 +290,8 @@ impl Validate for FunctionDefinition {
265
290
invalid_items. push ( InvalidItem {
266
291
kind : Validator :: Src ,
267
292
file : dent. path ( ) . display ( ) . to_string ( ) ,
268
- text : name. clone ( ) ,
293
+ text : name. to_string ( ) ,
294
+ line : offset_to_line ( content, self . loc . start ( ) ) ,
269
295
} ) ;
270
296
}
271
297
@@ -278,8 +304,6 @@ fn validate(paths: [&str; 3]) -> Result<ValidationResults, Box<dyn Error>> {
278
304
let mut results = ValidationResults :: default ( ) ;
279
305
280
306
for path in paths {
281
- let is_script = path == "./script" ;
282
-
283
307
for result in WalkDir :: new ( path) {
284
308
let dent = match result {
285
309
Ok ( dent) => dent,
@@ -293,32 +317,37 @@ fn validate(paths: [&str; 3]) -> Result<ValidationResults, Box<dyn Error>> {
293
317
continue
294
318
}
295
319
320
+ // Executable script files are expected to end with `.s.sol`, whereas non-executable
321
+ // helper contracts in the scripts dir just end with `.sol`.
322
+ let is_script =
323
+ path == "./script" && dent. path ( ) . to_str ( ) . expect ( "Bad path" ) . ends_with ( ".s.sol" ) ;
324
+
296
325
// Get the parse tree (pt) of the file.
297
326
let content = fs:: read_to_string ( dent. path ( ) ) ?;
298
327
let ( pt, _comments) = solang_parser:: parse ( & content, 0 ) . expect ( "Parsing failed" ) ;
299
328
300
329
// Variables used to track status of checks that are file-wide.
301
- let mut num_public_script_methods = 0 ;
330
+ let mut public_methods : Vec < String > = Vec :: new ( ) ;
302
331
303
332
// Run checks.
304
333
for element in pt. 0 {
305
334
match element {
306
335
SourceUnitPart :: FunctionDefinition ( f) => {
307
- results. invalid_items . extend ( f. validate ( & dent) ) ;
336
+ results. invalid_items . extend ( f. validate ( & content , & dent) ) ;
308
337
}
309
338
SourceUnitPart :: VariableDefinition ( v) => {
310
- results. invalid_items . extend ( v. validate ( & dent) ) ;
339
+ results. invalid_items . extend ( v. validate ( & content , & dent) ) ;
311
340
}
312
341
SourceUnitPart :: ContractDefinition ( c) => {
313
342
for el in c. parts {
314
343
match el {
315
344
ContractPart :: VariableDefinition ( v) => {
316
- results. invalid_items . extend ( v. validate ( & dent) ) ;
345
+ results. invalid_items . extend ( v. validate ( & content , & dent) ) ;
317
346
}
318
347
ContractPart :: FunctionDefinition ( f) => {
319
- results. invalid_items . extend ( f. validate ( & dent) ) ;
348
+ results. invalid_items . extend ( f. validate ( & content , & dent) ) ;
320
349
321
- let name = f. name . unwrap ( ) . name ;
350
+ let name = f. name ( ) ;
322
351
let is_private = f. attributes . iter ( ) . any ( |a| match a {
323
352
FunctionAttribute :: Visibility ( v) => {
324
353
matches ! (
@@ -328,8 +357,13 @@ fn validate(paths: [&str; 3]) -> Result<ValidationResults, Box<dyn Error>> {
328
357
}
329
358
_ => false ,
330
359
} ) ;
331
- if is_script && !is_private && name != "setUp" {
332
- num_public_script_methods += 1 ;
360
+
361
+ if is_script &&
362
+ !is_private &&
363
+ name != "setUp" &&
364
+ name != "constructor"
365
+ {
366
+ public_methods. push ( name) ;
333
367
}
334
368
}
335
369
_ => ( ) ,
@@ -340,15 +374,38 @@ fn validate(paths: [&str; 3]) -> Result<ValidationResults, Box<dyn Error>> {
340
374
}
341
375
}
342
376
343
- // Validate scripts only have a single run method.
344
- // TODO Script checks don't really fit nicely into InvalidItem, refactor needed to log
345
- // more details about the invalid script's ABI.
346
- if num_public_script_methods > 1 {
347
- results. invalid_items . push ( InvalidItem {
348
- kind : Validator :: Script ,
349
- file : dent. path ( ) . display ( ) . to_string ( ) ,
350
- text : String :: new ( ) ,
351
- } ) ;
377
+ // Validate scripts only have a single public run method, or no public methods (i.e.
378
+ // it's a helper contract not a script).
379
+ if is_script {
380
+ // If we have no public methods, the `run` method is missing.
381
+ match public_methods. len ( ) {
382
+ 0 => {
383
+ results. invalid_items . push ( InvalidItem {
384
+ kind : Validator :: Script ,
385
+ file : dent. path ( ) . display ( ) . to_string ( ) ,
386
+ text : "No `run` method found" . to_string ( ) ,
387
+ line : 0 , // This spans multiple lines, so we don't have a line number.
388
+ } ) ;
389
+ }
390
+ 1 => {
391
+ if public_methods[ 0 ] != "run" {
392
+ results. invalid_items . push ( InvalidItem {
393
+ kind : Validator :: Script ,
394
+ file : dent. path ( ) . display ( ) . to_string ( ) ,
395
+ text : "The only public method must be named `run`" . to_string ( ) ,
396
+ line : 0 ,
397
+ } ) ;
398
+ }
399
+ }
400
+ _ => {
401
+ results. invalid_items . push ( InvalidItem {
402
+ kind : Validator :: Script ,
403
+ file : dent. path ( ) . display ( ) . to_string ( ) ,
404
+ text : format ! ( "Scripts must have a single public method named `run` (excluding `setUp`), but the following methods were found: {public_methods:?}" ) ,
405
+ line : 0 ,
406
+ } ) ;
407
+ }
408
+ }
352
409
}
353
410
}
354
411
}
@@ -365,3 +422,20 @@ fn is_valid_test_name(name: &str) -> bool {
365
422
fn is_valid_constant_name ( name : & str ) -> bool {
366
423
RE_VALID_CONSTANT_NAME . is_match ( name)
367
424
}
425
+
426
+ // Converts the start offset of a `Loc` to `(line, col)`. Modified from https://github.com/foundry-rs/foundry/blob/45b9dccdc8584fb5fbf55eb190a880d4e3b0753f/fmt/src/helpers.rs#L54-L70
427
+ fn offset_to_line ( content : & str , start : usize ) -> usize {
428
+ debug_assert ! ( content. len( ) > start) ;
429
+
430
+ let mut line_counter = 1 ; // First line is `1`.
431
+ for ( offset, c) in content. chars ( ) . enumerate ( ) {
432
+ if c == '\n' {
433
+ line_counter += 1 ;
434
+ }
435
+ if offset > start {
436
+ return line_counter
437
+ }
438
+ }
439
+
440
+ unreachable ! ( "content.len() > start" )
441
+ }
0 commit comments