/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
cli/lsp/diagnostics.rs
2 651 строка
85 KB
Bartek Iwańczuk
fix(lsp): respect lint rule exclusion for no-slow-types diagnostics (#35924)
10 июл 2026, 15:11
Не верифицирован
10 июл 2026, 15:11
1c87314
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. use std::collections::HashMap; use std::str::FromStr; use std::sync::Arc; use std::thread; use console_static_text::ansi::strip_ansi_codes; use deno_ast::MediaType; use deno_ast::diagnostics::Diagnostic as _; use deno_core::ModuleSpecifier; use deno_core::anyhow::anyhow; use deno_core::error::AnyError; use deno_core::futures::StreamExt; use deno_core::parking_lot::RwLock; use deno_core::resolve_url; use deno_core::serde::Deserialize; use deno_core::serde_json; use deno_core::serde_json::json; use deno_core::url::Url; use deno_error::JsErrorClass; use deno_graph::Resolution; use deno_graph::ResolutionError; use deno_graph::SpecifierError; use deno_graph::fast_check::FastCheckDiagnostic; use deno_graph::source::ResolveError; use deno_lib::args::CaData; use deno_resolver::deno_json::CompilerOptionsKey; use deno_resolver::graph::enhanced_resolution_error_message; use deno_resolver::workspace::sloppy_imports_resolve; use deno_runtime::deno_node; use deno_runtime::tokio_util::create_basic_runtime; use deno_semver::jsr::JsrPackageReqReference; use deno_semver::npm::NpmPackageReqReference; use deno_semver::package::PackageReq; use import_map::ImportMap; use import_map::ImportMapDiagnostic; use import_map::ImportMapErrorKind; use log::error; use lsp_types::Uri; use node_resolver::NodeResolutionKind; use text_size::TextSize; use tokio::sync::mpsc; use tokio_util::sync::CancellationToken; use tokio_util::task::AbortOnDropHandle; use tower_lsp::lsp_types as lsp; use super::analysis; use super::analysis::import_map_lookup_for_diagnostic; use super::client::Client; use super::config::ConfigWatchedFileType; use super::documents::Document; use super::documents::DocumentModule; use super::documents::DocumentModules; use super::language_server; use super::language_server::StateSnapshot; use super::performance::Performance; use super::ts_server::TsServer; use crate::args::Flags; use crate::args::InternalFlags; use crate::args::PermissionFlags; use crate::args::TypeCheckMode; use crate::factory::CliFactory; use crate::graph_util::CreatePublishGraphOptions; use crate::lsp::documents::OpenDocument; use crate::lsp::language_server::OnceCellMap; use crate::lsp::lint::LspLinter; use crate::lsp::logging::lsp_warn; use crate::lsp::urls::uri_to_url; use crate::sys::CliSys; use crate::tools::lint::collect_no_slow_type_diagnostics; use crate::type_checker::ambient_modules_to_regex_string; use crate::util::path::to_percent_decoded_str; #[derive(Debug)] pub struct DiagnosticsUpdateMessage { pub snapshot: Arc<StateSnapshot>, } #[derive(Debug, Hash, PartialEq, Eq, Copy, Clone)] pub enum DiagnosticSource { Deno, Doc, Lint, Ts, } impl DiagnosticSource { pub fn as_lsp_source(&self) -> &'static str { match self { Self::Deno => "deno", Self::Doc => "deno-doc", Self::Lint => "deno-lint", Self::Ts => "deno-ts", } } } /// No-slow-types (fast check) diagnostics are computed for an entire /// publishable JSR package at once and can land on any module within it, so /// they're cached per package scope and then distributed to the individual /// documents they belong to. The inner map is keyed by the specifier of the /// module each diagnostic points at. pub type NoSlowTypesDiagnosticsCache = OnceCellMap< Arc<ModuleSpecifier>, Arc<HashMap<ModuleSpecifier, Vec<lsp::Diagnostic>>>, >; fn should_send_diagnostic_batch_notifications() -> bool { deno_lib::args::has_flag_env_var( &CliSys::default(), "DENO_INTERNAL_DIAGNOSTIC_BATCH_NOTIFICATIONS", ) } #[derive(Debug)] struct DocumentDiagnosticsState { version: i32, ts_diagnostics: Arc<Vec<lsp::Diagnostic>>, no_cache_diagnostics: Arc<Vec<lsp::Diagnostic>>, } #[derive(Debug, Default)] pub struct DiagnosticsState { documents: RwLock<HashMap<Uri, DocumentDiagnosticsState>>, } impl DiagnosticsState { fn update(&self, uri: &Uri, version: i32, diagnostics: &[lsp::Diagnostic]) { let mut specifiers = self.documents.write(); let current_version = specifiers.get(uri).map(|s| s.version); if let Some(current_version) = current_version && version < current_version { return; } let mut ts_diagnostics = vec![]; let mut no_cache_diagnostics = vec![]; for diagnostic in diagnostics { if diagnostic.source.as_deref() == Some(DiagnosticSource::Ts.as_lsp_source()) { ts_diagnostics.push(diagnostic.clone()); } if diagnostic.code == Some(lsp::NumberOrString::String("no-cache".to_string())) || diagnostic.code == Some(lsp::NumberOrString::String("not-installed-jsr".to_string())) || diagnostic.code == Some(lsp::NumberOrString::String("not-installed-npm".to_string())) { no_cache_diagnostics.push(diagnostic.clone()); } } specifiers.insert( uri.clone(), DocumentDiagnosticsState { version, ts_diagnostics: Arc::new(ts_diagnostics), no_cache_diagnostics: Arc::new(no_cache_diagnostics), }, ); } pub fn clear(&self, uri: &Uri) { self.documents.write().remove(uri); } pub fn ts_diagnostics(&self, uri: &Uri) -> Arc<Vec<lsp::Diagnostic>> { self .documents .read() .get(uri) .map(|s| s.ts_diagnostics.clone()) .unwrap_or_default() } pub fn has_no_cache_diagnostics(&self, uri: &Uri) -> bool { self .documents .read() .get(uri) .map(|s| !s.no_cache_diagnostics.is_empty()) .unwrap_or(false) } pub fn no_cache_diagnostics(&self, uri: &Uri) -> Arc<Vec<lsp::Diagnostic>> { self .documents .read() .get(uri) .map(|s| s.no_cache_diagnostics.clone()) .unwrap_or_default() } } pub struct DiagnosticsServer { channel: Option<mpsc::UnboundedSender<DiagnosticsUpdateMessage>>, client: Client, performance: Arc<Performance>, ts_server: Arc<TsServer>, pub state: Arc<DiagnosticsState>, } impl std::fmt::Debug for DiagnosticsServer { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("DiagnosticsServer") .field("channel", &self.channel) .field("client", &self.client) .field("performance", &self.performance) .field("ts_server", &self.ts_server) .field("state", &self.state) .finish() } } impl DiagnosticsServer { pub fn new( client: Client, performance: Arc<Performance>, ts_server: Arc<TsServer>, ) -> Self { DiagnosticsServer { channel: Default::default(), client, performance, ts_server, state: Default::default(), } } pub fn start(&mut self) { let (tx, mut rx) = mpsc::unbounded_channel::<DiagnosticsUpdateMessage>(); self.channel = Some(tx); let client = self.client.clone(); let state = self.state.clone(); let performance = self.performance.clone(); let ts_server = self.ts_server.clone(); let should_send_batch_notifications = should_send_diagnostic_batch_notifications(); let _join_handle = thread::spawn(move || { let runtime = create_basic_runtime(); runtime.block_on(async { let ambient_modules_regex_cache = Arc::new(OnceCellMap::< (CompilerOptionsKey, Option<Arc<Uri>>), Option<regex::Regex>, >::new()); let mut _previous_handle; while let Some(message) = rx.recv().await { let _mark = performance.measure_scope("lsp.update_diagnostics"); let client = client.clone(); let state = state.clone(); let ts_server = ts_server.clone(); let ambient_modules_regex_cache = ambient_modules_regex_cache.clone(); let join_handle = tokio::task::spawn(async move { let token = CancellationToken::new(); let _drop_guard = token.drop_guard_ref(); let DiagnosticsUpdateMessage { snapshot } = message; if should_send_batch_notifications { client.send_diagnostic_batch_start_notification(); } // Computed lazily and shared across all documents of a package so // the (potentially expensive) graph build only happens once per // package scope per diagnostics run. let no_slow_types_cache = Arc::new(NoSlowTypesDiagnosticsCache::default()); let open_docs = snapshot.document_modules.documents.open_docs().cloned(); deno_core::futures::stream::iter(open_docs.map(|document| { let snapshot = snapshot.clone(); let ts_server = ts_server.clone(); let ambient_modules_regex_cache = ambient_modules_regex_cache.clone(); let no_slow_types_cache = no_slow_types_cache.clone(); let token = token.clone(); AbortOnDropHandle::new(tokio::task::spawn(async move { let diagnostics = generate_document_diagnostics( &document, &snapshot, &ts_server, &ambient_modules_regex_cache, &no_slow_types_cache, &token, ) .await .unwrap_or_else(|err| { lsp_warn!( "Couldn't generate diagnostics for \"{}\": {err:#}", document.uri.as_str() ); vec![] }); (document, diagnostics) })) })) .buffered( std::thread::available_parallelism() .map(From::from) .unwrap_or(8), ) .for_each(|result| async { let (document, diagnostics) = match result { Ok(r) => r, Err(err) => { lsp_warn!("Diagnostics task join error: {err:#}"); return; } }; publish_document_diagnostics( &document, diagnostics, &client, &state, &token, ) .await }) .await; if should_send_batch_notifications { client.send_diagnostic_batch_end_notification(); } }); _previous_handle = AbortOnDropHandle::new(join_handle); } }) }); } pub fn update( &self, message: DiagnosticsUpdateMessage, ) -> Result<(), AnyError> { // todo(dsherret): instead of queuing up messages, it would be better to // instead only store the latest message (ex. maybe using a // tokio::sync::watch::channel) if let Some(tx) = &self.channel { tx.send(message).map_err(|err| err.into()) } else { Err(anyhow!("diagnostics server not started")) } } } impl<'a> From<&'a crate::tsc::DiagnosticCategory> for lsp::DiagnosticSeverity { fn from(category: &'a crate::tsc::DiagnosticCategory) -> Self { match category { crate::tsc::DiagnosticCategory::Error => lsp::DiagnosticSeverity::ERROR, crate::tsc::DiagnosticCategory::Warning => { lsp::DiagnosticSeverity::WARNING } crate::tsc::DiagnosticCategory::Suggestion => { lsp::DiagnosticSeverity::HINT } crate::tsc::DiagnosticCategory::Message => { lsp::DiagnosticSeverity::INFORMATION } } } } impl<'a> From<&'a crate::tsc::Position> for lsp::Position { fn from(pos: &'a crate::tsc::Position) -> Self { Self { line: pos.line as u32, character: pos.character as u32, } } } fn get_diagnostic_message(diagnostic: &crate::tsc::Diagnostic) -> String { if let Some(message) = diagnostic.message_text.clone() { message } else if let Some(message_chain) = diagnostic.message_chain.clone() { message_chain.format_message(0) } else { "[missing message]".to_string() } } fn to_lsp_range( start: &crate::tsc::Position, end: &crate::tsc::Position, ) -> lsp::Range { lsp::Range { start: start.into(), end: end.into(), } } fn to_lsp_related_information( related_information: &Option<Vec<crate::tsc::Diagnostic>>, module: &DocumentModule, document_modules: &DocumentModules, ) -> Option<Vec<lsp::DiagnosticRelatedInformation>> { related_information.as_ref().map(|related| { related .iter() .filter_map(|ri| { if let (Some(file_name), Some(start), Some(end)) = (&ri.file_name, &ri.start, &ri.end) { let uri = resolve_url(file_name) .ok() .and_then(|s| { document_modules.module_for_specifier( &s, module.scope.as_deref(), Some(&module.compiler_options_key), ) }) .map(|m| m.uri.as_ref().clone()) .unwrap_or_else(|| Uri::from_str("unknown:").unwrap()); Some(lsp::DiagnosticRelatedInformation { location: lsp::Location { uri, range: to_lsp_range(start, end), }, message: get_diagnostic_message(ri), }) } else { None } }) .collect() }) } pub fn ts_json_to_diagnostics( diagnostics: Vec<crate::tsc::Diagnostic>, module: &DocumentModule, document_modules: &DocumentModules, ) -> Vec<lsp::Diagnostic> { diagnostics .iter() .filter_map(|d| { if let (Some(start), Some(end)) = (&d.start, &d.end) { Some(lsp::Diagnostic { range: to_lsp_range(start, end), severity: Some((&d.category).into()), code: Some(lsp::NumberOrString::Number(d.code as i32)), code_description: None, source: Some(DiagnosticSource::Ts.as_lsp_source().to_string()), message: get_diagnostic_message(d), related_information: to_lsp_related_information( &d.related_information, module, document_modules, ), tags: match d.code { // These are codes that indicate the variable is unused. 2695 | 6133 | 6138 | 6192 | 6196 | 6198 | 6199 | 6205 | 7027 | 7028 => Some(vec![lsp::DiagnosticTag::UNNECESSARY]), // These are codes that indicated the variable is deprecated. 2789 | 6385 | 6387 => Some(vec![lsp::DiagnosticTag::DEPRECATED]), _ => None, }, data: None, }) } else { None } }) .collect() } /// Generates lint diagnostics for a module, caching them on the module /// instance. Lint diagnostics depend only on the module's own contents, so a /// given module instance always produces the same result and can be cached. /// /// Returns `Err` if linting was cancelled, in which case nothing is cached and /// the result is recomputed on the next request. fn generate_document_lint_diagnostics( module: &DocumentModule, linter: &LspLinter, token: CancellationToken, ) -> Result<Arc<Vec<lsp::Diagnostic>>, AnyError> { module .lint_diagnostics .get_or_try_init(|| { compute_document_lint_diagnostics(module, linter, token).map(Arc::new) }) .cloned() } fn compute_document_lint_diagnostics( module: &DocumentModule, linter: &LspLinter, token: CancellationToken, ) -> Result<Vec<lsp::Diagnostic>, AnyError> { if !module.is_diagnosable() || !linter .lint_config .files .matches_specifier(&module.specifier) { return Ok(Vec::new()); } match &module .open_data .as_ref() .and_then(|d| d.parsed_source.as_ref()) { Some(Ok(parsed_source)) => { let references = analysis::get_lint_references(parsed_source, &linter.inner, token)?; Ok( references .into_iter() .map(|r| r.to_diagnostic()) .collect::<Vec<_>>(), ) } Some(Err(_)) => Ok(Vec::new()), None => { error!("Missing file contents for: {}", &module.specifier); Ok(Vec::new()) } } } /// Computes an LSP range to highlight for a `deno doc --lint` diagnostic. /// /// `deno_doc` only reports the byte offset of the start of the offending /// symbol, so highlight the identifier-like token that begins there (falling /// back to a single character) to produce a visible squiggle. fn doc_diagnostic_range( module: &DocumentModule, byte_index: usize, ) -> lsp::Range { let text = &*module.text; let bytes = text.as_bytes(); let start = byte_index.min(bytes.len()); let mut end = start; while end < bytes.len() { let b = bytes[end]; if b.is_ascii_alphanumeric() || b == b'_' || b == b'$' { end += 1; } else { break; } } if end == start { // Ensure a non-empty range so the diagnostic renders. end = (start + 1).min(bytes.len()); while end < bytes.len() && !text.is_char_boundary(end) { end += 1; } } lsp::Range { start: module .line_index .position_utf16(TextSize::from(start as u32)), end: module.line_index.position_utf16(TextSize::from(end as u32)), } } /// Generates `deno doc --lint` diagnostics for a module that is a published /// package's export entrypoint, caching them on the module instance. Like lint /// diagnostics, these are derived solely from the module's own contents, so a /// given module instance always produces the same result. /// /// Returns `Err` if generation was cancelled, in which case nothing is cached /// and the result is recomputed on the next request. fn generate_document_doc_diagnostics( module: &DocumentModule, token: &CancellationToken, ) -> Result<Arc<Vec<lsp::Diagnostic>>, AnyError> { module .doc_diagnostics .get_or_try_init(|| { compute_document_doc_diagnostics(module, token).map(Arc::new) }) .cloned() } fn compute_document_doc_diagnostics( module: &DocumentModule, token: &CancellationToken, ) -> Result<Vec<lsp::Diagnostic>, AnyError> { if token.is_cancelled() { return Err(anyhow!("cancelled")); } if !module.is_diagnosable() { return Ok(Vec::new()); } // Only open documents have parsed sources to lint against. if !module .open_data .as_ref() .and_then(|d| d.parsed_source.as_ref()) .is_some_and(|r| r.is_ok()) { return Ok(Vec::new()); } let analyzer = deno_graph::ast::CapturingModuleAnalyzer::default(); let specifier = module.specifier.as_ref().clone(); let loader = deno_graph::source::MemoryLoader::new( vec![( specifier.to_string(), deno_graph::source::Source::Module { specifier: specifier.to_string(), content: module.text.to_string(), maybe_headers: None, }, )], Vec::new(), ); let roots = vec![specifier.clone()]; let mut graph = deno_graph::ModuleGraph::new(deno_graph::GraphKind::TypesOnly); deno_core::futures::executor::block_on(graph.build( roots.clone(), Vec::new(), &loader, deno_graph::BuildOptions { is_dynamic: false, skip_dynamic_deps: false, passthrough_jsr_specifiers: false, executor: Default::default(), file_system: &deno_graph::source::NullFileSystem, jsr_metadata_store: None, jsr_url_provider: Default::default(), jsr_version_resolver: Default::default(), locker: None, module_analyzer: &analyzer, module_info_cacher: Default::default(), npm_resolver: None, reporter: None, resolver: None, unstable_bytes_imports: false, unstable_text_imports: false, unstable_css_imports: false, unstable_config_imports: false, }, )); if token.is_cancelled() { return Err(anyhow!("cancelled")); } let doc_parser = match deno_doc::DocParser::new( &graph, &analyzer, &roots, deno_doc::DocParserOptions { diagnostics: true, private: false, }, ) { Ok(doc_parser) => doc_parser, Err(err) => { lsp_warn!("Failed to create doc parser for \"{specifier}\": {err:#}"); return Ok(Vec::new()); } }; if let Err(err) = doc_parser.parse() { lsp_warn!("Failed to parse docs for \"{specifier}\": {err:#}"); return Ok(Vec::new()); } let specifier_str = specifier.as_str(); Ok( doc_parser .take_diagnostics() .into_iter() // Only report diagnostics that point at the linted module itself. With a // single-module graph cross-module references aren't resolved, so any // diagnostic for another file would be spurious. .filter(|diagnostic| { diagnostic.location.filename.as_ref() == specifier_str }) .map(|diagnostic| { use deno_ast::diagnostics::Diagnostic; lsp::Diagnostic { range: doc_diagnostic_range(module, diagnostic.location.byte_index), severity: Some(lsp::DiagnosticSeverity::WARNING), code: Some(lsp::NumberOrString::String( diagnostic.code().into_owned(), )), code_description: None, source: Some(DiagnosticSource::Doc.as_lsp_source().to_string()), message: diagnostic.message().into_owned(), related_information: None, tags: None, data: None, } }) .collect(), ) } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct DiagnosticDataSpecifier { pub specifier: ModuleSpecifier, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct DiagnosticDataStrSpecifier { pub specifier: String, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct DiagnosticDataRedirect { pub redirect: ModuleSpecifier, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct DiagnosticDataNoLocal { pub to: ModuleSpecifier, pub message: String, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct DiagnosticDataImportMapRemap { pub from: String, pub to: String, } /// An enum which represents diagnostic errors which originate from Deno itself. pub enum DenoDiagnostic { /// A `x-deno-warning` is associated with the specifier and should be displayed /// as a warning to the user. DenoWarn(String), /// The import map file has invalid JSON or invalid import map entries. ImportMapDiagnostic(String), /// An informational diagnostic that indicates an existing specifier can be /// remapped to an import map import specifier. ImportMapRemap { from: String, to: String }, /// The import assertion type is incorrect. InvalidAttributeType(String), /// A module requires an attribute type to be a valid import. NoAttributeType, /// A remote module was not found in the cache. NoCache(ModuleSpecifier), /// A jsr package reference was not found in the cache. NotInstalledJsr(PackageReq, ModuleSpecifier), /// An npm package reference was not found in the cache. NotInstalledNpm(PackageReq, ModuleSpecifier), /// An npm package reference was not exported by its package. NoExportNpm(NpmPackageReqReference), /// A local module was not found on the local file system. NoLocal(ModuleSpecifier), /// An error occurred when resolving the specifier string. The second field /// holds the names of packages importable by bare specifier (workspace /// members and packages linked via the "links" field), used to enhance the /// error with a better hint. ResolutionError(deno_graph::ResolutionError, Vec<String>), /// Unknown `node:` specifier. UnknownNodeSpecifier(ModuleSpecifier), /// Bare specifier is used for `node:` specifier BareNodeSpecifier(String), } impl DenoDiagnostic { fn code(&self) -> &str { match self { Self::DenoWarn(_) => "deno-warn", Self::ImportMapDiagnostic(_) => "import-map-diagnostic", Self::ImportMapRemap { .. } => "import-map-remap", Self::InvalidAttributeType(_) => "invalid-attribute-type", Self::NoAttributeType => "no-attribute-type", Self::NoCache(_) => "no-cache", Self::NotInstalledJsr(_, _) => "not-installed-jsr", Self::NotInstalledNpm(_, _) => "not-installed-npm", Self::NoExportNpm(_) => "no-export-npm", Self::NoLocal(_) => "no-local", Self::ResolutionError(err, _) => { if deno_resolver::graph::get_resolution_error_bare_node_specifier(err) .is_some() { "import-node-prefix-missing" } else { match err { ResolutionError::InvalidDowngrade { .. } => "invalid-downgrade", ResolutionError::InvalidJsrHttpsTypesImport { .. } => { "invalid-jsr-https-types-import" } ResolutionError::InvalidLocalImport { .. } => { "invalid-local-import" } ResolutionError::InvalidSpecifier { error, .. } => match error { SpecifierError::ImportPrefixMissing { .. } => { "import-prefix-missing" } SpecifierError::InvalidUrl(_) => "invalid-url", }, ResolutionError::ResolverError { .. } => "resolver-error", } } } Self::UnknownNodeSpecifier(_) => "resolver-error", Self::BareNodeSpecifier(_) => "import-node-prefix-missing", } } /// A "static" method which for a diagnostic that originated from the /// structure returns a code action which can resolve the diagnostic. pub fn get_code_action( uri: &Uri, specifier: &ModuleSpecifier, diagnostic: &lsp::Diagnostic, ) -> Result<lsp::CodeAction, AnyError> { if let Some(lsp::NumberOrString::String(code)) = &diagnostic.code { let code_action = match code.as_str() { "import-map-remap" => { let data = diagnostic .data .clone() .ok_or_else(|| anyhow!("Diagnostic is missing data"))?; let DiagnosticDataImportMapRemap { from, to } = serde_json::from_value(data)?; lsp::CodeAction { title: format!("Update \"{from}\" to \"{to}\" to use import map."), kind: Some(lsp::CodeActionKind::QUICKFIX), diagnostics: Some(vec![diagnostic.clone()]), edit: Some(lsp::WorkspaceEdit { changes: Some(HashMap::from([( uri.clone(), vec![lsp::TextEdit { new_text: format!("\"{to}\""), range: diagnostic.range, }], )])), ..Default::default() }), ..Default::default() } } "no-attribute-type" => lsp::CodeAction { title: "Insert import attribute.".to_string(), kind: Some(lsp::CodeActionKind::QUICKFIX), diagnostics: Some(vec![diagnostic.clone()]), edit: Some(lsp::WorkspaceEdit { changes: Some(HashMap::from([( uri.clone(), vec![lsp::TextEdit { new_text: " with { type: \"json\" }".to_string(), range: lsp::Range { start: diagnostic.range.end, end: diagnostic.range.end, }, }], )])), ..Default::default() }), ..Default::default() }, "no-cache" | "not-installed-jsr" | "not-installed-npm" => { let data = diagnostic .data .clone() .ok_or_else(|| anyhow!("Diagnostic is missing data"))?; let data: DiagnosticDataSpecifier = serde_json::from_value(data)?; let title = if matches!( code.as_str(), "not-installed-jsr" | "not-installed-npm" ) { format!("Install \"{}\" and its dependencies.", data.specifier) } else { format!("Cache \"{}\" and its dependencies.", data.specifier) }; lsp::CodeAction { title, kind: Some(lsp::CodeActionKind::QUICKFIX), diagnostics: Some(vec![diagnostic.clone()]), command: Some(lsp::Command { title: "".to_string(), command: "deno.cache".to_string(), arguments: Some(vec![json!([data.specifier]), json!(&specifier)]), }), ..Default::default() } } "no-local" => { let data = diagnostic .data .clone() .ok_or_else(|| anyhow!("Diagnostic is missing data"))?; let data: DiagnosticDataNoLocal = serde_json::from_value(data)?; lsp::CodeAction { title: data.message, kind: Some(lsp::CodeActionKind::QUICKFIX), diagnostics: Some(vec![diagnostic.clone()]), edit: Some(lsp::WorkspaceEdit { changes: Some(HashMap::from([( uri.clone(), vec![lsp::TextEdit { new_text: format!( "\"{}\"", relative_specifier(&data.to, specifier) ), range: diagnostic.range, }], )])), ..Default::default() }), ..Default::default() } } "redirect" => { let data = diagnostic .data .clone() .ok_or_else(|| anyhow!("Diagnostic is missing data"))?; let data: DiagnosticDataRedirect = serde_json::from_value(data)?; lsp::CodeAction { title: "Update specifier to its redirected specifier.".to_string(), kind: Some(lsp::CodeActionKind::QUICKFIX), diagnostics: Some(vec![diagnostic.clone()]), edit: Some(lsp::WorkspaceEdit { changes: Some(HashMap::from([( uri.clone(), vec![lsp::TextEdit { new_text: format!( "\"{}\"", specifier_text_for_redirected(&data.redirect, specifier) ), range: diagnostic.range, }], )])), ..Default::default() }), ..Default::default() } } "import-node-prefix-missing" => { let data = diagnostic .data .clone() .ok_or_else(|| anyhow!("Diagnostic is missing data"))?; let data: DiagnosticDataStrSpecifier = serde_json::from_value(data)?; lsp::CodeAction { title: format!("Update specifier to node:{}", data.specifier), kind: Some(lsp::CodeActionKind::QUICKFIX), diagnostics: Some(vec![diagnostic.clone()]), edit: Some(lsp::WorkspaceEdit { changes: Some(HashMap::from([( uri.clone(), vec![lsp::TextEdit { new_text: format!("\"node:{}\"", data.specifier), range: diagnostic.range, }], )])), ..Default::default() }), ..Default::default() } } _ => { return Err(anyhow!( "Unsupported diagnostic code (\"{}\") provided.", code )); } }; Ok(code_action) } else { Err(anyhow!("Unsupported diagnostic code provided.")) } } /// Given a reference to the code from an LSP diagnostic, determine if the /// diagnostic is fixable or not pub fn is_fixable(diagnostic: &lsp_types::Diagnostic) -> bool { if let Some(lsp::NumberOrString::String(code)) = &diagnostic.code { match code.as_str() { "import-map-remap" | "no-cache" | "not-installed-jsr" | "not-installed-npm" | "no-attribute-type" | "redirect" | "import-node-prefix-missing" => true, "no-local" => diagnostic.data.is_some(), _ => false, } } else { false } } /// Convert to an lsp Diagnostic when the range the diagnostic applies to is /// provided. pub fn to_lsp_diagnostic(&self, range: &lsp::Range) -> lsp::Diagnostic { fn no_local_message( specifier: &ModuleSpecifier, suggestion_message: Option<String>, ) -> String { let mut message = format!( "Unable to load a local module: {}\n", to_percent_decoded_str(specifier.as_ref()) ); if let Some(suggestion_message) = suggestion_message { message.push_str(&suggestion_message); message.push('.'); } else { message.push_str("Please check the file path."); } message } let (severity, message, data) = match self { Self::DenoWarn(message) => (lsp::DiagnosticSeverity::WARNING, message.to_string(), None), Self::ImportMapDiagnostic(message) => (lsp::DiagnosticSeverity::ERROR, message.to_string(), None), Self::ImportMapRemap { from, to } => (lsp::DiagnosticSeverity::HINT, format!("The import specifier can be remapped to \"{to}\" which will resolve it via the active import map."), Some(json!({ "from": from, "to": to }))), Self::InvalidAttributeType(assert_type) => (lsp::DiagnosticSeverity::ERROR, format!("The module is a JSON module and expected an attribute type of \"json\". Instead got \"{assert_type}\"."), None), Self::NoAttributeType => (lsp::DiagnosticSeverity::ERROR, "The module is a JSON module and not being imported with an import attribute. Consider adding `with { type: \"json\" }` to the import statement.".to_string(), None), Self::NoCache(specifier) => (lsp::DiagnosticSeverity::ERROR, format!("Uncached or missing remote URL: {specifier}"), Some(json!({ "specifier": specifier }))), Self::NotInstalledJsr(pkg_req, specifier) => (lsp::DiagnosticSeverity::ERROR, format!("JSR package \"{pkg_req}\" is not installed or doesn't exist."), Some(json!({ "specifier": specifier }))), Self::NotInstalledNpm(pkg_req, specifier) => (lsp::DiagnosticSeverity::ERROR, format!("npm package \"{pkg_req}\" is not installed or doesn't exist."), Some(json!({ "specifier": specifier }))), Self::NoExportNpm(pkg_ref) => (lsp::DiagnosticSeverity::ERROR, format!("NPM package \"{}\" does not define an export \"{}\".", pkg_ref.req(), pkg_ref.sub_path().unwrap_or(".")), None), Self::NoLocal(specifier) => { let sloppy_resolution = sloppy_imports_resolve(specifier, deno_resolver::workspace::ResolutionKind::Execution, CliSys::default()); let data = sloppy_resolution.as_ref().map(|(resolved, sloppy_reason)| { json!({ "specifier": specifier, "to": resolved, "message": sloppy_reason.quick_fix_message_for_specifier(resolved), }) }); (lsp::DiagnosticSeverity::ERROR, no_local_message(specifier, sloppy_resolution.as_ref().map(|(resolved, sloppy_reason)| sloppy_reason.suggestion_message_for_specifier(resolved))), data) }, Self::ResolutionError(err, bare_importable_pkg_names) => { let message = strip_ansi_codes(&enhanced_resolution_error_message(err, bare_importable_pkg_names)).into_owned(); ( lsp::DiagnosticSeverity::ERROR, message, deno_resolver::graph::get_resolution_error_bare_node_specifier(err) .map(|specifier| json!({ "specifier": specifier })) )}, Self::UnknownNodeSpecifier(specifier) => (lsp::DiagnosticSeverity::ERROR, format!("No such built-in module: node:{}", specifier.path()), None), Self::BareNodeSpecifier(specifier) => (lsp::DiagnosticSeverity::WARNING, format!("\"{0}\" is resolved to \"node:{0}\". If you want to use a built-in Node module, add a \"node:\" prefix.", specifier), Some(json!({ "specifier": specifier }))), }; lsp::Diagnostic { range: *range, severity: Some(severity), code: Some(lsp::NumberOrString::String(self.code().to_string())), source: Some(DiagnosticSource::Deno.as_lsp_source().to_string()), message, data, ..Default::default() } } } fn specifier_text_for_redirected(redirect: &Url, referrer: &Url) -> String { if redirect.scheme() == "file" && referrer.scheme() == "file" { // use a relative specifier when it's going to a file url relative_specifier(redirect, referrer) } else { redirect.to_string() } } fn relative_specifier(specifier: &Url, referrer: &Url) -> String { match referrer.make_relative(specifier) { Some(relative) => { if relative.starts_with('.') { relative } else { format!("./{}", relative) } } None => specifier.to_string(), } } fn import_map_resolves_dependency_key( import_map: &ImportMap, dependency_key: &str, resolved: &Url, referrer: &Url, ) -> bool { for entry in import_map.entries_for_referrer(referrer) { let Some(address) = entry.value else { continue; }; let raw_key = entry.raw_key; let is_match = dependency_key == raw_key || (raw_key.ends_with('/') && dependency_key.starts_with(raw_key)); if is_match && let Ok(specifier) = import_map.resolve(dependency_key, referrer) && specifier == *resolved && (dependency_key == raw_key || address.as_str().ends_with('/')) { return true; } } false } fn maybe_ambient_import_specifier( diagnostic: &DenoDiagnostic, ) -> Option<String> { match diagnostic { DenoDiagnostic::NoCache(url) | DenoDiagnostic::NoLocal(url) => { Some(url.to_string()) } DenoDiagnostic::ResolutionError(err, _) => { maybe_ambient_specifier_resolution_err(err) } _ => None, } } fn maybe_ambient_specifier_resolution_err( err: &ResolutionError, ) -> Option<String> { match err { ResolutionError::InvalidDowngrade { .. } | ResolutionError::InvalidJsrHttpsTypesImport { .. } | ResolutionError::InvalidLocalImport { .. } => None, ResolutionError::InvalidSpecifier { error, .. } => match error { SpecifierError::InvalidUrl(..) => None, SpecifierError::ImportPrefixMissing { specifier, .. } => { Some(specifier.to_string()) } }, ResolutionError::ResolverError { error, .. } => match &**error { ResolveError::Specifier(specifier_error) => match specifier_error { SpecifierError::InvalidUrl(..) => None, SpecifierError::ImportPrefixMissing { specifier, .. } => { Some(specifier.to_string()) } }, ResolveError::Other(err) => { let import_map_error = err.get_ref().downcast_ref::<import_map::ImportMapError>()?; match import_map_error.as_kind() { ImportMapErrorKind::UnmappedBareSpecifier(spec, _) => { Some(spec.clone()) } ImportMapErrorKind::JsonParse(_) | ImportMapErrorKind::ImportMapNotObject | ImportMapErrorKind::ImportsFieldNotObject | ImportMapErrorKind::ScopesFieldNotObject | ImportMapErrorKind::ScopePrefixNotObject(_) | ImportMapErrorKind::BlockedByNullEntry(_) | ImportMapErrorKind::SpecifierResolutionFailure { .. } | ImportMapErrorKind::SpecifierBacktracksAbovePrefix { .. } => None, } } }, } } #[allow(clippy::too_many_arguments, reason = "private code")] fn diagnose_resolution( snapshot: &language_server::StateSnapshot, dependency_key: &str, resolution: &Resolution, is_dynamic: bool, maybe_assert_type: Option<&str>, referrer_module: &DocumentModule, import_map: Option<&ImportMap>, ) -> (Vec<DenoDiagnostic>, Vec<DenoDiagnostic>) { let mut diagnostics = vec![]; let mut deferred_diagnostics = vec![]; match resolution { Resolution::Ok(resolved) => { let specifier = &resolved.specifier; let scoped_resolver = snapshot .resolver .get_scoped_resolver(referrer_module.scope.as_deref()); let managed_npm_resolver = scoped_resolver.as_maybe_managed_npm_resolver(); for (_, headers) in scoped_resolver.redirect_chain_headers(specifier) { if let Some(message) = headers.get("x-deno-warning") { diagnostics.push(DenoDiagnostic::DenoWarn(message.clone())); } } match snapshot.document_modules.module_for_specifier( specifier, referrer_module.scope.as_deref(), Some(&referrer_module.compiler_options_key), ) { Some(module) => { if let Some(headers) = &module.headers && let Some(message) = headers.get("x-deno-warning") { diagnostics.push(DenoDiagnostic::DenoWarn(message.clone())); } if module.media_type == MediaType::Json { match maybe_assert_type { // The module has the correct assertion type, no diagnostic Some("json" | "text" | "bytes") => (), // The dynamic import statement is missing an attribute type, which // we might not be able to statically detect, therefore we will // not provide a potentially incorrect diagnostic. None if is_dynamic => (), // The module has an incorrect assertion type, diagnostic Some(assert_type) => diagnostics.push( DenoDiagnostic::InvalidAttributeType(assert_type.to_string()), ), // The module is missing an attribute type, diagnostic None => diagnostics.push(DenoDiagnostic::NoAttributeType), } } } _ => { match JsrPackageReqReference::from_specifier(specifier) { Ok(pkg_ref) => { let req = pkg_ref.into_inner().req; diagnostics .push(DenoDiagnostic::NotInstalledJsr(req, specifier.clone())); } _ => { match NpmPackageReqReference::from_specifier(specifier) { Ok(pkg_ref) => { if let Some(npm_resolver) = managed_npm_resolver { // show diagnostics for npm package references that aren't cached let req = pkg_ref.req(); if !npm_resolver.is_pkg_req_folder_cached(req) { diagnostics.push(DenoDiagnostic::NotInstalledNpm( req.clone(), specifier.clone(), )); } else { let resolution_kinds = [ NodeResolutionKind::Types, NodeResolutionKind::Execution, ]; if resolution_kinds.into_iter().all(|k| { scoped_resolver .npm_to_file_url( &pkg_ref, &referrer_module.specifier, k, referrer_module.resolution_mode, ) .is_none() }) { diagnostics .push(DenoDiagnostic::NoExportNpm(pkg_ref.clone())); } } } } _ => { if let Some(module_name) = specifier.as_str().strip_prefix("node:") { if !deno_node::is_builtin_node_module(module_name) { diagnostics.push(DenoDiagnostic::UnknownNodeSpecifier( specifier.clone(), )); } else if module_name == dependency_key { let mut is_mapped = false; if let Some(import_map) = import_map && let Resolution::Ok(resolved) = &resolution && import_map .resolve(module_name, &resolved.specifier) .is_ok() { is_mapped = true; } // show diagnostics for bare node specifiers that aren't mapped by import map if !is_mapped { diagnostics.push(DenoDiagnostic::BareNodeSpecifier( module_name.to_string(), )); } } } else { // When the document is not available, it means that it cannot be found // in the cache or locally on the disk, so we want to issue a diagnostic // about that. // these may be invalid, however, if this is an ambient module with // no real source (as in the case of a virtual module). let deno_diagnostic = match specifier.scheme() { "file" => DenoDiagnostic::NoLocal(specifier.clone()), _ => DenoDiagnostic::NoCache(specifier.clone()), }; deferred_diagnostics.push(deno_diagnostic); } } } } } } } } // The specifier resolution resulted in an error, so we want to issue a // diagnostic for that. Resolution::Err(err) => { // Names of packages importable by bare specifier (workspace members and // packages linked via the "links" field) in the referrer's scope, used // to enhance the error with a better hint. let bare_importable_pkg_names = snapshot .config .tree .data_for_specifier(&referrer_module.specifier) .map(|d| { d.member_dir .workspace .resolver_jsr_pkgs() .map(|pkg| pkg.name) .collect::<Vec<_>>() }) .unwrap_or_default(); if maybe_ambient_specifier_resolution_err(err).is_none() { diagnostics.push(DenoDiagnostic::ResolutionError( *err.clone(), bare_importable_pkg_names, )) } else { deferred_diagnostics.push(DenoDiagnostic::ResolutionError( *err.clone(), bare_importable_pkg_names, )); } } _ => (), } (diagnostics, deferred_diagnostics) } /// Surface an unresolved import of a `.d.ts` entrypoint as a resolution error. /// /// A `.d.ts` document is itself an entrypoint in the editor, so its own /// unresolved imports should be reported (e.g. `TS2307` from `deno check`). /// However deno_graph records a bare specifier in a `.d.ts` as /// `Resolution::None` (a `.ts` file records `Resolution::Err`), which /// `diagnose_resolution()` ignores, and tsc skips them under `skipLibCheck`. So /// handle them explicitly here, mirroring `cli/type_checker.rs`. fn diagnose_dts_entrypoint_unresolved_import( diagnostics: &mut Vec<lsp::Diagnostic>, snapshot: &language_server::StateSnapshot, referrer_module: &DocumentModule, dependency: &deno_graph::Dependency, ) { // Only handle imports deno_graph left fully unresolved; a bare specifier in a // `.d.ts` lands here as `None`/`None`. Resolved (`Ok`) and errored (`Err`) // deps are already surfaced by `diagnose_resolution()`, so handling them here // too would double report. if !matches!(dependency.maybe_code, Resolution::None) || !matches!(dependency.maybe_type, Resolution::None) { return; } // Names of packages importable by bare specifier (workspace members and // packages linked via the "links" field) in the referrer's scope, used to // enhance the error with a better hint. let bare_importable_pkg_names = snapshot .config .tree .data_for_specifier(&referrer_module.specifier) .map(|d| { d.member_dir .workspace .resolver_jsr_pkgs() .map(|pkg| pkg.name) .collect::<Vec<_>>() }) .unwrap_or_default(); for import in &dependency.imports { // Only surface real `import`/`export`/`import =` statements. Triple slash // `/// <reference />` directives, JSDoc and `@jsxImportSource` imports are // intentionally left to tsc's `skipLibCheck` handling. if !matches!( import.kind, deno_graph::ImportKind::Es | deno_graph::ImportKind::TsType | deno_graph::ImportKind::Require ) { continue; } // A bare specifier in a `.d.ts` lands here as fully unresolved, so building // the resolution error reuses the same message and quick fixes as a `.ts` // file's `Resolution::Err`. if let Err(error) = deno_path_util::resolve_import( &import.specifier, &referrer_module.specifier, ) { let resolution_error = ResolutionError::InvalidSpecifier { error, range: import.specifier_range.clone(), }; diagnostics.push( DenoDiagnostic::ResolutionError( resolution_error, bare_importable_pkg_names.clone(), ) .to_lsp_diagnostic(&language_server::to_lsp_range( &import.specifier_range, )), ); } } } /// Generate diagnostics related to a dependency. The dependency is analyzed to /// determine if it can be remapped to the active import map as well as surface /// any diagnostics related to the resolved code or type dependency. fn diagnose_dependency( diagnostics: &mut Vec<lsp::Diagnostic>, deferred_diagnostics: &mut Vec<(String, lsp::Diagnostic)>, snapshot: &language_server::StateSnapshot, referrer_module: &DocumentModule, dependency_key: &str, dependency: &deno_graph::Dependency, ) { if snapshot .resolver .in_node_modules(&referrer_module.specifier) { return; // ignore, surface typescript errors instead } if referrer_module.media_type.is_declaration() { // A `.d.ts` document opened in the editor is itself an entrypoint, so its // own unresolved imports should be surfaced (matching `deno check`, which // reports `TS2307` for them). This must happen regardless of `skipLibCheck` // and before the early return below, mirroring `cli/type_checker.rs`. diagnose_dts_entrypoint_unresolved_import( diagnostics, snapshot, referrer_module, dependency, ); let compiler_options_data = snapshot .compiler_options_resolver .for_key(&referrer_module.compiler_options_key); if compiler_options_data.is_none() { lsp_warn!( "Key was not in sync with resolver while checking `skipLibCheck`. This should be impossible." ); #[cfg(debug_assertions)] unreachable!(); } if compiler_options_data.is_some_and(|d| d.skip_lib_check) { return; } } let import_map = snapshot .resolver .get_scoped_resolver(referrer_module.scope.as_deref()) .as_workspace_resolver() .maybe_import_map(); if let Some(import_map) = import_map { let resolved = dependency .maybe_code .ok() .or_else(|| dependency.maybe_type.ok()); if let Some(resolved) = resolved && let Some(to) = import_map_lookup_for_diagnostic( import_map, &resolved.specifier, &referrer_module.specifier, ) && dependency_key != to && !import_map_resolves_dependency_key( import_map, dependency_key, &resolved.specifier, &referrer_module.specifier, ) { diagnostics.push( DenoDiagnostic::ImportMapRemap { from: dependency_key.to_string(), to, } .to_lsp_diagnostic(&language_server::to_lsp_range(&resolved.range)), ); } } let import_ranges: Vec<_> = dependency .imports .iter() .map(|i| language_server::to_lsp_range(&i.specifier_range)) .collect(); // TODO(nayeemrmn): This is a crude way of detecting `@ts-types` which has // a different specifier and therefore needs a separate call to // `diagnose_resolution()`. It would be much cleaner if that were modelled as // a separate dependency: https://github.com/denoland/deno_graph/issues/247. let is_types_deno_types = !dependency.maybe_type.is_none() && !dependency.imports.iter().any(|i| { dependency .maybe_type .includes(i.specifier_range.range.start) .is_some() }); let resolution = if dependency.maybe_code.is_none() // If not @ts-types, diagnose the types if the code errored because // it's likely resolving into the node_modules folder, which might be // erroring correctly due to resolution only being for bundlers. Let this // fail at runtime if necessary, but don't bother erroring in the editor || !is_types_deno_types && matches!(dependency.maybe_type, Resolution::Ok(_)) && matches!(dependency.maybe_code, Resolution::Err(_)) { &dependency.maybe_type } else { &dependency.maybe_code }; let (resolution_diagnostics, deferred) = diagnose_resolution( snapshot, dependency_key, resolution, dependency.is_dynamic, dependency.maybe_attribute_type.as_deref(), referrer_module, import_map, ); diagnostics.extend(resolution_diagnostics.iter().flat_map(|diag| { import_ranges .iter() .map(|range| diag.to_lsp_diagnostic(range)) })); deferred_diagnostics.extend( deferred .iter() .filter_map(|diag| { maybe_ambient_import_specifier(diag).map(|spec| { import_ranges .iter() .map(move |range| (spec.clone(), diag.to_lsp_diagnostic(range))) }) }) .flatten(), ); if is_types_deno_types { let range = match &dependency.maybe_type { Resolution::Ok(resolved) => { language_server::to_lsp_range(&resolved.range) } Resolution::Err(error) => language_server::to_lsp_range(error.range()), Resolution::None => unreachable!(), }; let (resolution_diagnostics, deferred) = diagnose_resolution( snapshot, dependency_key, &dependency.maybe_type, dependency.is_dynamic, dependency.maybe_attribute_type.as_deref(), referrer_module, import_map, ); diagnostics.extend( resolution_diagnostics .iter() .map(|diag| diag.to_lsp_diagnostic(&range)), ); deferred_diagnostics.extend(Box::new(deferred.iter().filter_map(|diag| { maybe_ambient_import_specifier(diag) .map(|spec| (spec, diag.to_lsp_diagnostic(&range))) }))); } } fn import_map_diagnostic_range( document: &OpenDocument, text: &str, maybe_text: Option<&str>, ) -> lsp::Range { if let Some(text_to_find) = maybe_text && let Some(index) = text.find(text_to_find) { let start = TextSize::from(index as u32); let end = TextSize::from((index + text_to_find.len()) as u32); return lsp::Range { start: document.line_index.position_utf16(start), end: document.line_index.position_utf16(end), }; } lsp::Range::default() } fn json_error_range(err: &serde_json::Error) -> lsp::Range { let line = err.line().saturating_sub(1) as u32; let character = err.column().saturating_sub(1) as u32; lsp::Range { start: lsp::Position { line, character }, end: lsp::Position { line, character }, } } fn import_map_parse_diagnostic_to_lsp( diagnostic: &ImportMapDiagnostic, ) -> lsp::Diagnostic { DenoDiagnostic::ImportMapDiagnostic(diagnostic.to_string()) .to_lsp_diagnostic(&lsp::Range::default()) } fn diagnose_import_map_entry_value( diagnostics: &mut Vec<lsp::Diagnostic>, document: &OpenDocument, text: &str, scope: &ModuleSpecifier, snapshot: &StateSnapshot, raw_value: Option<&str>, value: Option<&ModuleSpecifier>, ) { let Some(value) = value else { return; }; let diagnostic = match value.scheme() { "http" | "https" => { let http_cache = snapshot.cache.for_specifier(Some(scope)); if http_cache.contains(value) { return; } DenoDiagnostic::NoCache(value.clone()) } "jsr" => { let Ok(pkg_ref) = JsrPackageReqReference::from_specifier(value) else { return; }; let scoped_resolver = snapshot.resolver.get_scoped_resolver(Some(scope)); if scoped_resolver.jsr_to_resource_url(&pkg_ref).is_some() { return; } let req = pkg_ref.into_inner().req; DenoDiagnostic::NotInstalledJsr(req, value.clone()) } "npm" => { let Ok(pkg_ref) = NpmPackageReqReference::from_specifier(value) else { return; }; let req = pkg_ref.req().clone(); let scoped_resolver = snapshot.resolver.get_scoped_resolver(Some(scope)); if scoped_resolver .as_maybe_managed_npm_resolver() .is_some_and(|npm_resolver| npm_resolver.is_pkg_req_folder_cached(&req)) { return; } DenoDiagnostic::NotInstalledNpm(req, value.clone()) } _ => return, }; let range = import_map_diagnostic_range(document, text, raw_value); diagnostics.push(diagnostic.to_lsp_diagnostic(&range)); } pub(crate) fn generate_import_map_diagnostics( document: &Arc<OpenDocument>, snapshot: &StateSnapshot, ) -> Vec<lsp::Diagnostic> { let specifier = uri_to_url(&document.uri); let Some((scope, ConfigWatchedFileType::ImportMap)) = snapshot.config.tree.watched_file_type(&specifier) else { return Vec::new(); }; let text = document.text.as_ref(); let parse_result = import_map::parse_from_json(specifier.clone(), text); match parse_result { Ok(import_map) => { let mut diagnostics = import_map .diagnostics .iter() .map(import_map_parse_diagnostic_to_lsp) .collect::<Vec<_>>(); for entry in import_map.import_map.imports().entries() { diagnose_import_map_entry_value( &mut diagnostics, document, text, scope, snapshot, entry.raw_value, entry.value, ); } for scope_entry in import_map.import_map.scopes() { for entry in scope_entry.imports.entries() { diagnose_import_map_entry_value( &mut diagnostics, document, text, scope, snapshot, entry.raw_value, entry.value, ); } } diagnostics } Err(err) => match err.as_kind() { ImportMapErrorKind::JsonParse(err) => vec![ DenoDiagnostic::ImportMapDiagnostic(format!( "Unable to parse import map JSON: {err}" )) .to_lsp_diagnostic(&json_error_range(err)), ], _ => vec![ DenoDiagnostic::ImportMapDiagnostic(err.to_string()) .to_lsp_diagnostic(&lsp::Range::default()), ], }, } } async fn publish_document_diagnostics( document: &Arc<OpenDocument>, diagnostics: Vec<lsp::Diagnostic>, client: &Client, state: &DiagnosticsState, _token: &CancellationToken, ) { state.update(&document.uri, document.version, &diagnostics); client .publish_diagnostics( document.uri.as_ref().clone(), diagnostics, Some(document.version), ) .await; } async fn generate_document_diagnostics( document: &Arc<OpenDocument>, snapshot: &Arc<StateSnapshot>, ts_server: &TsServer, ambient_modules_regex_cache: &OnceCellMap< (CompilerOptionsKey, Option<Arc<Uri>>), Option<regex::Regex>, >, no_slow_types_cache: &NoSlowTypesDiagnosticsCache, token: &CancellationToken, ) -> Result<Vec<lsp::Diagnostic>, AnyError> { let mut import_map_diagnostics = generate_import_map_diagnostics(document, snapshot); if !document.is_diagnosable() { return Ok(import_map_diagnostics); } let document_ = Document::Open(document.clone()); let module = match snapshot.document_modules.primary_module(&document_) { Some(module) => module, None => { let url = uri_to_url(document_.uri()); if url.scheme() == "file" && !snapshot.resolver.in_node_modules(&url) && !snapshot.cache.in_cache_directory(&url) { return Ok(Vec::new()); } // If this document represents a non-local module, the module may not be // retrievable until its referrer is known through some other request. // Wait and try one more time. tokio::time::sleep(std::time::Duration::from_millis(200)).await; let Some(module) = snapshot.document_modules.primary_module(&document_) else { return Ok(Vec::new()); }; module } }; if !snapshot.config.specifier_enabled(&module.specifier) { return Ok(Vec::new()); } generate_module_diagnostics( &module, snapshot, ts_server, ambient_modules_regex_cache, no_slow_types_cache, token, ) .await .map(|mut diagnostics| { import_map_diagnostics.append(&mut diagnostics); import_map_diagnostics }) } pub async fn generate_module_diagnostics( module: &Arc<DocumentModule>, snapshot: &Arc<StateSnapshot>, ts_server: &TsServer, ambient_modules_regex_cache: &OnceCellMap< (CompilerOptionsKey, Option<Arc<Uri>>), Option<regex::Regex>, >, no_slow_types_cache: &NoSlowTypesDiagnosticsCache, token: &CancellationToken, ) -> Result<Vec<lsp::Diagnostic>, AnyError> { let deps_handle = tokio::task::spawn_blocking({ let snapshot = snapshot.clone(); let module = module.clone(); let token = token.clone(); move || { let mut diagnostics = Vec::new(); let mut deferred = Vec::new(); for (dependency_key, dependency) in module.dependencies.iter() { if token.is_cancelled() { return (Vec::new(), Vec::new()); } diagnose_dependency( &mut diagnostics, &mut deferred, &snapshot, &module, dependency_key, dependency, ); } (diagnostics, deferred) } }); let lint_handle = tokio::task::spawn_blocking({ let snapshot = snapshot.clone(); let module = module.clone(); let token = token.clone(); move || -> Arc<Vec<lsp::Diagnostic>> { // TODO(nayeemrmn): Support linting notebooks cells. Will require // stitching cells from the same notebook into one module, linting it // and then splitting/relocating the diagnostics to each cell. if token.is_cancelled() || module.notebook_uri.is_some() || module.specifier.scheme() != "file" || snapshot.resolver.in_node_modules(&module.specifier) { return Default::default(); } let settings = snapshot .config .workspace_settings_for_specifier(&module.specifier); if !settings.lint { return Default::default(); } let linter = snapshot.linter_resolver.for_module(&module); generate_document_lint_diagnostics(&module, &linter, token) .unwrap_or_default() } }); let doc_handle = tokio::task::spawn_blocking({ let snapshot = snapshot.clone(); let module = module.clone(); let token = token.clone(); move || -> Arc<Vec<lsp::Diagnostic>> { if token.is_cancelled() || module.notebook_uri.is_some() || module.specifier.scheme() != "file" || snapshot.resolver.in_node_modules(&module.specifier) { return Default::default(); } let settings = snapshot .config .workspace_settings_for_specifier(&module.specifier); if !settings.lint { return Default::default(); } if !module_is_package_export(&snapshot, &module) { return Default::default(); } generate_document_doc_diagnostics(&module, &token).unwrap_or_default() } }); let mut diagnostics = ts_server .provide_diagnostics(module, snapshot.clone(), token) .await?; let (deps_diagnostics, deferred_deps_diagnostics) = deps_handle .await .inspect_err(|err| { lsp_warn!("Deps diagnostics task join error: {err:#}"); }) .unwrap_or_default(); diagnostics.extend(deps_diagnostics); let ambient_modules_regex_cell = ambient_modules_regex_cache .entry(( module.compiler_options_key.clone(), module.notebook_uri.clone(), )) .or_default() .clone(); let ambient_modules_regex = ambient_modules_regex_cell .get_or_init(async || { ts_server .get_ambient_modules( &module.compiler_options_key, module.notebook_uri.as_ref(), snapshot.clone(), token, ) .await .inspect_err(|err| { if !token.is_cancelled() { lsp_warn!("Unable to get ambient modules: {:#}", err); } }) .ok() .filter(|a| !a.is_empty()) .and_then(|ambient_modules| { let regex_string = ambient_modules_to_regex_string(&ambient_modules); regex::Regex::new(®ex_string).inspect_err(|err| { lsp_warn!("Failed to compile ambient modules pattern: {err:#} (pattern is {regex_string:?})"); }).ok() }) }).await; if let Some(ambient_modules_regex) = ambient_modules_regex { diagnostics.extend(deferred_deps_diagnostics.into_iter().filter_map( |(import_url, diag)| { if ambient_modules_regex.is_match(import_url.as_str()) { return None; } Some(diag) }, )); } else { diagnostics.extend(deferred_deps_diagnostics.into_iter().map(|(_, d)| d)); } let lint_diagnostics = lint_handle .await .inspect_err(|err| { lsp_warn!("Lint task join error: {err:#}"); }) .unwrap_or_default(); diagnostics.extend(lint_diagnostics.iter().cloned()); let doc_diagnostics = doc_handle .await .inspect_err(|err| { lsp_warn!("Doc lint task join error: {err:#}"); }) .unwrap_or_default(); diagnostics.extend(doc_diagnostics.iter().cloned()); diagnostics.extend( generate_no_slow_types_diagnostics(module, snapshot, no_slow_types_cache) .await, ); Ok(diagnostics) } /// Returns the `no-slow-types` (fast check) diagnostics that land on `module`. /// /// These diagnostics are reported by `deno publish` / `deno lint` for /// publishable JSR packages and require a fully built fast check module graph /// of the whole package. Because a single slow type can produce diagnostics on /// any module reachable from a package's exports, the graph is built once per /// package scope (cached in `no_slow_types_cache`) and the resulting /// diagnostics are distributed to the document they point at. async fn generate_no_slow_types_diagnostics( module: &Arc<DocumentModule>, snapshot: &Arc<StateSnapshot>, no_slow_types_cache: &NoSlowTypesDiagnosticsCache, ) -> Vec<lsp::Diagnostic> { if module.specifier.scheme() != "file" || module.notebook_uri.is_some() || snapshot.resolver.in_node_modules(&module.specifier) { return Vec::new(); } let settings = snapshot .config .workspace_settings_for_specifier(&module.specifier); if !settings.lint { return Vec::new(); } let Some(config_data) = snapshot.config.tree.data_for_specifier(&module.specifier) else { return Vec::new(); }; // Only publishable JSR packages have a public API to check for slow types. if config_data.member_dir.jsr_packages_for_publish().is_empty() { return Vec::new(); } // Respect the lint configuration: if `no-slow-types` is excluded (or not // otherwise enabled) for this document, don't report its diagnostics. This // keeps the LSP in sync with `deno lint`, which filters the rule out. let linter = snapshot.linter_resolver.for_module(module); if !linter.inner.has_package_rule("no-slow-types") { return Vec::new(); } let scope = config_data.scope.clone(); let cell = no_slow_types_cache .entry(scope.clone()) .or_default() .clone(); let diagnostics_by_specifier = cell .get_or_init(|| { let snapshot = snapshot.clone(); async move { Arc::new( tokio::task::spawn_blocking(move || { collect_scope_no_slow_types_diagnostics(&snapshot, &scope) }) .await .inspect_err(|err| { lsp_warn!("No-slow-types diagnostics task join error: {err:#}"); }) .unwrap_or_default(), ) } }) .await; diagnostics_by_specifier .get(&module.specifier) .cloned() .unwrap_or_default() } /// Builds the fast check graph for the publishable packages within `scope` and /// collects the resulting `no-slow-types` diagnostics, grouped by the specifier /// of the module each one points at. Runs on a blocking thread since building /// the graph involves I/O and uses `!Send` types internally. fn collect_scope_no_slow_types_diagnostics( snapshot: &Arc<StateSnapshot>, scope: &ModuleSpecifier, ) -> HashMap<ModuleSpecifier, Vec<lsp::Diagnostic>> { let result = (|| -> Result<_, AnyError> { let Some(config_data) = snapshot.config.tree.data_for_specifier(scope) else { return Ok(HashMap::new()); }; let packages = config_data.member_dir.jsr_packages_for_publish(); if packages.is_empty() { return Ok(HashMap::new()); } let workspace_settings = snapshot.config.workspace_settings_for_specifier(scope); let mut factory = CliFactory::from_flags(Arc::new(Flags { internal: InternalFlags { cache_path: Some(snapshot.cache.deno_dir().root.clone()), lockfile_skip_write: true, ..Default::default() }, ca_stores: workspace_settings.certificate_stores.clone(), ca_data: workspace_settings.tls_certificate.clone().map(CaData::File), unsafely_ignore_certificate_errors: workspace_settings .unsafely_ignore_certificate_errors .clone(), type_check_mode: TypeCheckMode::None, permissions: PermissionFlags { // allow remote import permissions in the lsp for now allow_import: Some(vec![]), ..Default::default() }, ..Default::default() })); factory.set_initial_cwd(config_data.member_dir.dir_path()); factory.set_workspace_dir(config_data.member_dir.clone()); let runtime = create_basic_runtime(); let local_set = tokio::task::LocalSet::new(); local_set.block_on(&runtime, async move { let module_graph_creator = factory.module_graph_creator().await?; let graph = module_graph_creator .create_publish_graph(CreatePublishGraphOptions { packages: &packages, build_fast_check_graph: true, validate_graph: false, skip_unanalyzable_exports: true, }) .await?; let mut diagnostics_by_specifier: HashMap< ModuleSpecifier, Vec<lsp::Diagnostic>, > = HashMap::new(); for package in &packages { let export_urls = package.config_file.resolve_export_value_urls()?; for diagnostic in collect_no_slow_type_diagnostics(&graph, &export_urls) { if let Some(lsp_diagnostic) = no_slow_types_diagnostic_to_lsp(&diagnostic) { diagnostics_by_specifier .entry(diagnostic.specifier().clone()) .or_default() .push(lsp_diagnostic); } } } Ok(diagnostics_by_specifier) }) })(); result.unwrap_or_else(|err: AnyError| { lsp_warn!("Failed to generate no-slow-types diagnostics: {err:#}"); HashMap::new() }) } fn no_slow_types_diagnostic_to_lsp( diagnostic: &FastCheckDiagnostic, ) -> Option<lsp::Diagnostic> { let range = diagnostic.range()?; let mut message = diagnostic.message().to_string(); if let Some(hint) = diagnostic.hint() { message.push_str("\n\n"); message.push_str(&hint); } Some(lsp::Diagnostic { range: analysis::source_range_to_lsp_range(&range.range, &range.text_info), severity: Some(lsp::DiagnosticSeverity::WARNING), code: Some(lsp::NumberOrString::String("no-slow-types".to_string())), code_description: diagnostic .docs_url() .and_then(|url| Uri::from_str(&url).ok()) .map(|href| lsp::CodeDescription { href }), source: Some(DiagnosticSource::Lint.as_lsp_source().to_string()), message, related_information: None, tags: None, data: None, }) } /// Returns whether the module is an export entrypoint of a publishable /// (JSR) package in the workspace, i.e. a file that `deno doc --lint` and /// `deno publish` would check for documentation issues. fn module_is_package_export( snapshot: &StateSnapshot, module: &DocumentModule, ) -> bool { let Some(config_file) = snapshot .config .tree .data_for_specifier(&module.specifier) .and_then(|d| d.maybe_deno_json()) else { return false; }; if !config_file.is_package() || !config_file.should_publish() { return false; } match config_file.resolve_export_value_urls() { Ok(urls) => urls.iter().any(|url| url == module.specifier.as_ref()), Err(err) => { lsp_warn!( "Failed to resolve export URLs for \"{}\": {err:#}", config_file.specifier, ); false } } } #[cfg(test)] mod tests { use std::str::FromStr; use std::sync::Arc; use deno_config::deno_json::ConfigFile; use pretty_assertions::assert_eq; use test_util::TempDir; use super::*; use crate::lsp::cache::LspCache; use crate::lsp::compiler_options::LspCompilerOptionsResolver; use crate::lsp::config::Config; use crate::lsp::config::WorkspaceSettings; use crate::lsp::documents::DocumentModules; use crate::lsp::documents::LanguageId; use crate::lsp::language_server::StateSnapshot; use crate::lsp::lint::LspLinterResolver; use crate::lsp::resolver::LspResolver; use crate::lsp::urls::url_to_uri; async fn setup( sources: &[(&str, &str, i32, LanguageId)], maybe_import_map: Option<(&str, &str)>, ) -> (TempDir, StateSnapshot) { let temp_dir = TempDir::new(); let root_url = temp_dir.url(); let cache = LspCache::new(Some(root_url.join(".deno_dir").unwrap())); let mut config = Config::new_with_roots([root_url.clone()]); let enabled_settings = serde_json::from_str::<WorkspaceSettings>(r#"{ "enable": true }"#) .unwrap(); config.set_workspace_settings( enabled_settings.clone(), vec![(Arc::new(root_url.clone()), enabled_settings)], ); if let Some((relative_path, json_string)) = maybe_import_map { let base_url = root_url.join(relative_path).unwrap(); let config_file = ConfigFile::new(json_string, base_url).unwrap(); config.tree.inject_config_file(config_file).await; } let resolver = Arc::new(LspResolver::from_config(&config, &cache, None).await); let compiler_options_resolver = Arc::new(LspCompilerOptionsResolver::new(&config, &resolver, None)); resolver.set_compiler_options_resolver(&compiler_options_resolver.inner); let linter_resolver = Arc::new(LspLinterResolver::new( &config, &compiler_options_resolver, &resolver, )); let mut document_modules = DocumentModules::default(); document_modules.update_config( &config, &compiler_options_resolver, &resolver, &cache, &Default::default(), ); for (relative_path, source, version, language_id) in sources { let specifier = root_url.join(relative_path).unwrap(); document_modules.open_document( url_to_uri(&specifier).unwrap(), *version, *language_id, (*source).into(), None, ); } ( temp_dir, StateSnapshot { project_version: 0, document_modules, config: Arc::new(config), compiler_options_resolver, linter_resolver, resolver, cache: Arc::new(cache), client_needs_file_uris_for_virtual_documents: false, }, ) } fn generate_all_deno_diagnostics( snapshot: &StateSnapshot, ) -> Vec<(Uri, Vec<lsp::Diagnostic>)> { snapshot .document_modules .documents .open_docs() .filter_map(|doc| { if !doc.is_diagnosable() { return None; } let module = snapshot .document_modules .primary_module(&Document::Open(doc.clone()))?; if !snapshot.config.specifier_enabled(&module.specifier) { return None; } let mut diagnostics = Vec::new(); let mut deferred = Vec::new(); for (dependency_key, dependency) in module.dependencies.iter() { diagnose_dependency( &mut diagnostics, &mut deferred, snapshot, &module, dependency_key, dependency, ); } diagnostics.extend(deferred.into_iter().map(|(_, d)| d)); Some((doc.uri.as_ref().clone(), diagnostics)) }) .collect::<Vec<_>>() } fn generate_all_import_map_diagnostics( snapshot: &StateSnapshot, ) -> Vec<(Uri, Vec<lsp::Diagnostic>)> { snapshot .document_modules .documents .open_docs() .map(|doc| { ( doc.uri.as_ref().clone(), generate_import_map_diagnostics(doc, snapshot), ) }) .collect::<Vec<_>>() } #[tokio::test] async fn test_import_map_document_diagnostics() { let (temp_dir, snapshot) = setup( &[( "import-map.json", r#"{ "imports": { "remote": "https://deno.land/x/example/mod.ts", "bad": null }, "unexpected": true }"#, 1, LanguageId::Json, )], Some(( "deno.json", r#"{ "importMap": "./import-map.json" }"#, )), ) .await; let actual = generate_all_import_map_diagnostics(&snapshot); assert_eq!( json!(actual), json!([ [ url_to_uri(&temp_dir.url().join("import-map.json").unwrap()).unwrap(), [ { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 }, }, "severity": 1, "code": "import-map-diagnostic", "source": "deno", "message": "Invalid address \"null\" for the specifier key \"bad\". Addresses must be strings.", }, { "range": { "start": { "line": 0, "character": 0 }, "end": { "line": 0, "character": 0 }, }, "severity": 1, "code": "import-map-diagnostic", "source": "deno", "message": "Invalid top-level key \"unexpected\". Only \"imports\" and \"scopes\" can be present.", }, { "range": { "start": { "line": 2, "character": 15 }, "end": { "line": 2, "character": 49 }, }, "severity": 1, "code": "no-cache", "source": "deno", "message": "Uncached or missing remote URL: https://deno.land/x/example/mod.ts", "data": { "specifier": "https://deno.land/x/example/mod.ts", }, }, ], ], ]), ); } #[tokio::test] async fn test_import_map_document_json_parse_diagnostic() { let (temp_dir, snapshot) = setup( &[("import-map.json", r#"{ "imports": "#, 1, LanguageId::Json)], Some(( "deno.json", r#"{ "importMap": "./import-map.json" }"#, )), ) .await; let actual = generate_all_import_map_diagnostics(&snapshot); assert_eq!( json!(actual), json!([ [ url_to_uri(&temp_dir.url().join("import-map.json").unwrap()).unwrap(), [ { "range": { "start": { "line": 0, "character": 12 }, "end": { "line": 0, "character": 12 }, }, "severity": 1, "code": "import-map-diagnostic", "source": "deno", "message": "Unable to parse import map JSON: EOF while parsing a value at line 1 column 13", }, ], ], ]), ); } #[tokio::test] async fn test_deno_diagnostics_with_import_map() { let (temp_dir, snapshot) = setup( &[ ( "std/assert/mod.ts", "export function assert() {}", 1, LanguageId::TypeScript, ), ( "a/file.ts", "import { assert } from \"../std/assert/mod.ts\";\n\nassert();\nexport function a() {}", 1, LanguageId::TypeScript, ), ( "a/file2.ts", "import { a } from './file.ts';\nconsole.log(a);\n", 1, LanguageId::TypeScript, ), ], Some(( "a/deno.json", r#"{ "imports": { "/~/std/": "../std/", "a/": "../a/", "$@/": "./", "$a/": "../a/" } }"#, )), ) .await; let actual = generate_all_deno_diagnostics(&snapshot); assert_eq!( json!(actual), json!([ [ url_to_uri(&temp_dir.url().join("std/assert/mod.ts").unwrap()).unwrap(), [], ], [ url_to_uri(&temp_dir.url().join("a/file.ts").unwrap()).unwrap(), [ { "range": { "start": { "line": 0, "character": 23 }, "end": { "line": 0, "character": 45 }, }, "severity": 4, "code": "import-map-remap", "source": "deno", "message": "The import specifier can be remapped to \"/~/std/assert/mod.ts\" which will resolve it via the active import map.", "data": { "from": "../std/assert/mod.ts", "to": "/~/std/assert/mod.ts", }, }, ], ], [ url_to_uri(&temp_dir.url().join("a/file2.ts").unwrap()).unwrap(), [], ], ]), ); } #[test] fn test_get_code_action_import_map_remap() { let uri = Uri::from_str("file:///a/file.ts").unwrap(); let specifier = ModuleSpecifier::parse("file:///a/file.ts").unwrap(); let result = DenoDiagnostic::get_code_action(&uri, &specifier, &lsp::Diagnostic { range: lsp::Range { start: lsp::Position { line: 0, character: 23 }, end: lsp::Position { line: 0, character: 50 }, }, severity: Some(lsp::DiagnosticSeverity::HINT), code: Some(lsp::NumberOrString::String("import-map-remap".to_string())), source: Some("deno".to_string()), message: "The import specifier can be remapped to \"/~/std/assert/mod.ts\" which will resolve it via the active import map.".to_string(), data: Some(json!({ "from": "../std/assert/mod.ts", "to": "/~/std/assert/mod.ts" })), ..Default::default() }); assert!(result.is_ok()); let actual = result.unwrap(); assert_eq!( json!(actual), json!({ "title": "Update \"../std/assert/mod.ts\" to \"/~/std/assert/mod.ts\" to use import map.", "kind": "quickfix", "diagnostics": [ { "range": { "start": { "line": 0, "character": 23 }, "end": { "line": 0, "character": 50 } }, "severity": 4, "code": "import-map-remap", "source": "deno", "message": "The import specifier can be remapped to \"/~/std/assert/mod.ts\" which will resolve it via the active import map.", "data": { "from": "../std/assert/mod.ts", "to": "/~/std/assert/mod.ts" } } ], "edit": { "changes": { "file:///a/file.ts": [ { "range": { "start": { "line": 0, "character": 23 }, "end": { "line": 0, "character": 50 } }, "newText": "\"/~/std/assert/mod.ts\"" } ] } } }) ); } #[tokio::test] async fn duplicate_diagnostics_for_duplicate_imports() { let (temp_dir, snapshot) = setup( &[( "a.ts", r#" // @ts-types="bad.d.ts" import "bad.js"; import "bad.js"; "#, 1, LanguageId::TypeScript, )], None, ) .await; let actual = generate_all_deno_diagnostics(&snapshot); assert_eq!( json!(actual), json!([ [ url_to_uri(&temp_dir.url().join("a.ts").unwrap()).unwrap(), [ { "range": { "start": { "line": 2, "character": 15 }, "end": { "line": 2, "character": 23 }, }, "severity": 1, "code": "import-prefix-missing", "source": "deno", "message": "Import \"bad.js\" not a dependency", }, { "range": { "start": { "line": 3, "character": 15 }, "end": { "line": 3, "character": 23 }, }, "severity": 1, "code": "import-prefix-missing", "source": "deno", "message": "Import \"bad.js\" not a dependency", }, { "range": { "start": { "line": 1, "character": 21 }, "end": { "line": 1, "character": 31 }, }, "severity": 1, "code": "import-prefix-missing", "source": "deno", "message": "Import \"bad.d.ts\" not a dependency", }, ], ], ]), ); } #[tokio::test] async fn unable_to_load_a_local_module() { let (temp_dir, snapshot) = setup( &[( "a.ts", r#" import { 東京 } from "./🦕.ts"; "#, 1, LanguageId::TypeScript, )], None, ) .await; let actual = generate_all_deno_diagnostics(&snapshot); assert_eq!( json!(actual), json!([ [ url_to_uri(&temp_dir.url().join("a.ts").unwrap()).unwrap(), [ { "range": { "start": { "line": 1, "character": 27 }, "end": { "line": 1, "character": 35 }, }, "severity": 1, "code": "no-local", "source": "deno", "message": format!( "Unable to load a local module: {}🦕.ts\nPlease check the file path.", temp_dir.url(), ), }, ], ], ]), ); } #[test] fn test_specifier_text_for_redirected() { #[track_caller] fn run_test(specifier: &str, referrer: &str, expected: &str) { let result = specifier_text_for_redirected( &ModuleSpecifier::parse(specifier).unwrap(), &ModuleSpecifier::parse(referrer).unwrap(), ); assert_eq!(result, expected); } run_test("file:///a/a.ts", "file:///a/mod.ts", "./a.ts"); run_test("file:///a/a.ts", "file:///a/sub_dir/mod.ts", "../a.ts"); run_test( "file:///a/sub_dir/a.ts", "file:///a/mod.ts", "./sub_dir/a.ts", ); run_test( "https://deno.land/x/example/mod.ts", "file:///a/sub_dir/a.ts", "https://deno.land/x/example/mod.ts", ); } }