Skip to content

Commit d3bff51

Browse files
authored
Bug fixes, bring back line numbers (#9)
* fix: handle functions that don't have names * fix: properly validate script files * feat: log line numbers for findings * fix: tweak script rules * fix: assume .s.sol files are scripts, other extensions are not * fix: properly validate script interface, and log more info about invalid interface * chore: clarify script ABI requirement
1 parent 8c44058 commit d3bff51

File tree

2 files changed

+108
-34
lines changed

2 files changed

+108
-34
lines changed

Diff for: README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Formatting and checking does the following:
1212
- Uses the `taplo` crate to format TOML.
1313
- Validates test names follow a convention of `test(Fork)?(Fuzz)?_(Revert(If_|When_){1})?\w{1,}`.
1414
- Validates constants and immutables are in `ALL_CAPS`.
15-
- Validates function names and visibility in forge scripts to 1 public `run` method per script.
15+
- Validates function names and visibility in forge scripts to 1 public `run` method per script (executable script files are expected to end with `.s.sol`, whereas non-executable helper contracts in the scripts dir just end with `.sol`).
1616
- Validates internal functions in `src/` start with a leading underscore.
1717

1818
## Usage

Diff for: src/lib.rs

+107-33
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ use once_cell::sync::Lazy;
77

88
use regex::Regex;
99
use solang_parser::pt::{
10-
ContractPart, FunctionAttribute, FunctionDefinition, SourceUnitPart, VariableAttribute,
11-
VariableDefinition, Visibility,
10+
ContractPart, FunctionAttribute, FunctionDefinition, FunctionTy, SourceUnitPart,
11+
VariableAttribute, VariableDefinition, Visibility,
1212
};
1313
use std::{error::Error, ffi::OsStr, fmt, fs, process};
1414
use walkdir::{DirEntry, WalkDir};
@@ -170,24 +170,32 @@ enum Validator {
170170
}
171171

172172
struct InvalidItem {
173-
// TODO Map solang `File` info to line number.
174173
kind: Validator,
175174
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.
177177
}
178178

179179
impl InvalidItem {
180180
fn description(&self) -> String {
181181
match self.kind {
182182
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)
184184
}
185185
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)
187193
}
188-
Validator::Script => format!("Invalid script interface in {}", self.file),
189194
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+
)
191199
}
192200
}
193201
}
@@ -214,11 +222,15 @@ impl ValidationResults {
214222
}
215223

216224
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;
218230
}
219231

220232
impl Validate for VariableDefinition {
221-
fn validate(&self, dent: &DirEntry) -> Vec<InvalidItem> {
233+
fn validate(&self, content: &str, dent: &DirEntry) -> Vec<InvalidItem> {
222234
let mut invalid_items = Vec::new();
223235
let name = &self.name.name;
224236

@@ -232,24 +244,37 @@ impl Validate for VariableDefinition {
232244
kind: Validator::Constant,
233245
file: dent.path().display().to_string(),
234246
text: name.clone(),
247+
line: offset_to_line(content, self.loc.start()),
235248
});
236249
}
237250

238251
invalid_items
239252
}
240253
}
241254

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+
242266
impl Validate for FunctionDefinition {
243-
fn validate(&self, dent: &DirEntry) -> Vec<InvalidItem> {
267+
fn validate(&self, content: &str, dent: &DirEntry) -> Vec<InvalidItem> {
244268
let mut invalid_items = Vec::new();
245-
let name = &self.name.as_ref().unwrap().name;
269+
let name = &self.name();
246270

247271
// Validate test names match the required pattern.
248272
if dent.path().starts_with("./test") && !is_valid_test_name(name) {
249273
invalid_items.push(InvalidItem {
250274
kind: Validator::Test,
251275
file: dent.path().display().to_string(),
252-
text: name.clone(),
276+
text: name.to_string(),
277+
line: offset_to_line(content, self.loc.start()),
253278
});
254279
}
255280

@@ -265,7 +290,8 @@ impl Validate for FunctionDefinition {
265290
invalid_items.push(InvalidItem {
266291
kind: Validator::Src,
267292
file: dent.path().display().to_string(),
268-
text: name.clone(),
293+
text: name.to_string(),
294+
line: offset_to_line(content, self.loc.start()),
269295
});
270296
}
271297

@@ -278,8 +304,6 @@ fn validate(paths: [&str; 3]) -> Result<ValidationResults, Box<dyn Error>> {
278304
let mut results = ValidationResults::default();
279305

280306
for path in paths {
281-
let is_script = path == "./script";
282-
283307
for result in WalkDir::new(path) {
284308
let dent = match result {
285309
Ok(dent) => dent,
@@ -293,32 +317,37 @@ fn validate(paths: [&str; 3]) -> Result<ValidationResults, Box<dyn Error>> {
293317
continue
294318
}
295319

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+
296325
// Get the parse tree (pt) of the file.
297326
let content = fs::read_to_string(dent.path())?;
298327
let (pt, _comments) = solang_parser::parse(&content, 0).expect("Parsing failed");
299328

300329
// 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();
302331

303332
// Run checks.
304333
for element in pt.0 {
305334
match element {
306335
SourceUnitPart::FunctionDefinition(f) => {
307-
results.invalid_items.extend(f.validate(&dent));
336+
results.invalid_items.extend(f.validate(&content, &dent));
308337
}
309338
SourceUnitPart::VariableDefinition(v) => {
310-
results.invalid_items.extend(v.validate(&dent));
339+
results.invalid_items.extend(v.validate(&content, &dent));
311340
}
312341
SourceUnitPart::ContractDefinition(c) => {
313342
for el in c.parts {
314343
match el {
315344
ContractPart::VariableDefinition(v) => {
316-
results.invalid_items.extend(v.validate(&dent));
345+
results.invalid_items.extend(v.validate(&content, &dent));
317346
}
318347
ContractPart::FunctionDefinition(f) => {
319-
results.invalid_items.extend(f.validate(&dent));
348+
results.invalid_items.extend(f.validate(&content, &dent));
320349

321-
let name = f.name.unwrap().name;
350+
let name = f.name();
322351
let is_private = f.attributes.iter().any(|a| match a {
323352
FunctionAttribute::Visibility(v) => {
324353
matches!(
@@ -328,8 +357,13 @@ fn validate(paths: [&str; 3]) -> Result<ValidationResults, Box<dyn Error>> {
328357
}
329358
_ => false,
330359
});
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);
333367
}
334368
}
335369
_ => (),
@@ -340,15 +374,38 @@ fn validate(paths: [&str; 3]) -> Result<ValidationResults, Box<dyn Error>> {
340374
}
341375
}
342376

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+
}
352409
}
353410
}
354411
}
@@ -365,3 +422,20 @@ fn is_valid_test_name(name: &str) -> bool {
365422
fn is_valid_constant_name(name: &str) -> bool {
366423
RE_VALID_CONSTANT_NAME.is_match(name)
367424
}
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

Comments
 (0)