Skip to content

Commit 48e90b7

Browse files
committed
Unified: Fix locations of various tokens
Handles things like `try!` (which is represented as two separate tokens -- we explicitly union their ranges) and "let" binding modifiers (where we reuse the bindingSpecifier, getting its location and string value for free).
1 parent 8de2eab commit 48e90b7

6 files changed

Lines changed: 130 additions & 70 deletions

File tree

‎shared/yeast/src/build.rs‎

Lines changed: 3 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,6 @@ pub struct BuildCtx<'a, C: 'a = ()> {
3333
pub ast: &'a mut Ast,
3434
pub captures: &'a Captures,
3535
pub fresh: &'a FreshScope,
36-
/// Optional source range explicitly inherited by every synthetic node built
37-
/// through this context.
38-
pub source_range: Option<Range>,
3936
/// Source range of the node matched by the current rule.
4037
///
4138
/// The `rule!` macro applies this range to locally-created result roots
@@ -66,26 +63,6 @@ impl<'a, C> BuildCtx<'a, C> {
6663
ast,
6764
captures,
6865
fresh,
69-
source_range: None,
70-
matched_source_range: None,
71-
user_ctx,
72-
translator: None,
73-
created_nodes: BTreeSet::new(),
74-
}
75-
}
76-
77-
pub fn with_source_range(
78-
ast: &'a mut Ast,
79-
captures: &'a Captures,
80-
fresh: &'a FreshScope,
81-
source_range: Option<Range>,
82-
user_ctx: &'a mut C,
83-
) -> Self {
84-
Self {
85-
ast,
86-
captures,
87-
fresh,
88-
source_range,
8966
matched_source_range: None,
9067
user_ctx,
9168
translator: None,
@@ -107,7 +84,6 @@ impl<'a, C> BuildCtx<'a, C> {
10784
ast,
10885
captures,
10986
fresh,
110-
source_range: None,
11187
matched_source_range: source_range,
11288
user_ctx,
11389
translator: Some(translator),
@@ -153,7 +129,7 @@ impl<'a, C> BuildCtx<'a, C> {
153129
fields: BTreeMap<FieldId, Vec<Id>>,
154130
is_named: bool,
155131
) -> Id {
156-
self.create_node_with_range(kind, content, fields, is_named, self.source_range)
132+
self.create_node_with_range(kind, content, fields, is_named, None)
157133
}
158134

159135
/// Create a named token and record it as constructed by this rule invocation.
@@ -179,7 +155,7 @@ impl<'a, C> BuildCtx<'a, C> {
179155

180156
/// Create a named token using this context's explicit default source range.
181157
pub fn create_named_token(&mut self, kind: &'static str, content: String) -> Id {
182-
self.create_named_token_with_range(kind, content, self.source_range)
158+
self.create_named_token_with_range(kind, content, None)
183159
}
184160

185161
/// Finish the current rule invocation by applying the matched source range
@@ -275,11 +251,7 @@ impl<'a, C> BuildCtx<'a, C> {
275251
value: &str,
276252
source_range: Option<Range>,
277253
) -> Id {
278-
self.create_named_token_with_range(
279-
kind,
280-
value.to_string(),
281-
source_range.or(self.source_range),
282-
)
254+
self.create_named_token_with_range(kind, value.to_string(), source_range)
283255
}
284256

285257
/// Create a literal with an empty range at another node's start.
@@ -360,7 +332,6 @@ impl<C: Clone> BuildCtx<'_, C> {
360332
ast: &mut *self.ast,
361333
captures: self.captures,
362334
fresh: self.fresh,
363-
source_range: self.source_range,
364335
matched_source_range: self.matched_source_range,
365336
user_ctx: &mut child_user_ctx,
366337
translator: self.translator,

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

Lines changed: 73 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use codeql_extractor::extractor::desugaring;
2-
use yeast::{ConcreteDesugarer, DesugaringConfig, PhaseKind, Rule, rule, tree, tree_at};
2+
use yeast::{
3+
ConcreteDesugarer, DesugaringConfig, PhaseKind, Rule, rule, tree, tree_at, tree_spanning,
4+
};
35

46
/// User context propagated from outer rules down to the inner rules that
57
/// emit the corresponding output declarations, so that each emitted node
@@ -97,7 +99,9 @@ fn and_chain(
9799
conds
98100
.into_iter()
99101
.reduce(|acc, elem| {
100-
tree!((binary_expr operator: (infix_operator "&&") left: {acc} right: {elem}))
102+
let operator_range = ctx.empty_source_range_between(acc, elem);
103+
let operator = ctx.literal_with_source_range("infix_operator", "&&", operator_range);
104+
tree!((binary_expr operator: {operator} left: {acc} right: {elem}))
101105
})
102106
.expect("control-flow statement must have at least one condition")
103107
}
@@ -123,21 +127,15 @@ fn member_chain(
123127
ctx: &mut yeast::build::BuildCtx<'_, SwiftContext>,
124128
parts: Vec<yeast::Id>,
125129
) -> yeast::Id {
126-
// `member_chain` builds the imported expression inside the larger import
127-
// declaration rule. The imported expression should span the import path,
128-
// not the whole declaration including the `import` keyword.
129-
let source_range = ctx.source_range.take();
130130
let mut iter = parts.into_iter();
131131
let first = iter
132132
.next()
133133
.expect("identifier with `part:` must have at least one part");
134134
let init = tree!((identifier #{first}));
135-
let result = iter.fold(
135+
iter.fold(
136136
init,
137137
|acc, elem| tree!((member_access_expr base: {acc} member_name_node: (identifier #{elem}))),
138-
);
139-
ctx.source_range = source_range;
140-
result
138+
)
141139
}
142140

143141
/// Compound-assignment operator spellings (`+=`, `<<=`, ...). Used to tell a
@@ -495,14 +493,24 @@ fn translation_rules() -> Vec<Rule<SwiftContext>> {
495493
// `enumCaseDecl` rule below) and are tagged `enum_case`, after any
496494
// `chained_declaration` tag.
497495
rule!(
498-
(enumCaseElement name: @name parameterClause: (enumCaseParameterClause parameters: _* @params))
499-
=>
500-
(class_like_declaration
501-
modifier: {ctx.outer_modifiers.clone()}
502-
modifier: {chained_modifier(&mut ctx)}
503-
modifier: (modifier "enum_case")
504-
name_node: (identifier #{name})
505-
member: (constructor_declaration parameter: {params} body: (block)))
496+
(enumCaseElement
497+
name: @name
498+
parameterClause: (enumCaseParameterClause parameters: _* @params)) @@element
499+
=>
500+
class_like_declaration {
501+
let body = tree!((block));
502+
let constructor = tree_at!(
503+
ctx,
504+
element,
505+
(constructor_declaration parameter: {params} body: {body})
506+
);
507+
tree!((class_like_declaration
508+
modifier: {ctx.outer_modifiers.clone()}
509+
modifier: {chained_modifier(&mut ctx)}
510+
modifier: (modifier "enum_case")
511+
name_node: (identifier #{name})
512+
member: {constructor}))
513+
}
506514
),
507515
rule!(
508516
(enumCaseElement name: @name rawValue: (initializerClause value: @val))
@@ -702,12 +710,17 @@ fn translation_rules() -> Vec<Rule<SwiftContext>> {
702710
label: _? @@lbl
703711
expression: (functionCallExpr
704712
calledExpression: @constructor
705-
arguments: _* @elements))
713+
arguments: _* @elements) @@call)
706714
=>
707715
argument {
716+
let value = tree_at!(
717+
ctx,
718+
call,
719+
(call_expr callee: {constructor} argument: {elements})
720+
);
708721
tree!((argument
709722
name_node: (identifier #{lbl})?
710-
value: (call_expr callee: {constructor} argument: {elements})))
723+
value: {value}))
711724
}
712725
),
713726
rule!(
@@ -880,6 +893,7 @@ fn translation_rules() -> Vec<Rule<SwiftContext>> {
880893
// form is matched first.
881894
rule!(
882895
(optionalBindingCondition
896+
bindingSpecifier: @@spec
883897
pattern: (identifierPattern identifier: @name)
884898
initializer: (initializerClause value: @val))
885899
=>
@@ -888,18 +902,20 @@ fn translation_rules() -> Vec<Rule<SwiftContext>> {
888902
pattern: (call_expr
889903
callee: (member_access_expr base: (identifier "Optional") member_name_node: (identifier "some"))
890904
argument: (argument value: (expr_pattern
891-
modifier: (modifier "let")
905+
modifier: (modifier #{spec})
892906
expr: (identifier #{name})))))
893907
),
894908
rule!(
895-
(optionalBindingCondition pattern: (identifierPattern identifier: @name))
909+
(optionalBindingCondition
910+
bindingSpecifier: @@spec
911+
pattern: (identifierPattern identifier: @name))
896912
=>
897913
(pattern_guard_expr
898914
value: (identifier #{name})
899915
pattern: (call_expr
900916
callee: (member_access_expr base: (identifier "Optional") member_name_node: (identifier "some"))
901917
argument: (argument value: (expr_pattern
902-
modifier: (modifier "let")
918+
modifier: (modifier #{spec})
903919
expr: (identifier #{name})))))
904920
),
905921
// A single condition in an `if`/`while`/`guard` condition list unwraps to
@@ -983,11 +999,19 @@ fn translation_rules() -> Vec<Rule<SwiftContext>> {
983999
}),
9841000
// try/try?/try! expr → unary_expr with operator "try", "try?" or "try!"
9851001
rule!(
986-
(tryExpr questionOrExclamationMark: _? @@m expression: @e)
1002+
(tryExpr
1003+
tryKeyword: @@keyword
1004+
questionOrExclamationMark: _? @@m
1005+
expression: @e)
9871006
=>
9881007
expr {
9891008
let op = format!("try{}", m.map(|m| ctx.source_text(m)).unwrap_or_default());
990-
tree!((unary_expr operator: (prefix_operator #{op}) operand: {e}))
1009+
let operator = tree_spanning!(
1010+
ctx,
1011+
std::iter::once(keyword).chain(m),
1012+
(prefix_operator #{op})
1013+
);
1014+
tree!((unary_expr operator: {operator} operand: {e}))
9911015
}
9921016
),
9931017
// Do-catch → try_expr
@@ -1021,17 +1045,29 @@ fn translation_rules() -> Vec<Rule<SwiftContext>> {
10211045
// Catch block without error binding
10221046
rule!((catchClause body: @body) => (catch_clause body: {body})),
10231047
// As expression (type cast) — as?, as!
1024-
rule!((asExpr expression: @val questionOrExclamationMark: _? @@mark type: @ty) => type_cast_expr {
1048+
rule!((asExpr expression: @val asKeyword: @@keyword questionOrExclamationMark: _? @@mark type: @ty) => type_cast_expr {
10251049
let op = format!("as{}", mark.map(|m| ctx.source_text(m)).unwrap_or_default());
1026-
tree!((type_cast_expr expr: {val} operator: (infix_operator #{op}) type: {ty}))
1050+
let operator = tree_spanning!(
1051+
ctx,
1052+
std::iter::once(keyword).chain(mark),
1053+
(infix_operator #{op})
1054+
);
1055+
tree!((type_cast_expr expr: {val} operator: {operator} type: {ty}))
10271056
}),
10281057
// Check expression (`x is T`) → type_test_expr
1029-
rule!((isExpr expression: @val type: @ty) => (type_test_expr expr: {val} operator: (infix_operator "is") type: {ty})),
1058+
rule!((isExpr expression: @val isKeyword: @@keyword type: @ty) => (type_test_expr
1059+
expr: {val}
1060+
operator: {tree_at!(ctx, keyword, (infix_operator "is"))}
1061+
type: {ty})),
10301062
// Await expression → unary_expr with operator "await"
1031-
rule!((awaitExpr expression: @val) => (unary_expr operator: (prefix_operator "await") operand: {val})),
1063+
rule!((awaitExpr awaitKeyword: @@keyword expression: @val) => (unary_expr
1064+
operator: {tree_at!(ctx, keyword, (prefix_operator "await"))}
1065+
operand: {val})),
10321066
// Force-unwrap (`x!`) → postfix unary_expr, via swift-syntax's dedicated
10331067
// `forceUnwrapExpr` node.
1034-
rule!((forceUnwrapExpr expression: @e) => (unary_expr operator: (postfix_operator "!") operand: {e})),
1068+
rule!((forceUnwrapExpr expression: @e exclamationMark: @@mark) => (unary_expr
1069+
operator: {tree_at!(ctx, mark, (postfix_operator "!"))}
1070+
operand: {e})),
10351071
// ---- Imports ----
10361072
// An import declaration. The dotted path (a list of
10371073
// `importPathComponent`s) becomes a `name_node`/`member_access_expr`
@@ -1046,14 +1082,17 @@ fn translation_rules() -> Vec<Rule<SwiftContext>> {
10461082
attributes: _* @attrs
10471083
modifiers: _* @mods
10481084
importKindSpecifier: _? @@kind
1049-
path: (importPathComponent name: @@parts)*)
1085+
path: (importPathComponent name: @@parts)*) @@decl
10501086
=>
10511087
import_declaration {
10521088
let last = *parts.last().ok_or("import has no path")?;
10531089
let pattern = match kind {
1054-
None => tree!((named_pattern
1055-
name_node: (identifier #{last})
1056-
sub_pattern: (bulk_importing_pattern))),
1090+
None => {
1091+
let bulk = tree_at!(ctx, decl, (bulk_importing_pattern));
1092+
tree!((named_pattern
1093+
name_node: (identifier #{last})
1094+
sub_pattern: {bulk}))
1095+
}
10571096
Some(_) => tree!((identifier #{last})),
10581097
};
10591098
tree!((import_declaration

‎unified/extractor/tests/corpus/swift/desugar/import-with-deeply-nested-path-three-parts.output‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,4 @@ top_level
3838
pattern:
3939
named_pattern
4040
name_node: identifier "URLSession"
41-
sub_pattern: bulk_importing_pattern
41+
sub_pattern: bulk_importing_pattern "import Foundation.Networking.URLSession"

‎unified/extractor/tests/corpus/swift/desugar/import-with-dotted-path-two-parts.output‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,4 @@ top_level
3232
pattern:
3333
named_pattern
3434
name_node: identifier "Networking"
35-
sub_pattern: bulk_importing_pattern
35+
sub_pattern: bulk_importing_pattern "import Foundation.Networking"

‎unified/extractor/tests/corpus/swift/desugar/simple-import-with-single-name.output‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,4 @@ top_level
2626
pattern:
2727
named_pattern
2828
name_node: identifier "Foundation"
29-
sub_pattern: bulk_importing_pattern
29+
sub_pattern: bulk_importing_pattern "import Foundation"

‎unified/extractor/tests/location_tests.rs‎

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,13 +71,63 @@ fn generic_type_children_have_local_ranges() {
7171
assert_has_span(&ast, source, "identifier", Some("Foo"), "Foo");
7272
}
7373

74+
#[test]
75+
fn nested_calls_include_their_delimiters() {
76+
let source = r#"sink(source("first"), source("second"))"#;
77+
let ast = desugar(source);
78+
79+
assert_has_span(&ast, source, "call_expr", None, r#"source("first")"#);
80+
assert_has_span(&ast, source, "call_expr", None, r#"source("second")"#);
81+
}
82+
83+
#[test]
84+
fn enum_case_constructors_include_their_parameter_clause() {
85+
let source = "enum Result<T> { case success(T) }";
86+
let ast = desugar(source);
87+
88+
assert_has_span(&ast, source, "constructor_declaration", None, "success(T)");
89+
}
90+
91+
#[test]
92+
fn synthesized_condition_and_switch_nodes_use_child_ranges() {
93+
let source = "if a, b { c }\nswitch x { case a, b: c }";
94+
let ast = desugar(source);
95+
96+
assert_has_span(&ast, source, "binary_expr", None, "a, b");
97+
assert_has_empty_span(
98+
&ast,
99+
"infix_operator",
100+
Some("&&"),
101+
source.find(',').unwrap(),
102+
);
103+
assert_has_span(&ast, source, "or_pattern", None, "a, b");
104+
assert_has_span(&ast, source, "block", None, "c");
105+
}
106+
74107
#[test]
75108
fn declaration_and_operator_tokens_keep_precise_ranges() {
76-
let source = "func f() { return x }";
109+
let source = "func f() { return x }\nlet y = try? await value! as? T\nlet z = value is T";
77110
let ast = desugar(source);
78111

79112
assert_has_span(&ast, source, "block", None, "{ return x }");
80113
assert_has_span(&ast, source, "return_expr", None, "return x");
114+
assert_has_span(&ast, source, "prefix_operator", Some("try?"), "try?");
115+
assert_has_span(&ast, source, "prefix_operator", Some("await"), "await");
116+
assert_has_span(&ast, source, "postfix_operator", Some("!"), "!");
117+
assert_has_span(&ast, source, "infix_operator", Some("as?"), "as?");
118+
assert_has_span(&ast, source, "infix_operator", Some("is"), "is");
119+
}
120+
121+
#[test]
122+
fn synthetic_optional_binding_nodes_anchor_to_binding_keyword() {
123+
let source = "if let value = optional {}";
124+
let ast = desugar(source);
125+
let binding_start = source.find("let").unwrap();
126+
127+
assert_has_empty_span(&ast, "member_access_expr", None, binding_start);
128+
assert_has_empty_span(&ast, "identifier", Some("Optional"), binding_start);
129+
assert_has_empty_span(&ast, "identifier", Some("some"), binding_start);
130+
assert_has_span(&ast, source, "modifier", Some("let"), "let");
81131
}
82132

83133
#[test]

0 commit comments

Comments
 (0)