mirror of
https://github.com/denoland/deno.git
synced 2025-01-22 06:09:25 -05:00
fix(lsp): provide diagnostics for unresolved modules (#8872)
This commit is contained in:
parent
06fa5eb7f3
commit
a4d557126e
6 changed files with 166 additions and 21 deletions
|
@ -86,7 +86,6 @@ pub fn references_to_diagnostics(
|
||||||
severity: Some(lsp_types::DiagnosticSeverity::Warning),
|
severity: Some(lsp_types::DiagnosticSeverity::Warning),
|
||||||
code: Some(lsp_types::NumberOrString::String(code)),
|
code: Some(lsp_types::NumberOrString::String(code)),
|
||||||
code_description: None,
|
code_description: None,
|
||||||
// TODO(@kitsonk) this won't make sense for every diagnostic
|
|
||||||
source: Some("deno-lint".to_string()),
|
source: Some("deno-lint".to_string()),
|
||||||
message,
|
message,
|
||||||
related_information: None,
|
related_information: None,
|
||||||
|
@ -100,12 +99,13 @@ pub fn references_to_diagnostics(
|
||||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||||
pub struct Dependency {
|
pub struct Dependency {
|
||||||
pub is_dynamic: bool,
|
pub is_dynamic: bool,
|
||||||
pub maybe_code: Option<ResolvedImport>,
|
pub maybe_code: Option<ResolvedDependency>,
|
||||||
pub maybe_type: Option<ResolvedImport>,
|
pub maybe_code_specifier_range: Option<Range>,
|
||||||
|
pub maybe_type: Option<ResolvedDependency>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum ResolvedImport {
|
pub enum ResolvedDependency {
|
||||||
Resolved(ModuleSpecifier),
|
Resolved(ModuleSpecifier),
|
||||||
Err(String),
|
Err(String),
|
||||||
}
|
}
|
||||||
|
@ -114,7 +114,7 @@ pub fn resolve_import(
|
||||||
specifier: &str,
|
specifier: &str,
|
||||||
referrer: &ModuleSpecifier,
|
referrer: &ModuleSpecifier,
|
||||||
maybe_import_map: &Option<ImportMap>,
|
maybe_import_map: &Option<ImportMap>,
|
||||||
) -> ResolvedImport {
|
) -> ResolvedDependency {
|
||||||
let maybe_mapped = if let Some(import_map) = maybe_import_map {
|
let maybe_mapped = if let Some(import_map) = maybe_import_map {
|
||||||
if let Ok(maybe_specifier) =
|
if let Ok(maybe_specifier) =
|
||||||
import_map.resolve(specifier, referrer.as_str())
|
import_map.resolve(specifier, referrer.as_str())
|
||||||
|
@ -132,13 +132,13 @@ pub fn resolve_import(
|
||||||
} else {
|
} else {
|
||||||
match ModuleSpecifier::resolve_import(specifier, referrer.as_str()) {
|
match ModuleSpecifier::resolve_import(specifier, referrer.as_str()) {
|
||||||
Ok(resolved) => resolved,
|
Ok(resolved) => resolved,
|
||||||
Err(err) => return ResolvedImport::Err(err.to_string()),
|
Err(err) => return ResolvedDependency::Err(err.to_string()),
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let referrer_scheme = referrer.as_url().scheme();
|
let referrer_scheme = referrer.as_url().scheme();
|
||||||
let specifier_scheme = specifier.as_url().scheme();
|
let specifier_scheme = specifier.as_url().scheme();
|
||||||
if referrer_scheme == "https" && specifier_scheme == "http" {
|
if referrer_scheme == "https" && specifier_scheme == "http" {
|
||||||
return ResolvedImport::Err(
|
return ResolvedDependency::Err(
|
||||||
"Modules imported via https are not allowed to import http modules."
|
"Modules imported via https are not allowed to import http modules."
|
||||||
.to_string(),
|
.to_string(),
|
||||||
);
|
);
|
||||||
|
@ -147,10 +147,10 @@ pub fn resolve_import(
|
||||||
&& !(specifier_scheme == "https" || specifier_scheme == "http")
|
&& !(specifier_scheme == "https" || specifier_scheme == "http")
|
||||||
&& !remapped
|
&& !remapped
|
||||||
{
|
{
|
||||||
return ResolvedImport::Err("Remote modules are not allowed to import local modules. Consider using a dynamic import instead.".to_string());
|
return ResolvedDependency::Err("Remote modules are not allowed to import local modules. Consider using a dynamic import instead.".to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
ResolvedImport::Resolved(specifier)
|
ResolvedDependency::Resolved(specifier)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(@kitsonk) a lot of this logic is duplicated in module_graph.rs in
|
// TODO(@kitsonk) a lot of this logic is duplicated in module_graph.rs in
|
||||||
|
@ -160,7 +160,7 @@ pub fn analyze_dependencies(
|
||||||
source: &str,
|
source: &str,
|
||||||
media_type: &MediaType,
|
media_type: &MediaType,
|
||||||
maybe_import_map: &Option<ImportMap>,
|
maybe_import_map: &Option<ImportMap>,
|
||||||
) -> Option<(HashMap<String, Dependency>, Option<ResolvedImport>)> {
|
) -> Option<(HashMap<String, Dependency>, Option<ResolvedDependency>)> {
|
||||||
let specifier_str = specifier.to_string();
|
let specifier_str = specifier.to_string();
|
||||||
let source_map = Rc::new(swc_common::SourceMap::default());
|
let source_map = Rc::new(swc_common::SourceMap::default());
|
||||||
let mut maybe_type = None;
|
let mut maybe_type = None;
|
||||||
|
@ -222,7 +222,21 @@ pub fn analyze_dependencies(
|
||||||
| swc_ecmascript::dep_graph::DependencyKind::ImportType => {
|
| swc_ecmascript::dep_graph::DependencyKind::ImportType => {
|
||||||
dep.maybe_type = Some(resolved_import)
|
dep.maybe_type = Some(resolved_import)
|
||||||
}
|
}
|
||||||
_ => dep.maybe_code = Some(resolved_import),
|
_ => {
|
||||||
|
dep.maybe_code_specifier_range = Some(Range {
|
||||||
|
start: Position {
|
||||||
|
line: (desc.specifier_line - 1) as u32,
|
||||||
|
character: desc.specifier_col as u32,
|
||||||
|
},
|
||||||
|
end: Position {
|
||||||
|
line: (desc.specifier_line - 1) as u32,
|
||||||
|
character: (desc.specifier_col
|
||||||
|
+ desc.specifier.chars().count()
|
||||||
|
+ 2) as u32,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
dep.maybe_code = Some(resolved_import);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if maybe_resolved_type_import.is_some() && dep.maybe_type.is_none() {
|
if maybe_resolved_type_import.is_some() && dep.maybe_type.is_none() {
|
||||||
dep.maybe_type = maybe_resolved_type_import;
|
dep.maybe_type = maybe_resolved_type_import;
|
||||||
|
@ -293,27 +307,47 @@ mod tests {
|
||||||
actual.get("https://cdn.skypack.dev/react").cloned(),
|
actual.get("https://cdn.skypack.dev/react").cloned(),
|
||||||
Some(Dependency {
|
Some(Dependency {
|
||||||
is_dynamic: false,
|
is_dynamic: false,
|
||||||
maybe_code: Some(ResolvedImport::Resolved(
|
maybe_code: Some(ResolvedDependency::Resolved(
|
||||||
ModuleSpecifier::resolve_url("https://cdn.skypack.dev/react")
|
ModuleSpecifier::resolve_url("https://cdn.skypack.dev/react")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
)),
|
)),
|
||||||
maybe_type: Some(ResolvedImport::Resolved(
|
maybe_type: Some(ResolvedDependency::Resolved(
|
||||||
ModuleSpecifier::resolve_url(
|
ModuleSpecifier::resolve_url(
|
||||||
"https://deno.land/x/types/react/index.d.ts"
|
"https://deno.land/x/types/react/index.d.ts"
|
||||||
)
|
)
|
||||||
.unwrap()
|
.unwrap()
|
||||||
)),
|
)),
|
||||||
|
maybe_code_specifier_range: Some(Range {
|
||||||
|
start: Position {
|
||||||
|
line: 8,
|
||||||
|
character: 27,
|
||||||
|
},
|
||||||
|
end: Position {
|
||||||
|
line: 8,
|
||||||
|
character: 58,
|
||||||
|
}
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
actual.get("https://deno.land/x/oak@v6.3.2/mod.ts").cloned(),
|
actual.get("https://deno.land/x/oak@v6.3.2/mod.ts").cloned(),
|
||||||
Some(Dependency {
|
Some(Dependency {
|
||||||
is_dynamic: false,
|
is_dynamic: false,
|
||||||
maybe_code: Some(ResolvedImport::Resolved(
|
maybe_code: Some(ResolvedDependency::Resolved(
|
||||||
ModuleSpecifier::resolve_url("https://deno.land/x/oak@v6.3.2/mod.ts")
|
ModuleSpecifier::resolve_url("https://deno.land/x/oak@v6.3.2/mod.ts")
|
||||||
.unwrap()
|
.unwrap()
|
||||||
)),
|
)),
|
||||||
maybe_type: None,
|
maybe_type: None,
|
||||||
|
maybe_code_specifier_range: Some(Range {
|
||||||
|
start: Position {
|
||||||
|
line: 5,
|
||||||
|
character: 11,
|
||||||
|
},
|
||||||
|
end: Position {
|
||||||
|
line: 5,
|
||||||
|
character: 50,
|
||||||
|
}
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
use super::analysis::get_lint_references;
|
use super::analysis::get_lint_references;
|
||||||
use super::analysis::references_to_diagnostics;
|
use super::analysis::references_to_diagnostics;
|
||||||
|
use super::analysis::ResolvedDependency;
|
||||||
use super::language_server::StateSnapshot;
|
use super::language_server::StateSnapshot;
|
||||||
use super::memory_cache::FileId;
|
use super::memory_cache::FileId;
|
||||||
use super::tsc;
|
use super::tsc;
|
||||||
|
@ -9,6 +10,7 @@ use super::tsc;
|
||||||
use crate::diagnostics;
|
use crate::diagnostics;
|
||||||
use crate::media_type::MediaType;
|
use crate::media_type::MediaType;
|
||||||
|
|
||||||
|
use deno_core::error::custom_error;
|
||||||
use deno_core::error::AnyError;
|
use deno_core::error::AnyError;
|
||||||
use deno_core::serde_json;
|
use deno_core::serde_json;
|
||||||
use deno_core::serde_json::Value;
|
use deno_core::serde_json::Value;
|
||||||
|
@ -19,6 +21,7 @@ use std::mem;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
|
||||||
pub enum DiagnosticSource {
|
pub enum DiagnosticSource {
|
||||||
|
Deno,
|
||||||
Lint,
|
Lint,
|
||||||
TypeScript,
|
TypeScript,
|
||||||
}
|
}
|
||||||
|
@ -261,3 +264,76 @@ pub async fn generate_ts_diagnostics(
|
||||||
|
|
||||||
Ok(diagnostics)
|
Ok(diagnostics)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn generate_dependency_diagnostics(
|
||||||
|
state_snapshot: StateSnapshot,
|
||||||
|
diagnostic_collection: DiagnosticCollection,
|
||||||
|
) -> Result<DiagnosticVec, AnyError> {
|
||||||
|
tokio::task::spawn_blocking(move || {
|
||||||
|
let mut diagnostics = Vec::new();
|
||||||
|
|
||||||
|
let file_cache = state_snapshot.file_cache.read().unwrap();
|
||||||
|
let mut sources = if let Ok(sources) = state_snapshot.sources.write() {
|
||||||
|
sources
|
||||||
|
} else {
|
||||||
|
return Err(custom_error("Deadlock", "deadlock locking sources"));
|
||||||
|
};
|
||||||
|
for (specifier, doc_data) in state_snapshot.doc_data.iter() {
|
||||||
|
let file_id = file_cache.lookup(specifier).unwrap();
|
||||||
|
let version = doc_data.version;
|
||||||
|
let current_version = diagnostic_collection.get_version(&file_id);
|
||||||
|
if version != current_version {
|
||||||
|
let mut diagnostic_list = Vec::new();
|
||||||
|
if let Some(dependencies) = &doc_data.dependencies {
|
||||||
|
for (_, dependency) in dependencies.iter() {
|
||||||
|
if let (Some(code), Some(range)) = (
|
||||||
|
&dependency.maybe_code,
|
||||||
|
&dependency.maybe_code_specifier_range,
|
||||||
|
) {
|
||||||
|
match code.clone() {
|
||||||
|
ResolvedDependency::Err(message) => {
|
||||||
|
diagnostic_list.push(lsp_types::Diagnostic {
|
||||||
|
range: *range,
|
||||||
|
severity: Some(lsp_types::DiagnosticSeverity::Error),
|
||||||
|
code: None,
|
||||||
|
code_description: None,
|
||||||
|
source: Some("deno".to_string()),
|
||||||
|
message,
|
||||||
|
related_information: None,
|
||||||
|
tags: None,
|
||||||
|
data: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ResolvedDependency::Resolved(specifier) => {
|
||||||
|
if !(state_snapshot.doc_data.contains_key(&specifier) || sources.contains(&specifier)) {
|
||||||
|
let is_local = specifier.as_url().scheme() == "file";
|
||||||
|
diagnostic_list.push(lsp_types::Diagnostic {
|
||||||
|
range: *range,
|
||||||
|
severity: Some(lsp_types::DiagnosticSeverity::Error),
|
||||||
|
code: None,
|
||||||
|
code_description: None,
|
||||||
|
source: Some("deno".to_string()),
|
||||||
|
message: if is_local {
|
||||||
|
format!("Unable to load a local module: \"{}\".\n Please check the file path.", specifier)
|
||||||
|
} else {
|
||||||
|
format!("Unable to load the module: \"{}\".\n If the module exists, running `deno cache {}` should resolve this error.", specifier, specifier)
|
||||||
|
},
|
||||||
|
related_information: None,
|
||||||
|
tags: None,
|
||||||
|
data: None,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
diagnostics.push((file_id, version, diagnostic_list))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(diagnostics)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
|
@ -178,9 +178,35 @@ impl LanguageServer {
|
||||||
Ok::<(), AnyError>(())
|
Ok::<(), AnyError>(())
|
||||||
};
|
};
|
||||||
|
|
||||||
let (lint_res, ts_res) = tokio::join!(lint, ts);
|
let deps = async {
|
||||||
|
if enabled {
|
||||||
|
let diagnostics_collection = self.diagnostics.read().unwrap().clone();
|
||||||
|
let diagnostics = diagnostics::generate_dependency_diagnostics(
|
||||||
|
self.snapshot(),
|
||||||
|
diagnostics_collection,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
{
|
||||||
|
let mut diagnostics_collection = self.diagnostics.write().unwrap();
|
||||||
|
for (file_id, version, diagnostics) in diagnostics {
|
||||||
|
diagnostics_collection.set(
|
||||||
|
file_id,
|
||||||
|
DiagnosticSource::Deno,
|
||||||
|
version,
|
||||||
|
diagnostics,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.publish_diagnostics().await?
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok::<(), AnyError>(())
|
||||||
|
};
|
||||||
|
|
||||||
|
let (lint_res, ts_res, deps_res) = tokio::join!(lint, ts, deps);
|
||||||
lint_res?;
|
lint_res?;
|
||||||
ts_res?;
|
ts_res?;
|
||||||
|
deps_res?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -213,6 +239,11 @@ impl LanguageServer {
|
||||||
.diagnostics_for(file_id, DiagnosticSource::TypeScript)
|
.diagnostics_for(file_id, DiagnosticSource::TypeScript)
|
||||||
.cloned(),
|
.cloned(),
|
||||||
);
|
);
|
||||||
|
diagnostics.extend(
|
||||||
|
diagnostics_collection
|
||||||
|
.diagnostics_for(file_id, DiagnosticSource::Deno)
|
||||||
|
.cloned(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
let specifier = {
|
let specifier = {
|
||||||
let file_cache = self.file_cache.read().unwrap();
|
let file_cache = self.file_cache.read().unwrap();
|
||||||
|
|
|
@ -23,7 +23,7 @@ use std::time::SystemTime;
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
struct Metadata {
|
struct Metadata {
|
||||||
dependencies: Option<HashMap<String, analysis::Dependency>>,
|
dependencies: Option<HashMap<String, analysis::Dependency>>,
|
||||||
maybe_types: Option<analysis::ResolvedImport>,
|
maybe_types: Option<analysis::ResolvedDependency>,
|
||||||
media_type: MediaType,
|
media_type: MediaType,
|
||||||
source: String,
|
source: String,
|
||||||
version: String,
|
version: String,
|
||||||
|
@ -255,7 +255,7 @@ impl Sources {
|
||||||
let dependencies = &metadata.dependencies?;
|
let dependencies = &metadata.dependencies?;
|
||||||
let dependency = dependencies.get(specifier)?;
|
let dependency = dependencies.get(specifier)?;
|
||||||
if let Some(type_dependency) = &dependency.maybe_type {
|
if let Some(type_dependency) = &dependency.maybe_type {
|
||||||
if let analysis::ResolvedImport::Resolved(resolved_specifier) =
|
if let analysis::ResolvedDependency::Resolved(resolved_specifier) =
|
||||||
type_dependency
|
type_dependency
|
||||||
{
|
{
|
||||||
self.resolution_result(resolved_specifier)
|
self.resolution_result(resolved_specifier)
|
||||||
|
@ -264,7 +264,7 @@ impl Sources {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let code_dependency = &dependency.maybe_code.clone()?;
|
let code_dependency = &dependency.maybe_code.clone()?;
|
||||||
if let analysis::ResolvedImport::Resolved(resolved_specifier) =
|
if let analysis::ResolvedDependency::Resolved(resolved_specifier) =
|
||||||
code_dependency
|
code_dependency
|
||||||
{
|
{
|
||||||
self.resolution_result(resolved_specifier)
|
self.resolution_result(resolved_specifier)
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
|
// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
|
||||||
|
|
||||||
use super::analysis::ResolvedImport;
|
use super::analysis::ResolvedDependency;
|
||||||
use super::language_server::StateSnapshot;
|
use super::language_server::StateSnapshot;
|
||||||
use super::text;
|
use super::text;
|
||||||
use super::utils;
|
use super::utils;
|
||||||
|
@ -839,9 +839,10 @@ fn resolve(state: &mut State, args: Value) -> Result<Value, AnyError> {
|
||||||
} else if let Some(resolved_import) = &dependency.maybe_code {
|
} else if let Some(resolved_import) = &dependency.maybe_code {
|
||||||
resolved_import.clone()
|
resolved_import.clone()
|
||||||
} else {
|
} else {
|
||||||
ResolvedImport::Err("missing dependency".to_string())
|
ResolvedDependency::Err("missing dependency".to_string())
|
||||||
};
|
};
|
||||||
if let ResolvedImport::Resolved(resolved_specifier) = resolved_import
|
if let ResolvedDependency::Resolved(resolved_specifier) =
|
||||||
|
resolved_import
|
||||||
{
|
{
|
||||||
if state
|
if state
|
||||||
.state_snapshot
|
.state_snapshot
|
||||||
|
|
|
@ -128,6 +128,9 @@ delete Object.prototype.__proto__;
|
||||||
// TS2691: An import path cannot end with a '.ts' extension. Consider
|
// TS2691: An import path cannot end with a '.ts' extension. Consider
|
||||||
// importing 'bad-module' instead.
|
// importing 'bad-module' instead.
|
||||||
2691,
|
2691,
|
||||||
|
// TS2792: Cannot find module. Did you mean to set the 'moduleResolution'
|
||||||
|
// option to 'node', or to add aliases to the 'paths' option?
|
||||||
|
2792,
|
||||||
// TS5009: Cannot find the common subdirectory path for the input files.
|
// TS5009: Cannot find the common subdirectory path for the input files.
|
||||||
5009,
|
5009,
|
||||||
// TS5055: Cannot write file
|
// TS5055: Cannot write file
|
||||||
|
|
Loading…
Add table
Reference in a new issue