Skip to content

Commit 59ebda6

Browse files
committed
feat: avoid reparsing for interface_repr_hash
1 parent fb5ebc2 commit 59ebda6

File tree

8 files changed

+121
-103
lines changed

8 files changed

+121
-103
lines changed

crates/artifacts/solc/src/sources.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,13 @@ impl Source {
198198
/// Generate a non-cryptographically secure checksum of the file's content.
199199
#[cfg(feature = "checksum")]
200200
pub fn content_hash(&self) -> String {
201-
alloy_primitives::hex::encode(<md5::Md5 as md5::Digest>::digest(self.content.as_bytes()))
201+
Self::content_hash_of(&self.content)
202+
}
203+
204+
/// Generate a non-cryptographically secure checksum of the given source.
205+
#[cfg(feature = "checksum")]
206+
pub fn content_hash_of(src: &str) -> String {
207+
alloy_primitives::hex::encode(<md5::Md5 as md5::Digest>::digest(src))
202208
}
203209
}
204210

crates/compilers/src/cache.rs

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,9 @@ use crate::{
44
buildinfo::RawBuildInfo,
55
compilers::{Compiler, CompilerSettings, Language},
66
output::Builds,
7-
preprocessor::interface_representation_hash,
87
resolver::GraphEdges,
9-
ArtifactFile, ArtifactOutput, Artifacts, ArtifactsMap, Graph, OutputContext, Project,
10-
ProjectPaths, ProjectPathsConfig, SourceCompilationKind,
8+
ArtifactFile, ArtifactOutput, Artifacts, ArtifactsMap, Graph, OutputContext, ParsedSource,
9+
Project, ProjectPaths, ProjectPathsConfig, SourceCompilationKind,
1110
};
1211
use foundry_compilers_artifacts::{
1312
sources::{Source, Sources},
@@ -366,10 +365,7 @@ impl<S: CompilerSettings> CompilerCache<S> {
366365
{
367366
match tokio::task::spawn_blocking(f).await {
368367
Ok(res) => res,
369-
Err(_) => Err(SolcError::io(
370-
std::io::Error::new(std::io::ErrorKind::Other, "background task failed"),
371-
"",
372-
)),
368+
Err(_) => Err(SolcError::io(std::io::Error::other("background task failed"), "")),
373369
}
374370
}
375371
}
@@ -673,8 +669,7 @@ impl<T: ArtifactOutput<CompilerContract = C::CompilerContract>, C: Compiler>
673669
{
674670
/// Whether given file is a source file or a test/script file.
675671
fn is_source_file(&self, file: &Path) -> bool {
676-
!file.starts_with(&self.project.paths.tests)
677-
&& !file.starts_with(&self.project.paths.scripts)
672+
!self.project.paths.is_test_or_script(file)
678673
}
679674

680675
/// Creates a new cache entry for the file
@@ -686,8 +681,8 @@ impl<T: ArtifactOutput<CompilerContract = C::CompilerContract>, C: Compiler>
686681
.map(|import| strip_prefix(import, self.project.root()).into())
687682
.collect();
688683

689-
let interface_repr_hash = if self.cache.preprocessed {
690-
self.is_source_file(&file).then(|| interface_representation_hash(source, &file))
684+
let interface_repr_hash = if self.cache.preprocessed && self.is_source_file(&file) {
685+
self.edges.get_parsed_source(&file).and_then(ParsedSource::interface_repr_hash)
691686
} else {
692687
None
693688
};
@@ -961,16 +956,17 @@ impl<T: ArtifactOutput<CompilerContract = C::CompilerContract>, C: Compiler>
961956
/// Adds the file's hashes to the set if not set yet
962957
fn fill_hashes(&mut self, sources: &Sources) {
963958
for (file, source) in sources {
964-
if let hash_map::Entry::Vacant(entry) = self.content_hashes.entry(file.clone()) {
965-
entry.insert(source.content_hash());
966-
}
959+
let content_hash =
960+
self.content_hashes.entry(file.clone()).or_insert_with(|| source.content_hash());
961+
967962
// Fill interface representation hashes for source files
968-
if self.cache.preprocessed && self.is_source_file(file) {
969-
if let hash_map::Entry::Vacant(entry) =
970-
self.interface_repr_hashes.entry(file.clone())
971-
{
972-
entry.insert(interface_representation_hash(source, file));
973-
}
963+
if self.cache.preprocessed && self.project.paths.is_source_file(file) {
964+
self.interface_repr_hashes.entry(file.clone()).or_insert_with(|| {
965+
self.edges
966+
.get_parsed_source(file)
967+
.and_then(ParsedSource::interface_repr_hash)
968+
.unwrap_or_else(|| content_hash.clone())
969+
});
974970
}
975971
}
976972
}

crates/compilers/src/compilers/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,11 @@ pub trait ParsedSource: Debug + Sized + Send + Clone {
186186
{
187187
vec![].into_iter()
188188
}
189+
190+
/// Returns the hash of the interface of the source.
191+
fn interface_repr_hash(&self) -> Option<String> {
192+
None
193+
}
189194
}
190195

191196
/// Error returned by compiler. Might also represent a warning or informational message.

crates/compilers/src/compilers/multi.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,13 @@ impl ParsedSource for MultiCompilerParsedSource {
387387
}
388388
.into_iter()
389389
}
390+
391+
fn interface_repr_hash(&self) -> Option<String> {
392+
match self {
393+
Self::Solc(parsed) => parsed.interface_repr_hash(),
394+
Self::Vyper(parsed) => parsed.interface_repr_hash(),
395+
}
396+
}
390397
}
391398

392399
impl CompilationError for MultiCompilerError {

crates/compilers/src/compilers/solc/mod.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,10 @@ impl ParsedSource for SolData {
397397
{
398398
imported_nodes.filter_map(|(path, node)| (!node.libraries.is_empty()).then_some(path))
399399
}
400+
401+
fn interface_repr_hash(&self) -> Option<String> {
402+
self.interface_repr_hash.clone()
403+
}
400404
}
401405

402406
impl CompilationError for Error {

crates/compilers/src/config.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,16 @@ impl<L> ProjectPathsConfig<L> {
250250
Self::dapptools(&std::env::current_dir().map_err(|err| SolcError::io(err, "."))?)
251251
}
252252

253+
pub(crate) fn is_test_or_script(&self, path: &Path) -> bool {
254+
let test_dir = self.tests.strip_prefix(&self.root).unwrap_or(&self.tests);
255+
let script_dir = self.scripts.strip_prefix(&self.root).unwrap_or(&self.scripts);
256+
path.starts_with(test_dir) || path.starts_with(script_dir)
257+
}
258+
259+
pub(crate) fn is_source_file(&self, path: &Path) -> bool {
260+
!self.is_test_or_script(path)
261+
}
262+
253263
/// Returns a new [ProjectPaths] instance that contains all directories configured for this
254264
/// project
255265
pub fn paths(&self) -> ProjectPaths {

crates/compilers/src/preprocessor/mod.rs

Lines changed: 61 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,8 @@ use crate::{
1010
solc::{SolcCompiler, SolcVersionedInput},
1111
Compiler, ProjectPathsConfig, Result,
1212
};
13-
use alloy_primitives::hex;
14-
use foundry_compilers_artifacts::{SolcLanguage, Source};
13+
use foundry_compilers_artifacts::SolcLanguage;
1514
use foundry_compilers_core::{error::SolcError, utils};
16-
use md5::Digest;
1715
use solar_parse::{
1816
ast::{FunctionKind, ItemKind, Span, Visibility},
1917
interface::{diagnostics::EmittedDiagnostics, source_map::FileName, Session, SourceMap},
@@ -48,7 +46,7 @@ impl Preprocessor<SolcCompiler> for TestOptimizerPreprocessor {
4846
) -> Result<()> {
4947
let sources = &mut input.input.sources;
5048
// Skip if we are not preprocessing any tests or scripts. Avoids unnecessary AST parsing.
51-
if sources.iter().all(|(path, _)| !is_test_or_script(path, paths)) {
49+
if sources.iter().all(|(path, _)| !paths.is_test_or_script(path)) {
5250
trace!("no tests or sources to preprocess");
5351
return Ok(());
5452
}
@@ -72,7 +70,7 @@ impl Preprocessor<SolcCompiler> for TestOptimizerPreprocessor {
7270
// Add the sources into the context.
7371
let mut preprocessed_paths = vec![];
7472
for (path, source) in sources.iter() {
75-
if is_test_or_script(path, paths) {
73+
if paths.is_test_or_script(path) {
7674
if let Ok(src_file) =
7775
sess.source_map().new_source_file(path.clone(), source.content.as_str())
7876
{
@@ -136,91 +134,86 @@ impl Preprocessor<MultiCompiler> for TestOptimizerPreprocessor {
136134
}
137135
}
138136

139-
/// Helper function to compute hash of [`interface_representation`] of the source.
140-
pub(crate) fn interface_representation_hash(source: &Source, file: &Path) -> String {
141-
let Ok(repr) = interface_representation(&source.content, file) else {
142-
return source.content_hash();
143-
};
144-
let mut hasher = md5::Md5::new();
145-
hasher.update(&repr);
146-
let result = hasher.finalize();
147-
hex::encode(result)
137+
pub(crate) fn parse_one_source<R>(
138+
content: &str,
139+
path: &Path,
140+
f: impl FnOnce(solar_sema::ast::SourceUnit<'_>) -> R,
141+
) -> Result<R, EmittedDiagnostics> {
142+
let sess = Session::builder().with_buffer_emitter(Default::default()).build();
143+
let res = sess.enter(|| -> solar_parse::interface::Result<_> {
144+
let arena = solar_parse::ast::Arena::new();
145+
let filename = FileName::Real(path.to_path_buf());
146+
let mut parser = Parser::from_source_code(&sess, &arena, filename, content.to_string())?;
147+
let ast = parser.parse_file().map_err(|e| e.emit())?;
148+
Ok(f(ast))
149+
});
150+
151+
// Return if any diagnostics emitted during content parsing.
152+
if let Err(err) = sess.emitted_errors().unwrap() {
153+
trace!("failed parsing {path:?}:\n{err}");
154+
return Err(err);
155+
}
156+
157+
Ok(res.unwrap())
148158
}
149159

150160
/// Helper function to remove parts of the contract which do not alter its interface:
151161
/// - Internal functions
152162
/// - External functions bodies
153163
///
154164
/// Preserves all libraries and interfaces.
155-
fn interface_representation(content: &str, file: &Path) -> Result<String, EmittedDiagnostics> {
165+
pub(crate) fn interface_representation_ast(
166+
content: &str,
167+
ast: &solar_parse::ast::SourceUnit<'_>,
168+
) -> String {
156169
let mut spans_to_remove: Vec<Span> = Vec::new();
157-
let sess = Session::builder().with_buffer_emitter(Default::default()).build();
158-
sess.enter(|| {
159-
let arena = solar_parse::ast::Arena::new();
160-
let filename = FileName::Real(file.to_path_buf());
161-
let Ok(mut parser) = Parser::from_source_code(&sess, &arena, filename, content.to_string())
162-
else {
163-
return;
170+
for item in ast.items.iter() {
171+
let ItemKind::Contract(contract) = &item.kind else {
172+
continue;
164173
};
165-
let Ok(ast) = parser.parse_file().map_err(|e| e.emit()) else { return };
166-
for item in ast.items {
167-
let ItemKind::Contract(contract) = &item.kind else {
168-
continue;
169-
};
170-
171-
if contract.kind.is_interface() || contract.kind.is_library() {
172-
continue;
173-
}
174174

175-
for contract_item in contract.body.iter() {
176-
if let ItemKind::Function(function) = &contract_item.kind {
177-
let is_exposed = match function.kind {
178-
// Function with external or public visibility
179-
FunctionKind::Function => {
180-
function.header.visibility >= Some(Visibility::Public)
181-
}
182-
FunctionKind::Constructor
183-
| FunctionKind::Fallback
184-
| FunctionKind::Receive => true,
185-
FunctionKind::Modifier => false,
186-
};
187-
188-
// If function is not exposed we remove the entire span (signature and
189-
// body). Otherwise we keep function signature and
190-
// remove only the body.
191-
if !is_exposed {
192-
spans_to_remove.push(contract_item.span);
193-
} else {
194-
spans_to_remove.push(function.body_span);
175+
if contract.kind.is_interface() || contract.kind.is_library() {
176+
continue;
177+
}
178+
179+
for contract_item in contract.body.iter() {
180+
if let ItemKind::Function(function) = &contract_item.kind {
181+
let is_exposed = match function.kind {
182+
// Function with external or public visibility
183+
FunctionKind::Function => {
184+
function.header.visibility >= Some(Visibility::Public)
185+
}
186+
FunctionKind::Constructor | FunctionKind::Fallback | FunctionKind::Receive => {
187+
true
195188
}
189+
FunctionKind::Modifier => false,
190+
};
191+
192+
// If function is not exposed we remove the entire span (signature and
193+
// body). Otherwise we keep function signature and
194+
// remove only the body.
195+
if !is_exposed {
196+
spans_to_remove.push(contract_item.span);
197+
} else {
198+
spans_to_remove.push(function.body_span);
196199
}
197200
}
198201
}
199-
});
200-
201-
// Return if any diagnostics emitted during content parsing.
202-
if let Err(err) = sess.emitted_errors().unwrap() {
203-
trace!("failed parsing {file:?}: {err}");
204-
return Err(err);
205202
}
206-
207203
let content =
208204
replace_source_content(content, spans_to_remove.iter().map(|span| (span.to_range(), "")))
209205
.replace("\n", "");
210-
Ok(utils::RE_TWO_OR_MORE_SPACES.replace_all(&content, "").to_string())
211-
}
212-
213-
/// Checks if the given path is a test/script file.
214-
fn is_test_or_script<L>(path: &Path, paths: &ProjectPathsConfig<L>) -> bool {
215-
let test_dir = paths.tests.strip_prefix(&paths.root).unwrap_or(&paths.root);
216-
let script_dir = paths.scripts.strip_prefix(&paths.root).unwrap_or(&paths.root);
217-
path.starts_with(test_dir) || path.starts_with(script_dir)
206+
utils::RE_TWO_OR_MORE_SPACES.replace_all(&content, "").into_owned()
218207
}
219208

220209
#[cfg(test)]
221210
mod tests {
222211
use super::*;
223-
use std::path::PathBuf;
212+
213+
fn interface_representation(content: &str) -> String {
214+
parse_one_source(content, Path::new(""), |ast| interface_representation_ast(content, &ast))
215+
.unwrap()
216+
}
224217

225218
#[test]
226219
fn test_interface_representation() {
@@ -242,7 +235,7 @@ contract A {
242235
}
243236
}"#;
244237

245-
let result = interface_representation(content, &PathBuf::new()).unwrap();
238+
let result = interface_representation(content);
246239
assert_eq!(
247240
result,
248241
r#"library Lib {function libFn() internal {// logic to keep}}contract A {function a() externalfunction b() publicfunction e() external }"#

crates/compilers/src/resolver/parse.rs

Lines changed: 11 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ use std::{
88

99
/// Represents various information about a Solidity file.
1010
#[derive(Clone, Debug)]
11+
#[non_exhaustive]
1112
pub struct SolData {
1213
pub license: Option<Spanned<String>>,
1314
pub version: Option<Spanned<String>>,
@@ -18,6 +19,7 @@ pub struct SolData {
1819
pub contract_names: Vec<String>,
1920
pub is_yul: bool,
2021
pub parse_result: Result<(), String>,
22+
pub interface_repr_hash: Option<String>,
2123
}
2224

2325
impl SolData {
@@ -49,20 +51,10 @@ impl SolData {
4951
let mut libraries = Vec::new();
5052
let mut contract_names = Vec::new();
5153
let mut parse_result = Ok(());
54+
let mut interface_repr_hash = None;
5255

53-
let sess = solar_parse::interface::Session::builder()
54-
.with_buffer_emitter(Default::default())
55-
.build();
56-
sess.enter(|| {
57-
let arena = ast::Arena::new();
58-
let filename = solar_parse::interface::source_map::FileName::Real(file.to_path_buf());
59-
let Ok(mut parser) =
60-
solar_parse::Parser::from_source_code(&sess, &arena, filename, content.to_string())
61-
else {
62-
return;
63-
};
64-
let Ok(ast) = parser.parse_file().map_err(|e| e.emit()) else { return };
65-
for item in ast.items {
56+
let result = crate::preprocessor::parse_one_source(content, file, |ast| {
57+
for item in ast.items.iter() {
6658
let loc = item.span.lo().to_usize()..item.span.hi().to_usize();
6759
match &item.kind {
6860
ast::ItemKind::Pragma(pragma) => match &pragma.tokens {
@@ -111,9 +103,13 @@ impl SolData {
111103

112104
_ => {}
113105
}
106+
107+
interface_repr_hash = Some(foundry_compilers_artifacts::Source::content_hash_of(
108+
&crate::preprocessor::interface_representation_ast(content, &ast),
109+
));
114110
}
115111
});
116-
if let Err(e) = sess.emitted_errors().unwrap() {
112+
if let Err(e) = result {
117113
let e = e.to_string();
118114
trace!("failed parsing {file:?}: {e}");
119115
parse_result = Err(e);
@@ -157,6 +153,7 @@ impl SolData {
157153
contract_names,
158154
is_yul,
159155
parse_result,
156+
interface_repr_hash,
160157
}
161158
}
162159

0 commit comments

Comments
 (0)