summaryrefslogtreecommitdiff
path: root/cli/lsp/analysis.rs
diff options
context:
space:
mode:
authorKitson Kelly <me@kitsonkelly.com>2020-12-07 21:46:39 +1100
committerGitHub <noreply@github.com>2020-12-07 21:46:39 +1100
commit301d3e4b6849d24154ac2d65c00a9b30223d000e (patch)
treeab3bc074493e6c9be8d1875233bc141bdc0da3b4 /cli/lsp/analysis.rs
parentc8e9b2654ec0d54c77bb3f49fa31c3986203d517 (diff)
feat: add mvp language server (#8515)
Resolves #8400
Diffstat (limited to 'cli/lsp/analysis.rs')
-rw-r--r--cli/lsp/analysis.rs324
1 files changed, 324 insertions, 0 deletions
diff --git a/cli/lsp/analysis.rs b/cli/lsp/analysis.rs
new file mode 100644
index 000000000..370b41c45
--- /dev/null
+++ b/cli/lsp/analysis.rs
@@ -0,0 +1,324 @@
+// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
+
+use crate::ast;
+use crate::import_map::ImportMap;
+use crate::media_type::MediaType;
+use crate::module_graph::parse_deno_types;
+use crate::module_graph::parse_ts_reference;
+use crate::module_graph::TypeScriptReference;
+use crate::tools::lint::create_linter;
+
+use deno_core::error::AnyError;
+use deno_core::ModuleSpecifier;
+use deno_lint::rules;
+use lsp_types::Position;
+use lsp_types::Range;
+use std::cell::RefCell;
+use std::collections::HashMap;
+use std::rc::Rc;
+
+/// Category of self-generated diagnostic messages (those not coming from)
+/// TypeScript.
+pub enum Category {
+ /// A lint diagnostic, where the first element is the message.
+ Lint {
+ message: String,
+ code: String,
+ hint: Option<String>,
+ },
+}
+
+/// A structure to hold a reference to a diagnostic message.
+pub struct Reference {
+ category: Category,
+ range: Range,
+}
+
+fn as_lsp_range(range: &deno_lint::diagnostic::Range) -> Range {
+ Range {
+ start: Position {
+ line: (range.start.line - 1) as u32,
+ character: range.start.col as u32,
+ },
+ end: Position {
+ line: (range.end.line - 1) as u32,
+ character: range.end.col as u32,
+ },
+ }
+}
+
+pub fn get_lint_references(
+ specifier: &ModuleSpecifier,
+ media_type: &MediaType,
+ source_code: &str,
+) -> Result<Vec<Reference>, AnyError> {
+ let syntax = ast::get_syntax(media_type);
+ let lint_rules = rules::get_recommended_rules();
+ let mut linter = create_linter(syntax, lint_rules);
+ // TODO(@kitsonk) we should consider caching the swc source file versions for
+ // reuse by other processes
+ let (_, lint_diagnostics) =
+ linter.lint(specifier.to_string(), source_code.to_string())?;
+
+ Ok(
+ lint_diagnostics
+ .into_iter()
+ .map(|d| Reference {
+ category: Category::Lint {
+ message: d.message,
+ code: d.code,
+ hint: d.hint,
+ },
+ range: as_lsp_range(&d.range),
+ })
+ .collect(),
+ )
+}
+
+pub fn references_to_diagnostics(
+ references: Vec<Reference>,
+) -> Vec<lsp_types::Diagnostic> {
+ references
+ .into_iter()
+ .map(|r| match r.category {
+ Category::Lint { message, code, .. } => lsp_types::Diagnostic {
+ range: r.range,
+ severity: Some(lsp_types::DiagnosticSeverity::Warning),
+ code: Some(lsp_types::NumberOrString::String(code)),
+ code_description: None,
+ // TODO(@kitsonk) this won't make sense for every diagnostic
+ source: Some("deno-lint".to_string()),
+ message,
+ related_information: None,
+ tags: None, // we should tag unused code
+ data: None,
+ },
+ })
+ .collect()
+}
+
+#[derive(Debug, Default, Clone, PartialEq, Eq)]
+pub struct Dependency {
+ pub is_dynamic: bool,
+ pub maybe_code: Option<ResolvedImport>,
+ pub maybe_type: Option<ResolvedImport>,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum ResolvedImport {
+ Resolved(ModuleSpecifier),
+ Err(String),
+}
+
+pub fn resolve_import(
+ specifier: &str,
+ referrer: &ModuleSpecifier,
+ maybe_import_map: Option<Rc<RefCell<ImportMap>>>,
+) -> ResolvedImport {
+ let maybe_mapped = if let Some(import_map) = maybe_import_map {
+ if let Ok(maybe_specifier) =
+ import_map.borrow().resolve(specifier, referrer.as_str())
+ {
+ maybe_specifier
+ } else {
+ None
+ }
+ } else {
+ None
+ };
+ let remapped = maybe_mapped.is_some();
+ let specifier = if let Some(remapped) = maybe_mapped {
+ remapped
+ } else {
+ match ModuleSpecifier::resolve_import(specifier, referrer.as_str()) {
+ Ok(resolved) => resolved,
+ Err(err) => return ResolvedImport::Err(err.to_string()),
+ }
+ };
+ let referrer_scheme = referrer.as_url().scheme();
+ let specifier_scheme = specifier.as_url().scheme();
+ if referrer_scheme == "https" && specifier_scheme == "http" {
+ return ResolvedImport::Err(
+ "Modules imported via https are not allowed to import http modules."
+ .to_string(),
+ );
+ }
+ if (referrer_scheme == "https" || referrer_scheme == "http")
+ && !(specifier_scheme == "https" || specifier_scheme == "http")
+ && !remapped
+ {
+ return ResolvedImport::Err("Remote modules are not allowed to import local modules. Consider using a dynamic import instead.".to_string());
+ }
+
+ ResolvedImport::Resolved(specifier)
+}
+
+// TODO(@kitsonk) a lot of this logic is duplicated in module_graph.rs in
+// Module::parse() and should be refactored out to a common function.
+pub fn analyze_dependencies(
+ specifier: &ModuleSpecifier,
+ source: &str,
+ media_type: &MediaType,
+ maybe_import_map: Option<Rc<RefCell<ImportMap>>>,
+) -> Option<(HashMap<String, Dependency>, Option<ResolvedImport>)> {
+ let specifier_str = specifier.to_string();
+ let source_map = Rc::new(swc_common::SourceMap::default());
+ let mut maybe_type = None;
+ if let Ok(parsed_module) =
+ ast::parse_with_source_map(&specifier_str, source, &media_type, source_map)
+ {
+ let mut dependencies = HashMap::<String, Dependency>::new();
+
+ // 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) {
+ match ts_reference {
+ TypeScriptReference::Path(import) => {
+ let dep = dependencies.entry(import.clone()).or_default();
+ let resolved_import =
+ resolve_import(&import, specifier, maybe_import_map.clone());
+ dep.maybe_code = Some(resolved_import);
+ }
+ TypeScriptReference::Types(import) => {
+ let resolved_import =
+ resolve_import(&import, specifier, maybe_import_map.clone());
+ 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);
+ }
+ }
+ }
+ }
+ }
+
+ // Parse ES and type only imports
+ let descriptors = parsed_module.analyze_dependencies();
+ for desc in descriptors.into_iter().filter(|desc| {
+ desc.kind != swc_ecmascript::dep_graph::DependencyKind::Require
+ }) {
+ let resolved_import =
+ resolve_import(&desc.specifier, specifier, maybe_import_map.clone());
+
+ // Check for `@deno-types` pragmas that effect the import
+ let maybe_resolved_type_import =
+ if let Some(comment) = desc.leading_comments.last() {
+ if let Some(deno_types) = parse_deno_types(&comment.text).as_ref() {
+ Some(resolve_import(
+ deno_types,
+ specifier,
+ maybe_import_map.clone(),
+ ))
+ } else {
+ None
+ }
+ } else {
+ None
+ };
+
+ let dep = dependencies.entry(desc.specifier.to_string()).or_default();
+ dep.is_dynamic = desc.is_dynamic;
+ match desc.kind {
+ swc_ecmascript::dep_graph::DependencyKind::ExportType
+ | swc_ecmascript::dep_graph::DependencyKind::ImportType => {
+ dep.maybe_type = Some(resolved_import)
+ }
+ _ => dep.maybe_code = Some(resolved_import),
+ }
+ if maybe_resolved_type_import.is_some() && dep.maybe_type.is_none() {
+ dep.maybe_type = maybe_resolved_type_import;
+ }
+ }
+
+ Some((dependencies, maybe_type))
+ } else {
+ None
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_as_lsp_range() {
+ let fixture = deno_lint::diagnostic::Range {
+ start: deno_lint::diagnostic::Position {
+ line: 1,
+ col: 2,
+ byte_pos: 23,
+ },
+ end: deno_lint::diagnostic::Position {
+ line: 2,
+ col: 0,
+ byte_pos: 33,
+ },
+ };
+ let actual = as_lsp_range(&fixture);
+ assert_eq!(
+ actual,
+ lsp_types::Range {
+ start: lsp_types::Position {
+ line: 0,
+ character: 2,
+ },
+ end: lsp_types::Position {
+ line: 1,
+ character: 0,
+ },
+ }
+ );
+ }
+
+ #[test]
+ fn test_analyze_dependencies() {
+ let specifier =
+ ModuleSpecifier::resolve_url("file:///a.ts").expect("bad specifier");
+ let source = r#"import {
+ Application,
+ Context,
+ Router,
+ Status,
+ } from "https://deno.land/x/oak@v6.3.2/mod.ts";
+
+ // @deno-types="https://deno.land/x/types/react/index.d.ts";
+ import * as React from "https://cdn.skypack.dev/react";
+ "#;
+ let actual =
+ analyze_dependencies(&specifier, source, &MediaType::TypeScript, None);
+ assert!(actual.is_some());
+ let (actual, maybe_type) = actual.unwrap();
+ assert!(maybe_type.is_none());
+ assert_eq!(actual.len(), 2);
+ assert_eq!(
+ actual.get("https://cdn.skypack.dev/react").cloned(),
+ Some(Dependency {
+ is_dynamic: false,
+ maybe_code: Some(ResolvedImport::Resolved(
+ ModuleSpecifier::resolve_url("https://cdn.skypack.dev/react")
+ .unwrap()
+ )),
+ maybe_type: Some(ResolvedImport::Resolved(
+ ModuleSpecifier::resolve_url(
+ "https://deno.land/x/types/react/index.d.ts"
+ )
+ .unwrap()
+ )),
+ })
+ );
+ assert_eq!(
+ actual.get("https://deno.land/x/oak@v6.3.2/mod.ts").cloned(),
+ Some(Dependency {
+ is_dynamic: false,
+ maybe_code: Some(ResolvedImport::Resolved(
+ ModuleSpecifier::resolve_url("https://deno.land/x/oak@v6.3.2/mod.ts")
+ .unwrap()
+ )),
+ maybe_type: None,
+ })
+ );
+ }
+}