mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-18 22:02:49 +08:00
* fix(csharp): walk field and property initializers as the member Field declarators and property `= initializer` values were skipped by both extractors, so `private readonly ILogger _log = LogManager.GetLogger(typeof(X));` linked nothing, and a method or class used only from an initializer looked unused. - tree-sitter.ts / csharp.rs: walk each C# field declarator whole with its field on the stack, and every property `value:` (not only `=> expr`) with the property on the stack. Calls, instantiations, static reads, lambda bodies and fn-ref candidates (varinit included) belong to the member; the fn-ref scan skips the walked subtrees, so each candidate is captured once. Attributes stay unwalked. - A target-typed `new()` that is the initializer instantiates the declared type (`List<Foo> _items = new();` -> List). In bodies it stays invisible, as before. - name-matcher.ts: a .NET type position never names a `constant` (the kind `const` / `static readonly` fields get). Walking initializers exposed `new Version(5, 18)` binding to a `const string Version` and `static readonly Meter Meter = new(...)` instantiating itself. Kernel/wasm parity: 0 diffs on serilog, Newtonsoft.Json and jellyfin, and full-index dumps byte-identical between the two arms. No EXTRACTION_VERSION bump: #2345 already moved it to 28 this release. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs(changelog): credit the report and contributor behind the constant change The type-position change here is what issue #2337 reports (`new Station { … }` unresolved beside a `private const string Station`) and the same one-line change as @drakeo338's #2339. Verified on the issue's own three-file repro: the `instantiates` edge to `Demo.Core::Station`, missing on main, is present with this branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,8 +5,8 @@
|
||||
//! path, bug-for-bug, verified by scripts/kernel-parity.mjs and the full-index
|
||||
//! dump-diff gate. The authoritative quirk list is
|
||||
//! docs/design/csharp-kernel-port-checklist.md — including every deliberate
|
||||
//! emission hole (field/property initializers, constructor initializers,
|
||||
//! delegates/events/operators/indexers, top-level locals) and garbage ref
|
||||
//! emission hole (constructor initializers, delegates/events/operators/
|
||||
//! indexers, top-level locals) and garbage ref
|
||||
//! (`(repo)` primary-ctor extends, `: byte` enum extends, `nameof` calls)
|
||||
//! this file preserves on purpose. Positions in UTF-16 code units. Files whose
|
||||
//! parse tree contains ERRORS defer to the wasm extractor.
|
||||
@@ -572,16 +572,18 @@ impl<'t> Walker<'t> {
|
||||
self.extract_enum(node);
|
||||
skip_children = true;
|
||||
} else if kind == "property_declaration" && self.inside_class_like() {
|
||||
// The code a property runs — its accessor bodies and `=> expr` —
|
||||
// is walked with the property on the stack (propertyBodies). The
|
||||
// candidates-only scan covers the rest (an `= initializer`),
|
||||
// The code a property runs — its accessor bodies, `=> expr` and
|
||||
// `= initializer` — is walked with the property on the stack
|
||||
// (propertyBodies). The candidates-only scan covers the rest,
|
||||
// skipping what the body walk captured.
|
||||
let walked: Vec<usize> = match self.extract_property(node) {
|
||||
Some((row, name)) => {
|
||||
let bodies = property_bodies(node);
|
||||
if !bodies.is_empty() {
|
||||
self.stack.push(Scope { row, kind: "property", name });
|
||||
let declared = node.child_by_field_name("type");
|
||||
for body in &bodies {
|
||||
self.extract_target_typed_new(Some(*body), declared);
|
||||
self.visit_function_body(*body);
|
||||
}
|
||||
self.stack.pop();
|
||||
@@ -593,8 +595,11 @@ impl<'t> Walker<'t> {
|
||||
self.scan_fn_ref_subtree(node, 0, &walked);
|
||||
skip_children = true;
|
||||
} else if kind == "field_declaration" && self.inside_class_like() {
|
||||
self.extract_field(node);
|
||||
self.scan_fn_ref_subtree(node, 0, &[]);
|
||||
// Each declarator, `= initializer` included, is walked with its
|
||||
// field on the stack (extract_field); the candidates-only scan
|
||||
// covers the rest, skipping the declarators that walk captured.
|
||||
let walked = self.extract_field(node);
|
||||
self.scan_fn_ref_subtree(node, 0, &walked);
|
||||
skip_children = true;
|
||||
} else if kind == "local_declaration_statement" && !self.inside_class_like() {
|
||||
// Top-level statements: extractVariable's generic fallback finds no
|
||||
@@ -864,8 +869,11 @@ impl<'t> Walker<'t> {
|
||||
}
|
||||
|
||||
/// extractField (2046) — field_declaration; each declarator becomes a
|
||||
/// field/constant node anchored at the DECLARATOR.
|
||||
fn extract_field(&mut self, node: Node<'t>) {
|
||||
/// field/constant node anchored at the DECLARATOR, and the declarator —
|
||||
/// its `= initializer` — is walked with that node on the stack. Returns
|
||||
/// the walked declarators, for the fn-ref scan to skip.
|
||||
fn extract_field(&mut self, node: Node<'t>) -> Vec<usize> {
|
||||
let mut walked = Vec::new();
|
||||
let docstring = preceding_docstring(node, self.src);
|
||||
let visibility = Some(self.visibility_of(node));
|
||||
let is_static = Some(self.is_static(node));
|
||||
@@ -932,6 +940,15 @@ impl<'t> Walker<'t> {
|
||||
// multi-declarator fields emit the type refs once PER
|
||||
// declarator, each from its own field node.
|
||||
self.extract_csharp_type_refs(node, row);
|
||||
// The initializer is the declarator's last, unnamed child:
|
||||
// its calls, instantiations, static reads and fn-ref
|
||||
// candidates (varinit included) are the field's.
|
||||
self.stack.push(Scope { row, kind: field_kind, name });
|
||||
let declared = var_decl.and_then(|vd| vd.child_by_field_name("type"));
|
||||
self.extract_target_typed_new(last_named_child(decl), declared);
|
||||
self.visit_function_body(decl);
|
||||
self.stack.pop();
|
||||
walked.push(decl.id());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -951,6 +968,44 @@ impl<'t> Walker<'t> {
|
||||
);
|
||||
}
|
||||
}
|
||||
walked
|
||||
}
|
||||
|
||||
/// extractTargetTypedNew (tree-sitter.ts) — a target-typed `new()`
|
||||
/// (implicit_object_creation_expression) names no type, so it is no
|
||||
/// instantiation kind; as a field's or property's initializer it
|
||||
/// constructs the declared type (`List<Foo> _items = new();` → List).
|
||||
/// Emitted from the stack top (the member) at the `new()`.
|
||||
fn extract_target_typed_new(&mut self, value: Option<Node<'t>>, declared: Option<Node<'t>>) {
|
||||
let Some(value) = value else { return };
|
||||
if value.kind() != "implicit_object_creation_expression" || self.stack.is_empty() {
|
||||
return;
|
||||
}
|
||||
let Some(class_name) = declared.and_then(|t| self.class_type_name(t)) else { return };
|
||||
let from = self.top_row();
|
||||
self.push_ref_at(from, &class_name, edge_kind_index("instantiates").unwrap(), value);
|
||||
}
|
||||
|
||||
/// csharpClassTypeName (tree-sitter.ts) — the class a declared type
|
||||
/// names, as `new T()` would name it: `List<Foo>` → List, `Ns.Foo` /
|
||||
/// `global::Foo` → Foo, `Foo?` → Foo. Predefined, array, tuple and pointer
|
||||
/// types name no class.
|
||||
fn class_type_name(&self, node: Node) -> Option<String> {
|
||||
match node.kind() {
|
||||
"identifier" => {
|
||||
let text = self.text(node);
|
||||
(!text.is_empty()).then(|| text.to_string())
|
||||
}
|
||||
"generic_name" => {
|
||||
let ident = (0..node.named_child_count())
|
||||
.filter_map(|i| node.named_child(i))
|
||||
.find(|c| c.kind() == "identifier")?;
|
||||
self.class_type_name(ident)
|
||||
}
|
||||
"qualified_name" | "alias_qualified_name" => self.class_type_name(node.child_by_field_name("name")?),
|
||||
"nullable_type" => self.class_type_name(node.child_by_field_name("type")?),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// extractMethod (1737) — method_declaration + constructor_declaration.
|
||||
@@ -1142,7 +1197,8 @@ impl<'t> Walker<'t> {
|
||||
let Some(ctor) = ctor else { return };
|
||||
// `new List<Foo>()` → `List`; `new Ns.Foo()` → `Foo`. Target-typed
|
||||
// `new()` / anonymous `new { }` / arrays `new T[n]` never reach here
|
||||
// (not in INSTANTIATION_KINDS) — invisible by design.
|
||||
// (not in INSTANTIATION_KINDS) — invisible by design, except a `new()`
|
||||
// that initializes a field or property (extract_target_typed_new).
|
||||
let class_name = strip_generic_and_qualifier(self.text(ctor));
|
||||
if !class_name.is_empty() {
|
||||
let from = self.top_row();
|
||||
@@ -1492,8 +1548,9 @@ impl<'t> Walker<'t> {
|
||||
return;
|
||||
}
|
||||
// functionTypes is EMPTY for C#; the literal halt list applies —
|
||||
// lambda_expression IS C#'s lambda, so initializer lambdas stop the
|
||||
// scan; anonymous_method_expression is NOT listed and scans through.
|
||||
// lambda_expression IS C#'s lambda, so a lambda the scan reaches (a
|
||||
// top-level statement's) stops it; anonymous_method_expression is NOT
|
||||
// listed and scans through. Member initializers are walked instead.
|
||||
if depth > 0
|
||||
&& matches!(
|
||||
node.kind(),
|
||||
@@ -1643,8 +1700,9 @@ impl<'t> Walker<'t> {
|
||||
}
|
||||
|
||||
/// propertyBodies (tree-sitter.ts) — the parts of a property that run code:
|
||||
/// each accessor's `body` (a block or `=> expr`) and an expression-bodied
|
||||
/// property's `=> …` value. An `= initializer` value is not a body.
|
||||
/// each accessor's `body` (a block or `=> expr`), then the property's
|
||||
/// `value` — an expression body's `=> …` or an `= initializer`. Attributes
|
||||
/// are not walked.
|
||||
fn property_bodies(node: Node) -> Vec<Node> {
|
||||
let mut bodies = Vec::new();
|
||||
if let Some(accessors) = node.child_by_field_name("accessors") {
|
||||
@@ -1659,13 +1717,15 @@ fn property_bodies(node: Node) -> Vec<Node> {
|
||||
}
|
||||
}
|
||||
if let Some(value) = node.child_by_field_name("value") {
|
||||
if value.kind() == "arrow_expression_clause" {
|
||||
bodies.push(value);
|
||||
}
|
||||
bodies.push(value);
|
||||
}
|
||||
bodies
|
||||
}
|
||||
|
||||
fn last_named_child(node: Node) -> Option<Node> {
|
||||
node.named_child(node.named_child_count().checked_sub(1)?)
|
||||
}
|
||||
|
||||
fn find_anonymous_class_body(node: Node) -> Option<Node> {
|
||||
for i in 0..node.named_child_count() {
|
||||
if let Some(child) = node.named_child(i) {
|
||||
|
||||
Reference in New Issue
Block a user