mirror of
https://github.com/windmill-labs/windmill.git
synced 2026-08-24 00:00:46 +00:00
96229575e6
* chore: remove wm-cursor, add local-review skill, update PR skill for EE - Remove the unused wm-cursor script and all references to it in README_WORKMUX_DEV.md and worktree-common.sh - Add /local-review skill for code review (bugs + CLAUDE.md compliance) - Add EE companion PR workflow to the /pr skill Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: add wm-ts-nav tree-sitter navigator and fix format hooks - Add wm-ts-nav: standalone tree-sitter code navigator with SQLite index for fast symbol search, definition lookup, and file outlines across Rust, TypeScript, and Svelte files (~12ms warm, ~1s cold for 482 files) - Fix format hooks: surface errors instead of swallowing with 2>/dev/null, use direct prettier path with svelte plugin, add success feedback - Add wm-ts-nav commands to settings allow list - Document wm-ts-nav usage in CLAUDE.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(wm-ts-nav): add refs command and --parent filter - refs: find usages of a symbol in code, skipping comments and strings (tree-sitter AST walk, ~46ms for 482 files vs grep's 4ms but no noise) - --parent filter on search: find all methods on a type across all files (e.g. search "%" --kind function --parent ServiceName) - Update CLAUDE.md with clearer when-to-use guidance Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(wm-ts-nav): index refs in DB with import-path resolution Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat(wm-ts-nav): add body, callers, callees commands and refs --file/--caller - body: extract a symbol's source code from disk using indexed line ranges - callers: cross-file call graph via SQL join of refs + symbols tables - callees: list all identifiers referenced within a symbol's body - refs --file: scope results to files matching a substring - refs --caller: annotate each ref with the containing function name Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat(wm-ts-nav): add auto-rebuilding wrapper script The `wm-ts-nav/nav` wrapper checks if source files are newer than the binary and rebuilds automatically. Invoked via `sh wm-ts-nav/nav` to avoid needing executable permissions after clone. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: tighten CLAUDE.md nav section for actionable guidance Remove redundant question→command mapping, latency numbers, and excessive examples. Lead with "prefer wm-ts-nav over Read to save context window" and keep only the patterns that change behavior. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: revert backend/Cargo.lock to main Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * chore: promote wm-ts-nav in workflow, copy binary to worktrees - CLAUDE.md: integrate wm-ts-nav into Workflow step 1 and Core Principles so agents use outline/body before full file reads - workmux: copy built binary via files.copy - worktree-common.sh: copy binary in wm_copy_dependencies for webmux Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(wm-ts-nav): fix double indexing, add TSX grammar, remove needless clone - Reuse index stats from the pre-query update instead of indexing twice on the Index command - Add Lang::Tsx variant so .tsx/.jsx files use LANGUAGE_TSX instead of LANGUAGE_TYPESCRIPT (Svelte stays on TS since script blocks are pure TS) - Remove source.clone() for non-Svelte files — move directly instead Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(wm-ts-nav): fix svelte line numbers, add class methods, innermost caller Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
717 lines
26 KiB
Rust
717 lines
26 KiB
Rust
use anyhow::{Context, Result};
|
|
use std::path::Path;
|
|
use tree_sitter::{Node, Parser};
|
|
|
|
#[derive(Debug, Clone, serde::Serialize)]
|
|
pub struct Symbol {
|
|
pub name: String,
|
|
pub kind: String,
|
|
pub line: usize,
|
|
pub end_line: usize,
|
|
pub signature: Option<String>,
|
|
pub parent: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct IdentRef {
|
|
pub name: String,
|
|
pub line: usize,
|
|
/// Resolved import path if known (e.g. "windmill_common::error::Error")
|
|
pub import_path: Option<String>,
|
|
}
|
|
|
|
/// A `use` import with its scope
|
|
#[derive(Debug, Clone)]
|
|
pub struct ImportEntry {
|
|
/// The short name (e.g. "Error")
|
|
pub name: String,
|
|
/// Full path (e.g. "windmill_common::error::Error")
|
|
pub full_path: String,
|
|
/// Line where the use is declared
|
|
pub line: usize,
|
|
/// End of the scope this use lives in (file end for top-level, block end for scoped)
|
|
pub scope_end: usize,
|
|
}
|
|
|
|
pub struct ParseResult {
|
|
pub symbols: Vec<Symbol>,
|
|
pub refs: Vec<IdentRef>,
|
|
}
|
|
|
|
pub enum Lang {
|
|
Rust,
|
|
Typescript,
|
|
Tsx,
|
|
}
|
|
|
|
impl Lang {
|
|
pub fn from_path(path: &Path) -> Option<Self> {
|
|
match path.extension()?.to_str()? {
|
|
"rs" => Some(Self::Rust),
|
|
"tsx" | "jsx" => Some(Self::Tsx),
|
|
"ts" | "js" => Some(Self::Typescript),
|
|
"svelte" => Some(Self::Typescript), // we extract <script> block, which is pure TS
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn parse_file(path: &Path) -> Result<ParseResult> {
|
|
let lang = Lang::from_path(path).context("unsupported file type")?;
|
|
let source = std::fs::read_to_string(path)
|
|
.with_context(|| format!("reading {}", path.display()))?;
|
|
|
|
let code = match lang {
|
|
Lang::Typescript if path.extension().map(|e| e == "svelte").unwrap_or(false) => {
|
|
extract_svelte_script(&source)
|
|
}
|
|
_ => source,
|
|
};
|
|
|
|
let mut parser = Parser::new();
|
|
match lang {
|
|
Lang::Rust => {
|
|
parser.set_language(&tree_sitter_rust::LANGUAGE.into())?;
|
|
}
|
|
Lang::Typescript => {
|
|
parser.set_language(&tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into())?;
|
|
}
|
|
Lang::Tsx => {
|
|
parser.set_language(&tree_sitter_typescript::LANGUAGE_TSX.into())?;
|
|
}
|
|
}
|
|
|
|
let tree = parser
|
|
.parse(&code, None)
|
|
.context("tree-sitter parse failed")?;
|
|
let root = tree.root_node();
|
|
|
|
let mut symbols = Vec::new();
|
|
match lang {
|
|
Lang::Rust => extract_rust_symbols(root, &code, &mut symbols, None),
|
|
Lang::Typescript | Lang::Tsx => extract_ts_symbols(root, &code, &mut symbols, None),
|
|
}
|
|
|
|
let mut imports = Vec::new();
|
|
let file_end = code.lines().count();
|
|
match lang {
|
|
Lang::Rust => collect_rust_imports(root, &code, &mut imports, file_end),
|
|
Lang::Typescript | Lang::Tsx => collect_ts_imports(root, &code, &mut imports, file_end),
|
|
}
|
|
|
|
let mut refs = Vec::new();
|
|
collect_ident_refs(root, &code, &mut refs);
|
|
|
|
// Resolve import paths for refs
|
|
resolve_refs(&mut refs, &imports);
|
|
|
|
Ok(ParseResult { symbols, refs })
|
|
}
|
|
|
|
fn extract_svelte_script(source: &str) -> String {
|
|
// Preserve original line positions: script lines stay at their original line numbers,
|
|
// non-script lines become empty. Tree-sitter then reports correct line numbers.
|
|
let mut result = String::new();
|
|
let mut in_script = false;
|
|
for line in source.lines() {
|
|
let trimmed = line.trim_start();
|
|
if !in_script && trimmed.starts_with("<script") {
|
|
result.push('\n');
|
|
in_script = true;
|
|
} else if in_script && trimmed.starts_with("</script") {
|
|
result.push('\n');
|
|
in_script = false;
|
|
} else if in_script {
|
|
result.push_str(line);
|
|
result.push('\n');
|
|
} else {
|
|
result.push('\n');
|
|
}
|
|
}
|
|
result
|
|
}
|
|
|
|
fn extract_rust_symbols(node: Node, source: &str, symbols: &mut Vec<Symbol>, parent: Option<&str>) {
|
|
let mut cursor = node.walk();
|
|
for child in node.children(&mut cursor) {
|
|
match child.kind() {
|
|
"function_item" => {
|
|
if let Some(sym) = rust_function(child, source, parent) {
|
|
symbols.push(sym);
|
|
}
|
|
}
|
|
"struct_item" => {
|
|
if let Some(name) = child_by_field(child, "name", source) {
|
|
let sig = signature_up_to_body(child, source);
|
|
symbols.push(Symbol {
|
|
name: name.clone(),
|
|
kind: "struct".into(),
|
|
line: child.start_position().row + 1,
|
|
end_line: child.end_position().row + 1,
|
|
signature: Some(sig),
|
|
parent: parent.map(String::from),
|
|
});
|
|
}
|
|
}
|
|
"enum_item" => {
|
|
if let Some(name) = child_by_field(child, "name", source) {
|
|
let sig = signature_up_to_body(child, source);
|
|
symbols.push(Symbol {
|
|
name: name.clone(),
|
|
kind: "enum".into(),
|
|
line: child.start_position().row + 1,
|
|
end_line: child.end_position().row + 1,
|
|
signature: Some(sig),
|
|
parent: parent.map(String::from),
|
|
});
|
|
}
|
|
}
|
|
"trait_item" => {
|
|
if let Some(name) = child_by_field(child, "name", source) {
|
|
let sig = signature_up_to_body(child, source);
|
|
symbols.push(Symbol {
|
|
name: name.clone(),
|
|
kind: "trait".into(),
|
|
line: child.start_position().row + 1,
|
|
end_line: child.end_position().row + 1,
|
|
signature: Some(sig),
|
|
parent: parent.map(String::from),
|
|
});
|
|
// Recurse into trait body for methods
|
|
if let Some(body) = child.child_by_field_name("body") {
|
|
extract_rust_symbols(body, source, symbols, Some(&name));
|
|
}
|
|
}
|
|
}
|
|
"impl_item" => {
|
|
let impl_name = rust_impl_name(child, source);
|
|
let sig = signature_up_to_body(child, source);
|
|
let parent_name = impl_name.as_deref().unwrap_or("impl");
|
|
symbols.push(Symbol {
|
|
name: parent_name.to_string(),
|
|
kind: "impl".into(),
|
|
line: child.start_position().row + 1,
|
|
end_line: child.end_position().row + 1,
|
|
signature: Some(sig),
|
|
parent: parent.map(String::from),
|
|
});
|
|
// Recurse into impl body for methods
|
|
if let Some(body) = child.child_by_field_name("body") {
|
|
extract_rust_symbols(body, source, symbols, Some(parent_name));
|
|
}
|
|
}
|
|
"type_item" => {
|
|
if let Some(name) = child_by_field(child, "name", source) {
|
|
symbols.push(Symbol {
|
|
name,
|
|
kind: "type_alias".into(),
|
|
line: child.start_position().row + 1,
|
|
end_line: child.end_position().row + 1,
|
|
signature: Some(node_text(child, source).to_string()),
|
|
parent: parent.map(String::from),
|
|
});
|
|
}
|
|
}
|
|
"const_item" | "static_item" => {
|
|
if let Some(name) = child_by_field(child, "name", source) {
|
|
symbols.push(Symbol {
|
|
name,
|
|
kind: child.kind().trim_end_matches("_item").into(),
|
|
line: child.start_position().row + 1,
|
|
end_line: child.end_position().row + 1,
|
|
signature: Some(signature_up_to_body(child, source)),
|
|
parent: parent.map(String::from),
|
|
});
|
|
}
|
|
}
|
|
"mod_item" => {
|
|
if let Some(name) = child_by_field(child, "name", source) {
|
|
symbols.push(Symbol {
|
|
name,
|
|
kind: "mod".into(),
|
|
line: child.start_position().row + 1,
|
|
end_line: child.end_position().row + 1,
|
|
signature: None,
|
|
parent: parent.map(String::from),
|
|
});
|
|
}
|
|
}
|
|
"macro_definition" => {
|
|
if let Some(name) = child_by_field(child, "name", source) {
|
|
symbols.push(Symbol {
|
|
name,
|
|
kind: "macro".into(),
|
|
line: child.start_position().row + 1,
|
|
end_line: child.end_position().row + 1,
|
|
signature: None,
|
|
parent: parent.map(String::from),
|
|
});
|
|
}
|
|
}
|
|
// Recurse into declaration_list (impl body, trait body)
|
|
"declaration_list" => {
|
|
extract_rust_symbols(child, source, symbols, parent);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn rust_function(node: Node, source: &str, parent: Option<&str>) -> Option<Symbol> {
|
|
let name = child_by_field(node, "name", source)?;
|
|
let sig = rust_fn_signature(node, source);
|
|
Some(Symbol {
|
|
name,
|
|
kind: "function".into(),
|
|
line: node.start_position().row + 1,
|
|
end_line: node.end_position().row + 1,
|
|
signature: Some(sig),
|
|
parent: parent.map(String::from),
|
|
})
|
|
}
|
|
|
|
fn rust_fn_signature(node: Node, source: &str) -> String {
|
|
// Capture everything up to the block (the `{`)
|
|
let text = node_text(node, source);
|
|
if let Some(brace) = text.find('{') {
|
|
text[..brace].trim().to_string()
|
|
} else {
|
|
// No body (trait declaration)
|
|
text.lines().next().unwrap_or("").to_string()
|
|
}
|
|
}
|
|
|
|
fn rust_impl_name(node: Node, source: &str) -> Option<String> {
|
|
// impl [Trait for] Type
|
|
let mut cursor = node.walk();
|
|
let mut type_name = None;
|
|
let mut trait_name = None;
|
|
for child in node.children(&mut cursor) {
|
|
match child.kind() {
|
|
"type_identifier" | "scoped_type_identifier" | "generic_type" => {
|
|
if trait_name.is_none() && type_name.is_none() {
|
|
type_name = Some(node_text(child, source).to_string());
|
|
} else if type_name.is_some() {
|
|
// This is the type after "for"
|
|
trait_name = type_name.take();
|
|
type_name = Some(node_text(child, source).to_string());
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
match (trait_name, type_name) {
|
|
(Some(t), Some(ty)) => Some(format!("{t} for {ty}")),
|
|
(None, Some(ty)) => Some(ty),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn extract_ts_symbols(node: Node, source: &str, symbols: &mut Vec<Symbol>, parent: Option<&str>) {
|
|
let mut cursor = node.walk();
|
|
for child in node.children(&mut cursor) {
|
|
match child.kind() {
|
|
"function_declaration" => {
|
|
if let Some(name) = child_by_field(child, "name", source) {
|
|
symbols.push(Symbol {
|
|
name,
|
|
kind: "function".into(),
|
|
line: child.start_position().row + 1,
|
|
end_line: child.end_position().row + 1,
|
|
signature: Some(signature_up_to_body(child, source)),
|
|
parent: parent.map(String::from),
|
|
});
|
|
}
|
|
}
|
|
"interface_declaration" => {
|
|
if let Some(name) = child_by_field(child, "name", source) {
|
|
symbols.push(Symbol {
|
|
name,
|
|
kind: "interface".into(),
|
|
line: child.start_position().row + 1,
|
|
end_line: child.end_position().row + 1,
|
|
signature: Some(signature_up_to_body(child, source)),
|
|
parent: parent.map(String::from),
|
|
});
|
|
}
|
|
}
|
|
"type_alias_declaration" => {
|
|
if let Some(name) = child_by_field(child, "name", source) {
|
|
symbols.push(Symbol {
|
|
name,
|
|
kind: "type_alias".into(),
|
|
line: child.start_position().row + 1,
|
|
end_line: child.end_position().row + 1,
|
|
signature: Some(node_text(child, source).to_string()),
|
|
parent: parent.map(String::from),
|
|
});
|
|
}
|
|
}
|
|
"enum_declaration" => {
|
|
if let Some(name) = child_by_field(child, "name", source) {
|
|
symbols.push(Symbol {
|
|
name,
|
|
kind: "enum".into(),
|
|
line: child.start_position().row + 1,
|
|
end_line: child.end_position().row + 1,
|
|
signature: Some(signature_up_to_body(child, source)),
|
|
parent: parent.map(String::from),
|
|
});
|
|
}
|
|
}
|
|
"class_declaration" => {
|
|
if let Some(name) = child_by_field(child, "name", source) {
|
|
symbols.push(Symbol {
|
|
name: name.clone(),
|
|
kind: "class".into(),
|
|
line: child.start_position().row + 1,
|
|
end_line: child.end_position().row + 1,
|
|
signature: Some(signature_up_to_body(child, source)),
|
|
parent: parent.map(String::from),
|
|
});
|
|
if let Some(body) = child.child_by_field_name("body") {
|
|
extract_ts_symbols(body, source, symbols, Some(&name));
|
|
}
|
|
}
|
|
}
|
|
"method_definition" | "abstract_method_definition"
|
|
| "abstract_method_signature" | "method_signature" => {
|
|
if let Some(name) = child_by_field(child, "name", source) {
|
|
symbols.push(Symbol {
|
|
name,
|
|
kind: "method".into(),
|
|
line: child.start_position().row + 1,
|
|
end_line: child.end_position().row + 1,
|
|
signature: Some(signature_up_to_body(child, source)),
|
|
parent: parent.map(String::from),
|
|
});
|
|
}
|
|
}
|
|
"public_field_definition" => {
|
|
if let Some(name) = child_by_field(child, "name", source) {
|
|
let kind = if let Some(value) = child.child_by_field_name("value") {
|
|
match value.kind() {
|
|
"arrow_function" | "function_expression" | "function" => "method",
|
|
_ => "property",
|
|
}
|
|
} else {
|
|
"property"
|
|
};
|
|
symbols.push(Symbol {
|
|
name,
|
|
kind: kind.into(),
|
|
line: child.start_position().row + 1,
|
|
end_line: child.end_position().row + 1,
|
|
signature: Some(signature_up_to_body(child, source)),
|
|
parent: parent.map(String::from),
|
|
});
|
|
}
|
|
}
|
|
"export_statement" | "program" => {
|
|
extract_ts_symbols(child, source, symbols, parent);
|
|
}
|
|
"lexical_declaration" | "variable_declaration" => {
|
|
// const/let/var foo = ...
|
|
ts_variable_decl(child, source, symbols, parent);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn ts_variable_decl(
|
|
node: Node,
|
|
source: &str,
|
|
symbols: &mut Vec<Symbol>,
|
|
parent: Option<&str>,
|
|
) {
|
|
let mut cursor = node.walk();
|
|
for child in node.children(&mut cursor) {
|
|
if child.kind() == "variable_declarator" {
|
|
if let Some(name) = child_by_field(child, "name", source) {
|
|
// Check if the value is an arrow function or function expression
|
|
let kind = if let Some(value) = child.child_by_field_name("value") {
|
|
match value.kind() {
|
|
"arrow_function" | "function_expression" | "function" => "function",
|
|
_ => "const",
|
|
}
|
|
} else {
|
|
"const"
|
|
};
|
|
symbols.push(Symbol {
|
|
name,
|
|
kind: kind.into(),
|
|
line: node.start_position().row + 1,
|
|
end_line: node.end_position().row + 1,
|
|
signature: Some(signature_up_to_body(node, source)),
|
|
parent: parent.map(String::from),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn child_by_field(node: Node, field: &str, source: &str) -> Option<String> {
|
|
Some(node_text(node.child_by_field_name(field)?, source).to_string())
|
|
}
|
|
|
|
fn node_text<'a>(node: Node, source: &'a str) -> &'a str {
|
|
&source[node.byte_range()]
|
|
}
|
|
|
|
fn signature_up_to_body(node: Node, source: &str) -> String {
|
|
let text = node_text(node, source);
|
|
// Find first `{` that starts a block body
|
|
if let Some(pos) = text.find('{') {
|
|
let sig = text[..pos].trim();
|
|
// Collapse whitespace
|
|
sig.split_whitespace().collect::<Vec<_>>().join(" ")
|
|
} else {
|
|
let first_line = text.lines().next().unwrap_or("");
|
|
first_line.trim().to_string()
|
|
}
|
|
}
|
|
|
|
/// Collect all identifier references in code, skipping comments and strings.
|
|
fn collect_ident_refs(node: Node, source: &str, refs: &mut Vec<IdentRef>) {
|
|
match node.kind() {
|
|
// Skip non-code nodes
|
|
"line_comment" | "block_comment" | "string_literal" | "raw_string_literal"
|
|
| "string" | "template_string" | "string_fragment" | "comment"
|
|
| "string_content" | "char_literal" => return,
|
|
_ => {}
|
|
}
|
|
|
|
if is_identifier_node(node.kind()) {
|
|
let text = node_text(node, source);
|
|
// Skip single-char identifiers and keywords
|
|
if text.len() > 1 {
|
|
refs.push(IdentRef {
|
|
name: text.to_string(),
|
|
line: node.start_position().row + 1,
|
|
import_path: None, // resolved later
|
|
});
|
|
}
|
|
}
|
|
|
|
let mut cursor = node.walk();
|
|
for child in node.children(&mut cursor) {
|
|
collect_ident_refs(child, source, refs);
|
|
}
|
|
}
|
|
|
|
fn is_identifier_node(kind: &str) -> bool {
|
|
matches!(
|
|
kind,
|
|
"identifier"
|
|
| "type_identifier"
|
|
| "field_identifier"
|
|
| "property_identifier"
|
|
| "shorthand_field_identifier"
|
|
)
|
|
}
|
|
|
|
/// Collect `use` declarations from Rust source.
|
|
/// Handles: `use foo::bar::Baz;`, `use foo::bar::{Baz, Qux};`, `use foo::bar as Alias;`
|
|
fn collect_rust_imports(node: Node, source: &str, imports: &mut Vec<ImportEntry>, file_end: usize) {
|
|
let mut cursor = node.walk();
|
|
for child in node.children(&mut cursor) {
|
|
match child.kind() {
|
|
"use_declaration" => {
|
|
let scope_end = find_scope_end(child, file_end);
|
|
let text = node_text(child, source);
|
|
parse_rust_use(text, child.start_position().row + 1, scope_end, imports);
|
|
}
|
|
// Recurse into blocks/functions to find scoped imports
|
|
"function_item" | "block" | "impl_item" | "mod_item" => {
|
|
let block_end = child.end_position().row + 1;
|
|
collect_rust_imports_scoped(child, source, imports, block_end);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn collect_rust_imports_scoped(
|
|
node: Node,
|
|
source: &str,
|
|
imports: &mut Vec<ImportEntry>,
|
|
scope_end: usize,
|
|
) {
|
|
let mut cursor = node.walk();
|
|
for child in node.children(&mut cursor) {
|
|
match child.kind() {
|
|
"use_declaration" => {
|
|
let text = node_text(child, source);
|
|
parse_rust_use(text, child.start_position().row + 1, scope_end, imports);
|
|
}
|
|
"block" | "declaration_list" => {
|
|
collect_rust_imports_scoped(child, source, imports, scope_end);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Parse a Rust `use` statement text into ImportEntry items.
|
|
fn parse_rust_use(text: &str, line: usize, scope_end: usize, imports: &mut Vec<ImportEntry>) {
|
|
// Strip `use ` prefix and `;` suffix
|
|
let text = text.trim();
|
|
let text = text.strip_prefix("use ").unwrap_or(text);
|
|
let text = text.strip_suffix(';').unwrap_or(text).trim();
|
|
// Strip visibility (pub, pub(crate), etc.)
|
|
let text = if text.starts_with("pub") {
|
|
if let Some(rest) = text.strip_prefix("pub(") {
|
|
// pub(crate) use ..., pub(super) use ...
|
|
if let Some(after) = rest.find(')') {
|
|
rest[after + 1..].trim()
|
|
} else {
|
|
text
|
|
}
|
|
} else {
|
|
text.strip_prefix("pub ").unwrap_or(text).trim()
|
|
}
|
|
} else {
|
|
text
|
|
};
|
|
|
|
// Handle `use foo::bar::{A, B, C};`
|
|
if let Some(brace_start) = text.find('{') {
|
|
let prefix = &text[..brace_start];
|
|
let brace_end = text.rfind('}').unwrap_or(text.len());
|
|
let inner = &text[brace_start + 1..brace_end];
|
|
for item in inner.split(',') {
|
|
let item = item.trim();
|
|
if item.is_empty() {
|
|
continue;
|
|
}
|
|
// Handle `Foo as Bar`
|
|
let (orig, alias) = if let Some(as_pos) = item.find(" as ") {
|
|
(&item[..as_pos], &item[as_pos + 4..])
|
|
} else {
|
|
(item, item)
|
|
};
|
|
let alias = alias.trim();
|
|
let full = format!("{}{}", prefix, orig.trim());
|
|
if !alias.is_empty() && alias != "self" && alias != "*" {
|
|
imports.push(ImportEntry {
|
|
name: alias.to_string(),
|
|
full_path: full,
|
|
line,
|
|
scope_end,
|
|
});
|
|
}
|
|
}
|
|
} else {
|
|
// Simple: `use foo::bar::Baz;` or `use foo::bar::Baz as Alias;`
|
|
let (path, alias) = if let Some(as_pos) = text.find(" as ") {
|
|
(&text[..as_pos], &text[as_pos + 4..])
|
|
} else {
|
|
let name = text.rsplit("::").next().unwrap_or(text);
|
|
(text, name)
|
|
};
|
|
let alias = alias.trim();
|
|
if !alias.is_empty() && alias != "self" && alias != "*" {
|
|
imports.push(ImportEntry {
|
|
name: alias.to_string(),
|
|
full_path: path.trim().to_string(),
|
|
line,
|
|
scope_end,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Collect `import` declarations from TypeScript/Svelte.
|
|
fn collect_ts_imports(node: Node, source: &str, imports: &mut Vec<ImportEntry>, file_end: usize) {
|
|
let mut cursor = node.walk();
|
|
for child in node.children(&mut cursor) {
|
|
if child.kind() == "import_statement" {
|
|
let text = node_text(child, source);
|
|
parse_ts_import(text, child.start_position().row + 1, file_end, imports);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Parse a TS `import { A, B } from 'module'` into ImportEntry items.
|
|
fn parse_ts_import(text: &str, line: usize, scope_end: usize, imports: &mut Vec<ImportEntry>) {
|
|
// Extract module path from `from '...'` or `from "..."`
|
|
let module = if let Some(from_pos) = text.find("from ") {
|
|
let rest = &text[from_pos + 5..];
|
|
rest.trim()
|
|
.trim_matches(|c| c == '\'' || c == '"' || c == ';')
|
|
.to_string()
|
|
} else {
|
|
return;
|
|
};
|
|
|
|
// Extract imported names from `{ A, B as C }`
|
|
if let Some(brace_start) = text.find('{') {
|
|
let brace_end = text.find('}').unwrap_or(text.len());
|
|
let inner = &text[brace_start + 1..brace_end];
|
|
for item in inner.split(',') {
|
|
let item = item.trim();
|
|
if item.is_empty() {
|
|
continue;
|
|
}
|
|
let (orig, alias) = if let Some(as_pos) = item.find(" as ") {
|
|
(&item[..as_pos], &item[as_pos + 4..])
|
|
} else {
|
|
(item, item)
|
|
};
|
|
let alias = alias.trim();
|
|
if !alias.is_empty() {
|
|
imports.push(ImportEntry {
|
|
name: alias.to_string(),
|
|
full_path: format!("{}.{}", module, orig.trim()),
|
|
line,
|
|
scope_end,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
// Default import: `import Foo from '...'`
|
|
else {
|
|
let text_trimmed = text.trim().strip_prefix("import ").unwrap_or("");
|
|
if let Some(name_end) = text_trimmed.find(|c: char| c.is_whitespace()) {
|
|
let name = &text_trimmed[..name_end];
|
|
if !name.is_empty() && name != "type" && !name.starts_with('{') {
|
|
imports.push(ImportEntry {
|
|
name: name.to_string(),
|
|
full_path: format!("{}.default", module),
|
|
line,
|
|
scope_end,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Find the end line of the enclosing scope for a node.
|
|
fn find_scope_end(node: Node, file_end: usize) -> usize {
|
|
let mut parent = node.parent();
|
|
while let Some(p) = parent {
|
|
match p.kind() {
|
|
"block" | "declaration_list" | "function_item" => {
|
|
return p.end_position().row + 1;
|
|
}
|
|
_ => parent = p.parent(),
|
|
}
|
|
}
|
|
file_end
|
|
}
|
|
|
|
/// Resolve import paths for identifier refs.
|
|
fn resolve_refs(refs: &mut [IdentRef], imports: &[ImportEntry]) {
|
|
for r in refs.iter_mut() {
|
|
// Find the best matching import: same name, declared before the ref, ref within scope
|
|
let best = imports
|
|
.iter()
|
|
.filter(|imp| imp.name == r.name && imp.line <= r.line && r.line <= imp.scope_end)
|
|
.last(); // last = most recently declared (innermost scope)
|
|
if let Some(imp) = best {
|
|
r.import_path = Some(imp.full_path.clone());
|
|
}
|
|
}
|
|
}
|