From f44539ec2fe34cb147a1ba3e567d2fb89825b163 Mon Sep 17 00:00:00 2001 From: christoph_xd Date: Sun, 16 Nov 2025 20:55:25 +0100 Subject: [PATCH] Auto stash before rebase of "relpace_lsp_with_rust" onto "origin/relpace_lsp_with_rust" --- server/Cargo.lock | 10 ++ server/Cargo.toml | 3 +- server/crates/analysis/Cargo.toml | 1 + server/crates/analysis/examples/dump.rs | 22 ++++ server/tests/analysis-tests/Cargo.toml | 12 ++ server/tests/analysis-tests/src/lib.rs | 28 +++++ server/tests/analysis-tests/src/main.rs | 32 +++++ .../analysis-tests/tests/tcl_grammar_only.rs | 113 ++++++++++++++++++ 8 files changed, 220 insertions(+), 1 deletion(-) create mode 100644 server/crates/analysis/examples/dump.rs create mode 100644 server/tests/analysis-tests/Cargo.toml create mode 100644 server/tests/analysis-tests/src/lib.rs create mode 100644 server/tests/analysis-tests/src/main.rs create mode 100644 server/tests/analysis-tests/tests/tcl_grammar_only.rs diff --git a/server/Cargo.lock b/server/Cargo.lock index 9eac86b..6d4bac7 100644 --- a/server/Cargo.lock +++ b/server/Cargo.lock @@ -20,6 +20,16 @@ dependencies = [ "serde", "serde_json", "tree-sitter", + "tree-sitter-tcl", +] + +[[package]] +name = "analysis-tests" +version = "0.1.0" +dependencies = [ + "analysis", + "tree-sitter", + "tree-sitter-tcl", ] [[package]] diff --git a/server/Cargo.toml b/server/Cargo.toml index 30a0c71..e55e815 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -4,6 +4,7 @@ members = [ "crates/text", "crates/analysis", "crates/features", + "tests/analysis-tests", ] resolver = "2" @@ -21,4 +22,4 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" tree-sitter = "0.25.8" tree-sitter-tcl = { git = "https://github.com/tree-sitter-grammars/tree-sitter-tcl" } -url = "2" \ No newline at end of file +url = "2" diff --git a/server/crates/analysis/Cargo.toml b/server/crates/analysis/Cargo.toml index 0346d17..f33c808 100644 --- a/server/crates/analysis/Cargo.toml +++ b/server/crates/analysis/Cargo.toml @@ -7,6 +7,7 @@ edition = "2021" anyhow = { workspace = true } lsp-types = { workspace = true } tree-sitter = { workspace = true } +tree-sitter-tcl = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/server/crates/analysis/examples/dump.rs b/server/crates/analysis/examples/dump.rs new file mode 100644 index 0000000..0a3bb8d --- /dev/null +++ b/server/crates/analysis/examples/dump.rs @@ -0,0 +1,22 @@ +use tree_sitter::Parser; +use analysis::collect_procs; + +fn main() { + let src = r#"namespace eval foo { + proc bar {x y} { return $x } +} +proc top {} {} +"#; + let mut parser = Parser::new(); + parser + .set_language(&tree_sitter_tcl::LANGUAGE.into()) + .expect("load tcl grammar"); + let tree = parser.parse(src, None).expect("parse"); + println!("tree: {}", tree.root_node().to_sexp()); + let procs = collect_procs(&tree, src); + println!("found {} procs via collect_procs", procs.len()); + for p in procs { + println!("- {} {:?}", p.name, p.params); + } +} + diff --git a/server/tests/analysis-tests/Cargo.toml b/server/tests/analysis-tests/Cargo.toml new file mode 100644 index 0000000..e30d492 --- /dev/null +++ b/server/tests/analysis-tests/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "analysis-tests" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +tree-sitter = { workspace = true } +tree-sitter-tcl = { workspace = true } + +[dev-dependencies] +analysis = { path = "../../crates/analysis" } diff --git a/server/tests/analysis-tests/src/lib.rs b/server/tests/analysis-tests/src/lib.rs new file mode 100644 index 0000000..33c86a9 --- /dev/null +++ b/server/tests/analysis-tests/src/lib.rs @@ -0,0 +1,28 @@ +//! Helpers for workspace-level tests. Provides simple Tree-sitter Tcl utilities +//! so you can inspect the parsed tree output while writing tests. + +use tree_sitter::{Parser, Tree}; + +/// Parse the given Tcl source and return the syntax tree. +pub fn parse_tcl(src: &str) -> Tree { + let mut parser = Parser::new(); + parser + .set_language(&tree_sitter_tcl::LANGUAGE.into()) + .expect("load tcl grammar"); + parser.parse(src, None).expect("parse") +} + +/// Return the s-expression (to_sexp) of the root node for quick inspection. +pub fn tcl_to_sexp(src: &str) -> String { + let tree = parse_tcl(src); + tree.root_node().to_sexp() +} + +/// Print the s-expression to stdout. Run tests with `-- --nocapture` to see it. +/// +/// Example: +/// cargo test -p analysis-tests -- --nocapture +pub fn print_tcl_sexp(src: &str) { + println!("{}", tcl_to_sexp(src)); +} + diff --git a/server/tests/analysis-tests/src/main.rs b/server/tests/analysis-tests/src/main.rs new file mode 100644 index 0000000..9d013d4 --- /dev/null +++ b/server/tests/analysis-tests/src/main.rs @@ -0,0 +1,32 @@ +//! Small CLI to inspect Tree-sitter Tcl output. +//! Examples: +//! cargo run -p analysis-tests -- "proc hello {a} { puts $a }" +//! cargo run -p analysis-tests -- --file path/to/file.tcl + +use std::{ env, fs }; + +fn print_usage() { + eprintln!( + "Usage:\n cargo run -p analysis-tests -- [--file PATH] [SRC]\n\nExamples:\n cargo run -p analysis-tests -- \"proc hello {{a b c {{optArg 0}} {{ puts $a }}\"\n cargo run -p analysis-tests -- --file script.tcl\n" + ); +} + +fn main() { + let args: Vec = env::args().skip(1).collect(); + if args.iter().any(|a| (a == "-h" || a == "--help")) { + print_usage(); + return; + } + + let src = if args.len() >= 2 && args[0] == "--file" { + fs::read_to_string(&args[1]).expect("read file") + } else if !args.is_empty() { + args.join(" ") + } else { + // Default sample + String::from("proc hello {a} { puts $a }") + }; + + // Print S-expression of the parsed tree + analysis_tests::print_tcl_sexp(&src); +} diff --git a/server/tests/analysis-tests/tests/tcl_grammar_only.rs b/server/tests/analysis-tests/tests/tcl_grammar_only.rs new file mode 100644 index 0000000..fd8a0eb --- /dev/null +++ b/server/tests/analysis-tests/tests/tcl_grammar_only.rs @@ -0,0 +1,113 @@ +//! Grammar-focused tests: exercise Tree-sitter Tcl directly. +//! These do not rely on the `analysis` crate — just the parser/AST. + +use tree_sitter::{ Node, Parser }; + +fn parse_tcl(src: &str) -> tree_sitter::Tree { + let mut parser = Parser::new(); + parser.set_language(&tree_sitter_tcl::LANGUAGE.into()).expect("load tcl grammar"); + parser.parse(src, None).expect("parse") +} + +fn text<'a>(node: Node<'a>, src: &'a str) -> &'a str { + node.utf8_text(src.as_bytes()).expect("utf8 text") +} + +#[test] +fn procedure_node_and_fields() { + let src = r#"proc hello {a b} { return $a }"#; + let tree = parse_tcl(src); + let root = tree.root_node(); + + // The top-level child for a proc is a builtin `procedure` node + // (per grammar.js: `procedure: seq('proc', field('name', _word), field('arguments', arguments), field('body', _word))`). + // The source_file is a list of (optional _command, terminator). The first named child + // should be the `_command` node itself; for a `proc` that is `procedure`. + let proc = root.named_child(0).expect("first child"); + assert_eq!(proc.kind(), "procedure"); + + // Fields: name, arguments, body + let name = proc.child_by_field_name("name").expect("name field"); + assert_eq!(text(name, src), "hello"); + + let args = proc.child_by_field_name("arguments").expect("args field"); + assert_eq!(args.kind(), "arguments"); + // Arguments contain one or more `argument` nodes when braced. + let mut cursor = args.walk(); + let mut arg_names = vec![]; + for ch in args.named_children(&mut cursor) { + if ch.kind() == "argument" { + if let Some(n) = ch.child_by_field_name("name") { + arg_names.push(text(n, src).to_string()); + } + } + } + assert_eq!(arg_names, vec!["a", "b"]); + + let body = proc.child_by_field_name("body").expect("body field"); + // A braced block body is parsed as `braced_word`. + assert_eq!(body.kind(), "braced_word"); +} + +#[test] +fn generic_command_and_arguments() { + let src = "puts hello"; + let tree = parse_tcl(src); + let root = tree.root_node(); + let cmd = root.named_child(0).expect("first child"); + assert_eq!(cmd.kind(), "command"); + + let name = cmd.child_by_field_name("name").expect("name field"); + assert_eq!(text(name, src), "puts"); + + let args = cmd.child_by_field_name("arguments").expect("arguments field (word_list)"); + assert_eq!(args.kind(), "word_list"); + + // `word_list` has word-like children; collect their surface text. + let mut cursor = args.walk(); + let words: Vec<_> = args + .named_children(&mut cursor) + .map(|w| text(w, src).to_string()) + .collect(); + assert_eq!(words, vec!["hello"]); +} + +#[test] +fn namespace_eval_contains_nested_proc() { + let src = r#" +namespace eval foo { + proc bar {x y} { return $x } +} +"#; + let tree = parse_tcl(src); + let root = tree.root_node(); + let ns = root.named_child(0).expect("first child"); + assert_eq!(ns.kind(), "namespace"); + + // The `namespace`'s `word_list` contains: `eval`, `foo`, and a braced block. + let mut cursor = ns.walk(); + let mut braced_block: Option = None; + for ch in ns.named_children(&mut cursor) { + if ch.kind() == "word_list" { + let mut cur2 = ch.walk(); + for w in ch.named_children(&mut cur2) { + if w.kind() == "braced_word" { + braced_block = Some(w); + } + } + } + } + let block = braced_block.expect("braced block in namespace eval"); + + // Inside the braced block, we expect a `procedure` command for `proc bar ...`. + let mut cur = block.walk(); + let mut found_proc = false; + for n in block.named_children(&mut cur) { + if n.kind() == "procedure" { + found_proc = true; + let name = n.child_by_field_name("name").expect("proc name"); + assert_eq!(text(name, src), "bar"); + } + } + assert!(found_proc, "expected nested procedure inside namespace block"); +}