diff --git a/Cargo.lock b/Cargo.lock index 24b5e5952c8..d496a08dd5b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4287,6 +4287,7 @@ dependencies = [ name = "sway-lsp" version = "0.25.2" dependencies = [ + "anyhow", "async-trait", "dashmap", "forc-pkg", @@ -4301,6 +4302,7 @@ dependencies = [ "sway-types", "sway-utils", "swayfmt", + "thiserror", "tokio", "tower", "tower-lsp", diff --git a/sway-lsp/Cargo.toml b/sway-lsp/Cargo.toml index 8d8175b5670..23984b17d50 100644 --- a/sway-lsp/Cargo.toml +++ b/sway-lsp/Cargo.toml @@ -9,6 +9,7 @@ repository = "https://github.com/FuelLabs/sway" description = "LSP server for Sway." [dependencies] +anyhow = "1.0.41" dashmap = "5.4" forc-pkg = { version = "0.25.2", path = "../forc-pkg" } forc-util = { version = "0.25.2", path = "../forc-util" } @@ -20,6 +21,7 @@ sway-error = { version = "0.25.2", path = "../sway-error" } sway-types = { version = "0.25.2", path = "../sway-types" } sway-utils = { version = "0.25.2", path = "../sway-utils" } swayfmt = { version = "0.25.2", path = "../swayfmt" } +thiserror = "1.0.30" tokio = { version = "1.3", features = ["io-std", "io-util", "macros", "net", "rt-multi-thread", "sync", "time"] } tower-lsp = "0.17" tracing = "0.1" diff --git a/sway-lsp/src/core/document.rs b/sway-lsp/src/core/document.rs index a326204796b..76e6e714ba2 100644 --- a/sway-lsp/src/core/document.rs +++ b/sway-lsp/src/core/document.rs @@ -1,6 +1,8 @@ #![allow(dead_code)] use ropey::Rope; -use tower_lsp::lsp_types::{Diagnostic, Position, Range, TextDocumentContentChangeEvent}; +use tower_lsp::lsp_types::{Position, Range, TextDocumentContentChangeEvent}; + +use crate::error::DocumentError; #[derive(Debug)] pub struct TextDocument { @@ -14,15 +16,14 @@ pub struct TextDocument { impl TextDocument { pub fn build_from_path(path: &str) -> Result { - match std::fs::read_to_string(&path) { - Ok(content) => Ok(Self { + std::fs::read_to_string(&path) + .map(|content| Self { language_id: "sway".into(), version: 1, uri: path.into(), content: Rope::from_str(&content), - }), - Err(_) => Err(DocumentError::DocumentNotFound), - } + }) + .map_err(|_| DocumentError::DocumentNotFound { path: path.into() }) } pub fn get_uri(&self) -> &str { @@ -106,9 +107,23 @@ struct EditText<'text> { change_text: &'text str, } -#[derive(Debug)] -pub enum DocumentError { - FailedToParse(Vec), - DocumentNotFound, - DocumentAlreadyStored, +#[cfg(test)] +mod tests { + use crate::test_utils::get_absolute_path; + + use super::*; + + #[test] + fn build_from_path_returns_text_document() { + let path = get_absolute_path("sway-lsp/test/fixtures/cats.txt"); + let result = TextDocument::build_from_path(&path); + assert!(result.is_ok(), "result = {:?}", result); + } + + #[test] + fn build_from_path_returns_document_not_found_error() { + let path = get_absolute_path("not/a/real/file/path"); + let result = TextDocument::build_from_path(&path).expect_err("expected DocumentNotFound"); + assert_eq!(result, DocumentError::DocumentNotFound { path }); + } } diff --git a/sway-lsp/src/core/session.rs b/sway-lsp/src/core/session.rs index 50ae2978106..bda867c9c06 100644 --- a/sway-lsp/src/core/session.rs +++ b/sway-lsp/src/core/session.rs @@ -5,10 +5,11 @@ use crate::{ runnable::{Runnable, RunnableType}, }, core::{ - document::{DocumentError, TextDocument}, + document::TextDocument, token::{Token, TokenMap, TypeDefinition}, {traverse_parse_tree, traverse_typed_tree}, }, + error::{DocumentError, LanguageServerError}, utils, }; use dashmap::DashMap; @@ -122,23 +123,24 @@ impl Session { // Document pub fn store_document(&self, text_document: TextDocument) -> Result<(), DocumentError> { - match self - .documents - .insert(text_document.get_uri().into(), text_document) - { - None => Ok(()), - _ => Err(DocumentError::DocumentAlreadyStored), - } + let uri = text_document.get_uri().to_string(); + self.documents + .insert(uri.clone(), text_document) + .map_or(Ok(()), |_| { + Err(DocumentError::DocumentAlreadyStored { path: uri }) + }) } pub fn remove_document(&self, url: &Url) -> Result { - match self.documents.remove(url.path()) { - Some((_, text_document)) => Ok(text_document), - None => Err(DocumentError::DocumentNotFound), - } + self.documents + .remove(url.path()) + .ok_or(DocumentError::DocumentNotFound { + path: url.path().to_string(), + }) + .map(|(_, text_document)| text_document) } - pub fn parse_project(&self, uri: &Url) -> Result, DocumentError> { + pub fn parse_project(&self, uri: &Url) -> Result, LanguageServerError> { self.token_map.clear(); self.runnables.clear(); @@ -146,108 +148,108 @@ impl Session { let locked = false; let offline = false; - // TODO: match on any errors and report them back to the user in a future PR - if let Ok(manifest) = pkg::PackageManifestFile::from_dir(&manifest_dir) { - if let Ok(plan) = pkg::BuildPlan::from_lock_and_manifest(&manifest, locked, offline) { - //we can then use them directly to convert them to a Vec - if let Ok(CompileResult { - value, - warnings, - errors, - }) = pkg::check(&plan, true) - { - // FIXME(Centril): Refactor parse_ast_to_tokens + parse_ast_to_typed_tokens - // due to the new API. - let (parsed, typed) = match value { - None => (None, None), - Some((pp, tp)) => (Some(pp), tp), - }; - // First, populate our token_map with un-typed ast nodes. - let parsed_res = CompileResult::new(parsed, warnings.clone(), errors.clone()); - let _ = self.parse_ast_to_tokens(parsed_res); - // Next, populate our token_map with typed ast nodes. - let ast_res = CompileResult::new(typed, warnings, errors); - return self.parse_ast_to_typed_tokens(ast_res); - } + let manifest = pkg::PackageManifestFile::from_dir(&manifest_dir).map_err(|_| { + DocumentError::ManifestFileNotFound { + dir: uri.path().into(), } - } - Err(DocumentError::FailedToParse(vec![])) + })?; + + let plan = pkg::BuildPlan::from_lock_and_manifest(&manifest, locked, offline) + .map_err(LanguageServerError::BuildPlanFailed)?; + + // We can convert these destructured elements to a Vec later on. + let CompileResult { + value, + warnings, + errors, + } = pkg::check(&plan, true).map_err(LanguageServerError::FailedToCompile)?; + + // FIXME(Centril): Refactor parse_ast_to_tokens + parse_ast_to_typed_tokens + // due to the new API.g + let (parsed, typed) = match value { + None => (None, None), + Some((pp, tp)) => (Some(pp), tp), + }; + + // First, populate our token_map with un-typed ast nodes. + let parsed_res = CompileResult::new(parsed, warnings.clone(), errors.clone()); + let _ = self.parse_ast_to_tokens(parsed_res); + // Next, populate our token_map with typed ast nodes. + let ast_res = CompileResult::new(typed, warnings, errors); + + self.parse_ast_to_typed_tokens(ast_res) } fn parse_ast_to_tokens( &self, parsed_result: CompileResult, - ) -> Result, DocumentError> { - match parsed_result.value { - None => { - let diagnostics = capabilities::diagnostic::get_diagnostics( - parsed_result.warnings, - parsed_result.errors, - ); - Err(DocumentError::FailedToParse(diagnostics)) - } - Some(parse_program) => { - for node in &parse_program.root.tree.root_nodes { - traverse_parse_tree::traverse_node(node, &self.token_map); - } - - for (_, submodule) in &parse_program.root.submodules { - for node in &submodule.module.tree.root_nodes { - traverse_parse_tree::traverse_node(node, &self.token_map); - } - } - - if let LockResult::Ok(mut program) = self.compiled_program.write() { - program.parsed = Some(parse_program); - } + ) -> Result, LanguageServerError> { + let parse_program = parsed_result.value.ok_or_else(|| { + let diagnostics = capabilities::diagnostic::get_diagnostics( + parsed_result.warnings.clone(), + parsed_result.errors.clone(), + ); + LanguageServerError::FailedToParse { diagnostics } + })?; + + for node in &parse_program.root.tree.root_nodes { + traverse_parse_tree::traverse_node(node, &self.token_map); + } - Ok(capabilities::diagnostic::get_diagnostics( - parsed_result.warnings, - parsed_result.errors, - )) + for (_, submodule) in &parse_program.root.submodules { + for node in &submodule.module.tree.root_nodes { + traverse_parse_tree::traverse_node(node, &self.token_map); } } + + if let LockResult::Ok(mut program) = self.compiled_program.write() { + program.parsed = Some(parse_program); + } + + Ok(capabilities::diagnostic::get_diagnostics( + parsed_result.warnings, + parsed_result.errors, + )) } fn parse_ast_to_typed_tokens( &self, ast_res: CompileResult, - ) -> Result, DocumentError> { - match ast_res.value { - None => Err(DocumentError::FailedToParse( - capabilities::diagnostic::get_diagnostics(ast_res.warnings, ast_res.errors), - )), - Some(typed_program) => { - if let TyProgramKind::Script { - ref main_function, .. - } = typed_program.kind - { - let main_fn_location = - utils::common::get_range_from_span(&main_function.name.span()); - let runnable = Runnable::new(main_fn_location, typed_program.kind.tree_type()); - self.runnables.insert(RunnableType::MainFn, runnable); - } + ) -> Result, LanguageServerError> { + let typed_program = ast_res.value.ok_or(LanguageServerError::FailedToParse { + diagnostics: capabilities::diagnostic::get_diagnostics( + ast_res.warnings.clone(), + ast_res.errors.clone(), + ), + })?; + + if let TyProgramKind::Script { + ref main_function, .. + } = typed_program.kind + { + let main_fn_location = utils::common::get_range_from_span(&main_function.name.span()); + let runnable = Runnable::new(main_fn_location, typed_program.kind.tree_type()); + self.runnables.insert(RunnableType::MainFn, runnable); + } - let root_nodes = typed_program.root.all_nodes.iter(); - let sub_nodes = typed_program - .root - .submodules - .iter() - .flat_map(|(_, submodule)| &submodule.module.all_nodes); - root_nodes - .chain(sub_nodes) - .for_each(|node| traverse_typed_tree::traverse_node(node, &self.token_map)); - - if let LockResult::Ok(mut program) = self.compiled_program.write() { - program.typed = Some(typed_program); - } + let root_nodes = typed_program.root.all_nodes.iter(); + let sub_nodes = typed_program + .root + .submodules + .iter() + .flat_map(|(_, submodule)| &submodule.module.all_nodes); + root_nodes + .chain(sub_nodes) + .for_each(|node| traverse_typed_tree::traverse_node(node, &self.token_map)); - Ok(capabilities::diagnostic::get_diagnostics( - ast_res.warnings, - ast_res.errors, - )) - } + if let LockResult::Ok(mut program) = self.compiled_program.write() { + program.typed = Some(typed_program); } + + Ok(capabilities::diagnostic::get_diagnostics( + ast_res.warnings, + ast_res.errors, + )) } pub fn contains_sway_file(&self, url: &Url) -> bool { @@ -328,3 +330,48 @@ impl Session { } } } + +#[cfg(test)] +mod tests { + + use crate::test_utils::{get_absolute_path, get_url}; + + use super::*; + + #[test] + fn store_document_returns_empty_tuple() { + let session = Session::new(); + let path = get_absolute_path("sway-lsp/test/fixtures/cats.txt"); + let document = TextDocument::build_from_path(&path).unwrap(); + let result = Session::store_document(&session, document); + assert!(result.is_ok()); + } + + #[test] + fn store_document_returns_document_already_stored_error() { + let session = Session::new(); + let path = get_absolute_path("sway-lsp/test/fixtures/cats.txt"); + let document = TextDocument::build_from_path(&path).unwrap(); + Session::store_document(&session, document).expect("expected successfully stored"); + let document = TextDocument::build_from_path(&path).unwrap(); + let result = Session::store_document(&session, document) + .expect_err("expected DocumentAlreadyStored"); + assert_eq!(result, DocumentError::DocumentAlreadyStored { path }); + } + + #[test] + fn parse_project_returns_manifest_file_not_found() { + let session = Session::new(); + let dir = get_absolute_path("sway-lsp/test/fixtures"); + let uri = get_url(&dir); + let result = + Session::parse_project(&session, &uri).expect_err("expected ManifestFileNotFound"); + assert!(matches!( + result, + LanguageServerError::DocumentError( + DocumentError::ManifestFileNotFound { dir: test_dir } + ) + if test_dir == dir + )); + } +} diff --git a/sway-lsp/src/error.rs b/sway-lsp/src/error.rs new file mode 100644 index 00000000000..c008aa2450d --- /dev/null +++ b/sway-lsp/src/error.rs @@ -0,0 +1,25 @@ +use thiserror::Error; +use tower_lsp::lsp_types::Diagnostic; + +#[derive(Debug, Error)] +pub enum LanguageServerError { + #[error(transparent)] + DocumentError(#[from] DocumentError), + + #[error("Failed to create build plan. {0}")] + BuildPlanFailed(anyhow::Error), + #[error("Failed to compile. {0}")] + FailedToCompile(anyhow::Error), + #[error("Failed to parse document")] + FailedToParse { diagnostics: Vec }, +} + +#[derive(Debug, Error, PartialEq, Eq)] +pub enum DocumentError { + #[error("No document found at {:?}", path)] + DocumentNotFound { path: String }, + #[error("Missing Forc.toml in {:?}", dir)] + ManifestFileNotFound { dir: String }, + #[error("Document is already stored at {:?}", path)] + DocumentAlreadyStored { path: String }, +} diff --git a/sway-lsp/src/lib.rs b/sway-lsp/src/lib.rs index 5a71499742d..18092e2d87c 100644 --- a/sway-lsp/src/lib.rs +++ b/sway-lsp/src/lib.rs @@ -2,7 +2,10 @@ use tower_lsp::{LspService, Server}; mod capabilities; mod core; +pub mod error; mod server; +#[cfg(test)] +pub mod test_utils; pub mod utils; use server::Backend; use utils::debug::DebugFlags; diff --git a/sway-lsp/src/server.rs b/sway-lsp/src/server.rs index 0d0b8bc6e55..46f37a5ec0a 100644 --- a/sway-lsp/src/server.rs +++ b/sway-lsp/src/server.rs @@ -1,8 +1,7 @@ use crate::capabilities; -use crate::core::{ - document::{DocumentError, TextDocument}, - session::Session, -}; +use crate::core::{document::TextDocument, session::Session}; +pub use crate::error::DocumentError; +use crate::error::LanguageServerError; use crate::utils::debug::{self, DebugFlags}; use forc_util::find_manifest_dir; use serde::{Deserialize, Serialize}; @@ -33,6 +32,10 @@ impl Backend { self.client.log_message(MessageType::INFO, message).await; } + async fn log_error_message(&self, message: &str) { + self.client.log_message(MessageType::ERROR, message).await; + } + async fn parse_and_store_sway_files(&self) -> Result<(), DocumentError> { if let Some(path) = find_manifest_dir(&std::env::current_dir().unwrap()) { // Store the documents. @@ -49,7 +52,8 @@ impl Backend { let diagnostics = match self.session.parse_project(uri) { Ok(diagnostics) => diagnostics, Err(err) => { - if let DocumentError::FailedToParse(diagnostics) = err { + self.log_error_message(err.to_string().as_str()).await; + if let LanguageServerError::FailedToParse { diagnostics } = err { diagnostics } else { vec![] @@ -365,9 +369,11 @@ impl Backend { #[cfg(test)] mod tests { use serde_json::json; - use std::{borrow::Cow, env, fs, io::Read, path::PathBuf}; + use std::{borrow::Cow, fs, io::Read, path::PathBuf}; use tower::{Service, ServiceExt}; + use crate::test_utils::sway_workspace_dir; + use super::*; use serial_test::serial; use tower_lsp::{ @@ -375,10 +381,6 @@ mod tests { ExitedError, LspService, }; - fn sway_workspace_dir() -> PathBuf { - env::current_dir().unwrap().parent().unwrap().to_path_buf() - } - fn e2e_language_dir() -> PathBuf { PathBuf::from("test/src/e2e_vm_tests/test_programs/should_pass/language") } diff --git a/sway-lsp/src/test_utils.rs b/sway-lsp/src/test_utils.rs new file mode 100644 index 00000000000..6156c618d90 --- /dev/null +++ b/sway-lsp/src/test_utils.rs @@ -0,0 +1,15 @@ +use std::{env, path::PathBuf}; + +use tower_lsp::lsp_types::Url; + +pub fn sway_workspace_dir() -> PathBuf { + env::current_dir().unwrap().parent().unwrap().to_path_buf() +} + +pub fn get_absolute_path(path: &str) -> String { + sway_workspace_dir().join(path).to_str().unwrap().into() +} + +pub fn get_url(absolute_path: &str) -> Url { + Url::parse(&format!("file://{}", &absolute_path)).expect("expected URL") +} diff --git a/sway-lsp/test/fixtures/cats.txt b/sway-lsp/test/fixtures/cats.txt new file mode 100644 index 00000000000..a2768f6c61f --- /dev/null +++ b/sway-lsp/test/fixtures/cats.txt @@ -0,0 +1 @@ +boots \ No newline at end of file