Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(lsp): add test code lens #10874

Merged
merged 3 commits into from
Jun 7, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 23 additions & 4 deletions cli/lsp/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ There are several settings that the language server supports for a workspace:
- `deno.codeLens.implementations`
- `deno.codeLens.references`
- `deno.codeLens.referencesAllFunctions`
- `deno.codeLens.test`
- `deno.suggest.completeFunctionCalls`
- `deno.suggest.names`
- `deno.suggest.paths`
Expand All @@ -33,10 +34,11 @@ There are several settings that the language server supports for a workspace:
- `deno.lint`
- `deno.unstable`

There are settings that are support on a per resource basis by the language
There are settings that are supported on a per resource basis by the language
server:

- `deno.enable`
- `deno.codeLens.test`

There are several points in the process where Deno analyzes these settings.
First, when the `initialize` request from the client, the
Expand Down Expand Up @@ -68,7 +70,24 @@ settings.
If the client does not have the `workspaceConfiguration` capability, the
language server will assume the workspace setting applies to all resources.

## Custom requests
## Commands

There are several commands that might be issued by the language server to the
client, which the client is expected to implement:

- `deno.cache` - This is sent as a resolution code action when there is an
un-cached module specifier that is being imported into a module. It will be
sent with and argument that contains the resolved specifier as a string to be
cached.
- `deno.showReferences` - This is sent as the command on some code lenses to
show locations of references. The arguments contain the specifier that is the
subject of the command, the start position of the target and the locations of
the references to show.
- `deno.test` - This is sent as part of a test code lens to, of which the client
is expected to run a test based on the arguments, which are the specifier the
test is contained in and the name of the test to filter the tests on.

## Requests

The LSP currently supports the following custom requests. A client should
implement these in order to have a fully functioning client that integrates well
Expand Down Expand Up @@ -115,9 +134,9 @@ with Deno:
}
```

## Custom notifications
## Notifications

There is currently one custom notification that is send from the server to the
There is currently one custom notification that is sent from the server to the
client:

- `deno/registryStatus` - when `deno.suggest.imports.autoDiscover` is `true` and
Expand Down
298 changes: 297 additions & 1 deletion cli/lsp/code_lens.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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();
Expand All @@ -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,
Expand Down Expand Up @@ -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> {
Expand Down Expand Up @@ -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() {});
"#;
Comment on lines +513 to +520
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you squeeze in a test for:

const { test } = Deno;

test("test c", function yetAnotherTest() {});

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nvm, I see there are other tests like this later in the PR

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, the integration test is comprehensive, the unit test is sort of basic.

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,
}
]
);
}
}
Loading