Skip to content

Commit 3a0ce1d

Browse files
authored
Merge pull request #22531 from github/copilot/update-ast-transformation
Include elided tokens in yeast AST node locations
2 parents 3736516 + 94b78bf commit 3a0ce1d

5 files changed

Lines changed: 138 additions & 8 deletions

File tree

shared/yeast/src/lib.rs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -591,11 +591,17 @@ impl Ast {
591591
let source_range = match &content {
592592
// Parsed nodes already carry an exact source range in their content.
593593
NodeContent::Range(_) => source_range,
594-
// Synthesized nodes derive location from children when possible,
595-
// and fall back to the inherited rule-match range otherwise.
594+
// Synthesized nodes derive location from both their children and
595+
// the inherited rule-match range, so tokens matched by a rule but
596+
// elided from its output still contribute to the replacement range.
596597
_ => self
597598
.union_source_range_of_children(&fields)
598-
.or(source_range),
599+
.map_or(source_range, |child_range| {
600+
Some(match source_range {
601+
Some(source_range) => union_source_ranges(child_range, source_range),
602+
None => child_range,
603+
})
604+
}),
599605
};
600606
let id = self.nodes.len();
601607
self.nodes.push(Node {
@@ -786,6 +792,25 @@ impl Ast {
786792
}
787793
}
788794

795+
fn union_source_ranges(first: Range, second: Range) -> Range {
796+
let (start_byte, start_point) = if first.start_byte <= second.start_byte {
797+
(first.start_byte, first.start_point)
798+
} else {
799+
(second.start_byte, second.start_point)
800+
};
801+
let (end_byte, end_point) = if first.end_byte >= second.end_byte {
802+
(first.end_byte, first.end_point)
803+
} else {
804+
(second.end_byte, second.end_point)
805+
};
806+
Range {
807+
start_byte,
808+
end_byte,
809+
start_point,
810+
end_point,
811+
}
812+
}
813+
789814
/// A node in our AST
790815
#[derive(PartialEq, Eq, Debug, Clone, Serialize)]
791816
pub struct Node {

shared/yeast/tests/test.rs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1663,6 +1663,39 @@ fn test_hash_brace_uses_capture_location_for_leaf() {
16631663
assert_eq!(bar.end_byte(), 7);
16641664
}
16651665

1666+
/// Regression test: tokens matched by a rule but elided from the output still
1667+
/// contribute to the source location of the synthesized replacement node.
1668+
#[test]
1669+
fn test_elided_tokens_contribute_to_replacement_location() {
1670+
let rule: Rule = rule!(
1671+
(call
1672+
method: (identifier) @name
1673+
receiver: (identifier) @recv
1674+
)
1675+
=>
1676+
(call
1677+
method: {name}
1678+
)
1679+
);
1680+
1681+
let ast = run_and_ast("foo.bar()", vec![rule]);
1682+
let call_ids: Vec<yeast::Id> = ast
1683+
.reachable_node_ids()
1684+
.into_iter()
1685+
.filter(|&id| {
1686+
ast.get_node(id)
1687+
.is_some_and(|node| node.kind_name() == "call")
1688+
})
1689+
.collect();
1690+
1691+
assert_eq!(call_ids.len(), 1, "expected exactly one reachable call");
1692+
let call_id = call_ids[0];
1693+
let call = ast.get_node(call_id).unwrap();
1694+
1695+
assert_eq!(call.start_byte(), 0);
1696+
assert_eq!(call.end_byte(), 9);
1697+
}
1698+
16661699
// ---- `rules!` macro tests (compile-time type-checking) ----
16671700

16681701
/// `rules!` should accept well-typed rules using the bare-rule-body

unified/extractor/src/languages/swift/swift.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,15 +115,21 @@ fn member_chain(
115115
ctx: &mut yeast::build::BuildCtx<'_, SwiftContext>,
116116
parts: Vec<yeast::Id>,
117117
) -> yeast::Id {
118+
// `member_chain` builds the imported expression inside the larger import
119+
// declaration rule. The imported expression should span the import path,
120+
// not the whole declaration including the `import` keyword.
121+
let source_range = ctx.source_range.take();
118122
let mut iter = parts.into_iter();
119123
let first = iter
120124
.next()
121125
.expect("identifier with `part:` must have at least one part");
122126
let init = tree!((name_expr identifier: (identifier #{first})));
123-
iter.fold(
127+
let result = iter.fold(
124128
init,
125129
|acc, elem| tree!((member_access_expr base: {acc} member: (identifier #{elem}))),
126-
)
130+
);
131+
ctx.source_range = source_range;
132+
result
127133
}
128134

129135
/// Compound-assignment operator spellings (`+=`, `<<=`, ...). Used to tell a

unified/extractor/tests/corpus_tests.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -98,9 +98,8 @@ fn collect_corpus_stems(dir: &Path, out: &mut Vec<std::path::PathBuf>) {
9898

9999
#[cfg(bazel)]
100100
fn corpus_dir() -> std::path::PathBuf {
101-
let base = std::path::PathBuf::from(
102-
std::env::var("RUNFILES_DIR").expect("RUNFILES_DIR not set"),
103-
);
101+
let base =
102+
std::path::PathBuf::from(std::env::var("RUNFILES_DIR").expect("RUNFILES_DIR not set"));
104103
std::fs::read_dir(&base)
105104
.expect("failed to read RUNFILES_DIR")
106105
.filter_map(Result::ok)

unified/extractor/tests/swift_syntax_pipeline.rs

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,39 @@ mod languages;
1616
/// A real `swift-syntax-rs` JSON dump of the Swift source `let x = 1`.
1717
const LET_X_JSON: &str = include_str!("fixtures/let_x.swiftsyntax.json");
1818

19+
const IMPORT_FOUNDATION_JSON: &str = r#"{
20+
"kind": "sourceFile",
21+
"range": {"start":{"offset":0,"line":1,"column":1},"end":{"offset":17,"line":1,"column":18}},
22+
"statements": [
23+
{
24+
"kind": "codeBlockItem",
25+
"range": {"start":{"offset":0,"line":1,"column":1},"end":{"offset":17,"line":1,"column":18}},
26+
"item": {
27+
"kind": "importDecl",
28+
"range": {"start":{"offset":0,"line":1,"column":1},"end":{"offset":17,"line":1,"column":18}},
29+
"importKeyword": {
30+
"kind": "token",
31+
"tokenKind": "keyword(SwiftSyntax.Keyword.import)",
32+
"text": "import",
33+
"range": {"start":{"offset":0,"line":1,"column":1},"end":{"offset":6,"line":1,"column":7}}
34+
},
35+
"path": [
36+
{
37+
"kind": "importPathComponent",
38+
"range": {"start":{"offset":7,"line":1,"column":8},"end":{"offset":17,"line":1,"column":18}},
39+
"name": {
40+
"kind": "token",
41+
"tokenKind": "identifier(\"Foundation\")",
42+
"text": "Foundation",
43+
"range": {"start":{"offset":7,"line":1,"column":8},"end":{"offset":17,"line":1,"column":18}}
44+
}
45+
}
46+
]
47+
}
48+
}
49+
]
50+
}"#;
51+
1952
#[test]
2053
fn swift_syntax_json_runs_through_the_desugarer() {
2154
let lang = languages::all_language_specs()
@@ -47,3 +80,37 @@ fn swift_syntax_json_runs_through_the_desugarer() {
4780
assert!(dump.contains("top_level"), "unexpected dump: {dump}");
4881
assert!(dump.contains("block"), "unexpected dump: {dump}");
4982
}
83+
84+
#[test]
85+
fn import_name_expr_location_excludes_import_keyword() {
86+
let lang = languages::all_language_specs()
87+
.into_iter()
88+
.find(|l| l.file_globs.iter().any(|g| g.contains("swift")))
89+
.expect("swift language spec");
90+
let desugarer = lang.desugarer.as_ref();
91+
let adapted = languages::swift_adapter::json_to_ast(IMPORT_FOUNDATION_JSON)
92+
.expect("adapter should succeed");
93+
94+
let desugared = desugarer
95+
.run_from_ast(adapted.ast)
96+
.expect("desugaring an import should not error");
97+
98+
let name_expr_ids: Vec<yeast::Id> = desugared
99+
.reachable_node_ids()
100+
.into_iter()
101+
.filter(|&id| {
102+
desugared
103+
.get_node(id)
104+
.is_some_and(|node| node.kind_name() == "name_expr")
105+
})
106+
.collect();
107+
assert_eq!(
108+
name_expr_ids.len(),
109+
1,
110+
"expected exactly one reachable name_expr"
111+
);
112+
113+
let name_expr = desugared.get_node(name_expr_ids[0]).unwrap();
114+
assert_eq!(name_expr.start_byte(), 7);
115+
assert_eq!(name_expr.end_byte(), 17);
116+
}

0 commit comments

Comments
 (0)