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",
}
}
}
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();
}
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> {
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 {
2695 | 6133 | 6138 | 6192 | 6196 | 6198 | 6199 | 6205 | 7027
| 7028 => Some(vec![lsp::DiagnosticTag::UNNECESSARY]),
2789 | 6385 | 6387 => Some(vec![lsp::DiagnosticTag::DEPRECATED]),
_ => None,
},
data: None,
})
} else {
None
}
})
.collect()
}
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())
}
}
}
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 {
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)),
}
}
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());
}
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()
.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,
}
pub enum DenoDiagnostic {
DenoWarn(String),
ImportMapDiagnostic(String),
ImportMapRemap { from: String, to: String },
InvalidAttributeType(String),
NoAttributeType,
NoCache(ModuleSpecifier),
NotInstalledJsr(PackageReq, ModuleSpecifier),
NotInstalledNpm(PackageReq, ModuleSpecifier),
NoExportNpm(NpmPackageReqReference),
NoLocal(ModuleSpecifier),
ResolutionError(deno_graph::ResolutionError, Vec<String>),
UnknownNodeSpecifier(ModuleSpecifier),
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",
}
}
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."))
}
}
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
}
}
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" {
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 {
Some("json" | "text" | "bytes") => (),
None if is_dynamic => (),
Some(assert_type) => diagnostics.push(
DenoDiagnostic::InvalidAttributeType(assert_type.to_string()),
),
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 {
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;
}
if !is_mapped {
diagnostics.push(DenoDiagnostic::BareNodeSpecifier(
module_name.to_string(),
));
}
}
} else {
let deno_diagnostic = match specifier.scheme() {
"file" => DenoDiagnostic::NoLocal(specifier.clone()),
_ => DenoDiagnostic::NoCache(specifier.clone()),
};
deferred_diagnostics.push(deno_diagnostic);
}
}
}
}
}
}
}
}
Resolution::Err(err) => {
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)
}
fn diagnose_dts_entrypoint_unresolved_import(
diagnostics: &mut Vec<lsp::Diagnostic>,
snapshot: &language_server::StateSnapshot,
referrer_module: &DocumentModule,
dependency: &deno_graph::Dependency,
) {
if !matches!(dependency.maybe_code, Resolution::None)
|| !matches!(dependency.maybe_type, Resolution::None)
{
return;
}
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 {
if !matches!(
import.kind,
deno_graph::ImportKind::Es
| deno_graph::ImportKind::TsType
| deno_graph::ImportKind::Require
) {
continue;
}
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,
)),
);
}
}
}
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; }
if referrer_module.media_type.is_declaration() {
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();
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()
|| !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());
}
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>> {
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)
}
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();
};
if config_data.member_dir.jsr_packages_for_publish().is_empty() {
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()
}
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_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,
})
}
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",
);
}
}