Skip to content

Commit

Permalink
feat(lsp): diagnostics for deno types and triple-slash refs (#10699)
Browse files Browse the repository at this point in the history
Fixes #9823
  • Loading branch information
kitsonk authored and piscisaureus committed May 31, 2021
1 parent b0c958c commit bfc929e
Show file tree
Hide file tree
Showing 6 changed files with 335 additions and 75 deletions.
79 changes: 71 additions & 8 deletions cli/lsp/analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ pub struct Dependency {
pub maybe_code: Option<ResolvedDependency>,
pub maybe_code_specifier_range: Option<Range>,
pub maybe_type: Option<ResolvedDependency>,
pub maybe_type_specifier_range: Option<Range>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
Expand Down Expand Up @@ -259,25 +260,45 @@ pub fn analyze_dependencies(

// Parse leading comments for supported triple slash references.
for comment in parsed_module.get_leading_comments().iter() {
if let Some(ts_reference) = parse_ts_reference(&comment.text) {
if let Some((ts_reference, span)) = parse_ts_reference(&comment) {
let loc = parsed_module.source_map.lookup_char_pos(span.lo);
match ts_reference {
TypeScriptReference::Path(import) => {
let dep = dependencies.entry(import.clone()).or_default();
let resolved_import =
resolve_import(&import, specifier, maybe_import_map);
dep.maybe_code = Some(resolved_import);
dep.maybe_code_specifier_range = Some(Range {
start: Position {
line: (loc.line - 1) as u32,
character: loc.col_display as u32,
},
end: Position {
line: (loc.line - 1) as u32,
character: (loc.col_display + import.chars().count() + 2) as u32,
},
});
}
TypeScriptReference::Types(import) => {
let resolved_import =
resolve_import(&import, specifier, maybe_import_map);
if media_type == &MediaType::JavaScript
|| media_type == &MediaType::Jsx
{
maybe_type = Some(resolved_import)
} else {
let dep = dependencies.entry(import).or_default();
dep.maybe_type = Some(resolved_import);
maybe_type = Some(resolved_import.clone());
}
let dep = dependencies.entry(import.clone()).or_default();
dep.maybe_type = Some(resolved_import);
dep.maybe_type_specifier_range = Some(Range {
start: Position {
line: (loc.line - 1) as u32,
character: loc.col_display as u32,
},
end: Position {
line: (loc.line - 1) as u32,
character: (loc.col_display + import.chars().count() + 2) as u32,
},
});
}
}
}
Expand All @@ -294,7 +315,13 @@ pub fn analyze_dependencies(
let maybe_resolved_type_dependency =
// Check for `@deno-types` pragmas that affect the import
if let Some(comment) = desc.leading_comments.last() {
parse_deno_types(&comment.text).as_ref().map(|deno_types| resolve_import(deno_types, specifier, maybe_import_map))
parse_deno_types(&comment).as_ref().map(|(deno_types, span)| {
(
resolve_import(deno_types, specifier, maybe_import_map),
deno_types.clone(),
parsed_module.source_map.lookup_char_pos(span.lo)
)
})
} else {
None
};
Expand All @@ -304,6 +331,17 @@ pub fn analyze_dependencies(
match desc.kind {
swc_ecmascript::dep_graph::DependencyKind::ExportType
| swc_ecmascript::dep_graph::DependencyKind::ImportType => {
dep.maybe_type_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_type = Some(resolved_import)
}
_ => {
Expand All @@ -321,8 +359,22 @@ pub fn analyze_dependencies(
dep.maybe_code = Some(resolved_import);
}
}
if maybe_resolved_type_dependency.is_some() && dep.maybe_type.is_none() {
dep.maybe_type = maybe_resolved_type_dependency;
if dep.maybe_type.is_none() {
if let Some((resolved_dependency, specifier, loc)) =
maybe_resolved_type_dependency
{
dep.maybe_type_specifier_range = Some(Range {
start: Position {
line: (loc.line - 1) as u32,
character: (loc.col_display + 1) as u32,
},
end: Position {
line: (loc.line - 1) as u32,
character: (loc.col_display + 1 + specifier.chars().count()) as u32,
},
});
dep.maybe_type = Some(resolved_dependency);
}
}
}

Expand Down Expand Up @@ -723,6 +775,16 @@ mod tests {
character: 58,
}
}),
maybe_type_specifier_range: Some(Range {
start: Position {
line: 7,
character: 20,
},
end: Position {
line: 7,
character: 62,
}
})
})
);
assert_eq!(
Expand All @@ -743,6 +805,7 @@ mod tests {
character: 50,
}
}),
maybe_type_specifier_range: None,
})
);
}
Expand Down
123 changes: 75 additions & 48 deletions cli/lsp/diagnostics.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
// Copyright 2018-2021 the Deno authors. All rights reserved. MIT license.

use super::analysis;
use super::documents::DocumentCache;
use super::language_server;
use super::sources::Sources;
use super::tsc;

use crate::diagnostics;
use crate::media_type::MediaType;
use crate::tokio_util::create_basic_runtime;

use analysis::ResolvedDependency;
use deno_core::error::anyhow;
use deno_core::error::AnyError;
use deno_core::resolve_url;
Expand Down Expand Up @@ -392,6 +395,64 @@ async fn generate_ts_diagnostics(
Ok(diagnostics_vec)
}

fn diagnose_dependency(
diagnostics: &mut Vec<lsp::Diagnostic>,
documents: &DocumentCache,
sources: &Sources,
maybe_dependency: &Option<ResolvedDependency>,
maybe_range: &Option<lsp::Range>,
) {
if let (Some(dep), Some(range)) = (maybe_dependency, *maybe_range) {
match dep {
analysis::ResolvedDependency::Err(err) => {
diagnostics.push(lsp::Diagnostic {
range,
severity: Some(lsp::DiagnosticSeverity::Error),
code: Some(err.as_code()),
code_description: None,
source: Some("deno".to_string()),
message: err.to_string(),
related_information: None,
tags: None,
data: None,
})
}
analysis::ResolvedDependency::Resolved(specifier) => {
if !(documents.contains_key(&specifier)
|| sources.contains_key(&specifier))
{
let (code, message) = match specifier.scheme() {
"file" => (Some(lsp::NumberOrString::String("no-local".to_string())), format!("Unable to load a local module: \"{}\".\n Please check the file path.", specifier)),
"data" => (Some(lsp::NumberOrString::String("no-cache-data".to_string())), "Uncached data URL.".to_string()),
"blob" => (Some(lsp::NumberOrString::String("no-cache-blob".to_string())), "Uncached blob URL.".to_string()),
_ => (Some(lsp::NumberOrString::String("no-cache".to_string())), format!("Uncached or missing remote URL: \"{}\".", specifier)),
};
diagnostics.push(lsp::Diagnostic {
range,
severity: Some(lsp::DiagnosticSeverity::Error),
code,
source: Some("deno".to_string()),
message,
data: Some(json!({ "specifier": specifier })),
..Default::default()
});
} else if sources.contains_key(&specifier) {
if let Some(message) = sources.get_maybe_warning(&specifier) {
diagnostics.push(lsp::Diagnostic {
range,
severity: Some(lsp::DiagnosticSeverity::Warning),
code: Some(lsp::NumberOrString::String("deno-warn".to_string())),
source: Some("deno".to_string()),
message,
..Default::default()
})
}
}
}
}
}
}

/// Generate diagnostics for dependencies of a module, attempting to resolve
/// dependencies on the local file system or in the DENO_DIR cache.
async fn generate_deps_diagnostics(
Expand All @@ -417,54 +478,20 @@ async fn generate_deps_diagnostics(
let mut diagnostics = Vec::new();
if let Some(dependencies) = documents.dependencies(specifier) {
for (_, dependency) in dependencies {
// TODO(@kitsonk) add diagnostics for maybe_type dependencies
if let (Some(code), Some(range)) =
(dependency.maybe_code, dependency.maybe_code_specifier_range)
{
match code {
analysis::ResolvedDependency::Err(err) => diagnostics.push(lsp::Diagnostic {
range,
severity: Some(lsp::DiagnosticSeverity::Error),
code: Some(err.as_code()),
code_description: None,
source: Some("deno".to_string()),
message: err.to_string(),
related_information: None,
tags: None,
data: None,
}),
analysis::ResolvedDependency::Resolved(specifier) => {
if !(documents.contains_key(&specifier) || sources.contains_key(&specifier)) {
let (code, message) = match specifier.scheme() {
"file" => (Some(lsp::NumberOrString::String("no-local".to_string())), format!("Unable to load a local module: \"{}\".\n Please check the file path.", specifier)),
"data" => (Some(lsp::NumberOrString::String("no-cache-data".to_string())), "Uncached data URL.".to_string()),
"blob" => (Some(lsp::NumberOrString::String("no-cache-blob".to_string())), "Uncached blob URL.".to_string()),
_ => (Some(lsp::NumberOrString::String("no-cache".to_string())), format!("Uncached or missing remote URL: \"{}\".", specifier)),
};
diagnostics.push(lsp::Diagnostic {
range,
severity: Some(lsp::DiagnosticSeverity::Error),
code,
source: Some("deno".to_string()),
message,
data: Some(json!({ "specifier": specifier })),
..Default::default()
});
} else if sources.contains_key(&specifier) {
if let Some(message) = sources.get_maybe_warning(&specifier) {
diagnostics.push(lsp::Diagnostic {
range,
severity: Some(lsp::DiagnosticSeverity::Warning),
code: Some(lsp::NumberOrString::String("deno-warn".to_string())),
source: Some("deno".to_string()),
message,
..Default::default()
})
}
}
},
}
}
diagnose_dependency(
&mut diagnostics,
&documents,
&sources,
&dependency.maybe_code,
&dependency.maybe_code_specifier_range,
);
diagnose_dependency(
&mut diagnostics,
&documents,
&sources,
&dependency.maybe_type,
&dependency.maybe_type_specifier_range,
);
}
}
diagnostics_vec.push((specifier.clone(), version, diagnostics));
Expand Down
54 changes: 35 additions & 19 deletions cli/module_graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ use std::result;
use std::sync::Arc;
use std::sync::Mutex;
use std::time::Instant;
use swc_common::comments::Comment;
use swc_common::BytePos;
use swc_common::Span;

lazy_static::lazy_static! {
/// Matched the `@deno-types` pragma.
Expand Down Expand Up @@ -188,35 +191,48 @@ pub enum TypeScriptReference {
Types(String),
}

fn match_to_span(comment: &Comment, m: &regex::Match) -> Span {
Span {
lo: comment.span.lo + BytePos((m.start() + 1) as u32),
hi: comment.span.lo + BytePos((m.end() + 1) as u32),
ctxt: comment.span.ctxt,
}
}

/// Determine if a comment contains a triple slash reference and optionally
/// return its kind and value.
pub fn parse_ts_reference(comment: &str) -> Option<TypeScriptReference> {
if !TRIPLE_SLASH_REFERENCE_RE.is_match(comment) {
pub fn parse_ts_reference(
comment: &Comment,
) -> Option<(TypeScriptReference, Span)> {
if !TRIPLE_SLASH_REFERENCE_RE.is_match(&comment.text) {
None
} else if let Some(captures) = PATH_REFERENCE_RE.captures(comment) {
Some(TypeScriptReference::Path(
captures.get(1).unwrap().as_str().to_string(),
} else if let Some(captures) = PATH_REFERENCE_RE.captures(&comment.text) {
let m = captures.get(1).unwrap();
Some((
TypeScriptReference::Path(m.as_str().to_string()),
match_to_span(comment, &m),
))
} else {
TYPES_REFERENCE_RE.captures(comment).map(|captures| {
TypeScriptReference::Types(captures.get(1).unwrap().as_str().to_string())
TYPES_REFERENCE_RE.captures(&comment.text).map(|captures| {
let m = captures.get(1).unwrap();
(
TypeScriptReference::Types(m.as_str().to_string()),
match_to_span(comment, &m),
)
})
}
}

/// Determine if a comment contains a `@deno-types` pragma and optionally return
/// its value.
pub fn parse_deno_types(comment: &str) -> Option<String> {
if let Some(captures) = DENO_TYPES_RE.captures(comment) {
if let Some(m) = captures.get(1) {
Some(m.as_str().to_string())
} else if let Some(m) = captures.get(2) {
Some(m.as_str().to_string())
} else {
panic!("unreachable");
}
pub fn parse_deno_types(comment: &Comment) -> Option<(String, Span)> {
let captures = DENO_TYPES_RE.captures(&comment.text)?;
if let Some(m) = captures.get(1) {
Some((m.as_str().to_string(), match_to_span(comment, &m)))
} else if let Some(m) = captures.get(2) {
Some((m.as_str().to_string(), match_to_span(comment, &m)))
} else {
None
unreachable!();
}
}

Expand Down Expand Up @@ -327,7 +343,7 @@ impl Module {

// parse out any triple slash references
for comment in parsed_module.get_leading_comments().iter() {
if let Some(ts_reference) = parse_ts_reference(&comment.text) {
if let Some((ts_reference, _)) = parse_ts_reference(&comment) {
let location = parsed_module.get_location(&comment.span);
match ts_reference {
TypeScriptReference::Path(import) => {
Expand Down Expand Up @@ -392,7 +408,7 @@ impl Module {
// Parse out any `@deno-types` pragmas and modify dependency
let maybe_type = if !desc.leading_comments.is_empty() {
let comment = desc.leading_comments.last().unwrap();
if let Some(deno_types) = parse_deno_types(&comment.text).as_ref() {
if let Some((deno_types, _)) = parse_deno_types(&comment).as_ref() {
Some(self.resolve_import(deno_types, Some(location.clone()))?)
} else {
None
Expand Down
Loading

0 comments on commit bfc929e

Please sign in to comment.