/
githubmirror
/
deno
Обзор
Документация
Войти
/
githubmirror
/
deno
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
cli/lsp/code_lens.rs
1 426 строк
42 KB
em
feat(lsp): add "Debug" code lens for test steps (#34742)
04 июн 2026, 06:14
Не верифицирован
04 июн 2026, 06:14
551973f
Код
Авторство
О чём код?
// Copyright 2018-2026 the Deno authors. MIT license. use std::cell::RefCell; use std::collections::HashSet; use std::rc::Rc; use deno_ast::ParsedSource; use deno_ast::SourceRange; use deno_ast::SourceRangedForSpanned; use deno_ast::swc::ast; use deno_ast::swc::ecma_visit::Visit; use deno_ast::swc::ecma_visit::VisitWith; use deno_core::ModuleSpecifier; use deno_core::error::AnyError; use deno_core::serde::Deserialize; use deno_core::serde::Serialize; use deno_core::serde_json; use deno_core::serde_json::json; use lazy_regex::lazy_regex; use lsp_types::Uri; use once_cell::sync::Lazy; use regex::Regex; use tokio_util::sync::CancellationToken; use tower_lsp::jsonrpc::Error as LspError; use tower_lsp::jsonrpc::Result as LspResult; use tower_lsp::lsp_types as lsp; use super::analysis::source_range_to_lsp_range; use super::config::CodeLensSettings; use super::language_server; use super::testing::collectors::parse_test_context_param; use super::text::LineIndex; use super::tsc; use super::tsc::NavigationTree; static ABSTRACT_MODIFIER: Lazy<Regex> = lazy_regex!(r"\babstract\b"); static EXPORT_MODIFIER: Lazy<Regex> = lazy_regex!(r"\bexport\b"); #[derive(Debug, Copy, Clone, Eq, PartialEq, Deserialize, Serialize)] pub enum CodeLensSource { #[serde(rename = "implementations")] Implementations, #[serde(rename = "references")] References, } #[derive(Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct CodeLensData { pub source: CodeLensSource, pub uri: Uri, } /// The body of a test or test step registration call, i.e. the function passed /// to `Deno.test(...)`, `t.step(...)`, `describe(...)`, `it(...)`, etc. enum TestFnBody<'a> { Arrow(&'a ast::ArrowExpr), Fn(&'a ast::Function), } /// Extract the name of a test or test step from its registration call, mirroring /// the static collector in [`super::testing::collectors`]. Only string and /// single-quasi template literal names (and named function expressions) are /// resolvable statically. fn find_test_name(node: &ast::CallExpr) -> Option<String> { match node.args.first().map(|es| es.expr.as_ref())? { ast::Expr::Object(obj_lit) => { for prop in &obj_lit.props { if let ast::PropOrSpread::Prop(prop) = prop && let ast::Prop::KeyValue(key_value_prop) = prop.as_ref() && let ast::PropName::Ident(ast::IdentName { sym, .. }) = &key_value_prop.key && sym == "name" { match key_value_prop.value.as_ref() { ast::Expr::Lit(ast::Lit::Str(lit_str)) => { return Some(lit_str.value.to_string_lossy().to_string()); } ast::Expr::Tpl(tpl) if tpl.quasis.len() == 1 => { return Some(tpl.quasis.first().unwrap().raw.to_string()); } _ => {} } } } None } ast::Expr::Fn(fn_expr) => { fn_expr.ident.as_ref().map(|ident| ident.sym.to_string()) } ast::Expr::Lit(ast::Lit::Str(lit_str)) => { Some(lit_str.value.to_string_lossy().to_string()) } ast::Expr::Tpl(tpl) if tpl.quasis.len() == 1 => { Some(tpl.quasis.first().unwrap().raw.to_string()) } _ => None, } } /// Locate the test function body of a test or test step registration call so it /// can be searched for nested steps. fn find_test_fn_body(node: &ast::CallExpr) -> Option<TestFnBody<'_>> { match node.args.first().map(|es| es.expr.as_ref()) { Some(ast::Expr::Object(obj_lit)) => { for prop in &obj_lit.props { let ast::PropOrSpread::Prop(prop) = prop else { continue; }; match prop.as_ref() { ast::Prop::KeyValue(key_value_prop) => { if let ast::PropName::Ident(ast::IdentName { sym, .. }) = &key_value_prop.key && sym == "fn" { match key_value_prop.value.as_ref() { ast::Expr::Arrow(arrow_expr) => { return Some(TestFnBody::Arrow(arrow_expr)); } ast::Expr::Fn(fn_expr) => { return Some(TestFnBody::Fn(&fn_expr.function)); } _ => {} } } } ast::Prop::Method(method_prop) => { if let ast::PropName::Ident(ast::IdentName { sym, .. }) = &method_prop.key && sym == "fn" { return Some(TestFnBody::Fn(&method_prop.function)); } } _ => {} } } None } Some(ast::Expr::Fn(fn_expr)) => Some(TestFnBody::Fn(&fn_expr.function)), Some(ast::Expr::Arrow(arrow_expr)) => Some(TestFnBody::Arrow(arrow_expr)), _ => match node.args.get(1).map(|es| es.expr.as_ref()) { Some(ast::Expr::Fn(fn_expr)) => Some(TestFnBody::Fn(&fn_expr.function)), Some(ast::Expr::Arrow(arrow_expr)) => Some(TestFnBody::Arrow(arrow_expr)), _ => None, }, } } /// Push the "Run Test" and "Debug" code lenses for a test or test step. For a /// test step, `name` is the name of the enclosing top-level test (the only /// granularity `deno test --filter` supports), so debugging a step runs its /// parent test under the inspector. fn add_test_code_lenses( code_lenses: &mut Vec<lsp::CodeLens>, specifier: &ModuleSpecifier, parsed_source: &ParsedSource, name: &str, range: &SourceRange, ) { let range = source_range_to_lsp_range(range, parsed_source.text_info_lazy()); for (title, inspect) in [("▶\u{fe0e} Run Test", false), ("Debug", true)] { code_lenses.push(lsp::CodeLens { range, command: Some(lsp::Command { title: title.to_string(), command: "deno.client.test".to_string(), arguments: Some(vec![ json!(specifier), json!(name), json!({ "inspect": inspect }), ]), }), data: None, }); } } /// Walk a test function body and emit a "Run Test"/"Debug" code lens for each /// test step (`t.step(...)`, destructured `step(...)`, or BDD `it(...)`) found /// in it. Every step's lens filters by `root_name` — the enclosing top-level /// test — so it runs the same test the gutter "play" button does, but with the /// option to attach the debugger. fn collect_step_code_lenses( code_lenses: &mut Vec<lsp::CodeLens>, specifier: &ModuleSpecifier, parsed_source: &ParsedSource, root_name: &str, body: TestFnBody, is_describe: bool, ) { let param = match &body { TestFnBody::Arrow(arrow_expr) => arrow_expr.params.first(), TestFnBody::Fn(function) => function.params.first().map(|param| ¶m.pat), }; let (maybe_test_context, maybe_step_var) = if is_describe { (None, None) } else { match parse_test_context_param(param) { Some(r) => r, None => return, } }; let mut vars = HashSet::new(); if let Some(var) = maybe_step_var { vars.insert(var); } let mut collector = DenoTestStepCollector { code_lenses, specifier, parsed_source, root_name, maybe_test_context, vars, is_describe, }; match body { TestFnBody::Arrow(arrow_expr) => arrow_expr.body.visit_with(&mut collector), TestFnBody::Fn(function) => function.body.visit_with(&mut collector), } } struct DenoTestCollector { code_lenses: Vec<lsp::CodeLens>, parsed_source: ParsedSource, specifier: ModuleSpecifier, test_vars: HashSet<String>, } impl DenoTestCollector { pub fn new(specifier: ModuleSpecifier, parsed_source: ParsedSource) -> Self { Self { code_lenses: Vec::new(), parsed_source, specifier, test_vars: HashSet::new(), } } fn check_call_expr( &mut self, node: &ast::CallExpr, range: &SourceRange, is_describe: bool, ) { let Some(name) = find_test_name(node) else { return; }; add_test_code_lenses( &mut self.code_lenses, &self.specifier, &self.parsed_source, &name, range, ); if let Some(body) = find_test_fn_body(node) { collect_step_code_lenses( &mut self.code_lenses, &self.specifier, &self.parsed_source, &name, body, is_describe, ); } } /// 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) { let ast::Callee::Expr(callee_expr) = &node.callee else { return; }; let mut prop_chain = ["", "", ""]; let mut current_segment = callee_expr.as_ref(); let mut rightmost_symbol_range = None; for (i, name) in prop_chain.iter_mut().enumerate().rev() { match current_segment { ast::Expr::Ident(ident) => { *name = ident.sym.as_str(); rightmost_symbol_range.get_or_insert_with(|| ident.range()); break; } ast::Expr::Member(member_expr) => { if i == 0 { return; } let ast::MemberProp::Ident(right) = &member_expr.prop else { return; }; *name = right.sym.as_str(); rightmost_symbol_range.get_or_insert_with(|| right.range()); current_segment = &member_expr.obj; } _ => return, } } let Some(rightmost_symbol_range) = rightmost_symbol_range else { debug_assert!(false, "rightmost symbol range should always be defined"); return; }; let is_describe = match prop_chain { ["", "Deno", "test"] | ["Deno", "test", "ignore" | "only"] => false, ["", "", "describe"] | ["", "describe", "ignore" | "only" | "skip"] => { true } ["", "", s] if self.test_vars.contains(s) => false, _ => return, }; self.check_call_expr(node, &rightmost_symbol_range, is_describe); } fn visit_var_decl(&mut self, node: &ast::VarDecl) { 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 == "Deno" && 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 && key_ident.sym == "test" && 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::Expr::Ident(obj_ident) = member_expr.obj.as_ref() && obj_ident.sym == "Deno" && let ast::MemberProp::Ident(prop_ident) = &member_expr.prop && prop_ident.sym == "test" && let ast::Pat::Ident(binding_ident) = &decl.name { self.test_vars.insert(binding_ident.id.sym.to_string()); } } _ => (), } } } } } /// Walks a test function body emitting code lenses for each test step. Mirrors /// the step-detection logic of `super::testing::collectors`'s /// `TestStepCollector`, but instead of building a test hierarchy it pushes a /// "Run Test"/"Debug" code lens for each step (filtering by the enclosing /// top-level test `root_name`). struct DenoTestStepCollector<'a> { code_lenses: &'a mut Vec<lsp::CodeLens>, specifier: &'a ModuleSpecifier, parsed_source: &'a ParsedSource, root_name: &'a str, maybe_test_context: Option<String>, vars: HashSet<String>, is_describe: bool, } impl Visit for DenoTestStepCollector<'_> { fn visit_call_expr(&mut self, node: &ast::CallExpr) { let ast::Callee::Expr(callee_expr) = &node.callee else { return; }; let mut prop_chain = ["", ""]; let mut current_segment = callee_expr.as_ref(); let mut rightmost_symbol_range = None; for (i, name) in prop_chain.iter_mut().enumerate().rev() { match current_segment { ast::Expr::Ident(ident) => { *name = ident.sym.as_str(); rightmost_symbol_range.get_or_insert_with(|| ident.range()); break; } ast::Expr::Member(member_expr) => { if i == 0 { return; } let ast::MemberProp::Ident(right) = &member_expr.prop else { return; }; *name = right.sym.as_str(); rightmost_symbol_range.get_or_insert_with(|| right.range()); current_segment = &member_expr.obj; } _ => return, } } let Some(rightmost_symbol_range) = rightmost_symbol_range else { debug_assert!(false, "rightmost symbol range should always be defined"); return; }; // Only test steps are matched here: BDD `it(...)` inside a `describe`, a // `t.step(...)` call on the test context, or a destructured `step(...)`. match ( self.is_describe, self.maybe_test_context.as_deref(), prop_chain, ) { (true, _, ["", "it"] | ["it", "ignore" | "only" | "skip"]) => {} (false, Some(c), [s, "step"]) if s == c => {} (false, _, ["", s]) if self.vars.contains(s) => {} _ => return, }; add_test_code_lenses( self.code_lenses, self.specifier, self.parsed_source, self.root_name, &rightmost_symbol_range, ); // A step body can itself contain nested steps (`t.step(...)` within a // `t.step(...)`), so descend into it as a `Deno.test`-style step body. if let Some(body) = find_test_fn_body(node) { collect_step_code_lenses( self.code_lenses, self.specifier, self.parsed_source, self.root_name, body, false, ); } } fn visit_var_decl(&mut self, node: &ast::VarDecl) { let Some(test_context) = &self.maybe_test_context else { return; }; for decl in &node.decls { let Some(init) = &decl.init else { continue; }; match init.as_ref() { // Identify destructured assignments of `step` from the test context. ast::Expr::Ident(ident) => { if ident.sym != *test_context { continue; } let ast::Pat::Object(object_pat) = &decl.name else { continue; }; for prop in &object_pat.props { match prop { ast::ObjectPatProp::Assign(prop) if prop.key.sym.eq("step") => { self.vars.insert(prop.key.sym.to_string()); } ast::ObjectPatProp::KeyValue(prop) => { if let ast::PropName::Ident(key_ident) = &prop.key && key_ident.sym.eq("step") && let ast::Pat::Ident(value_ident) = &prop.value.as_ref() { self.vars.insert(value_ident.id.sym.to_string()); } } _ => (), } } } // Identify variable assignments where the init is test context `.step`. ast::Expr::Member(member_expr) => { let ast::Expr::Ident(obj_ident) = member_expr.obj.as_ref() else { continue; }; if obj_ident.sym != *test_context { continue; } let ast::MemberProp::Ident(prop_ident) = &member_expr.prop else { continue; }; if prop_ident.sym.eq("step") && let ast::Pat::Ident(binding_ident) = &decl.name { self.vars.insert(binding_ident.id.sym.to_string()); } } _ => (), } } } } async fn resolve_implementation_code_lens( code_lens: lsp::CodeLens, data: CodeLensData, language_server: &language_server::Inner, token: &CancellationToken, ) -> LspResult<lsp::CodeLens> { let locations = language_server .goto_implementation( lsp::request::GotoImplementationParams { text_document_position_params: lsp::TextDocumentPositionParams { text_document: lsp::TextDocumentIdentifier { uri: data.uri.clone(), }, position: code_lens.range.start, }, work_done_progress_params: Default::default(), partial_result_params: Default::default(), }, token, ) .await? .map(|r| match r { lsp::GotoDefinitionResponse::Scalar(location) => vec![location], lsp::GotoDefinitionResponse::Array(locations) => locations, lsp::GotoDefinitionResponse::Link(links) => links .into_iter() .map(|l| lsp::Location { uri: l.target_uri, range: l.target_selection_range, }) .collect(), }) .unwrap_or(Vec::new()); let title = if locations.len() == 1 { "1 implementation".to_string() } else { format!("{} implementations", locations.len()) }; let command = if locations.is_empty() { lsp::Command { title, command: String::new(), arguments: None, } } else { lsp::Command { title, command: "deno.client.showReferences".to_string(), arguments: Some(vec![ json!(data.uri), json!(code_lens.range.start), json!(locations), ]), } }; Ok(lsp::CodeLens { range: code_lens.range, command: Some(command), data: None, }) } async fn resolve_references_code_lens( code_lens: lsp::CodeLens, data: CodeLensData, language_server: &language_server::Inner, token: &CancellationToken, ) -> LspResult<lsp::CodeLens> { let locations = language_server .references( lsp::ReferenceParams { text_document_position: lsp::TextDocumentPositionParams { text_document: lsp::TextDocumentIdentifier { uri: data.uri.clone(), }, position: code_lens.range.start, }, work_done_progress_params: Default::default(), partial_result_params: Default::default(), context: lsp::ReferenceContext { include_declaration: false, }, }, token, ) .await? .unwrap_or_default(); let title = if locations.len() == 1 { "1 reference".to_string() } else { format!("{} references", locations.len()) }; let command = if locations.is_empty() { lsp::Command { title, command: String::new(), arguments: None, } } else { lsp::Command { title, command: "deno.client.showReferences".to_string(), arguments: Some(vec![ json!(data.uri), json!(code_lens.range.start), json!(locations), ]), } }; Ok(lsp::CodeLens { range: code_lens.range, command: Some(command), data: None, }) } pub async fn resolve_code_lens( code_lens: lsp::CodeLens, language_server: &language_server::Inner, token: &CancellationToken, ) -> LspResult<lsp::CodeLens> { let data: CodeLensData = serde_json::from_value(code_lens.data.clone().unwrap()).map_err(|err| { LspError::invalid_params(format!( "Unable to parse code lens data: {:#}", err )) })?; match data.source { CodeLensSource::Implementations => { resolve_implementation_code_lens(code_lens, data, language_server, token) .await } CodeLensSource::References => { resolve_references_code_lens(code_lens, data, language_server, token) .await } } } pub fn collect_test( specifier: &ModuleSpecifier, parsed_source: &ParsedSource, _token: &CancellationToken, ) -> Result<Vec<lsp::CodeLens>, AnyError> { // TODO(nayeemrmn): Do cancellation checks while collecting tests. let mut collector = DenoTestCollector::new(specifier.clone(), parsed_source.clone()); parsed_source.program().visit_with(&mut collector); Ok(collector.take()) } /// Return tsc navigation tree code lenses. pub fn collect_tsc( uri: &Uri, code_lens_settings: &CodeLensSettings, line_index: &LineIndex, navigation_tree: &NavigationTree, token: &CancellationToken, ) -> Result<Vec<lsp::CodeLens>, AnyError> { let code_lenses = Rc::new(RefCell::new(Vec::new())); navigation_tree.walk(token, &|i, mp| { let mut code_lenses = code_lenses.borrow_mut(); // TSC Implementations Code Lens if code_lens_settings.implementations { let source = CodeLensSource::Implementations; match i.kind { tsc::ScriptElementKind::InterfaceElement => { code_lenses.push(i.to_code_lens(line_index, uri, source)); } tsc::ScriptElementKind::ClassElement | tsc::ScriptElementKind::MemberFunctionElement | tsc::ScriptElementKind::MemberVariableElement | tsc::ScriptElementKind::MemberGetAccessorElement | tsc::ScriptElementKind::MemberSetAccessorElement if ABSTRACT_MODIFIER.is_match(&i.kind_modifiers) => { code_lenses.push(i.to_code_lens(line_index, uri, source)); } _ => (), } } // TSC References Code Lens if code_lens_settings.references { let source = CodeLensSource::References; if let Some(parent) = &mp && parent.kind == tsc::ScriptElementKind::EnumElement { code_lenses.push(i.to_code_lens(line_index, uri, source)); } match i.kind { tsc::ScriptElementKind::FunctionElement if code_lens_settings.references_all_functions => { code_lenses.push(i.to_code_lens(line_index, uri, source)); } tsc::ScriptElementKind::ConstElement | tsc::ScriptElementKind::LetElement | tsc::ScriptElementKind::VariableElement if EXPORT_MODIFIER.is_match(&i.kind_modifiers) => { code_lenses.push(i.to_code_lens(line_index, uri, source)); } tsc::ScriptElementKind::ClassElement if i.text != "<class>" => { code_lenses.push(i.to_code_lens(line_index, uri, source)); } tsc::ScriptElementKind::InterfaceElement | tsc::ScriptElementKind::TypeElement | tsc::ScriptElementKind::EnumElement => { code_lenses.push(i.to_code_lens(line_index, uri, source)); } tsc::ScriptElementKind::LocalFunctionElement | tsc::ScriptElementKind::MemberFunctionElement | tsc::ScriptElementKind::MemberGetAccessorElement | tsc::ScriptElementKind::MemberSetAccessorElement | tsc::ScriptElementKind::ConstructorImplementationElement | tsc::ScriptElementKind::MemberVariableElement => { if let Some(parent) = &mp && parent.spans[0].start != i.spans[0].start { match parent.kind { tsc::ScriptElementKind::ClassElement | tsc::ScriptElementKind::InterfaceElement | tsc::ScriptElementKind::TypeElement => { code_lenses.push(i.to_code_lens(line_index, uri, source)); } _ => (), } } } _ => (), } } })?; Ok(Rc::try_unwrap(code_lenses).unwrap().into_inner()) } #[cfg(test)] mod tests { use deno_ast::MediaType; use deno_core::resolve_url; use super::*; #[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(function useFnName() {}); Deno.test("test b", function anotherTest() {}); Deno.test.ignore("test ignore", () => {}); Deno.test.only("test only", () => {}); Deno.test(`test template literal name`, () => {}); describe("test describe", () => {}); describe.ignore("test describe ignore", () => {}); describe.only("test describe only", () => {}); describe.skip("test describe skip", () => {}); "#; let parsed_module = deno_ast::parse_module(deno_ast::ParseParams { specifier: specifier.clone(), text: source.into(), media_type: MediaType::TypeScript, capture_tokens: true, scope_analysis: true, maybe_syntax: None, }) .unwrap(); let mut collector = DenoTestCollector::new(specifier, parsed_module.clone()); parsed_module.program().visit_with(&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.client.test".to_string(), arguments: Some(vec![ json!("https://deno.land/x/mod.ts"), json!("test a"), json!({ "inspect": false, }), ]) }), data: None, }, lsp::CodeLens { range: lsp::Range { start: lsp::Position { line: 1, character: 11 }, end: lsp::Position { line: 1, character: 15 } }, command: Some(lsp::Command { title: "Debug".to_string(), command: "deno.client.test".to_string(), arguments: Some(vec![ json!("https://deno.land/x/mod.ts"), json!("test a"), json!({ "inspect": true, }), ]) }), 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.client.test".to_string(), arguments: Some(vec![ json!("https://deno.land/x/mod.ts"), json!("useFnName"), json!({ "inspect": false, }), ]) }), 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: "Debug".to_string(), command: "deno.client.test".to_string(), arguments: Some(vec![ json!("https://deno.land/x/mod.ts"), json!("useFnName"), json!({ "inspect": true, }), ]) }), data: None, }, lsp::CodeLens { range: lsp::Range { start: lsp::Position { line: 8, character: 11 }, end: lsp::Position { line: 8, character: 15 } }, command: Some(lsp::Command { title: "▶\u{fe0e} Run Test".to_string(), command: "deno.client.test".to_string(), arguments: Some(vec![ json!("https://deno.land/x/mod.ts"), json!("test b"), json!({ "inspect": false, }), ]) }), data: None, }, lsp::CodeLens { range: lsp::Range { start: lsp::Position { line: 8, character: 11 }, end: lsp::Position { line: 8, character: 15 } }, command: Some(lsp::Command { title: "Debug".to_string(), command: "deno.client.test".to_string(), arguments: Some(vec![ json!("https://deno.land/x/mod.ts"), json!("test b"), json!({ "inspect": true, }), ]) }), data: None, }, lsp::CodeLens { range: lsp::Range { start: lsp::Position { line: 10, character: 16, }, end: lsp::Position { line: 10, character: 22, }, }, command: Some(lsp::Command { title: "▶\u{fe0e} Run Test".to_string(), command: "deno.client.test".to_string(), arguments: Some(vec![ json!("https://deno.land/x/mod.ts"), json!("test ignore"), json!({ "inspect": false, }), ]), }), data: None, }, lsp::CodeLens { range: lsp::Range { start: lsp::Position { line: 10, character: 16, }, end: lsp::Position { line: 10, character: 22, }, }, command: Some(lsp::Command { title: "Debug".to_string(), command: "deno.client.test".to_string(), arguments: Some(vec![ json!("https://deno.land/x/mod.ts"), json!("test ignore"), json!({ "inspect": true, }), ]), }), data: None, }, lsp::CodeLens { range: lsp::Range { start: lsp::Position { line: 12, character: 16, }, end: lsp::Position { line: 12, character: 20, }, }, command: Some(lsp::Command { title: "▶\u{fe0e} Run Test".to_string(), command: "deno.client.test".to_string(), arguments: Some(vec![ json!("https://deno.land/x/mod.ts"), json!("test only"), json!({ "inspect": false, }), ]), }), data: None, }, lsp::CodeLens { range: lsp::Range { start: lsp::Position { line: 12, character: 16, }, end: lsp::Position { line: 12, character: 20, }, }, command: Some(lsp::Command { title: "Debug".to_string(), command: "deno.client.test".to_string(), arguments: Some(vec![ json!("https://deno.land/x/mod.ts"), json!("test only"), json!({ "inspect": true, }), ]), }), data: None, }, lsp::CodeLens { range: lsp::Range { start: lsp::Position { line: 14, character: 11, }, end: lsp::Position { line: 14, character: 15, }, }, command: Some(lsp::Command { title: "▶\u{fe0e} Run Test".to_string(), command: "deno.client.test".to_string(), arguments: Some(vec![ json!("https://deno.land/x/mod.ts"), json!("test template literal name"), json!({ "inspect": false, }), ]), }), data: None, }, lsp::CodeLens { range: lsp::Range { start: lsp::Position { line: 14, character: 11, }, end: lsp::Position { line: 14, character: 15, }, }, command: Some(lsp::Command { title: "Debug".to_string(), command: "deno.client.test".to_string(), arguments: Some(vec![ json!("https://deno.land/x/mod.ts"), json!("test template literal name"), json!({ "inspect": true, }), ]), }), data: None, }, lsp::CodeLens { range: lsp::Range { start: lsp::Position { line: 16, character: 6, }, end: lsp::Position { line: 16, character: 14, }, }, command: Some(lsp::Command { title: "▶\u{fe0e} Run Test".to_string(), command: "deno.client.test".to_string(), arguments: Some(vec![ json!("https://deno.land/x/mod.ts"), json!("test describe"), json!({ "inspect": false, }), ]), }), data: None, }, lsp::CodeLens { range: lsp::Range { start: lsp::Position { line: 16, character: 6, }, end: lsp::Position { line: 16, character: 14, }, }, command: Some(lsp::Command { title: "Debug".to_string(), command: "deno.client.test".to_string(), arguments: Some(vec![ json!("https://deno.land/x/mod.ts"), json!("test describe"), json!({ "inspect": true, }), ]), }), data: None, }, lsp::CodeLens { range: lsp::Range { start: lsp::Position { line: 18, character: 15, }, end: lsp::Position { line: 18, character: 21, }, }, command: Some(lsp::Command { title: "▶\u{fe0e} Run Test".to_string(), command: "deno.client.test".to_string(), arguments: Some(vec![ json!("https://deno.land/x/mod.ts"), json!("test describe ignore"), json!({ "inspect": false, }), ]), }), data: None, }, lsp::CodeLens { range: lsp::Range { start: lsp::Position { line: 18, character: 15, }, end: lsp::Position { line: 18, character: 21, }, }, command: Some(lsp::Command { title: "Debug".to_string(), command: "deno.client.test".to_string(), arguments: Some(vec![ json!("https://deno.land/x/mod.ts"), json!("test describe ignore"), json!({ "inspect": true, }), ]), }), data: None, }, lsp::CodeLens { range: lsp::Range { start: lsp::Position { line: 20, character: 15, }, end: lsp::Position { line: 20, character: 19, }, }, command: Some(lsp::Command { title: "▶\u{fe0e} Run Test".to_string(), command: "deno.client.test".to_string(), arguments: Some(vec![ json!("https://deno.land/x/mod.ts"), json!("test describe only"), json!({ "inspect": false, }), ]), }), data: None, }, lsp::CodeLens { range: lsp::Range { start: lsp::Position { line: 20, character: 15, }, end: lsp::Position { line: 20, character: 19, }, }, command: Some(lsp::Command { title: "Debug".to_string(), command: "deno.client.test".to_string(), arguments: Some(vec![ json!("https://deno.land/x/mod.ts"), json!("test describe only"), json!({ "inspect": true, }), ]), }), data: None, }, lsp::CodeLens { range: lsp::Range { start: lsp::Position { line: 22, character: 15, }, end: lsp::Position { line: 22, character: 19, }, }, command: Some(lsp::Command { title: "▶\u{fe0e} Run Test".to_string(), command: "deno.client.test".to_string(), arguments: Some(vec![ json!("https://deno.land/x/mod.ts"), json!("test describe skip"), json!({ "inspect": false, }), ]), }), data: None, }, lsp::CodeLens { range: lsp::Range { start: lsp::Position { line: 22, character: 15, }, end: lsp::Position { line: 22, character: 19, }, }, command: Some(lsp::Command { title: "Debug".to_string(), command: "deno.client.test".to_string(), arguments: Some(vec![ json!("https://deno.land/x/mod.ts"), json!("test describe skip"), json!({ "inspect": true, }), ]), }), data: None, }, ] ); } /// Reduce a list of code lenses to `(line, title, filter_name, inspect)` /// tuples for compact assertions. fn summarize( code_lenses: &[lsp::CodeLens], ) -> Vec<(u32, String, String, bool)> { code_lenses .iter() .map(|cl| { let command = cl.command.as_ref().unwrap(); let args = command.arguments.as_ref().unwrap(); let name = args[1].as_str().unwrap().to_string(); let inspect = args[2].get("inspect").unwrap().as_bool().unwrap(); (cl.range.start.line, command.title.clone(), name, inspect) }) .collect() } fn collect_code_lenses(source: &str) -> Vec<lsp::CodeLens> { let specifier = resolve_url("https://deno.land/x/mod.ts").unwrap(); let parsed_module = deno_ast::parse_module(deno_ast::ParseParams { specifier: specifier.clone(), text: source.into(), media_type: MediaType::TypeScript, capture_tokens: true, scope_analysis: true, maybe_syntax: None, }) .unwrap(); let mut collector = DenoTestCollector::new(specifier, parsed_module.clone()); parsed_module.program().visit_with(&mut collector); collector.take() } #[test] fn test_deno_test_collector_steps() { // Each `t.step(...)` gets "Run Test"/"Debug" code lenses that filter by the // enclosing top-level test name, so debugging a step runs its parent test // under the inspector. Regression test for // https://github.com/denoland/deno/issues/21664. let source = r#" Deno.test("test a", async (t) => { await t.step("step 1", () => {}); await t.step("step 2", async (t) => { await t.step("nested step", () => {}); }); }); "#; assert_eq!( summarize(&collect_code_lenses(source)), vec![ ( 1, "▶\u{fe0e} Run Test".to_string(), "test a".to_string(), false ), (1, "Debug".to_string(), "test a".to_string(), true), ( 2, "▶\u{fe0e} Run Test".to_string(), "test a".to_string(), false ), (2, "Debug".to_string(), "test a".to_string(), true), ( 3, "▶\u{fe0e} Run Test".to_string(), "test a".to_string(), false ), (3, "Debug".to_string(), "test a".to_string(), true), ( 4, "▶\u{fe0e} Run Test".to_string(), "test a".to_string(), false ), (4, "Debug".to_string(), "test a".to_string(), true), ] ); } #[test] fn test_deno_test_collector_steps_destructured() { // Destructured `step` from the test context is also detected. let source = r#" Deno.test("test a", async ({ step }) => { await step("step 1", () => {}); }); "#; assert_eq!( summarize(&collect_code_lenses(source)), vec![ ( 1, "▶\u{fe0e} Run Test".to_string(), "test a".to_string(), false ), (1, "Debug".to_string(), "test a".to_string(), true), ( 2, "▶\u{fe0e} Run Test".to_string(), "test a".to_string(), false ), (2, "Debug".to_string(), "test a".to_string(), true), ] ); } #[test] fn test_deno_test_collector_bdd_steps() { // BDD `it(...)` inside `describe(...)` gets step code lenses filtering by // the enclosing `describe` name. let source = r#" describe("suite", () => { it("does a thing", () => {}); it.only("does another", () => {}); }); "#; assert_eq!( summarize(&collect_code_lenses(source)), vec![ ( 1, "▶\u{fe0e} Run Test".to_string(), "suite".to_string(), false ), (1, "Debug".to_string(), "suite".to_string(), true), ( 2, "▶\u{fe0e} Run Test".to_string(), "suite".to_string(), false ), (2, "Debug".to_string(), "suite".to_string(), true), ( 3, "▶\u{fe0e} Run Test".to_string(), "suite".to_string(), false ), (3, "Debug".to_string(), "suite".to_string(), true), ] ); } }