diff --git a/crates/cli/src/opts/build/mod.rs b/crates/cli/src/opts/build/mod.rs index 8eeea817a8743..e286f65101907 100644 --- a/crates/cli/src/opts/build/mod.rs +++ b/crates/cli/src/opts/build/mod.rs @@ -9,7 +9,7 @@ mod paths; pub use self::paths::ProjectPathOpts; mod utils; -pub use self::utils::{configure_pcx, configure_pcx_from_compile_output, configure_pcx_from_solc}; +pub use self::utils::*; // A set of solc compiler settings that can be set via command line arguments, which are intended // to be merged into an existing `foundry_config::Config`. diff --git a/crates/cli/src/opts/build/utils.rs b/crates/cli/src/opts/build/utils.rs index 68d863f36b3c8..43165d1532318 100644 --- a/crates/cli/src/opts/build/utils.rs +++ b/crates/cli/src/opts/build/utils.rs @@ -7,12 +7,14 @@ use foundry_compilers::{ }; use foundry_config::{Config, semver::Version}; use rayon::prelude::*; -use solar::sema::ParsingContext; +use solar::{interface::MIN_SOLIDITY_VERSION as MSV, sema::ParsingContext}; use std::{ collections::{HashSet, VecDeque}, path::{Path, PathBuf}, }; +const MIN_SUPPORTED_VERSION: Version = Version::new(MSV.0, MSV.1, MSV.2); + /// Configures a [`ParsingContext`] from [`Config`]. /// /// - Configures include paths, remappings @@ -49,7 +51,7 @@ pub fn configure_pcx( // Only process sources with latest Solidity version to avoid conflicts. let graph = Graph::::resolve_sources(&project.paths, sources)?; - let (version, sources, _) = graph + let (version, sources) = graph // Resolve graph into mapping language -> version -> sources .into_sources_by_version(project)? .sources @@ -59,9 +61,15 @@ pub fn configure_pcx( .ok_or_else(|| eyre::eyre!("no Solidity sources"))? .1 .into_iter() + // Filter unsupported versions + .filter(|(v, _, _)| v >= &MIN_SUPPORTED_VERSION) // Always pick the latest version .max_by(|(v1, _, _), (v2, _, _)| v1.cmp(v2)) - .unwrap(); + .map_or((MIN_SUPPORTED_VERSION, Sources::default()), |(v, s, _)| (v, s)); + + if sources.is_empty() { + sh_warn!("no files found. Solar doesn't support Solidity versions prior to 0.8.0")?; + } let solc = SolcVersionedInput::build( sources, @@ -75,18 +83,17 @@ pub fn configure_pcx( Ok(()) } -/// Configures a [`ParsingContext`] from a [`ProjectCompileOutput`] and [`SolcVersionedInput`]. +/// Extracts Solar-compatible sources from a [`ProjectCompileOutput`]. /// /// # Note: /// uses `output.graph().source_files()` and `output.artifact_ids()` rather than `output.sources()` /// because sources aren't populated when build is skipped when there are no changes in the source /// code. -pub fn configure_pcx_from_compile_output( - pcx: &mut ParsingContext<'_>, +pub fn get_solar_sources_from_compile_output( config: &Config, output: &ProjectCompileOutput, target_paths: Option<&[PathBuf]>, -) -> Result<()> { +) -> Result { let is_solidity_file = |path: &Path| -> bool { path.extension().and_then(|s| s.to_str()).is_some_and(|ext| SOLC_EXTENSIONS.contains(&ext)) }; @@ -125,12 +132,14 @@ pub fn configure_pcx_from_compile_output( // Read all sources and find the latest version. let (version, sources) = { - let (mut max_version, mut sources) = (Version::new(0, 0, 0), Sources::new()); + let (mut max_version, mut sources) = (MIN_SUPPORTED_VERSION, Sources::new()); for (id, _) in output.artifact_ids() { if let Ok(path) = dunce::canonicalize(&id.source) && source_paths.remove(&path) { - if id.version > max_version { + if id.version < MIN_SUPPORTED_VERSION { + continue; + } else if max_version < id.version { max_version = id.version; }; @@ -149,6 +158,17 @@ pub fn configure_pcx_from_compile_output( version, ); + Ok(solc) +} + +/// Configures a [`ParsingContext`] from a [`ProjectCompileOutput`]. +pub fn configure_pcx_from_compile_output( + pcx: &mut ParsingContext<'_>, + config: &Config, + output: &ProjectCompileOutput, + target_paths: Option<&[PathBuf]>, +) -> Result<()> { + let solc = get_solar_sources_from_compile_output(config, output, target_paths)?; configure_pcx_from_solc(pcx, &config.project_paths(), &solc, true); Ok(()) } diff --git a/crates/forge/src/cmd/build.rs b/crates/forge/src/cmd/build.rs index 54bb43a879436..d37c73fd68cbb 100644 --- a/crates/forge/src/cmd/build.rs +++ b/crates/forge/src/cmd/build.rs @@ -3,7 +3,7 @@ use clap::Parser; use eyre::{Context, Result}; use forge_lint::{linter::Linter, sol::SolidityLinter}; use foundry_cli::{ - opts::BuildOpts, + opts::{BuildOpts, configure_pcx_from_solc, get_solar_sources_from_compile_output}, utils::{LoadConfig, cache_local_signatures}, }; use foundry_common::{compile::ProjectCompiler, shell}; @@ -174,10 +174,31 @@ impl BuildArgs { }) .collect::>(); - if !input_files.is_empty() { - let compiler = output.parser_mut().solc_mut().compiler_mut(); - linter.lint(&input_files, config.deny, compiler)?; + let solar_sources = + get_solar_sources_from_compile_output(config, output, Some(&input_files))?; + if solar_sources.input.sources.is_empty() { + if !input_files.is_empty() { + sh_warn!( + "unable to lint. Solar only supports Solidity versions prior to 0.8.0" + )?; + } + return Ok(()); } + + // NOTE(rusowsky): Once solar can drop unsupported versions, rather than creating a new + // compiler, we should reuse the parser from the project output. + let mut compiler = solar::sema::Compiler::new( + solar::interface::Session::builder().with_stderr_emitter().build(), + ); + + // Load the solar-compatible sources to the pcx before linting + compiler.enter_mut(|compiler| { + let mut pcx = compiler.parse(); + configure_pcx_from_solc(&mut pcx, &config.project_paths(), &solar_sources, true); + pcx.set_resolve_imports(true); + pcx.parse(); + }); + linter.lint(&input_files, config.deny, &mut compiler)?; } Ok(()) diff --git a/crates/forge/src/cmd/lint.rs b/crates/forge/src/cmd/lint.rs index e21c2c0a92d5f..ae4b8389a8ecc 100644 --- a/crates/forge/src/cmd/lint.rs +++ b/crates/forge/src/cmd/lint.rs @@ -5,7 +5,7 @@ use forge_lint::{ sol::{SolLint, SolLintError, SolidityLinter}, }; use foundry_cli::{ - opts::BuildOpts, + opts::{BuildOpts, configure_pcx_from_solc, get_solar_sources_from_compile_output}, utils::{FoundryPathExt, LoadConfig}, }; use foundry_common::{compile::ProjectCompiler, shell}; @@ -69,7 +69,7 @@ impl LintArgs { } else if path.is_sol() { inputs.push(path.to_path_buf()); } else { - warn!("Cannot process path {}", path.display()); + warn!("cannot process path {}", path.display()); } } inputs @@ -77,7 +77,7 @@ impl LintArgs { }; if input.is_empty() { - sh_println!("Nothing to lint")?; + sh_println!("nothing to lint")?; return Ok(()); } @@ -95,7 +95,7 @@ impl LintArgs { let severity = self.severity.unwrap_or(config.lint.severity.clone()); if project.compiler.solc.is_none() { - return Err(eyre!("Linting not supported for this language")); + return Err(eyre!("linting not supported for this language")); } let linter = SolidityLinter::new(path_config) @@ -106,9 +106,28 @@ impl LintArgs { .with_severity(if severity.is_empty() { None } else { Some(severity) }) .with_mixed_case_exceptions(&config.lint.mixed_case_exceptions); - let mut output = ProjectCompiler::new().files(input.iter().cloned()).compile(&project)?; - let compiler = output.parser_mut().solc_mut().compiler_mut(); - linter.lint(&input, config.deny, compiler)?; + let output = ProjectCompiler::new().files(input.iter().cloned()).compile(&project)?; + let solar_sources = get_solar_sources_from_compile_output(&config, &output, Some(&input))?; + if solar_sources.input.sources.is_empty() { + return Err(eyre!( + "unable to lint. Solar only supports Solidity versions prior to 0.8.0" + )); + } + + // NOTE(rusowsky): Once solar can drop unsupported versions, rather than creating a new + // compiler, we should reuse the parser from the project output. + let mut compiler = solar::sema::Compiler::new( + solar::interface::Session::builder().with_stderr_emitter().build(), + ); + + // Load the solar-compatible sources to the pcx before linting + compiler.enter_mut(|compiler| { + let mut pcx = compiler.parse(); + pcx.set_resolve_imports(true); + configure_pcx_from_solc(&mut pcx, &config.project_paths(), &solar_sources, true); + pcx.parse(); + }); + linter.lint(&input, config.deny, &mut compiler)?; Ok(()) } diff --git a/crates/forge/src/multi_runner.rs b/crates/forge/src/multi_runner.rs index 749151404959e..b79a3ac9cf297 100644 --- a/crates/forge/src/multi_runner.rs +++ b/crates/forge/src/multi_runner.rs @@ -575,10 +575,7 @@ impl MultiContractRunnerBuilder { if files.is_empty() { None } else { Some(&files) }, )?; pcx.parse(); - // Check if any sources exist, to avoid logging `error: no files found` - if !compiler.sess().source_map().is_empty() { - let _ = compiler.lower_asts(); - } + let _ = compiler.lower_asts(); Ok(()) })?; diff --git a/crates/forge/tests/cli/lint.rs b/crates/forge/tests/cli/lint.rs index fb3285491039b..da766cf099173 100644 --- a/crates/forge/tests/cli/lint.rs +++ b/crates/forge/tests/cli/lint.rs @@ -48,6 +48,10 @@ import { ContractWithLints } from "./ContractWithLints.sol"; import { _PascalCaseInfo } from "./ContractWithLints.sol"; import "./ContractWithLints.sol"; + +contract Dummy { + bool foo; +} "#; const COUNTER_A: &str = r#" @@ -778,3 +782,42 @@ fn ensure_no_privileged_lint_id() { assert_ne!(lint.id(), "all", "lint-id 'all' is reserved. Please use a different id"); } } + +forgetest!(skips_linting_for_old_solidity_versions, |prj, cmd| { + const OLD_CONTRACT: &str = r#" +// SPDX-License-Identifier: MIT +pragma solidity ^0.7.0; + +contract OldContract { + uint256 VARIABLE_MIXED_CASE_INFO; + + function FUNCTION_MIXED_CASE_INFO() public {} +} +"#; + + // Add a contract with Solidity 0.7.x which has lint issues + prj.add_source("OldContract", OLD_CONTRACT); + prj.update_config(|config| { + config.lint = LinterConfig { + severity: vec![], + exclude_lints: vec![], + ignore: vec![], + lint_on_build: true, + ..Default::default() + }; + }); + + // Run forge build - should SUCCEED without linting + cmd.arg("build").assert_success().stderr_eq(str![[ + r#"Warning: unable to lint. Solar only supports Solidity versions prior to 0.8.0 + +"# + ]]); + + // Run forge lint - should FAIL + cmd.forge_fuse().arg("lint").assert_failure().stderr_eq(str![[ + r#"Error: unable to lint. Solar only supports Solidity versions prior to 0.8.0 + +"# + ]]); +}); diff --git a/crates/lint/src/sol/mod.rs b/crates/lint/src/sol/mod.rs index 974fb6f4715ab..ed72b0724a3a4 100644 --- a/crates/lint/src/sol/mod.rs +++ b/crates/lint/src/sol/mod.rs @@ -8,6 +8,7 @@ use foundry_common::{ inline_config::{InlineConfig, InlineConfigItem}, }, errors::convert_solar_errors, + sh_warn, }; use foundry_compilers::{ProjectPathsConfig, solc::SolcLanguage}; use foundry_config::{DenyLevel, lint::Severity}; @@ -258,7 +259,10 @@ impl<'a> Linter for SolidityLinter<'a> { input.par_iter().for_each(|path| { let path = &self.path_config.root.join(path); let Some((_, ast_source)) = gcx.get_ast_source(path) else { - panic!("AST source not found for {}", path.display()); + // issue a warning rather than panicking, in case that some (but not all) of the + // input files have old solidity versions which are not supported by solar. + _ = sh_warn!("AST source not found for {}", path.display()); + return; }; let Some(ast) = &ast_source.ast else { panic!("AST missing for {}", path.display());