Skip to content

Commit ab479bc

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 3034f09 commit ab479bc

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
@@ -300,11 +276,7 @@ impl<'a, C> BuildCtx<'a, C> {
300276
value: &str,
301277
source_range: Option<Range>,
302278
) -> Id {
303-
self.create_named_token_with_range(
304-
kind,
305-
value.to_string(),
306-
source_range.or(self.source_range),
307-
)
279+
self.create_named_token_with_range(kind, value.to_string(), source_range)
308280
}
309281

310282
/// Create a literal with an empty range at another node's start.
@@ -385,7 +357,6 @@ impl<C: Clone> BuildCtx<'_, C> {
385357
ast: &mut *self.ast,
386358
captures: self.captures,
387359
fresh: self.fresh,
388-
source_range: self.source_range,
389360
matched_source_range: self.matched_source_range,
390361
user_ctx: &mut child_user_ctx,
391362
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
@@ -477,14 +475,24 @@ fn translation_rules() -> Vec<Rule<SwiftContext>> {
477475
// `enumCaseDecl` rule below) and are tagged `enum_case`, after any
478476
// `chained_declaration` tag.
479477
rule!(
480-
(enumCaseElement name: @name parameterClause: (enumCaseParameterClause parameters: _* @params))
481-
=>
482-
(class_like_declaration
483-
modifier: {ctx.outer_modifiers.clone()}
484-
modifier: {chained_modifier(&mut ctx)}
485-
modifier: (modifier "enum_case")
486-
name_node: (identifier #{name})
487-
member: (constructor_declaration parameter: {params} body: (block)))
478+
(enumCaseElement
479+
name: @name
480+
parameterClause: (enumCaseParameterClause parameters: _* @params)) @@element
481+
=>
482+
class_like_declaration {
483+
let body = tree!((block));
484+
let constructor = tree_at!(
485+
ctx,
486+
element,
487+
(constructor_declaration parameter: {params} body: {body})
488+
);
489+
tree!((class_like_declaration
490+
modifier: {ctx.outer_modifiers.clone()}
491+
modifier: {chained_modifier(&mut ctx)}
492+
modifier: (modifier "enum_case")
493+
name_node: (identifier #{name})
494+
member: {constructor}))
495+
}
488496
),
489497
rule!(
490498
(enumCaseElement name: @name rawValue: (initializerClause value: @val))
@@ -684,12 +692,17 @@ fn translation_rules() -> Vec<Rule<SwiftContext>> {
684692
label: _? @@lbl
685693
expression: (functionCallExpr
686694
calledExpression: @constructor
687-
arguments: _* @elements))
695+
arguments: _* @elements) @@call)
688696
=>
689697
argument {
698+
let value = tree_at!(
699+
ctx,
700+
call,
701+
(call_expr callee: {constructor} argument: {elements})
702+
);
690703
tree!((argument
691704
name_node: (identifier #{lbl})?
692-
value: (call_expr callee: {constructor} argument: {elements})))
705+
value: {value}))
693706
}
694707
),
695708
rule!(
@@ -862,6 +875,7 @@ fn translation_rules() -> Vec<Rule<SwiftContext>> {
862875
// form is matched first.
863876
rule!(
864877
(optionalBindingCondition
878+
bindingSpecifier: @@spec
865879
pattern: (identifierPattern identifier: @name)
866880
initializer: (initializerClause value: @val))
867881
=>
@@ -870,18 +884,20 @@ fn translation_rules() -> Vec<Rule<SwiftContext>> {
870884
pattern: (call_expr
871885
callee: (member_access_expr base: (identifier "Optional") member_name_node: (identifier "some"))
872886
argument: (argument value: (expr_pattern
873-
modifier: (modifier "let")
887+
modifier: (modifier #{spec})
874888
expr: (identifier #{name})))))
875889
),
876890
rule!(
877-
(optionalBindingCondition pattern: (identifierPattern identifier: @name))
891+
(optionalBindingCondition
892+
bindingSpecifier: @@spec
893+
pattern: (identifierPattern identifier: @name))
878894
=>
879895
(pattern_guard_expr
880896
value: (identifier #{name})
881897
pattern: (call_expr
882898
callee: (member_access_expr base: (identifier "Optional") member_name_node: (identifier "some"))
883899
argument: (argument value: (expr_pattern
884-
modifier: (modifier "let")
900+
modifier: (modifier #{spec})
885901
expr: (identifier #{name})))))
886902
),
887903
// A single condition in an `if`/`while`/`guard` condition list unwraps to
@@ -965,11 +981,19 @@ fn translation_rules() -> Vec<Rule<SwiftContext>> {
965981
}),
966982
// try/try?/try! expr → unary_expr with operator "try", "try?" or "try!"
967983
rule!(
968-
(tryExpr questionOrExclamationMark: _? @@m expression: @e)
984+
(tryExpr
985+
tryKeyword: @@keyword
986+
questionOrExclamationMark: _? @@m
987+
expression: @e)
969988
=>
970989
expr {
971990
let op = format!("try{}", m.map(|m| ctx.source_text(m)).unwrap_or_default());
972-
tree!((unary_expr operator: (prefix_operator #{op}) operand: {e}))
991+
let operator = tree_spanning!(
992+
ctx,
993+
std::iter::once(keyword).chain(m),
994+
(prefix_operator #{op})
995+
);
996+
tree!((unary_expr operator: {operator} operand: {e}))
973997
}
974998
),
975999
// Do-catch → try_expr
@@ -1003,17 +1027,29 @@ fn translation_rules() -> Vec<Rule<SwiftContext>> {
10031027
// Catch block without error binding
10041028
rule!((catchClause body: @body) => (catch_clause body: {body})),
10051029
// As expression (type cast) — as?, as!
1006-
rule!((asExpr expression: @val questionOrExclamationMark: _? @@mark type: @ty) => type_cast_expr {
1030+
rule!((asExpr expression: @val asKeyword: @@keyword questionOrExclamationMark: _? @@mark type: @ty) => type_cast_expr {
10071031
let op = format!("as{}", mark.map(|m| ctx.source_text(m)).unwrap_or_default());
1008-
tree!((type_cast_expr expr: {val} operator: (infix_operator #{op}) type: {ty}))
1032+
let operator = tree_spanning!(
1033+
ctx,
1034+
std::iter::once(keyword).chain(mark),
1035+
(infix_operator #{op})
1036+
);
1037+
tree!((type_cast_expr expr: {val} operator: {operator} type: {ty}))
10091038
}),
10101039
// Check expression (`x is T`) → type_test_expr
1011-
rule!((isExpr expression: @val type: @ty) => (type_test_expr expr: {val} operator: (infix_operator "is") type: {ty})),
1040+
rule!((isExpr expression: @val isKeyword: @@keyword type: @ty) => (type_test_expr
1041+
expr: {val}
1042+
operator: {tree_at!(ctx, keyword, (infix_operator "is"))}
1043+
type: {ty})),
10121044
// Await expression → unary_expr with operator "await"
1013-
rule!((awaitExpr expression: @val) => (unary_expr operator: (prefix_operator "await") operand: {val})),
1045+
rule!((awaitExpr awaitKeyword: @@keyword expression: @val) => (unary_expr
1046+
operator: {tree_at!(ctx, keyword, (prefix_operator "await"))}
1047+
operand: {val})),
10141048
// Force-unwrap (`x!`) → postfix unary_expr, via swift-syntax's dedicated
10151049
// `forceUnwrapExpr` node.
1016-
rule!((forceUnwrapExpr expression: @e) => (unary_expr operator: (postfix_operator "!") operand: {e})),
1050+
rule!((forceUnwrapExpr expression: @e exclamationMark: @@mark) => (unary_expr
1051+
operator: {tree_at!(ctx, mark, (postfix_operator "!"))}
1052+
operand: {e})),
10171053
// ---- Imports ----
10181054
// An import declaration. The dotted path (a list of
10191055
// `importPathComponent`s) becomes a `name_node`/`member_access_expr`
@@ -1028,14 +1064,17 @@ fn translation_rules() -> Vec<Rule<SwiftContext>> {
10281064
attributes: _* @attrs
10291065
modifiers: _* @mods
10301066
importKindSpecifier: _? @@kind
1031-
path: (importPathComponent name: @@parts)*)
1067+
path: (importPathComponent name: @@parts)*) @@decl
10321068
=>
10331069
import_declaration {
10341070
let last = *parts.last().ok_or("import has no path")?;
10351071
let pattern = match kind {
1036-
None => tree!((named_pattern
1037-
name_node: (identifier #{last})
1038-
sub_pattern: (bulk_importing_pattern))),
1072+
None => {
1073+
let bulk = tree_at!(ctx, decl, (bulk_importing_pattern));
1074+
tree!((named_pattern
1075+
name_node: (identifier #{last})
1076+
sub_pattern: {bulk}))
1077+
}
10391078
Some(_) => tree!((identifier #{last})),
10401079
};
10411080
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)