fix(csharp): namespaces, usings and nested types follow the language's scopes (#2220)

A block namespace qualifies only its own body (a nested one is Outer.Inner),
mirrored in the kernel; a using names the project's namespace, never another
file's using; a global using is its own project's; a nested type is reached
by bare name only from inside its owner or a type deriving from it, through
any partial part; visible C# types are filtered before ranking.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-09-30 23:56:34 +00:00
committed by GitHub
parent 03123335b0
commit ff75a6ca5f
7 changed files with 391 additions and 51 deletions
+53 -22
View File
@@ -205,34 +205,21 @@ pub fn extract(file_path: &str, source: &str) -> Result<EmitOut, String> {
w.node_ids.push(ids::file_node_id(file_path));
w.stack.push(Scope { row: 0, kind: "file", name: base_name.to_string() });
// extractFilePackage: the FIRST top-level namespace declaration mints ONE
// `namespace` node that stays pushed for the ENTIRE file — a second
// top-level namespace's types nest under the first's node/QN, nested
// namespaces leave no trace, and every import ref in a namespaced file
// hangs off this node (checklist §namespace).
// extractFilePackage: a top-level file-scoped `namespace X;` mints ONE
// `namespace` node that stays pushed for the ENTIRE file, so every import
// ref in the file hangs off it (checklist §namespace). A block namespace
// scopes only its own body — visit_node.
let root = tree.root_node();
let mut pkg_pushed = false;
for i in 0..root.named_child_count() {
let Some(child) = root.named_child(i) else { continue };
if child.kind() != "namespace_declaration"
&& child.kind() != "file_scoped_namespace_declaration"
{
if child.kind() != "file_scoped_namespace_declaration" {
continue;
}
// csharpExtractor.extractPackage: `name` field ?? first
// qualified_name/identifier named child. No trim.
let name_node = child.child_by_field_name("name").or_else(|| {
(0..child.named_child_count())
.filter_map(|j| child.named_child(j))
.find(|c| matches!(c.kind(), "qualified_name" | "identifier"))
});
if let Some(name_node) = name_node {
let pkg = w.text(name_node).to_string();
if !pkg.is_empty() {
if let Some(row) = w.create_node("namespace", &pkg, child, Extra::default()) {
w.stack.push(Scope { row, kind: "namespace", name: pkg });
pkg_pushed = true;
}
if let Some(pkg) = namespace_name(&w, child) {
if let Some(row) = w.create_node("namespace", &pkg, child, Extra::default()) {
w.stack.push(Scope { row, kind: "namespace", name: pkg });
pkg_pushed = true;
}
}
break;
@@ -265,6 +252,18 @@ fn record_is_struct(node: Node) -> bool {
.any(|c| c.kind() == "struct")
}
/// csharpExtractor.extractPackage: `name` field ?? first qualified_name /
/// identifier named child. No trim; None when empty.
fn namespace_name(w: &Walker, node: Node) -> Option<String> {
let name_node = node.child_by_field_name("name").or_else(|| {
(0..node.named_child_count())
.filter_map(|j| node.named_child(j))
.find(|c| matches!(c.kind(), "qualified_name" | "identifier"))
})?;
let name = w.text(name_node);
(!name.is_empty()).then(|| name.to_string())
}
impl<'t> Walker<'t> {
fn text(&self, node: Node) -> &'t str {
&self.src[node.byte_range()]
@@ -518,6 +517,38 @@ impl<'t> Walker<'t> {
let kind = node.kind();
let mut skip_children = false;
// A block namespace scopes only its body (tree-sitter.ts visitNode):
// one written inside another is `Outer.Inner`, taking the outer's
// place on the stack while its body is walked.
if kind == "namespace_declaration" {
if let Some(ns_name) = namespace_name(self, node) {
let outer = match self.stack.last() {
Some(top) if top.kind == "namespace" => self.stack.pop(),
_ => None,
};
let full = match &outer {
Some(o) => format!("{}.{}", o.name, ns_name),
None => ns_name,
};
let row = self.create_node("namespace", &full, node, Extra::default());
if let Some(row) = row {
self.stack.push(Scope { row, kind: "namespace", name: full });
}
for i in 0..node.named_child_count() {
if let Some(c) = node.named_child(i) {
self.visit_node(c);
}
}
if row.is_some() {
self.stack.pop();
}
if let Some(o) = outer {
self.stack.push(o);
}
return;
}
}
self.maybe_capture_fn_refs(node);
if kind == "class_declaration" || kind == "record_declaration" {