diff --git a/Cargo.toml b/Cargo.toml index b24f99fc..ba3e96b9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,7 +51,7 @@ semver = { version = "1.0", features = ["serde"] } serde = { version = "1", features = ["derive", "rc"] } serde_json = "1.0" similar-asserts = "1" -solang-parser = { version = "=0.3.3", default-features = false } +solar-parse = { version = "=0.1.0", default-features = false } svm = { package = "svm-rs", version = "0.5", default-features = false } tempfile = "3.9" thiserror = "1" diff --git a/crates/compilers/Cargo.toml b/crates/compilers/Cargo.toml index ecda8dc5..3d7f6339 100644 --- a/crates/compilers/Cargo.toml +++ b/crates/compilers/Cargo.toml @@ -28,7 +28,7 @@ md-5.workspace = true thiserror.workspace = true path-slash.workspace = true yansi.workspace = true -solang-parser.workspace = true +solar-parse.workspace = true once_cell = { workspace = true, optional = true } futures-util = { workspace = true, optional = true } tokio = { workspace = true, optional = true } diff --git a/crates/compilers/src/compilers/mod.rs b/crates/compilers/src/compilers/mod.rs index 506ea7ca..e5cc12a7 100644 --- a/crates/compilers/src/compilers/mod.rs +++ b/crates/compilers/src/compilers/mod.rs @@ -136,9 +136,18 @@ pub trait CompilerInput: Serialize + Send + Sync + Sized + Debug { pub trait ParsedSource: Debug + Sized + Send + Clone { type Language: Language; + /// Parses the content of the source file. fn parse(content: &str, file: &Path) -> Result; + + /// Returns the version requirement of the source. fn version_req(&self) -> Option<&VersionReq>; + /// Returns a list of contract names defined in the source. + fn contract_names(&self) -> &[String]; + + /// Returns the language of the source. + fn language(&self) -> Self::Language; + /// Invoked during import resolution. Should resolve imports for the given source, and populate /// include_paths for compilers which support this config. fn resolve_imports( @@ -146,7 +155,6 @@ pub trait ParsedSource: Debug + Sized + Send + Clone { paths: &ProjectPathsConfig, include_paths: &mut BTreeSet, ) -> Result>; - fn language(&self) -> Self::Language; /// Used to configure [OutputSelection] for sparse builds. In certain cases, we might want to /// include some of the file dependencies into the compiler output even if we might not be diff --git a/crates/compilers/src/compilers/multi.rs b/crates/compilers/src/compilers/multi.rs index 832cdf62..e2880bc8 100644 --- a/crates/compilers/src/compilers/multi.rs +++ b/crates/compilers/src/compilers/multi.rs @@ -316,14 +316,10 @@ impl ParsedSource for MultiCompilerParsedSource { } } - fn resolve_imports( - &self, - paths: &crate::ProjectPathsConfig, - include_paths: &mut BTreeSet, - ) -> Result> { + fn contract_names(&self) -> &[String] { match self { - Self::Solc(parsed) => parsed.resolve_imports(paths, include_paths), - Self::Vyper(parsed) => parsed.resolve_imports(paths, include_paths), + Self::Solc(parsed) => parsed.contract_names(), + Self::Vyper(parsed) => parsed.contract_names(), } } @@ -334,6 +330,17 @@ impl ParsedSource for MultiCompilerParsedSource { } } + fn resolve_imports( + &self, + paths: &crate::ProjectPathsConfig, + include_paths: &mut BTreeSet, + ) -> Result> { + match self { + Self::Solc(parsed) => parsed.resolve_imports(paths, include_paths), + Self::Vyper(parsed) => parsed.resolve_imports(paths, include_paths), + } + } + fn compilation_dependencies<'a>( &self, imported_nodes: impl Iterator, diff --git a/crates/compilers/src/compilers/solc/mod.rs b/crates/compilers/src/compilers/solc/mod.rs index 0bdb6f42..1223871b 100644 --- a/crates/compilers/src/compilers/solc/mod.rs +++ b/crates/compilers/src/compilers/solc/mod.rs @@ -12,7 +12,6 @@ use foundry_compilers_artifacts::{ Error, Settings, Severity, SolcInput, }; use foundry_compilers_core::error::Result; -use itertools::Itertools; use semver::Version; use serde::{Deserialize, Serialize}; use std::{ @@ -260,12 +259,8 @@ impl ParsedSource for SolData { self.version_req.as_ref() } - fn resolve_imports( - &self, - _paths: &crate::ProjectPathsConfig, - _include_paths: &mut BTreeSet, - ) -> Result> { - Ok(self.imports.iter().map(|i| i.data().path().to_path_buf()).collect_vec()) + fn contract_names(&self) -> &[String] { + &self.contract_names } fn language(&self) -> Self::Language { @@ -276,6 +271,14 @@ impl ParsedSource for SolData { } } + fn resolve_imports( + &self, + _paths: &crate::ProjectPathsConfig, + _include_paths: &mut BTreeSet, + ) -> Result> { + Ok(self.imports.iter().map(|i| i.data().path().to_path_buf()).collect()) + } + fn compilation_dependencies<'a>( &self, imported_nodes: impl Iterator, diff --git a/crates/compilers/src/compilers/vyper/parser.rs b/crates/compilers/src/compilers/vyper/parser.rs index e98f5ea2..7e55ebb0 100644 --- a/crates/compilers/src/compilers/vyper/parser.rs +++ b/crates/compilers/src/compilers/vyper/parser.rs @@ -52,6 +52,14 @@ impl ParsedSource for VyperParsedSource { self.version_req.as_ref() } + fn contract_names(&self) -> &[String] { + &[] + } + + fn language(&self) -> Self::Language { + VyperLanguage + } + fn resolve_imports( &self, paths: &ProjectPathsConfig, @@ -137,10 +145,6 @@ impl ParsedSource for VyperParsedSource { } Ok(imports) } - - fn language(&self) -> Self::Language { - VyperLanguage - } } /// Parses given source trying to find all import directives. diff --git a/crates/compilers/src/config.rs b/crates/compilers/src/config.rs index 3d9a4566..920ecdaa 100644 --- a/crates/compilers/src/config.rs +++ b/crates/compilers/src/config.rs @@ -131,10 +131,11 @@ impl ProjectPathsConfig { let mut result = String::new(); for path in ordered_deps.iter() { - let node_id = graph.files().get(path).ok_or_else(|| { + let node_id = *graph.files().get(path).ok_or_else(|| { SolcError::msg(format!("cannot resolve file at {}", path.display())) })?; - let node = graph.node(*node_id); + let node = graph.node(node_id); + node.data.parse_result()?; let content = node.content(); // Firstly we strip all licesnses, verson pragmas @@ -142,25 +143,25 @@ impl ProjectPathsConfig { let mut ranges_to_remove = Vec::new(); if let Some(license) = &node.data.license { - ranges_to_remove.push(license.loc()); + ranges_to_remove.push(license.span()); if *path == flatten_target { - result.push_str(&content[license.loc()]); + result.push_str(&content[license.span()]); result.push('\n'); } } if let Some(version) = &node.data.version { - let content = &content[version.loc()]; - ranges_to_remove.push(version.loc()); + let content = &content[version.span()]; + ranges_to_remove.push(version.span()); version_pragmas.push(content); } if let Some(experimental) = &node.data.experimental { - ranges_to_remove.push(experimental.loc()); + ranges_to_remove.push(experimental.span()); if experimental_pragma.is_none() { - experimental_pragma = Some(content[experimental.loc()].to_owned()); + experimental_pragma = Some(content[experimental.span()].to_owned()); } } for import in &node.data.imports { - ranges_to_remove.push(import.loc()); + ranges_to_remove.push(import.span()); } ranges_to_remove.sort_by_key(|loc| loc.start); diff --git a/crates/compilers/src/lib.rs b/crates/compilers/src/lib.rs index fc2f8c3a..4defcf32 100644 --- a/crates/compilers/src/lib.rs +++ b/crates/compilers/src/lib.rs @@ -50,7 +50,6 @@ use compile::output::contracts::VersionedContracts; use compilers::multi::MultiCompiler; use derivative::Derivative; use foundry_compilers_artifacts::solc::{ - output_selection::OutputSelection, sources::{Source, SourceCompilationKind, Sources}, Contract, Severity, SourceFile, StandardJsonCompilerInput, }; @@ -58,11 +57,9 @@ use foundry_compilers_core::error::{Result, SolcError, SolcIoError}; use output::sources::{VersionedSourceFile, VersionedSourceFiles}; use project::ProjectCompiler; use semver::Version; -use solang_parser::pt::SourceUnitPart; use solc::SolcSettings; use std::{ collections::{BTreeMap, HashMap, HashSet}, - fs, path::{Path, PathBuf}, }; @@ -361,39 +358,7 @@ impl Project { Ok(()) } - /// Runs solc compiler without requesting any output and collects a mapping from contract names - /// to source files containing artifact with given name. - fn collect_contract_names_solc(&self) -> Result>> - where - T: Clone, - C: Clone, - { - let mut temp_project = (*self).clone(); - temp_project.no_artifacts = true; - temp_project.settings.update_output_selection(|selection| { - *selection = OutputSelection::common_output_selection(["abi".to_string()]); - }); - - let output = temp_project.compile()?; - - if output.has_compiler_errors() { - return Err(SolcError::msg(output)); - } - - let contracts = output.into_artifacts().fold( - HashMap::new(), - |mut contracts: HashMap<_, Vec<_>>, (id, _)| { - contracts.entry(id.name).or_default().push(id.source); - contracts - }, - ); - - Ok(contracts) - } - - /// Parses project sources via solang parser, collecting mapping from contract name to source - /// files containing artifact with given name. On parser failure, fallbacks to - /// [Self::collect_contract_names_solc]. + /// Parses the sources in memory and collects all the contract names mapped to their file paths. fn collect_contract_names(&self) -> Result>> where T: Clone, @@ -401,22 +366,16 @@ impl Project { { let graph = Graph::::resolve(&self.paths)?; let mut contracts: HashMap> = HashMap::new(); - - for file in graph.files().keys() { - let src = fs::read_to_string(file).map_err(|e| SolcError::io(e, file))?; - let Ok((parsed, _)) = solang_parser::parse(&src, 0) else { - return self.collect_contract_names_solc(); - }; - - for part in parsed.0 { - if let SourceUnitPart::ContractDefinition(contract) = part { - if let Some(name) = contract.name { - contracts.entry(name.name).or_default().push(file.clone()); - } + if !graph.is_empty() { + for node in graph.nodes(0) { + for contract_name in node.data.contract_names() { + contracts + .entry(contract_name.clone()) + .or_default() + .push(node.path().to_path_buf()); } } } - Ok(contracts) } diff --git a/crates/compilers/src/resolver/mod.rs b/crates/compilers/src/resolver/mod.rs index b8a456ca..6b65c240 100644 --- a/crates/compilers/src/resolver/mod.rs +++ b/crates/compilers/src/resolver/mod.rs @@ -240,11 +240,16 @@ impl Graph { !self.edges.edges[index].is_empty() } - /// Returns all the resolved files and their index in the graph + /// Returns all the resolved files and their index in the graph. pub fn files(&self) -> &HashMap { &self.edges.indices } + /// Returns `true` if the graph is empty. + pub fn is_empty(&self) -> bool { + self.nodes.is_empty() + } + /// Gets a node by index. /// /// # Panics @@ -905,6 +910,12 @@ impl Node { Ok(Self { path: file.to_path_buf(), source, data }) } + /// Returns the path of the file. + pub fn path(&self) -> &Path { + &self.path + } + + /// Returns the contents of the file. pub fn content(&self) -> &str { &self.source.content } diff --git a/crates/compilers/src/resolver/parse.rs b/crates/compilers/src/resolver/parse.rs index 6200671a..064dbfc2 100644 --- a/crates/compilers/src/resolver/parse.rs +++ b/crates/compilers/src/resolver/parse.rs @@ -1,27 +1,34 @@ use foundry_compilers_core::utils; use semver::VersionReq; -use solang_parser::pt::{ - ContractPart, ContractTy, FunctionAttribute, FunctionDefinition, Import, ImportPath, Loc, - SourceUnitPart, Visibility, +use solar_parse::{ + ast, + interface::{sym, Pos}, }; use std::{ ops::Range, path::{Path, PathBuf}, }; -/// Represents various information about a solidity file parsed via [solang_parser] +/// Represents various information about a Solidity file. #[derive(Clone, Debug)] pub struct SolData { - pub license: Option>, - pub version: Option>, - pub experimental: Option>, - pub imports: Vec>, + pub license: Option>, + pub version: Option>, + pub experimental: Option>, + pub imports: Vec>, pub version_req: Option, pub libraries: Vec, + pub contract_names: Vec, pub is_yul: bool, + pub parse_result: Result<(), String>, } impl SolData { + /// Returns the result of parsing the file. + pub fn parse_result(&self) -> crate::Result<()> { + self.parse_result.clone().map_err(crate::SolcError::ParseError) + } + #[allow(dead_code)] pub fn fmt_version( &self, @@ -41,76 +48,79 @@ impl SolData { let is_yul = file.extension().map_or(false, |ext| ext == "yul"); let mut version = None; let mut experimental = None; - let mut imports = Vec::>::new(); + let mut imports = Vec::>::new(); let mut libraries = Vec::new(); - - match solang_parser::parse(content, 0) { - Ok((units, _)) => { - for unit in units.0 { - match unit { - SourceUnitPart::PragmaDirective(loc, Some(pragma), Some(value)) => { - if pragma.name == "solidity" { - // we're only interested in the solidity version pragma - version = Some(SolDataUnit::from_loc(value.string.clone(), loc)); - } - - if pragma.name == "experimental" { - experimental = Some(SolDataUnit::from_loc(value.string, loc)); - } + let mut contract_names = Vec::new(); + let mut parse_result = Ok(()); + + let sess = solar_parse::interface::Session::builder() + .with_buffer_emitter(Default::default()) + .build(); + sess.enter(|| { + let arena = ast::Arena::new(); + let filename = solar_parse::interface::source_map::FileName::Real(file.to_path_buf()); + let Ok(mut parser) = + solar_parse::Parser::from_source_code(&sess, &arena, filename, content.to_string()) + else { + return; + }; + let Ok(ast) = parser.parse_file().map_err(|e| e.emit()) else { return }; + for item in ast.items { + let loc = item.span.lo().to_usize()..item.span.hi().to_usize(); + match &item.kind { + ast::ItemKind::Pragma(pragma) => match &pragma.tokens { + ast::PragmaTokens::Version(name, req) if name.name == sym::solidity => { + version = Some(Spanned::new(req.to_string(), loc)); } - SourceUnitPart::ImportDirective(import) => { - let (import, ids, loc) = match import { - Import::Plain(s, l) => (s, vec![], l), - Import::GlobalSymbol(s, i, l) => (s, vec![(i, None)], l), - Import::Rename(s, i, l) => (s, i, l), - }; - let import = match import { - ImportPath::Filename(s) => s.string.clone(), - ImportPath::Path(p) => p.to_string(), - }; - let sol_import = SolImport::new(PathBuf::from(import)).set_aliases( - ids.into_iter() - .map(|(id, alias)| match alias { - Some(al) => SolImportAlias::Contract(al.name, id.name), - None => SolImportAlias::File(id.name), - }) - .collect(), - ); - imports.push(SolDataUnit::from_loc(sol_import, loc)); + ast::PragmaTokens::Custom(name, value) + if name.as_str() == "experimental" => + { + let value = + value.as_ref().map(|v| v.as_str().to_string()).unwrap_or_default(); + experimental = Some(Spanned::new(value, loc)); } - SourceUnitPart::ContractDefinition(def) => { - let functions = def - .parts - .into_iter() - .filter_map(|part| match part { - ContractPart::FunctionDefinition(f) => Some(*f), - _ => None, + _ => {} + }, + + ast::ItemKind::Import(import) => { + let path = import.path.value.to_string(); + let aliases = match &import.items { + ast::ImportItems::Plain(None) | ast::ImportItems::Glob(None) => &[][..], + ast::ImportItems::Plain(Some(alias)) + | ast::ImportItems::Glob(Some(alias)) => &[(*alias, None)][..], + ast::ImportItems::Aliases(aliases) => aliases, + }; + let sol_import = SolImport::new(PathBuf::from(path)).set_aliases( + aliases + .iter() + .map(|(id, alias)| match alias { + Some(al) => SolImportAlias::Contract( + al.name.to_string(), + id.name.to_string(), + ), + None => SolImportAlias::File(id.name.to_string()), }) - .collect(); - if let ContractTy::Library(_) = def.ty { - libraries.push(SolLibrary { functions }); - } + .collect(), + ); + imports.push(Spanned::new(sol_import, loc)); + } + + ast::ItemKind::Contract(contract) => { + if contract.kind.is_library() { + libraries.push(SolLibrary { is_inlined: library_is_inlined(contract) }); } - _ => {} + contract_names.push(contract.name.to_string()); } + + _ => {} } } - Err(err) => { - trace!( - "failed to parse \"{}\" ast: \"{:?}\". Falling back to regex to extract data", - file.display(), - err - ); - version = utils::capture_outer_and_inner( - content, - &utils::RE_SOL_PRAGMA_VERSION, - &["version"], - ) - .first() - .map(|(cap, name)| SolDataUnit::new(name.as_str().to_owned(), cap.range())); - imports = capture_imports(content); - } - }; + }); + if let Err(e) = sess.emitted_diagnostics().unwrap() { + let e = e.to_string(); + trace!("failed parsing {file:?}: {e}"); + parse_result = Err(e); + } let license = content.lines().next().and_then(|line| { utils::capture_outer_and_inner( line, @@ -118,11 +128,21 @@ impl SolData { &["license"], ) .first() - .map(|(cap, l)| SolDataUnit::new(l.as_str().to_owned(), cap.range())) + .map(|(cap, l)| Spanned::new(l.as_str().to_owned(), cap.range())) }); let version_req = version.as_ref().and_then(|v| Self::parse_version_req(v.data()).ok()); - Self { version_req, version, experimental, imports, license, libraries, is_yul } + Self { + version_req, + version, + experimental, + imports, + license, + libraries, + contract_names, + is_yul, + parse_result, + } } /// Returns the corresponding SemVer version requirement for the solidity version. @@ -179,7 +199,7 @@ impl SolImport { /// Minimal representation of a contract inside a solidity file #[derive(Clone, Debug)] pub struct SolLibrary { - pub functions: Vec, + pub is_inlined: bool, } impl SolLibrary { @@ -192,138 +212,55 @@ impl SolLibrary { /// /// See also pub fn is_inlined(&self) -> bool { - for f in self.functions.iter() { - for attr in f.attributes.iter() { - if let FunctionAttribute::Visibility( - Visibility::External(_) | Visibility::Public(_), - ) = attr - { - return false; - } - } - } - true + self.is_inlined } } -/// Represents an item in a solidity file with its location in the file +/// A spanned item. #[derive(Clone, Debug)] -pub struct SolDataUnit { - loc: Range, - data: T, +pub struct Spanned { + /// The byte range of `data` in the file. + pub span: Range, + /// The data of the item. + pub data: T, } -/// Solidity Data Unit decorated with its location within the file -impl SolDataUnit { - pub fn new(data: T, loc: Range) -> Self { - Self { data, loc } - } - - pub fn from_loc(data: T, loc: Loc) -> Self { - Self { - data, - loc: match loc { - Loc::File(_, start, end) => Range { start, end: end + 1 }, - _ => Range { start: 0, end: 0 }, - }, - } +impl Spanned { + /// Creates a new data unit with the given data and location. + pub fn new(data: T, span: Range) -> Self { + Self { data, span } } - /// Returns the underlying data for the unit + /// Returns the underlying data. pub fn data(&self) -> &T { &self.data } - /// Returns the location of the given data unit - pub fn loc(&self) -> Range { - self.loc.clone() + /// Returns the location. + pub fn span(&self) -> Range { + self.span.clone() } - /// Returns the location of the given data unit adjusted by an offset. - /// Used to determine new position of the unit within the file after - /// content manipulation. + /// Returns the location adjusted by an offset. + /// + /// Used to determine new position of the unit within the file after content manipulation. pub fn loc_by_offset(&self, offset: isize) -> Range { - utils::range_by_offset(&self.loc, offset) - } -} - -/// Capture the import statement information together with aliases -pub fn capture_imports(content: &str) -> Vec> { - let mut imports = vec![]; - for cap in utils::RE_SOL_IMPORT.captures_iter(content) { - if let Some(name_match) = ["p1", "p2", "p3", "p4"].iter().find_map(|name| cap.name(name)) { - let statement_match = cap.get(0).unwrap(); - let mut aliases = vec![]; - for alias_cap in utils::RE_SOL_IMPORT_ALIAS.captures_iter(statement_match.as_str()) { - if let Some(alias) = alias_cap.name("alias") { - let alias = alias.as_str().to_owned(); - let import_alias = match alias_cap.name("target") { - Some(target) => SolImportAlias::Contract(alias, target.as_str().to_owned()), - None => SolImportAlias::File(alias), - }; - aliases.push(import_alias); - } - } - let sol_import = - SolImport::new(PathBuf::from(name_match.as_str())).set_aliases(aliases); - imports.push(SolDataUnit::new(sol_import, statement_match.range())); - } + utils::range_by_offset(&self.span, offset) } - imports } -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn can_capture_curly_imports() { - let content = r#" -import { T } from "../Test.sol"; -import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; -import {DsTest} from "ds-test/test.sol"; -"#; - - let captured_imports = - capture_imports(content).into_iter().map(|s| s.data.path).collect::>(); - - let expected = - utils::find_import_paths(content).map(|m| m.as_str().into()).collect::>(); - - assert_eq!(captured_imports, expected); - - assert_eq!( - captured_imports, - vec![ - PathBuf::from("../Test.sol"), - "@openzeppelin/contracts/utils/ReentrancyGuard.sol".into(), - "ds-test/test.sol".into(), - ] - ); - } - - #[test] - fn cap_capture_aliases() { - let content = r#" -import * as T from "./Test.sol"; -import { DsTest as Test } from "ds-test/test.sol"; -import "ds-test/test.sol" as Test; -import { FloatMath as Math, Math as FloatMath } from "./Math.sol"; -"#; - - let caputred_imports = - capture_imports(content).into_iter().map(|s| s.data.aliases).collect::>(); - assert_eq!( - caputred_imports, - vec![ - vec![SolImportAlias::File("T".into())], - vec![SolImportAlias::Contract("Test".into(), "DsTest".into())], - vec![SolImportAlias::File("Test".into())], - vec![ - SolImportAlias::Contract("Math".into(), "FloatMath".into()), - SolImportAlias::Contract("FloatMath".into(), "Math".into()), - ], - ] - ); - } +fn library_is_inlined(contract: &ast::ItemContract<'_>) -> bool { + contract + .body + .iter() + .filter_map(|item| match &item.kind { + ast::ItemKind::Function(f) => Some(f), + _ => None, + }) + .all(|f| { + !matches!( + f.header.visibility, + Some(ast::Visibility::Public | ast::Visibility::External) + ) + }) } diff --git a/crates/compilers/tests/project.rs b/crates/compilers/tests/project.rs index 7a89c463..19618633 100644 --- a/crates/compilers/tests/project.rs +++ b/crates/compilers/tests/project.rs @@ -711,7 +711,7 @@ contract A { } } #[test] -fn can_flatten_on_solang_failure() { +fn cannot_flatten_on_failure() { let project = TempProject::::dapptools().unwrap(); project @@ -742,26 +742,8 @@ contract Contract { .unwrap(); let result = project.paths().clone().with_language::().flatten(target.as_path()); - assert!(result.is_ok()); - - let result = result.unwrap(); - assert_eq!( - result, - r"// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.10; - -// src/Lib.sol - -library Lib {} - -// src/Contract.sol - -// Intentionally erroneous code -contract Contract { - failure(); -} -" - ); + assert!(result.is_err()); + println!("{}", result.unwrap_err()); } #[test] diff --git a/crates/core/src/error.rs b/crates/core/src/error.rs index d58b0f44..0cecacf2 100644 --- a/crates/core/src/error.rs +++ b/crates/core/src/error.rs @@ -27,6 +27,8 @@ pub enum SolcError { /// Errors related to the Solc executable itself. #[error("solc exited with {0}\n{1}")] SolcError(std::process::ExitStatus, String), + #[error("failed to parse a file: {0}")] + ParseError(String), #[error("invalid UTF-8 in Solc output")] InvalidUtf8, #[error("missing pragma from Solidity file")]