diff --git a/toolchains/solidity/core/crates/foundry-compiler-server/src/affected_files_store.rs b/toolchains/solidity/core/crates/foundry-compiler-server/src/affected_files_store.rs index 79e20c6e..01589901 100644 --- a/toolchains/solidity/core/crates/foundry-compiler-server/src/affected_files_store.rs +++ b/toolchains/solidity/core/crates/foundry-compiler-server/src/affected_files_store.rs @@ -13,17 +13,31 @@ impl AffectedFilesStore { } pub fn add_project_file(&mut self, project_path: String, file: String) { - if !self.projects_files.contains_key(&project_path) { - self.projects_files.insert(project_path.clone(), vec![]); - } else { - let files = self.projects_files.get_mut(&project_path).unwrap(); + if let Some(files) = self.projects_files.get_mut(&project_path) { if !files.contains(&file) { files.push(file); } + } else { + self.projects_files.insert(project_path, vec![file]); } } - pub fn get_affected_files(&self, project_path: &str) -> Vec { - self.projects_files.get(project_path).unwrap().clone() + /** + * This function returns the list of files that previously raised an error and are not raising it anymore. + * It also updates the list of files that are raising an error. + * @param {Vec} raised_files List of files that are raising an error + * @param {String} project_path Project path + * @returns {Vec} List of files that are not raising an error anymore + */ + pub fn fill_affected_files(&mut self, raised_files: Vec, project_path: &str) -> Vec { + let mut affected_files = Vec::new(); + if let Some(project_files) = self.projects_files.get_mut(project_path) { + project_files.retain(|file| !raised_files.contains(&file)); + affected_files = project_files.clone(); + project_files.extend(raised_files); + } else { + self.projects_files.insert(project_path.to_string(), raised_files); + } + affected_files } } diff --git a/toolchains/solidity/core/crates/foundry-compiler-server/src/main.rs b/toolchains/solidity/core/crates/foundry-compiler-server/src/main.rs index 60db0d72..f447639a 100644 --- a/toolchains/solidity/core/crates/foundry-compiler-server/src/main.rs +++ b/toolchains/solidity/core/crates/foundry-compiler-server/src/main.rs @@ -7,7 +7,7 @@ use tower_lsp::jsonrpc::Result; use tower_lsp::lsp_types::*; use tower_lsp::{Client, LanguageServer, LspService, Server}; mod utils; -use utils::{convert_severity, get_root_path, normalize_path, slashify_path}; +use utils::{convert_severity, get_root_path, slashify_path, normalized_slash_path}; mod affected_files_store; use affected_files_store::AffectedFilesStore; @@ -30,18 +30,17 @@ impl LanguageServer for Backend { self.client .log_message(MessageType::INFO, "Foundry server initializing!") .await; - let opt_path = get_root_path(params.clone()); - if let Some(path) = opt_path { + if let Some(root_path) = get_root_path(params.clone()) { self.client .log_message( MessageType::INFO, &format!( "Foundry server initializing with workspace path: {:?}", - path + root_path ), ) .await; - self.load_workspace(normalize_path(path.as_str())).await; + let _ = self.load_workspace(root_path).await; } else { self.client .log_message( @@ -63,7 +62,7 @@ impl LanguageServer for Backend { async fn initialized(&self, _: InitializedParams) { self.client - .log_message(MessageType::INFO, "foundryserver initialized!") + .log_message(MessageType::INFO, "Foundry server initialized!") .await; } @@ -74,10 +73,7 @@ impl LanguageServer for Backend { format!("file opened!: {:}", params.text_document.uri), ) .await; - if params.text_document.uri.path().contains("forge-std") { - return; - } - self.compile(slashify_path(&normalize_path(params.text_document.uri.path()))) + let _ = self.compile(normalized_slash_path(params.text_document.uri.path())) .await; } @@ -88,7 +84,7 @@ impl LanguageServer for Backend { format!("file changed!: {:}", params.text_document.uri), ) .await; - self.compile(slashify_path(&normalize_path(params.text_document.uri.path()))) + let _ = self.compile(normalized_slash_path(params.text_document.uri.path())) .await; } @@ -98,7 +94,7 @@ impl LanguageServer for Backend { } impl Backend { - pub async fn load_workspace(&self, path: String) { + pub async fn load_workspace(&self, path: String) -> std::result::Result<(), ()> { let mut state = self.state.lock().await; match Compiler::new_with_executable_check() { Ok(compiler) => state.compiler = Some(compiler), @@ -106,13 +102,13 @@ impl Backend { self.client .show_message(MessageType::WARNING, "Foundry executable not found. Please install foundry and restart the extension.") .await; - return; + return Err(()); } Err(Error::InvalidFoundryVersion) => { self.client .show_message(MessageType::WARNING, "Foundry executable version is not compatible with this extension. Please update foundry and restart the extension.") .await; - return; + return Err(()); } Err(err) => { self.client @@ -121,7 +117,7 @@ impl Backend { &format!("Foundry server failed to initialize: {:?}", err), ) .await; - return; + return Err(()); } } if let Err(err) = state.compiler.as_mut().unwrap().load_workspace(path) { @@ -131,37 +127,50 @@ impl Backend { &format!("Foundry server failed to initialize: {:?}", err), ) .await; + return Err(()); } else { state.initialized = true; self.client .log_message(MessageType::INFO, "Foundry server initialized!") .await; } - drop(state); + Ok(()) } - pub async fn compile(&self, filepath: String) { - let mut state = self.state.lock().await; + /** + * This function initializes the workspace if it is not already initialized. + * @param {&str} filepath Filepath to compile + * @returns {Result<(), ()>} Result of the initialization + */ + async fn initialize_if_not(&self, filepath: &str) -> std::result::Result<(), ()> { + let state = self.state.lock().await; + if !state.initialized { - // unlock the mutex before calling load_workspace - drop(state); + drop(state); // unlock the mutex before calling load_workspace + self.client .log_message(MessageType::INFO, "Foundry server initializing!") .await; - let folder_path = Path::new(&filepath) + let folder_path = Path::new(filepath) .parent() .unwrap() .to_str() .unwrap() .to_string(); - self.load_workspace(folder_path).await; - state = self.state.lock().await; - } + self.load_workspace(folder_path).await? + } + Ok(()) + } + + pub async fn compile(&self, filepath: String) -> std::result::Result<(), ()> { + self.initialize_if_not(&filepath).await?; + let mut state = self.state.lock().await; + self.client .log_message(MessageType::INFO, "Foundry server compiling!") .await; - let output = state.compiler.as_mut().unwrap().compile(&filepath); - match output { + + match state.compiler.as_mut().unwrap().compile(&filepath) { Ok((project_path, output)) => { /*self.client .log_message(MessageType::INFO, format!("Compile errors: {:?}", output.get_errors())) @@ -179,66 +188,99 @@ impl Backend { .await; } } + Ok(()) } + /** + * Generate and publish diagnostics from compilation errors + * @param {String} project_path Project path + * @param {String} filepath Filepath to compile + * @param {ProjectCompileOutput} output Compilation output + */ pub async fn publish_errors_diagnostics( &self, project_path: String, filepath: String, output: ProjectCompileOutput, ) { - let mut diagnostics = HashMap::>::new(); + let mut raised_diagnostics = HashMap::>::new(); + for error in output.get_errors() { - eprintln!("error: {:?}", error); - let (source_content_filepath, range) = - match self.extract_diagnostic_range(&project_path, error).await { - Some((source_content_filepath, range)) => (source_content_filepath, range), - None => continue, - }; - let diagnostic = Diagnostic { - range: Range { - start: Position { - line: range.start.line, - character: range.start.column, - }, - end: Position { - line: range.end.line, - character: range.end.column, - }, - }, - severity: Some(convert_severity(error.get_severity())), - code: None, - code_description: None, - source: Some("osmium-solidity-foundry-compiler".to_string()), - message: error.get_message(), - related_information: None, - tags: None, - data: None, + // Generate diagnostic from compilation error + let (affected_file, diagnostic) = match self.extract_diagnostic(&error, &project_path).await { + Some(diagnostic) => diagnostic, + None => continue, }; - let url = match source_content_filepath.to_str() { + + // Add diagnostic to the hashmap + let url = match affected_file.to_str() { Some(source_path) => slashify_path(source_path), None => continue, }; - if !diagnostics.contains_key(&url) { - diagnostics.insert(url.clone(), vec![diagnostic]); + if !raised_diagnostics.contains_key(&url) { + raised_diagnostics.insert(url.clone(), vec![diagnostic]); } else { - diagnostics.get_mut(&url).unwrap().push(diagnostic); + raised_diagnostics.get_mut(&url).unwrap().push(diagnostic); } } - self.add_not_affected_files(project_path, filepath, &mut diagnostics) - .await; - for (uri, diags) in diagnostics.iter() { + self.reset_not_affected_files(project_path, filepath, &raised_diagnostics).await; + for (uri, diags) in raised_diagnostics.iter() { if let Ok(url) = Url::parse(&format!("file://{}", &uri)) { self.client .publish_diagnostics(url, diags.clone(), None) .await; } else { - self.client.log_message(MessageType::ERROR, "error, cannot parse file uri").await; + self.client.log_message(MessageType::ERROR, format!("error, cannot parse file uri : {}", uri)).await; } } } + /** + * Extract diagnostic from compilation error + * @param {CompilationError} compilation_error Compilation error + * @param {String} project_path Project path + * @returns {Option<(PathBuf, Diagnostic)>} Diagnostic + * @returns {None} If the diagnostic cannot be extracted + */ + async fn extract_diagnostic(&self, compilation_error: &CompilationError, project_path: &str) -> Option<(PathBuf, Diagnostic)> { + eprintln!("Compilation error: {:?}", compilation_error); + let (source_content_filepath, range) = + match self.extract_diagnostic_range(&project_path, compilation_error).await { + Some((source_content_filepath, range)) => (source_content_filepath, range), + None => return None, + }; + let diagnostic = Diagnostic { + range: Range { + start: Position { + line: range.start.line, + character: range.start.column, + }, + end: Position { + line: range.end.line, + character: range.end.column, + }, + }, + severity: Some(convert_severity(compilation_error.get_severity())), + code: None, + code_description: None, + source: Some("osmium-solidity-foundry-compiler".to_string()), + message: compilation_error.get_message(), + related_information: None, + tags: None, + data: None, + }; + Some((source_content_filepath, diagnostic)) + } + + /** + * Extract diagnostic range from compilation error's source location + * Open the file and get the range from the source location + * @param {String} project_path Project path + * @param {CompilationError} error Compilation error + * @returns {Option<(PathBuf, osmium_libs_foundry_wrapper::Range)>} Diagnostic range + * @returns {None} If the diagnostic range cannot be extracted + */ async fn extract_diagnostic_range( &self, project_path: &str, @@ -251,9 +293,9 @@ impl Backend { complete_path } None => { - /*self.client + self.client .log_message(MessageType::ERROR, format!("error, cannot get filepath: {:?}", error)) - .await;*/ + .await; return None; } }; @@ -287,29 +329,25 @@ impl Backend { Some((source_content_filepath, range)) } - async fn add_not_affected_files( + /** + * This function resets the diagnostics of the files that are not raising an error anymore. + * @param {String} project_path Project path + * @param {String} filepath Filepath to compile + * @param {HashMap>} raised_diagnostics Raised diagnostics + */ + async fn reset_not_affected_files( &self, project_path: String, filepath: String, - raised_diagnostics: &mut HashMap>, + raised_diagnostics: &HashMap>, ) { let mut state = self.state.lock().await; state .affected_files .add_project_file(project_path.clone(), filepath.clone()); - - let affected_files = state.affected_files.get_affected_files(&project_path); - drop(state); - let mut without_diagnostics = vec![]; - - for file in affected_files { - if !raised_diagnostics.contains_key(&file) { // if not potential not affected file is not in raised diags - if let std::collections::hash_map::Entry::Vacant(e) = files.entry(url) { - raised_diagnostics.insert(file.clone(), vec![]); - without_diagnostics.push(file); - } - } + let raised_files = raised_diagnostics.keys().cloned().collect::>(); + let without_diagnostics = state.affected_files.fill_affected_files(raised_files, &project_path); self.client .log_message( @@ -317,6 +355,18 @@ impl Backend { format!("files without diagnostic: {:?}", without_diagnostics), ) .await; + + for file in without_diagnostics.iter() { + if let Ok(url) = Url::parse(&format!("file://{}", &file)) { + self.client + .publish_diagnostics(url, vec![], None) + .await; + } else { + self.client.log_message(MessageType::ERROR, format!("error, cannot parse file uri : {}", file)).await; + } + } + + } } diff --git a/toolchains/solidity/core/crates/foundry-compiler-server/src/utils.rs b/toolchains/solidity/core/crates/foundry-compiler-server/src/utils.rs index 75a8a91b..d4be612b 100644 --- a/toolchains/solidity/core/crates/foundry-compiler-server/src/utils.rs +++ b/toolchains/solidity/core/crates/foundry-compiler-server/src/utils.rs @@ -1,11 +1,16 @@ use osmium_libs_foundry_wrapper::Severity; use tower_lsp::lsp_types::{DiagnosticSeverity, InitializeParams}; +/** + * This function returns the first workspace path from the InitializeParams. + * If there is no workspace path, it returns the root path. + * @returns {Option} Normalized path + */ pub fn get_root_path(params: InitializeParams) -> Option { - if let Some(root_uri) = params.root_uri { - return Some(root_uri.path().to_string()); - } else if let Some(folder) = params.workspace_folders?.get(0) { - return Some(folder.uri.path().to_string()); + if let Some(folder) = params.workspace_folders?.get(0) { + return Some(normalize_path(folder.uri.path())); + } else if let Some(root_uri) = params.root_uri { + return Some(normalize_path(root_uri.path())); } None } @@ -18,6 +23,12 @@ pub fn convert_severity(severity: Severity) -> DiagnosticSeverity { } } +/** + * This function normalizes the path for Windows. + * VSCode send the path starting with /%3A/ instead of {letter}:/ + * @param {&str} path Path to normalize + * @returns {String} Normalized path + */ #[cfg(target_family = "windows")] pub fn normalize_path(path: &str) -> String { let mut path = path.replace("%3A/", "://"); @@ -25,11 +36,26 @@ pub fn normalize_path(path: &str) -> String { path.to_string() } +/** + * This function normalizes the path for Linux and MacOS. Nothing to do. + * @param {&str} path Path to normalize + * @returns {String} Normalized path + */ #[cfg(not(target_family = "windows"))] pub fn normalize_path(path: &str) -> String { path.to_string() } +/** + * This function replaces all backslashes and double-slahes with slashes. + * This is useful for Windows paths. + * @param {&str} path Path to slashify + * @returns {String} Slashified path + */ pub fn slashify_path(path: &str) -> String { path.replace("\\", "/").replace("\\\\", "/").replace("//", "/") +} + +pub fn normalized_slash_path(path: &str) -> String { + slashify_path(&normalize_path(path)) } \ No newline at end of file diff --git a/toolchains/solidity/extension/src/extension.ts b/toolchains/solidity/extension/src/extension.ts index e74c904d..9d60596e 100644 --- a/toolchains/solidity/extension/src/extension.ts +++ b/toolchains/solidity/extension/src/extension.ts @@ -27,7 +27,8 @@ export async function activate(context: ExtensionContext) { if (folders) { const files = await workspace.findFiles('**/*.sol', `${folders[0].uri.fsPath}/**`); files.forEach(file => { - workspace.openTextDocument(file); + if (!file.path.includes('forge-std')) + workspace.openTextDocument(file); }); }