Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 165 additions & 3 deletions crates/ecp-analyzer/src/c_sharp/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,20 @@ struct CSharpCaptureIndices {
blind_activator_create: Option<u32>,
blind_method_invoke: Option<u32>,
function_anonymous: Option<u32>,
// Special-member root captures — handled with custom name synthesis in
// the parse loop (these nodes lack a plain `name: (identifier)` field).
operator_decl: Option<u32>,
conv_operator_decl: Option<u32>,
indexer_decl: Option<u32>,
// event_field root (variable_declaration pattern — multiple declarators).
event_field: Option<u32>,
// Roots for destructor and event_decl (name-based captures also exist in
// spec, but we need the root span separately like enum_member_node).
destructor: Option<u32>,
event_decl: Option<u32>,
// Name captures (not roots) — destructor `~` prefix; event_field multi-declarator keying.
destructor_name: Option<u32>,
event_field_name: Option<u32>,
}

pub struct CSharpProvider {
Expand Down Expand Up @@ -161,6 +175,14 @@ impl CSharpProvider {
blind_activator_create: query.capture_index_for_name("blind.activator_create"),
blind_method_invoke: query.capture_index_for_name("blind.method_invoke"),
function_anonymous: query.capture_index_for_name("function.anonymous"),
operator_decl: query.capture_index_for_name("operator_decl"),
conv_operator_decl: query.capture_index_for_name("conv_operator_decl"),
indexer_decl: query.capture_index_for_name("indexer_decl"),
event_field: query.capture_index_for_name("event_field"),
destructor: query.capture_index_for_name("destructor"),
event_decl: query.capture_index_for_name("event_decl"),
destructor_name: query.capture_index_for_name("destructor.name"),
event_field_name: query.capture_index_for_name("event_field.name"),
};

Ok(Self {
Expand Down Expand Up @@ -220,6 +242,9 @@ impl LanguageProvider for CSharpProvider {
let idx_enum_member_node = idx.enum_member_node;
let idx_struct = idx.struct_;

let idx_destructor_name = idx.destructor_name;
let idx_event_field_name = idx.event_field_name;

while let Some(m) = matches.next() {
let mut name_node = None;
let mut kind = None;
Expand All @@ -233,6 +258,17 @@ impl LanguageProvider for CSharpProvider {
let mut heritage_list = Vec::new();
let mut type_annotation = None;
let mut decorators = Vec::new();
// Set when the name capture comes from a destructor_declaration so
// the name string gets a `~` prefix appended below.
let mut is_destructor_name = false;
// Set for event_field_declaration declarator names so node_id keys
// on the identifier (same logic as Property/Variable multi-declarator).
let mut is_event_field_name = false;
// Deferred special-member root captures — node is emitted AFTER the
// capture loop so decorator/export/type metadata is fully collected.
let mut deferred_operator: Option<tree_sitter::Node<'_>> = None;
let mut deferred_conv_operator: Option<tree_sitter::Node<'_>> = None;
let mut deferred_indexer: Option<tree_sitter::Node<'_>> = None;

for cap in m.captures {
let cap_idx = cap.index;
Expand All @@ -248,6 +284,12 @@ impl LanguageProvider for CSharpProvider {
// Source of truth: CSharpSpec::CAPTURE_KIND in spec.rs.
name_node = Some(cap.node);
kind = Some(k_from_spec);
// Track special captures for name post-processing.
if Some(cap_idx) == idx_destructor_name {
is_destructor_name = true;
} else if Some(cap_idx) == idx_event_field_name {
is_event_field_name = true;
}
} else if Some(cap_idx) == idx_import_name {
import_name = Some(cap.node);
} else if Some(cap_idx) == idx_import_source {
Expand Down Expand Up @@ -329,6 +371,23 @@ impl LanguageProvider for CSharpProvider {
});
}
}
} else if Some(cap_idx) == idx.operator_decl {
// Defer: node emitted after capture loop with fully-collected metadata.
deferred_operator = Some(cap.node);
} else if Some(cap_idx) == idx.conv_operator_decl {
// Defer: node emitted after capture loop with fully-collected metadata.
deferred_conv_operator = Some(cap.node);
} else if Some(cap_idx) == idx.indexer_decl {
// Defer: node emitted after capture loop with fully-collected metadata.
deferred_indexer = Some(cap.node);
} else if Some(cap_idx) == idx.event_field {
// event_field_declaration root — span anchor for the per-declarator
// nodes emitted by the @event_field.name spec-driven captures.
// We only track this as a root; name_node / kind come from the
// spec dispatch above.
if root_span_node.is_none() {
root_span_node = Some(cap.node);
}
} else if (Some(cap_idx) == idx_function
|| Some(cap_idx) == idx_class
|| Some(cap_idx) == idx_method
Expand All @@ -339,13 +398,106 @@ impl LanguageProvider for CSharpProvider {
|| Some(cap_idx) == idx_namespace
|| Some(cap_idx) == idx_enum
|| Some(cap_idx) == idx_enum_member_node
|| Some(cap_idx) == idx_struct)
|| Some(cap_idx) == idx_struct
|| Some(cap_idx) == idx.destructor
|| Some(cap_idx) == idx.event_decl)
&& root_span_node.is_none()
{
root_span_node = Some(cap.node);
}
}

// Emit deferred special-member nodes (operator/conv-operator/indexer).
// These are processed AFTER the capture loop so that decorator/export/type
// metadata is fully accumulated before the RawNode is pushed.
let emit_special =
|n: tree_sitter::Node<'_>,
name: String,
is_exp: bool,
ty: Option<String>,
decs: Vec<String>,
nodes: &mut Vec<RawNode>,
id_map: &mut rustc_hash::FxHashMap<usize, usize>| {
let start = n.start_position();
let end = n.end_position();
id_map.entry(n.id()).or_insert_with(|| {
let i = nodes.len();
nodes.push(RawNode {
decorators: decs,
is_exported: is_exp,
heritage: Vec::new(),
type_annotation: ty,
name,
kind: NodeKind::Method,
span: (
start.row as u32,
start.column as u32,
end.row as u32,
end.column as u32,
),
calls: Vec::new(),
field_reads: Vec::new(),
owner_class: None,
content_hash: ecp_core::uid::xxh3_64_bytes(
&source[n.start_byte()..n.end_byte()],
),
});
i
});
};
if let Some(op_node) = deferred_operator {
// operator_declaration: name = "op_<token>" from the `operator:` field.
let op_name = op_node
.child_by_field_name("operator")
.and_then(|tok| {
std::str::from_utf8(&source[tok.start_byte()..tok.end_byte()]).ok()
})
.map(|tok| format!("op_{tok}"))
.unwrap_or_else(|| "op_unknown".to_string());
emit_special(
op_node,
op_name,
is_exported,
type_annotation.clone(),
decorators.clone(),
&mut nodes,
&mut node_id_to_idx,
);
}
if let Some(cv_node) = deferred_conv_operator {
// conversion_operator_declaration: name = "op_Implicit" / "op_Explicit".
let conv_name = {
let mut c = cv_node.walk();
let found = cv_node.children(&mut c).find_map(|ch| match ch.kind() {
"implicit" => Some("op_Implicit"),
"explicit" => Some("op_Explicit"),
_ => None,
});
found.unwrap_or("op_Conversion").to_string()
};
emit_special(
cv_node,
conv_name,
is_exported,
type_annotation.clone(),
decorators.clone(),
&mut nodes,
&mut node_id_to_idx,
);
}
if let Some(idx_node) = deferred_indexer {
// indexer_declaration: canonical name "this[...]".
emit_special(
idx_node,
"this[...]".to_string(),
is_exported,
type_annotation.clone(),
decorators.clone(),
&mut nodes,
&mut node_id_to_idx,
);
}

// Reclassify class declarations inheriting from `Attribute` (or any
// base whose name ends in `Attribute`) as NodeKind::Annotation —
// C# attribute-class convention. Heritage check alone is sufficient
Expand Down Expand Up @@ -402,19 +554,29 @@ impl LanguageProvider for CSharpProvider {
// For Property + Variable nodes, multiple declarators
// share the same root node id (`int x, y, z;`); key on
// the identifier node so each declarator is distinct.
let node_id = if matches!(k, NodeKind::Property | NodeKind::Variable) {
// event_field_declaration has the same multiple-declarator
// pattern (`event EventHandler E, F;`), so same keying.
let node_id = if matches!(k, NodeKind::Property | NodeKind::Variable)
|| is_event_field_name
{
n.id()
} else {
root.id()
};
let idx = *node_id_to_idx.entry(node_id).or_insert_with(|| {
// Destructors: prefix the bare class name with `~`.
let name = if is_destructor_name {
format!("~{name_str}")
} else {
name_str.to_string()
};
let i = nodes.len();
nodes.push(RawNode {
decorators: vec![],
is_exported,
heritage: Vec::new(),
type_annotation: type_annotation.clone(),
name: name_str.to_string(),
name,
kind: k,
span: (
start.row as u32,
Expand Down
58 changes: 58 additions & 0 deletions crates/ecp-analyzer/src/c_sharp/queries.scm
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,64 @@
name: (_) @namespace.name
) @namespace

;; Destructors — `~ClassName()`. The `name:` field is the bare class identifier;
;; parser.rs prepends `~` to produce the canonical name `~ClassName`.
;; Emitted as Method so body-calls attach via attach_to_enclosing.
(destructor_declaration
(attribute_list)* @decorator
(modifier)* @export
name: (identifier) @destructor.name
) @destructor

;; Event declarations with add/remove accessors — `public event EventHandler E { add{} remove{} }`.
;; Body-calls inside add/remove must attach to the event member. Emitted as
;; Method (not Property) so enclosing_containers includes the span.
(event_declaration
(attribute_list)* @decorator
(modifier)* @export
type: (_) @type
name: (identifier) @event.name
) @event_decl

;; Event field declarations — `public event EventHandler Click, Hover;`.
;; No body, but the node must exist for who-subscribes/who-raises queries.
;; Name extracted per declarator from the variable_declaration child.
;; Emitted as Method for consistency (matches event_declaration node kind).
(event_field_declaration
(attribute_list)* @decorator
(modifier)* @export
(variable_declaration
(variable_declarator
name: (identifier) @event_field.name))
) @event_field

;; Operator declarations — `public static Foo operator +(Foo a, Foo b) {}`.
;; The operator token (field `operator:`) is anonymous, so the name is
;; synthesised in parser.rs as `"op_" + token_text` (e.g. `op_+`, `op_==`).
;; Root captured as @operator_decl; parser.rs walks the node for the token.
(operator_declaration
(attribute_list)* @decorator
(modifier)* @export
) @operator_decl

;; Conversion operator declarations — `public static explicit operator int(Foo f) {}`.
;; Name synthesised in parser.rs as `op_Implicit` or `op_Explicit` based on the
;; implicit/explicit keyword, matching .NET operator-overload naming convention.
(conversion_operator_declaration
(attribute_list)* @decorator
(modifier)* @export
) @conv_operator_decl

;; Indexer declarations — `public int this[int idx] { get { ... } set { ... } }`.
;; No identifier name; parser.rs assigns the canonical name `"this[...]"` so the
;; node is unambiguously recognisable in ecp find/impact output.
;; Emitted as Method so body-calls inside get/set attach.
(indexer_declaration
(attribute_list)* @decorator
(modifier)* @export
type: (_) @type
) @indexer_decl

;; Anonymous callbacks passed as call arguments (`list.ForEach(x => ...)`,
;; `Task.Run(() => ...)`, `delegate { ... }`). Without a node here their
;; body's calls are dropped by attach_to_enclosing when no named enclosing
Expand Down
5 changes: 5 additions & 0 deletions crates/ecp-analyzer/src/c_sharp/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,5 +20,10 @@ impl LangSpec for CSharpSpec {
"enum.name" => NodeKind::Enum,
"enum_member.name" => NodeKind::EnumVariant,
"struct.name" => NodeKind::Struct,
// Special members — all emitted as Method so body-calls attach via
// enclosing_containers (which gates on Function|Method|Constructor).
"destructor.name" => NodeKind::Method,
"event.name" => NodeKind::Method,
"event_field.name" => NodeKind::Method,
};
}
40 changes: 39 additions & 1 deletion crates/ecp-analyzer/src/cpp/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,30 @@ fn is_cpp_reserved_keyword(name: &str) -> bool {
)
}

/// True if a file-scope `declaration` node carries a `const` or `constexpr`
/// qualifier. In tree-sitter-cpp a qualifying keyword appears as a
/// `type_qualifier` named child of the `declaration` with text `"const"` or
/// `"constexpr"`. Mutable globals have no such child.
///
/// This is intentionally scoped to the `@var` use-site (translation-unit
/// level `declaration` nodes). Class fields use `field_declaration` and
/// function parameters use `parameter_declaration` — neither reaches this
/// helper.
fn is_const_qualified(decl: tree_sitter::Node<'_>, source: &[u8]) -> bool {
let mut cursor = decl.walk();
for child in decl.children(&mut cursor) {
if child.kind() == "type_qualifier" {
if let Ok(text) = std::str::from_utf8(&source[child.start_byte()..child.end_byte()]) {
let text = text.trim();
if text == "const" || text == "constexpr" {
return true;
}
}
}
}
false
}

/// Per upstream `c-cpp.ts:414-431` `cppProvider.astFrameworkPatterns`.
/// Note: upstream's `cProvider` has no `astFrameworkPatterns`, so this is
/// C++-only.
Expand Down Expand Up @@ -502,20 +526,34 @@ impl LanguageProvider for CppProvider {
// parameters and type keywords as @var.name. Real var decls in
// well-formed code carry has_error=false and never use keywords
// as identifier names.
//
// const/constexpr globals → NodeKind::Const: a file-scope
// `declaration` whose first named child is a `type_qualifier`
// with text "const" or "constexpr" is a compile-time constant.
// The `@var` query already anchors to `(translation_unit
// (declaration ...))`, so `v_root` is always translation-unit
// level; const-qualified class fields use `field_declaration`
// (→ @field) and const params use `parameter_declaration`
// (→ @param) — neither reaches this branch.
if let (Some(v_root), Some(v_name)) = (var_root, var_name) {
if let Ok(name_str) =
std::str::from_utf8(&source[v_name.start_byte()..v_name.end_byte()])
{
if !v_root.has_error() && !is_cpp_reserved_keyword(name_str) {
let start = v_root.start_position();
let end = v_root.end_position();
let kind = if is_const_qualified(v_root, source) {
NodeKind::Const
} else {
NodeKind::Variable
};
nodes.push(RawNode {
decorators: vec![],
is_exported: is_header || is_exported_by_query,
heritage: vec![],
type_annotation: slice_type_before(v_root, v_name, source),
name: name_str.to_string(),
kind: NodeKind::Variable,
kind,
span: (
start.row as u32,
start.column as u32,
Expand Down
Loading
Loading