Skip to content

Commit 4231ee5

Browse files
authored
fix(resolve): v1.6.1 receiver evidence, store and object-literal bindings, and extraction recall (#277)
Ports the v1.6.1 resolution and extraction wave: receiver evidence for TS/JS member calls, Zustand store bindings, lexical object-literal member bindings, curried-wrapper object members, Python body docstrings and single-argument C/C++ macros, with the v1.6.1 audit document. Release notes come from the commit override in the pull request body.
1 parent f8e6039 commit 4231ee5

37 files changed

Lines changed: 5038 additions & 289 deletions

‎crates/codegraph-extract/src/lang/c.rs‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ impl LanguageSpec for CSpec {
5757
fn variable_types(&self) -> &'static [&'static str] {
5858
&["declaration"]
5959
}
60+
fn resolve_name(&self, node: Node<'_>, source: &str) -> Option<String> {
61+
crate::lang::cpp::recover_single_arg_macro_defined_name(node, source)
62+
}
6063
fn name_field(&self) -> &'static str {
6164
"declarator"
6265
}

‎crates/codegraph-extract/src/lang/cpp.rs‎

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,9 @@ impl LanguageSpec for CppSpec {
7070
"type"
7171
}
7272
fn resolve_name(&self, node: Node<'_>, source: &str) -> Option<String> {
73-
if let Some(name) = recover_cpp_macro_defined_name(node, source) {
73+
if let Some(name) = recover_single_arg_macro_defined_name(node, source)
74+
.or_else(|| recover_cpp_macro_defined_name(node, source))
75+
{
7476
return Some(name);
7577
}
7678
let qid = declarator_qualified_id(child_by_field(node, "declarator")?)?;
@@ -921,6 +923,108 @@ fn mask_quoted_literal(bytes: &[u8], mask: &mut [bool], start: usize, quote: u8)
921923
i
922924
}
923925

926+
/// Recover the function a single-argument macro defines —
927+
/// `#define NATIVE_FN(name) int name(void)` then `NATIVE_FN(get_version) { … }`
928+
/// (upstream #1373). Without its definition the shape is ambiguous, so a
929+
/// preceding local `#define` whose replacement uses its sole parameter as the
930+
/// function declarator is required; registration, token-pasting and `typedef`
931+
/// replacements, an `#undef` or redefinition in between, and any conditional
932+
/// block leave the name alone. C parses the macro as the return type with a
933+
/// `(name)` declarator; C++ as an implicit-return-type function.
934+
pub(crate) fn recover_single_arg_macro_defined_name(
935+
node: Node<'_>,
936+
source: &str,
937+
) -> Option<String> {
938+
if node.kind() != "function_definition" {
939+
return None;
940+
}
941+
let declarator = child_by_field(node, "declarator")?;
942+
let (macro_node, argument) = match declarator.kind() {
943+
"parenthesized_declarator" if declarator.named_child_count() == 1 => {
944+
let macro_node = child_by_field(node, "type")?;
945+
let argument = declarator.named_child(0)?;
946+
if macro_node.kind() != "type_identifier" || argument.kind() != "identifier" {
947+
return None;
948+
}
949+
(macro_node, argument)
950+
}
951+
"function_declarator" if child_by_field(node, "type").is_none() => {
952+
let macro_node = child_by_field(declarator, "declarator")?;
953+
let params = child_by_field(declarator, "parameters")?;
954+
let param = params.named_child(0)?;
955+
if macro_node.kind() != "identifier"
956+
|| params.named_child_count() != 1
957+
|| param.kind() != "parameter_declaration"
958+
|| param.named_child_count() != 1
959+
{
960+
return None;
961+
}
962+
let argument = param.named_child(0)?;
963+
if argument.kind() != "type_identifier" {
964+
return None;
965+
}
966+
(macro_node, argument)
967+
}
968+
_ => return None,
969+
};
970+
let macro_name = node_text(macro_node, source);
971+
static REPLACEMENT: OnceLock<Regex> = OnceLock::new();
972+
let replacement_pattern = REPLACEMENT.get_or_init(|| {
973+
Regex::new(
974+
r"^(?:[A-Za-z_][A-Za-z0-9_:]*\s+)+[*&\s]*([A-Za-z_][A-Za-z0-9_]*)\s*\([^(){};#]*\)\s*$",
975+
)
976+
.expect("single-argument macro replacement pattern")
977+
});
978+
static TYPEDEF: OnceLock<Regex> = OnceLock::new();
979+
let typedef = TYPEDEF.get_or_init(|| Regex::new(r"(?-u:\b)typedef(?-u:\b)").expect("typedef"));
980+
let mut scope = Some(node);
981+
while let Some(current) = scope {
982+
if current.kind() == "preproc_else" || current.kind().starts_with("preproc_elif") {
983+
return None;
984+
}
985+
let mut previous = current.prev_named_sibling();
986+
while let Some(prev) = previous {
987+
previous = prev.prev_named_sibling();
988+
if prev.kind().starts_with("preproc_if") {
989+
return None;
990+
}
991+
if prev.kind() == "preproc_call"
992+
&& child_by_field(prev, "directive")
993+
.is_some_and(|directive| node_text(directive, source) == "#undef")
994+
&& child_by_field(prev, "argument")
995+
.is_some_and(|argument| node_text(argument, source).trim() == macro_name)
996+
{
997+
return None;
998+
}
999+
if !matches!(prev.kind(), "preproc_function_def" | "preproc_def")
1000+
|| child_by_field(prev, "name").map(|name| node_text(name, source))
1001+
!= Some(macro_name.clone())
1002+
{
1003+
continue;
1004+
}
1005+
let params = child_by_field(prev, "parameters")?;
1006+
let param = params.named_child(0)?;
1007+
let value = child_by_field(prev, "value")?;
1008+
if params.named_child_count() != 1 || param.kind() != "identifier" {
1009+
return None;
1010+
}
1011+
let replacement = node_text(value, source)
1012+
.replace("\\\r\n", " ")
1013+
.replace("\\\n", " ");
1014+
let replacement = replacement.trim();
1015+
let declared = replacement_pattern
1016+
.captures(replacement)
1017+
.and_then(|captures| captures.get(1))?;
1018+
if typedef.is_match(replacement) || declared.as_str() != node_text(param, source) {
1019+
return None;
1020+
}
1021+
return Some(node_text(argument, source));
1022+
}
1023+
scope = current.parent();
1024+
}
1025+
None
1026+
}
1027+
9241028
/// Recover the real function name from the macro-definition idiom
9251029
/// `MACRO_NAME(real_name, typed args…) { body }` (flash-attention's
9261030
/// `DEFINE_FLASH_FORWARD_KERNEL(flash_fwd_kernel, bool Is_dropout, …) { … }`):

‎crates/codegraph-extract/src/lang/javascript.rs‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,16 @@ impl LanguageSpec for JavaScriptSpec {
4343
crate::lang::typescript::class_field_is_callable(node, "field_definition")
4444
}
4545

46+
fn resolve_name(&self, node: Node<'_>, source: &str) -> Option<String> {
47+
// JS `field_definition` names its key the `property` field (TS's
48+
// `public_field_definition` uses `name`); without this every JS class
49+
// field, handler fields included, is `<anonymous>` (upstream #808).
50+
if node.kind() == "field_definition" {
51+
return child_by_field(node, "property").map(|property| node_text(property, source));
52+
}
53+
None
54+
}
55+
4656
fn interface_types(&self) -> &'static [&'static str] {
4757
&[]
4858
}

‎crates/codegraph-extract/src/lang/jsx.rs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ impl LanguageSpec for JsxSpec {
3333
fn method_types(&self) -> &'static [&'static str] {
3434
JAVASCRIPT_SPEC.method_types()
3535
}
36+
fn class_member_is_method(&self, node: Node<'_>, source: &str) -> bool {
37+
JAVASCRIPT_SPEC.class_member_is_method(node, source)
38+
}
3639
fn interface_types(&self) -> &'static [&'static str] {
3740
&[]
3841
}
@@ -60,6 +63,9 @@ impl LanguageSpec for JsxSpec {
6063
fn name_field(&self) -> &'static str {
6164
JAVASCRIPT_SPEC.name_field()
6265
}
66+
fn resolve_name(&self, node: Node<'_>, source: &str) -> Option<String> {
67+
JAVASCRIPT_SPEC.resolve_name(node, source)
68+
}
6369
fn body_field(&self) -> &'static str {
6470
JAVASCRIPT_SPEC.body_field()
6571
}

‎crates/codegraph-extract/src/lang/python.rs‎

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,10 @@ impl LanguageSpec for PythonSpec {
7373
Some(signature)
7474
}
7575

76+
fn body_docstring(&self, node: Node<'_>, source: &str) -> Option<String> {
77+
python_body_docstring(node, source)
78+
}
79+
7680
fn is_async(&self, node: Node<'_>) -> bool {
7781
node.prev_sibling()
7882
.is_some_and(|prev| prev.kind() == "async")
@@ -96,3 +100,135 @@ impl LanguageSpec for PythonSpec {
96100
})
97101
}
98102
}
103+
104+
/// Python states intent in a docstring — a bare string literal as the first
105+
/// statement of a module, class or function body (upstream #1905). Reads the
106+
/// grammar's `string_content`, so `r`/`u` prefixes and both triple-quote forms
107+
/// work; bytes and f-strings are not docstrings, and a tuple is not a string.
108+
fn python_body_docstring(node: Node<'_>, source: &str) -> Option<String> {
109+
let body = if node.kind() == "module" {
110+
node
111+
} else {
112+
child_by_field(node, "body")?
113+
};
114+
let first = body
115+
.named_children(&mut body.walk())
116+
.find(|child| child.kind() != "comment")?;
117+
if first.kind() != "expression_statement"
118+
|| first.named_child_count() != 1
119+
|| first
120+
.children(&mut first.walk())
121+
.any(|child| child.kind() == ",")
122+
{
123+
return None;
124+
}
125+
let mut literal = first.named_child(0)?;
126+
while literal.kind() == "parenthesized_expression" {
127+
literal = literal
128+
.named_children(&mut literal.walk())
129+
.find(|child| child.kind() != "comment")?;
130+
}
131+
let strings = if literal.kind() == "concatenated_string" {
132+
literal
133+
.named_children(&mut literal.walk())
134+
.filter(|child| child.kind() != "comment")
135+
.collect::<Vec<_>>()
136+
} else {
137+
vec![literal]
138+
};
139+
let mut raw = String::new();
140+
for string in strings {
141+
if string.kind() != "string" {
142+
return None;
143+
}
144+
let parts = string
145+
.named_children(&mut string.walk())
146+
.collect::<Vec<_>>();
147+
let start = parts.iter().find(|part| part.kind() == "string_start")?;
148+
if node_text(*start, source)
149+
.chars()
150+
.any(|ch| matches!(ch, 'b' | 'B' | 'f' | 'F'))
151+
|| !parts.iter().any(|part| part.kind() == "string_end")
152+
{
153+
return None;
154+
}
155+
if let Some(content) = parts.iter().find(|part| part.kind() == "string_content") {
156+
raw.push_str(&node_text(*content, source));
157+
}
158+
}
159+
let docstring = dedent_docstring(&raw);
160+
(!docstring.is_empty()).then_some(docstring)
161+
}
162+
163+
/// PEP 257 cleaning: tabs expand to 8-column stops, every line after the first
164+
/// loses the common indentation of the non-blank ones (the first line starts
165+
/// right after the quotes), and blank edges are dropped.
166+
fn dedent_docstring(raw: &str) -> String {
167+
let lines = raw
168+
.replace("\r\n", "\n")
169+
.replace('\r', "\n")
170+
.split('\n')
171+
.map(|line| {
172+
let mut column = 0usize;
173+
let mut expanded = String::with_capacity(line.len());
174+
for ch in line.chars() {
175+
if ch == '\t' {
176+
let width = 8 - column % 8;
177+
expanded.extend(std::iter::repeat_n(' ', width));
178+
column += width;
179+
} else {
180+
expanded.push(ch);
181+
column += 1;
182+
}
183+
}
184+
expanded
185+
})
186+
.collect::<Vec<_>>();
187+
let leading = |line: &str| line.chars().take_while(|ch| ch.is_whitespace()).count();
188+
let indent = lines
189+
.iter()
190+
.skip(1)
191+
.filter(|line| !line.trim().is_empty())
192+
.map(|line| leading(line))
193+
.min()
194+
.unwrap_or(0);
195+
let mut out = Vec::with_capacity(lines.len());
196+
out.push(
197+
lines
198+
.first()
199+
.map(|line| line.trim().to_string())
200+
.unwrap_or_default(),
201+
);
202+
for line in lines.iter().skip(1) {
203+
out.push(
204+
line.chars()
205+
.skip(indent)
206+
.collect::<String>()
207+
.trim_end()
208+
.to_string(),
209+
);
210+
}
211+
while out.first().is_some_and(|line| line.trim().is_empty()) {
212+
out.remove(0);
213+
}
214+
while out.last().is_some_and(|line| line.trim().is_empty()) {
215+
out.pop();
216+
}
217+
out.join("\n")
218+
}
219+
220+
#[cfg(test)]
221+
mod docstring_tests {
222+
use super::dedent_docstring;
223+
224+
#[test]
225+
fn dedent_follows_pep_257() {
226+
assert_eq!(
227+
dedent_docstring("Summary line.\n\n Details here.\n indented\n "),
228+
"Summary line.\n\nDetails here.\n indented"
229+
);
230+
assert_eq!(dedent_docstring("\n Only body.\n "), "Only body.");
231+
assert_eq!(dedent_docstring("a\r\n\tb"), "a\nb");
232+
assert_eq!(dedent_docstring(" "), "");
233+
}
234+
}

‎crates/codegraph-extract/src/lang/tsx.rs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ impl LanguageSpec for TsxSpec {
3131
fn method_types(&self) -> &'static [&'static str] {
3232
TYPESCRIPT_SPEC.method_types()
3333
}
34+
fn property_types(&self) -> &'static [&'static str] {
35+
TYPESCRIPT_SPEC.property_types()
36+
}
37+
fn class_member_is_method(&self, node: Node<'_>, source: &str) -> bool {
38+
TYPESCRIPT_SPEC.class_member_is_method(node, source)
39+
}
3440
fn interface_types(&self) -> &'static [&'static str] {
3541
TYPESCRIPT_SPEC.interface_types()
3642
}

‎crates/codegraph-extract/src/spec.rs‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,13 @@ pub trait LanguageSpec: Sync {
114114
None
115115
}
116116

117+
/// Prose that lives INSIDE the node's body rather than in a preceding
118+
/// comment — Python's bare first-statement string (upstream
119+
/// `getBodyDocstring`, #1905). Cleaned text, or `None`.
120+
fn body_docstring(&self, _node: Node<'_>, _source: &str) -> Option<String> {
121+
None
122+
}
123+
117124
fn get_return_type(&self, _node: Node<'_>, _source: &str) -> Option<String> {
118125
None
119126
}

0 commit comments

Comments
 (0)