summaryrefslogtreecommitdiff
path: root/cli/lsp/diagnostics.rs
diff options
context:
space:
mode:
authorKitson Kelly <me@kitsonkelly.com>2022-02-04 18:14:57 +1100
committerGitHub <noreply@github.com>2022-02-04 18:14:57 +1100
commitaf5a373e00c944397dd95515b4c742485e241c9c (patch)
tree104e8268b46e5bbb9e96275f99fad184097f8769 /cli/lsp/diagnostics.rs
parent681c3be18de764d3844acbff4a05ac5f40bc3a5f (diff)
feat(lsp): add redirect diagnostic and quick fix (#13580)
Ref: #12864
Diffstat (limited to 'cli/lsp/diagnostics.rs')
-rw-r--r--cli/lsp/diagnostics.rs294
1 files changed, 228 insertions, 66 deletions
diff --git a/cli/lsp/diagnostics.rs b/cli/lsp/diagnostics.rs
index cb889eb74..420d95672 100644
--- a/cli/lsp/diagnostics.rs
+++ b/cli/lsp/diagnostics.rs
@@ -20,6 +20,8 @@ use deno_ast::MediaType;
use deno_core::anyhow::anyhow;
use deno_core::error::AnyError;
use deno_core::resolve_url;
+use deno_core::serde::Deserialize;
+use deno_core::serde_json;
use deno_core::serde_json::json;
use deno_core::ModuleSpecifier;
use deno_graph::Resolved;
@@ -563,29 +565,196 @@ async fn generate_ts_diagnostics(
Ok(diagnostics_vec)
}
-fn resolution_error_as_code(
- err: &deno_graph::ResolutionError,
-) -> lsp::NumberOrString {
- use deno_graph::ResolutionError;
- use deno_graph::SpecifierError;
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct DiagnosticDataSpecifier {
+ pub specifier: ModuleSpecifier,
+}
+
+#[derive(Debug, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct DiagnosticDataRedirect {
+ pub specifier: ModuleSpecifier,
+ pub redirect: ModuleSpecifier,
+}
- match err {
- ResolutionError::InvalidDowngrade { .. } => {
- lsp::NumberOrString::String("invalid-downgrade".to_string())
+/// An enum which represents diagnostic errors which originate from Deno itself.
+pub(crate) enum DenoDiagnostic {
+ /// A `x-deno-warn` is associated with the specifier and should be displayed
+ /// as a warning to the user.
+ DenoWarn(String),
+ /// The import assertion type is incorrect.
+ InvalidAssertType(String),
+ /// A module requires an assertion type to be a valid import.
+ NoAssertType,
+ /// A remote module was not found in the cache.
+ NoCache(ModuleSpecifier),
+ /// A blob module was not found in the cache.
+ NoCacheBlob,
+ /// A data module was not found in the cache.
+ NoCacheData(ModuleSpecifier),
+ /// A local module was not found on the local file system.
+ NoLocal(ModuleSpecifier),
+ /// The specifier resolved to a remote specifier that was redirected to
+ /// another specifier.
+ Redirect {
+ from: ModuleSpecifier,
+ to: ModuleSpecifier,
+ },
+ /// An error occurred when resolving the specifier string.
+ ResolutionError(deno_graph::ResolutionError),
+}
+
+impl DenoDiagnostic {
+ fn code(&self) -> &str {
+ use deno_graph::ResolutionError;
+ use deno_graph::SpecifierError;
+
+ match self {
+ Self::DenoWarn(_) => "deno-warn",
+ Self::InvalidAssertType(_) => "invalid-assert-type",
+ Self::NoAssertType => "no-assert-type",
+ Self::NoCache(_) => "no-cache",
+ Self::NoCacheBlob => "no-cache-blob",
+ Self::NoCacheData(_) => "no-cache-data",
+ Self::NoLocal(_) => "no-local",
+ Self::Redirect { .. } => "redirect",
+ Self::ResolutionError(err) => match err {
+ ResolutionError::InvalidDowngrade { .. } => "invalid-downgrade",
+ ResolutionError::InvalidLocalImport { .. } => "invalid-local-import",
+ ResolutionError::InvalidSpecifier { error, .. } => match error {
+ SpecifierError::ImportPrefixMissing(_, _) => "import-prefix-missing",
+ SpecifierError::InvalidUrl(_) => "invalid-url",
+ },
+ ResolutionError::ResolverError { .. } => "resolver-error",
+ },
}
- ResolutionError::InvalidLocalImport { .. } => {
- lsp::NumberOrString::String("invalid-local-import".to_string())
+ }
+
+ /// A "static" method which for a diagnostic that originated from the
+ /// structure returns a code action which can resolve the diagnostic.
+ pub(crate) fn get_code_action(
+ 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() {
+ "no-assert-type" => lsp::CodeAction {
+ title: "Insert import assertion.".to_string(),
+ kind: Some(lsp::CodeActionKind::QUICKFIX),
+ diagnostics: Some(vec![diagnostic.clone()]),
+ edit: Some(lsp::WorkspaceEdit {
+ changes: Some(HashMap::from([(
+ specifier.clone(),
+ vec![lsp::TextEdit {
+ new_text: " assert { type: \"json\" }".to_string(),
+ range: lsp::Range {
+ start: diagnostic.range.end,
+ end: diagnostic.range.end,
+ },
+ }],
+ )])),
+ ..Default::default()
+ }),
+ ..Default::default()
+ },
+ "no-cache" | "no-cache-data" => {
+ let data = diagnostic
+ .data
+ .clone()
+ .ok_or_else(|| anyhow!("Diagnostic is missing data"))?;
+ let data: DiagnosticDataSpecifier = serde_json::from_value(data)?;
+ let title = if code == "no-cache" {
+ format!("Cache \"{}\" and its dependencies.", data.specifier)
+ } else {
+ "Cache the data URL and its dependencies.".to_string()
+ };
+ 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])]),
+ }),
+ ..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([(
+ specifier.clone(),
+ vec![lsp::TextEdit {
+ new_text: format!("\"{}\"", data.redirect),
+ 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."))
}
- ResolutionError::InvalidSpecifier { error, .. } => match error {
- SpecifierError::ImportPrefixMissing(_, _) => {
- lsp::NumberOrString::String("import-prefix-missing".to_string())
- }
- SpecifierError::InvalidUrl(_) => {
- lsp::NumberOrString::String("invalid-url".to_string())
- }
- },
- ResolutionError::ResolverError { .. } => {
- lsp::NumberOrString::String("resolver-error".to_string())
+ }
+
+ /// Given a reference to the code from an LSP diagnostic, determine if the
+ /// diagnostic is fixable or not
+ pub(crate) fn is_fixable(code: &Option<lsp::NumberOrString>) -> bool {
+ if let Some(lsp::NumberOrString::String(code)) = code {
+ matches!(
+ code.as_str(),
+ "no-cache" | "no-cache-data" | "no-assert-type" | "redirect"
+ )
+ } else {
+ false
+ }
+ }
+
+ /// Convert to an lsp Diagnostic when the range the diagnostic applies to is
+ /// provided.
+ pub(crate) fn to_lsp_diagnostic(
+ &self,
+ range: &lsp::Range,
+ ) -> lsp::Diagnostic {
+ let (severity, message, data) = match self {
+ Self::DenoWarn(message) => (lsp::DiagnosticSeverity::WARNING, message.to_string(), None),
+ Self::InvalidAssertType(assert_type) => (lsp::DiagnosticSeverity::ERROR, format!("The module is a JSON module and expected an assertion type of \"json\". Instead got \"{}\".", assert_type), None),
+ Self::NoAssertType => (lsp::DiagnosticSeverity::ERROR, "The module is a JSON module and not being imported with an import assertion. Consider adding `assert { 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::NoCacheBlob => (lsp::DiagnosticSeverity::ERROR, "Uncached blob URL.".to_string(), None),
+ Self::NoCacheData(specifier) => (lsp::DiagnosticSeverity::ERROR, "Uncached data URL.".to_string(), Some(json!({ "specifier": specifier }))),
+ Self::NoLocal(specifier) => (lsp::DiagnosticSeverity::ERROR, format!("Unable to load a local module: \"{}\".\n Please check the file path.", specifier), None),
+ Self::Redirect { from, to} => (lsp::DiagnosticSeverity::INFORMATION, format!("The import of \"{}\" was redirected to \"{}\".", from, to), Some(json!({ "specifier": from, "redirect": to }))),
+ Self::ResolutionError(err) => (lsp::DiagnosticSeverity::ERROR, err.to_string(), None),
+ };
+ lsp::Diagnostic {
+ range: *range,
+ severity: Some(severity),
+ code: Some(lsp::NumberOrString::String(self.code().to_string())),
+ source: Some("deno".to_string()),
+ message,
+ data,
+ ..Default::default()
}
}
}
@@ -602,21 +771,31 @@ fn diagnose_dependency(
Resolved::Ok {
specifier, range, ..
} => {
+ let range = documents::to_lsp_range(range);
+ // If the module is a remote module and has a `X-Deno-Warn` header, we
+ // want a warning diagnostic with that message.
if let Some(metadata) = cache_metadata.get(specifier) {
if let Some(message) =
metadata.get(&cache::MetadataKey::Warning).cloned()
{
- diagnostics.push(lsp::Diagnostic {
- range: documents::to_lsp_range(range),
- severity: Some(lsp::DiagnosticSeverity::WARNING),
- code: Some(lsp::NumberOrString::String("deno-warn".to_string())),
- source: Some("deno".to_string()),
- message,
- ..Default::default()
- });
+ diagnostics
+ .push(DenoDiagnostic::DenoWarn(message).to_lsp_diagnostic(&range));
}
}
if let Some(doc) = documents.get(specifier) {
+ let doc_specifier = doc.specifier();
+ // If the module was redirected, we want to issue an informational
+ // diagnostic that indicates this. This then allows us to issue a code
+ // action to replace the specifier with the final redirected one.
+ if doc_specifier != specifier {
+ diagnostics.push(
+ DenoDiagnostic::Redirect {
+ from: specifier.clone(),
+ to: doc_specifier.clone(),
+ }
+ .to_lsp_diagnostic(&range),
+ );
+ }
if doc.media_type() == MediaType::Json {
match maybe_assert_type {
// The module has the correct assertion type, no diagnostic
@@ -626,51 +805,34 @@ fn diagnose_dependency(
// not provide a potentially incorrect diagnostic.
None if is_dynamic => (),
// The module has an incorrect assertion type, diagnostic
- Some(assert_type) => diagnostics.push(lsp::Diagnostic {
- range: documents::to_lsp_range(range),
- severity: Some(lsp::DiagnosticSeverity::ERROR),
- code: Some(lsp::NumberOrString::String("invalid-assert-type".to_string())),
- source: Some("deno".to_string()),
- message: format!("The module is a JSON module and expected an assertion type of \"json\". Instead got \"{}\".", assert_type),
- ..Default::default()
- }),
+ Some(assert_type) => diagnostics.push(
+ DenoDiagnostic::InvalidAssertType(assert_type.to_string())
+ .to_lsp_diagnostic(&range),
+ ),
// The module is missing an assertion type, diagnostic
- None => diagnostics.push(lsp::Diagnostic {
- range: documents::to_lsp_range(range),
- severity: Some(lsp::DiagnosticSeverity::ERROR),
- code: Some(lsp::NumberOrString::String("no-assert-type".to_string())),
- source: Some("deno".to_string()),
- message: "The module is a JSON module and not being imported with an import assertion. Consider adding `assert { type: \"json\" }` to the import statement.".to_string(),
- ..Default::default()
- }),
+ None => diagnostics
+ .push(DenoDiagnostic::NoAssertType.to_lsp_diagnostic(&range)),
}
}
} else {
- 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)),
+ // 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.
+ let deno_diagnostic = match specifier.scheme() {
+ "file" => DenoDiagnostic::NoLocal(specifier.clone()),
+ "data" => DenoDiagnostic::NoCacheData(specifier.clone()),
+ "blob" => DenoDiagnostic::NoCacheBlob,
+ _ => DenoDiagnostic::NoCache(specifier.clone()),
};
- diagnostics.push(lsp::Diagnostic {
- range: documents::to_lsp_range(range),
- severity: Some(lsp::DiagnosticSeverity::ERROR),
- code,
- source: Some("deno".to_string()),
- message,
- data: Some(json!({ "specifier": specifier })),
- ..Default::default()
- });
+ diagnostics.push(deno_diagnostic.to_lsp_diagnostic(&range));
}
}
- Resolved::Err(err) => diagnostics.push(lsp::Diagnostic {
- range: documents::to_lsp_range(err.range()),
- severity: Some(lsp::DiagnosticSeverity::ERROR),
- code: Some(resolution_error_as_code(err)),
- source: Some("deno".to_string()),
- message: err.to_string(),
- ..Default::default()
- }),
+ // The specifier resolution resulted in an error, so we want to issue a
+ // diagnostic for that.
+ Resolved::Err(err) => diagnostics.push(
+ DenoDiagnostic::ResolutionError(err.clone())
+ .to_lsp_diagnostic(&documents::to_lsp_range(err.range())),
+ ),
_ => (),
}
}