diff options
author | Kitson Kelly <me@kitsonkelly.com> | 2021-06-07 21:38:07 +1000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-06-07 21:38:07 +1000 |
commit | 3b3be024fa1895a9a1df0a2e67fe93aa888b198e (patch) | |
tree | 3a162e49195a5ce6da93855c13fe3f994ca0cb7f /cli/lsp/code_lens.rs | |
parent | d6f6e157bddc02a64beaa1c96fcda8d8ea27aa6d (diff) |
feat(lsp): add test code lens (#10874)
Ref #8643
Diffstat (limited to 'cli/lsp/code_lens.rs')
-rw-r--r-- | cli/lsp/code_lens.rs | 298 |
1 files changed, 297 insertions, 1 deletions
diff --git a/cli/lsp/code_lens.rs b/cli/lsp/code_lens.rs index 9be5bed2c..3ad644af3 100644 --- a/cli/lsp/code_lens.rs +++ b/cli/lsp/code_lens.rs @@ -1,5 +1,6 @@ // Copyright 2018-2021 the Deno authors. All rights reserved. MIT license. +use super::analysis; use super::language_server; use super::tsc; @@ -14,7 +15,14 @@ use deno_core::ModuleSpecifier; use lspower::lsp; use regex::Regex; use std::cell::RefCell; +use std::collections::HashSet; use std::rc::Rc; +use swc_common::SourceMap; +use swc_common::Span; +use swc_ecmascript::ast; +use swc_ecmascript::visit::Node; +use swc_ecmascript::visit::Visit; +use swc_ecmascript::visit::VisitWith; lazy_static::lazy_static! { static ref ABSTRACT_MODIFIER: Regex = Regex::new(r"\babstract\b").unwrap(); @@ -36,6 +44,174 @@ pub struct CodeLensData { pub specifier: ModuleSpecifier, } +fn span_to_range(span: &Span, source_map: Rc<SourceMap>) -> lsp::Range { + let start = source_map.lookup_char_pos(span.lo); + let end = source_map.lookup_char_pos(span.hi); + lsp::Range { + start: lsp::Position { + line: (start.line - 1) as u32, + character: start.col_display as u32, + }, + end: lsp::Position { + line: (end.line - 1) as u32, + character: end.col_display as u32, + }, + } +} + +struct DenoTestCollector { + code_lenses: Vec<lsp::CodeLens>, + source_map: Rc<SourceMap>, + specifier: ModuleSpecifier, + test_vars: HashSet<String>, +} + +impl DenoTestCollector { + pub fn new(specifier: ModuleSpecifier, source_map: Rc<SourceMap>) -> Self { + Self { + code_lenses: Vec::new(), + source_map, + specifier, + test_vars: HashSet::new(), + } + } + + fn add_code_lens<N: AsRef<str>>(&mut self, name: N, span: &Span) { + let range = span_to_range(span, self.source_map.clone()); + self.code_lenses.push(lsp::CodeLens { + range, + command: Some(lsp::Command { + title: "▶\u{fe0e} Run Test".to_string(), + command: "deno.test".to_string(), + arguments: Some(vec![json!(self.specifier), json!(name.as_ref())]), + }), + data: None, + }); + } + + fn check_call_expr(&mut self, node: &ast::CallExpr, span: &Span) { + if let Some(expr) = node.args.get(0).map(|es| es.expr.as_ref()) { + match expr { + ast::Expr::Object(obj_lit) => { + for prop in &obj_lit.props { + if let ast::PropOrSpread::Prop(prop) = prop { + if let ast::Prop::KeyValue(key_value_prop) = prop.as_ref() { + if let ast::PropName::Ident(ident) = &key_value_prop.key { + if ident.sym.to_string() == "name" { + if let ast::Expr::Lit(ast::Lit::Str(lit_str)) = + key_value_prop.value.as_ref() + { + let name = lit_str.value.to_string(); + self.add_code_lens(name, &span); + } + } + } + } + } + } + } + ast::Expr::Lit(ast::Lit::Str(lit_str)) => { + let name = lit_str.value.to_string(); + self.add_code_lens(name, &span); + } + _ => (), + } + } + } + + /// Move out the code lenses from the collector. + fn take(self) -> Vec<lsp::CodeLens> { + self.code_lenses + } +} + +impl Visit for DenoTestCollector { + fn visit_call_expr(&mut self, node: &ast::CallExpr, _parent: &dyn Node) { + if let ast::ExprOrSuper::Expr(callee_expr) = &node.callee { + match callee_expr.as_ref() { + ast::Expr::Ident(ident) => { + if self.test_vars.contains(&ident.sym.to_string()) { + self.check_call_expr(node, &ident.span); + } + } + ast::Expr::Member(member_expr) => { + if let ast::Expr::Ident(ns_prop_ident) = member_expr.prop.as_ref() { + if ns_prop_ident.sym.to_string() == "test" { + if let ast::ExprOrSuper::Expr(obj_expr) = &member_expr.obj { + if let ast::Expr::Ident(ident) = obj_expr.as_ref() { + if ident.sym.to_string() == "Deno" { + self.check_call_expr(node, &ns_prop_ident.span); + } + } + } + } + } + } + _ => (), + } + } + } + + fn visit_var_decl(&mut self, node: &ast::VarDecl, _parent: &dyn Node) { + for decl in &node.decls { + if let Some(init) = &decl.init { + match init.as_ref() { + // Identify destructured assignments of `test` from `Deno` + ast::Expr::Ident(ident) => { + if ident.sym.to_string() == "Deno" { + if let ast::Pat::Object(object_pat) = &decl.name { + for prop in &object_pat.props { + match prop { + ast::ObjectPatProp::Assign(prop) => { + let name = prop.key.sym.to_string(); + if name == "test" { + self.test_vars.insert(name); + } + } + ast::ObjectPatProp::KeyValue(prop) => { + if let ast::PropName::Ident(key_ident) = &prop.key { + if key_ident.sym.to_string() == "test" { + if let ast::Pat::Ident(value_ident) = + &prop.value.as_ref() + { + self + .test_vars + .insert(value_ident.id.sym.to_string()); + } + } + } + } + _ => (), + } + } + } + } + } + // Identify variable assignments where the init is `Deno.test` + ast::Expr::Member(member_expr) => { + if let ast::ExprOrSuper::Expr(expr) = &member_expr.obj { + if let ast::Expr::Ident(obj_ident) = expr.as_ref() { + if obj_ident.sym.to_string() == "Deno" { + if let ast::Expr::Ident(prop_ident) = + &member_expr.prop.as_ref() + { + if prop_ident.sym.to_string() == "test" { + if let ast::Pat::Ident(binding_ident) = &decl.name { + self.test_vars.insert(binding_ident.id.sym.to_string()); + } + } + } + } + } + } + } + _ => (), + } + } + } + } +} + async fn resolve_implementation_code_lens( code_lens: lsp::CodeLens, data: CodeLensData, @@ -189,8 +365,51 @@ pub(crate) async fn resolve_code_lens( } } +pub(crate) async fn collect( + specifier: &ModuleSpecifier, + language_server: &mut language_server::Inner, +) -> Result<Vec<lsp::CodeLens>, AnyError> { + let mut code_lenses = collect_test(specifier, language_server)?; + code_lenses.extend(collect_tsc(specifier, language_server).await?); + + Ok(code_lenses) +} + +fn collect_test( + specifier: &ModuleSpecifier, + language_server: &mut language_server::Inner, +) -> Result<Vec<lsp::CodeLens>, AnyError> { + if language_server.config.specifier_code_lens_test(specifier) { + let source = language_server + .get_text_content(specifier) + .ok_or_else(|| anyhow!("Missing text content: {}", specifier))?; + let media_type = language_server + .get_media_type(specifier) + .ok_or_else(|| anyhow!("Missing media type: {}", specifier))?; + // we swallow parsed errors, as they are meaningless here. + // TODO(@kitsonk) consider caching previous code_lens results to return if + // there is a parse error to avoid issues of lenses popping in and out + if let Ok(parsed_module) = + analysis::parse_module(specifier, &source, &media_type) + { + let mut collector = DenoTestCollector::new( + specifier.clone(), + parsed_module.source_map.clone(), + ); + parsed_module.module.visit_with( + &ast::Invalid { + span: swc_common::DUMMY_SP, + }, + &mut collector, + ); + return Ok(collector.take()); + } + } + Ok(Vec::new()) +} + /// Return tsc navigation tree code lenses. -pub(crate) async fn tsc_code_lenses( +async fn collect_tsc( specifier: &ModuleSpecifier, language_server: &mut language_server::Inner, ) -> Result<Vec<lsp::CodeLens>, AnyError> { @@ -282,3 +501,80 @@ pub(crate) async fn tsc_code_lenses( }); Ok(Rc::try_unwrap(code_lenses).unwrap().into_inner()) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::media_type::MediaType; + + #[test] + fn test_deno_test_collector() { + let specifier = resolve_url("https://deno.land/x/mod.ts").unwrap(); + let source = r#" + Deno.test({ + name: "test a", + fn() {} + }); + + Deno.test("test b", function anotherTest() {}); + "#; + let parsed_module = + analysis::parse_module(&specifier, source, &MediaType::TypeScript) + .unwrap(); + let mut collector = + DenoTestCollector::new(specifier, parsed_module.source_map.clone()); + parsed_module.module.visit_with( + &ast::Invalid { + span: swc_common::DUMMY_SP, + }, + &mut collector, + ); + assert_eq!( + collector.take(), + vec![ + lsp::CodeLens { + range: lsp::Range { + start: lsp::Position { + line: 1, + character: 11 + }, + end: lsp::Position { + line: 1, + character: 15 + } + }, + command: Some(lsp::Command { + title: "▶\u{fe0e} Run Test".to_string(), + command: "deno.test".to_string(), + arguments: Some(vec![ + json!("https://deno.land/x/mod.ts"), + json!("test a"), + ]) + }), + data: None, + }, + lsp::CodeLens { + range: lsp::Range { + start: lsp::Position { + line: 6, + character: 11 + }, + end: lsp::Position { + line: 6, + character: 15 + } + }, + command: Some(lsp::Command { + title: "▶\u{fe0e} Run Test".to_string(), + command: "deno.test".to_string(), + arguments: Some(vec![ + json!("https://deno.land/x/mod.ts"), + json!("test b"), + ]) + }), + data: None, + } + ] + ); + } +} |