diff --git a/cli/lsp/analysis.rs b/cli/lsp/analysis.rs index 0c3112ca8d..b79a9ec654 100644 --- a/cli/lsp/analysis.rs +++ b/cli/lsp/analysis.rs @@ -16,6 +16,7 @@ use deno_core::error::AnyError; use deno_core::serde::Deserialize; use deno_core::serde::Serialize; use deno_core::serde_json::json; +use deno_core::ModuleResolutionError; use deno_core::ModuleSpecifier; use deno_lint::rules; use lspower::lsp; @@ -23,6 +24,7 @@ use lspower::lsp::Position; use lspower::lsp::Range; use std::cmp::Ordering; use std::collections::HashMap; +use std::fmt; use std::rc::Rc; lazy_static! { @@ -142,10 +144,33 @@ pub struct Dependency { pub maybe_type: Option, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ResolvedDependencyErr { + InvalidDowngrade, + InvalidLocalImport, + InvalidSpecifier(ModuleResolutionError), + Missing, +} + +impl fmt::Display for ResolvedDependencyErr { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::InvalidDowngrade => { + write!(f, "HTTPS modules cannot import HTTP modules.") + } + Self::InvalidLocalImport => { + write!(f, "Remote modules cannot import local modules.") + } + Self::InvalidSpecifier(err) => write!(f, "{}", err), + Self::Missing => write!(f, "The module is unexpectedly missing."), + } + } +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum ResolvedDependency { Resolved(ModuleSpecifier), - Err(String), + Err(ResolvedDependencyErr), } pub fn resolve_import( @@ -170,22 +195,23 @@ pub fn resolve_import( } else { match ModuleSpecifier::resolve_import(specifier, referrer.as_str()) { Ok(resolved) => resolved, - Err(err) => return ResolvedDependency::Err(err.to_string()), + Err(err) => { + return ResolvedDependency::Err( + ResolvedDependencyErr::InvalidSpecifier(err), + ) + } } }; let referrer_scheme = referrer.as_url().scheme(); let specifier_scheme = specifier.as_url().scheme(); if referrer_scheme == "https" && specifier_scheme == "http" { - return ResolvedDependency::Err( - "Modules imported via https are not allowed to import http modules." - .to_string(), - ); + return ResolvedDependency::Err(ResolvedDependencyErr::InvalidDowngrade); } if (referrer_scheme == "https" || referrer_scheme == "http") && !(specifier_scheme == "https" || specifier_scheme == "http") && !remapped { - return ResolvedDependency::Err("Remote modules are not allowed to import local modules. Consider using a dynamic import instead.".to_string()); + return ResolvedDependency::Err(ResolvedDependencyErr::InvalidLocalImport); } ResolvedDependency::Resolved(specifier) @@ -241,8 +267,8 @@ pub fn analyze_dependencies( let resolved_import = resolve_import(&desc.specifier, specifier, maybe_import_map); - // Check for `@deno-types` pragmas that effect the import - let maybe_resolved_type_import = + let maybe_resolved_type_dependency = + // Check for `@deno-types` pragmas that affect the import if let Some(comment) = desc.leading_comments.last() { if let Some(deno_types) = parse_deno_types(&comment.text).as_ref() { Some(resolve_import(deno_types, specifier, maybe_import_map)) @@ -276,8 +302,8 @@ pub fn analyze_dependencies( dep.maybe_code = Some(resolved_import); } } - if maybe_resolved_type_import.is_some() && dep.maybe_type.is_none() { - dep.maybe_type = maybe_resolved_type_import; + if maybe_resolved_type_dependency.is_some() && dep.maybe_type.is_none() { + dep.maybe_type = maybe_resolved_type_dependency; } } diff --git a/cli/lsp/diagnostics.rs b/cli/lsp/diagnostics.rs index a9e4dbefb9..437a965432 100644 --- a/cli/lsp/diagnostics.rs +++ b/cli/lsp/diagnostics.rs @@ -279,14 +279,14 @@ pub async fn generate_dependency_diagnostics( &dependency.maybe_code_specifier_range, ) { match code.clone() { - ResolvedDependency::Err(message) => { + ResolvedDependency::Err(err) => { diagnostic_list.push(lsp::Diagnostic { range: *range, severity: Some(lsp::DiagnosticSeverity::Error), code: None, code_description: None, source: Some("deno".to_string()), - message, + message: format!("{}", err), related_information: None, tags: None, data: None, diff --git a/cli/lsp/documents.rs b/cli/lsp/documents.rs index 955ca1c782..e8ea0e2cf0 100644 --- a/cli/lsp/documents.rs +++ b/cli/lsp/documents.rs @@ -3,9 +3,6 @@ use super::analysis; use super::text::LineIndex; -use crate::import_map::ImportMap; -use crate::media_type::MediaType; - use deno_core::error::custom_error; use deno_core::error::AnyError; use deno_core::error::Context; @@ -76,9 +73,10 @@ impl DocumentData { } pub fn content(&self) -> Result, AnyError> { - if let Some(bytes) = self.bytes.clone() { + if let Some(bytes) = &self.bytes { Ok(Some( - String::from_utf8(bytes).context("cannot decode bytes to string")?, + String::from_utf8(bytes.clone()) + .context("cannot decode bytes to string")?, )) } else { Ok(None) @@ -92,46 +90,12 @@ pub struct DocumentCache { } impl DocumentCache { - pub fn analyze_dependencies( - &mut self, - specifier: &ModuleSpecifier, - maybe_import_map: &Option, - ) -> Result<(), AnyError> { - if !self.contains(specifier) { - return Err(custom_error( - "NotFound", - format!( - "The specifier (\"{}\") does not exist in the document cache.", - specifier - ), - )); - } - - let doc = self.docs.get_mut(specifier).unwrap(); - if let Some(source) = &doc.content()? { - if let Some((dependencies, _)) = analysis::analyze_dependencies( - specifier, - source, - &MediaType::from(specifier), - maybe_import_map, - ) { - doc.dependencies = Some(dependencies); - } else { - doc.dependencies = None; - } - } else { - doc.dependencies = None; - } - - Ok(()) - } - pub fn change( &mut self, specifier: &ModuleSpecifier, version: i32, content_changes: Vec, - ) -> Result<(), AnyError> { + ) -> Result, AnyError> { if !self.contains(specifier) { return Err(custom_error( "NotFound", @@ -145,7 +109,7 @@ impl DocumentCache { let doc = self.docs.get_mut(specifier).unwrap(); doc.apply_content_changes(content_changes)?; doc.version = Some(version); - Ok(()) + doc.content() } pub fn close(&mut self, specifier: &ModuleSpecifier) { @@ -187,12 +151,7 @@ impl DocumentCache { doc.line_index.clone() } - pub fn open( - &mut self, - specifier: ModuleSpecifier, - version: i32, - text: String, - ) { + pub fn open(&mut self, specifier: ModuleSpecifier, version: i32, text: &str) { self.docs.insert( specifier, DocumentData { @@ -218,6 +177,25 @@ impl DocumentCache { .collect() } + pub fn set_dependencies( + &mut self, + specifier: &ModuleSpecifier, + maybe_dependencies: Option>, + ) -> Result<(), AnyError> { + if let Some(doc) = self.docs.get_mut(specifier) { + doc.dependencies = maybe_dependencies; + Ok(()) + } else { + Err(custom_error( + "NotFound", + format!( + "The specifier (\"{}\") does not exist in the document cache.", + specifier + ), + )) + } + } + pub fn version(&self, specifier: &ModuleSpecifier) -> Option { self.docs.get(specifier).and_then(|doc| doc.version) } @@ -234,11 +212,7 @@ mod tests { let specifier = ModuleSpecifier::resolve_url("file:///a/b.ts").unwrap(); let missing_specifier = ModuleSpecifier::resolve_url("file:///a/c.ts").unwrap(); - document_cache.open( - specifier.clone(), - 1, - "console.log(\"Hello Deno\");\n".to_owned(), - ); + document_cache.open(specifier.clone(), 1, "console.log(\"Hello Deno\");\n"); assert!(document_cache.contains(&specifier)); assert!(!document_cache.contains(&missing_specifier)); } @@ -247,11 +221,7 @@ mod tests { fn test_document_cache_change() { let mut document_cache = DocumentCache::default(); let specifier = ModuleSpecifier::resolve_url("file:///a/b.ts").unwrap(); - document_cache.open( - specifier.clone(), - 1, - "console.log(\"Hello deno\");\n".to_owned(), - ); + document_cache.open(specifier.clone(), 1, "console.log(\"Hello deno\");\n"); document_cache .change( &specifier, @@ -282,11 +252,7 @@ mod tests { fn test_document_cache_change_utf16() { let mut document_cache = DocumentCache::default(); let specifier = ModuleSpecifier::resolve_url("file:///a/b.ts").unwrap(); - document_cache.open( - specifier.clone(), - 1, - "console.log(\"Hello 🦕\");\n".to_owned(), - ); + document_cache.open(specifier.clone(), 1, "console.log(\"Hello 🦕\");\n"); document_cache .change( &specifier, diff --git a/cli/lsp/language_server.rs b/cli/lsp/language_server.rs index fc2f80aebb..3f1d5f6c2d 100644 --- a/cli/lsp/language_server.rs +++ b/cli/lsp/language_server.rs @@ -26,14 +26,17 @@ use tokio::fs; use crate::deno_dir; use crate::import_map::ImportMap; +use crate::media_type::MediaType; use crate::tsc_config::parse_config; use crate::tsc_config::TsConfig; +use super::analysis; use super::analysis::ts_changes_to_edit; use super::analysis::CodeActionCollection; use super::analysis::CodeActionData; use super::analysis::CodeLensData; use super::analysis::CodeLensSource; +use super::analysis::ResolvedDependency; use super::capabilities; use super::config::Config; use super::diagnostics; @@ -82,7 +85,7 @@ pub(crate) struct Inner { /// file which will be used by the Deno LSP. maybe_config_uri: Option, /// An optional import map which is used to resolve modules. - maybe_import_map: Option, + pub maybe_import_map: Option, /// The URL for the import map which is used to determine relative imports. maybe_import_map_uri: Option, /// A map of all the cached navigation trees. @@ -128,6 +131,33 @@ impl Inner { } } + /// Analyzes dependencies of a document that has been opened in the editor and + /// sets the dependencies property on the document. + fn analyze_dependencies( + &mut self, + specifier: &ModuleSpecifier, + source: &str, + ) { + if let Some((mut deps, _)) = analysis::analyze_dependencies( + specifier, + source, + &MediaType::from(specifier), + &self.maybe_import_map, + ) { + for (_, dep) in deps.iter_mut() { + if dep.maybe_type.is_none() { + if let Some(ResolvedDependency::Resolved(resolved)) = &dep.maybe_code + { + dep.maybe_type = self.sources.get_maybe_types(resolved); + } + } + } + if let Err(err) = self.documents.set_dependencies(specifier, Some(deps)) { + error!("{}", err); + } + } + } + fn enabled(&self) -> bool { self.config.settings.enable } @@ -633,19 +663,11 @@ impl Inner { self.documents.open( specifier.clone(), params.text_document.version, - params.text_document.text, + ¶ms.text_document.text, ); - if let Err(err) = self - .documents - .analyze_dependencies(&specifier, &self.maybe_import_map) - { - error!("{}", err); - } - // there are scenarios where local documents with a nav tree are opened in - // the editor - self.navigation_trees.remove(&specifier); - + self.analyze_dependencies(&specifier, ¶ms.text_document.text); self.performance.measure(mark); + // TODO(@kitsonk): how to better lazily do this? if let Err(err) = self.prepare_diagnostics().await { error!("{}", err); @@ -655,22 +677,17 @@ impl Inner { async fn did_change(&mut self, params: DidChangeTextDocumentParams) { let mark = self.performance.mark("did_change"); let specifier = utils::normalize_url(params.text_document.uri); - if let Err(err) = self.documents.change( + match self.documents.change( &specifier, params.text_document.version, params.content_changes, ) { - error!("{}", err); + Ok(Some(source)) => self.analyze_dependencies(&specifier, &source), + Ok(_) => error!("No content returned from change."), + Err(err) => error!("{}", err), } - if let Err(err) = self - .documents - .analyze_dependencies(&specifier, &self.maybe_import_map) - { - error!("{}", err); - } - self.navigation_trees.remove(&specifier); - self.performance.measure(mark); + // TODO(@kitsonk): how to better lazily do this? if let Err(err) = self.prepare_diagnostics().await { error!("{}", err); @@ -1700,6 +1717,11 @@ impl lspower::LanguageServer for LanguageServer { self.0.lock().await.did_change(params).await } + async fn did_save(&self, _params: DidSaveTextDocumentParams) { + // We don't need to do anything on save at the moment, but if this isn't + // implemented, lspower complains about it not being implemented. + } + async fn did_close(&self, params: DidCloseTextDocumentParams) { self.0.lock().await.did_close(params).await } diff --git a/cli/lsp/sources.rs b/cli/lsp/sources.rs index fac1120fb0..c2ef27010b 100644 --- a/cli/lsp/sources.rs +++ b/cli/lsp/sources.rs @@ -98,6 +98,14 @@ impl Sources { Some(metadata.line_index) } + pub fn get_maybe_types( + &mut self, + specifier: &ModuleSpecifier, + ) -> Option { + let metadata = self.get_metadata(specifier)?; + metadata.maybe_types + } + pub fn get_media_type( &mut self, specifier: &ModuleSpecifier, @@ -115,6 +123,8 @@ impl Sources { } } } + // TODO(@kitsonk) this needs to be refactored, lots of duplicate logic and + // is really difficult to follow. let version = self.get_script_version(specifier)?; let path = self.get_path(specifier)?; if let Ok(bytes) = fs::read(path) { @@ -123,12 +133,13 @@ impl Sources { if let Ok(source) = get_source_from_bytes(bytes, Some(charset)) { let media_type = MediaType::from(specifier); let mut maybe_types = None; + let maybe_import_map = self.maybe_import_map.clone(); let dependencies = if let Some((dependencies, mt)) = analysis::analyze_dependencies( &specifier, &source, &media_type, - &None, + &maybe_import_map, ) { maybe_types = mt; Some(dependencies) @@ -165,12 +176,13 @@ impl Sources { } else { None }; + let maybe_import_map = self.maybe_import_map.clone(); let dependencies = if let Some((dependencies, mt)) = analysis::analyze_dependencies( &specifier, &source, &media_type, - &None, + &maybe_import_map, ) { if maybe_types.is_none() { maybe_types = mt; diff --git a/cli/lsp/tsc.rs b/cli/lsp/tsc.rs index 6614ce8e6e..588487517a 100644 --- a/cli/lsp/tsc.rs +++ b/cli/lsp/tsc.rs @@ -2,6 +2,7 @@ use super::analysis::CodeLensSource; use super::analysis::ResolvedDependency; +use super::analysis::ResolvedDependencyErr; use super::language_server; use super::language_server::StateSnapshot; use super::text; @@ -1096,7 +1097,7 @@ fn resolve(state: &mut State, args: Value) -> Result { } else if let Some(resolved_import) = &dependency.maybe_code { resolved_import.clone() } else { - ResolvedDependency::Err("missing dependency".to_string()) + ResolvedDependency::Err(ResolvedDependencyErr::Missing) }; if let ResolvedDependency::Resolved(resolved_specifier) = resolved_import @@ -1476,7 +1477,7 @@ mod tests { for (specifier, content, version) in sources { let specifier = ModuleSpecifier::resolve_url(specifier) .expect("failed to create specifier"); - documents.open(specifier, version, content.to_string()); + documents.open(specifier, version, content); } StateSnapshot { assets: Default::default(),