Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
8 changes: 8 additions & 0 deletions unified/extractor/ast_types.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ supertypes:
- binary_expr
- unary_expr
- call_expr
- macro_call_expr
- member_access_expr
- super_expr
- function_expr
Expand Down Expand Up @@ -171,6 +172,13 @@ named:
callee: expr
argument*: argument

# A macro call expression, such as `#expect(x == y)` in Swift or
# `assert!(x == y)` in Rust.
macro_call_expr:
macro_name_node: identifier
type_argument*: expr
argument*: argument

argument:
modifier*: modifier
name_node?: identifier
Expand Down
29 changes: 25 additions & 4 deletions unified/extractor/src/languages/swift/swift.rs
Original file line number Diff line number Diff line change
Expand Up @@ -712,6 +712,31 @@ fn translation_rules() -> Vec<Rule<SwiftContext>> {
=>
(argument name_node: (identifier #{lbl})? value: {val})
),
// An additional labeled trailing closure, such as the `c: { ... }` in
// `foo { 42 } c: { ... }`, is turned into an argument to the function.
rule!(
(multipleTrailingClosureElement label: @@label closure: @closure)
=>
(argument name_node: (identifier #{label}) value: {closure})
),
rule!(
(macroExpansionExpr

@paldepind paldepind Oct 2, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think handling these as macro calls might be just fine. And if not we can always tweak later.

macroName: @@name
genericArgumentClause: (genericArgumentClause arguments: (genericArgument argument: @type_args)*)?
Comment on lines +724 to +725

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're not handling moduleSelector anywhere yet, so this is consistent.

arguments: _* @args
trailingClosure: _? @trailing
additionalTrailingClosures: _* @additional)
=>
macro_call_expr {
let mut arguments = args;
arguments.extend(trailing.into_iter().map(|trailing| tree!((argument value: {trailing}))));
arguments.extend(additional);
tree!((macro_call_expr
macro_name_node: (identifier #{name})
type_argument: {type_args}
argument: {arguments}))
}
Comment thread
paldepind marked this conversation as resolved.
Resolved
),
// Member access (`list.append`). The `declName` is itself a
// `declReferenceExpr`; pull its `baseName` out as the member identifier.
// A leading-dot access (`.foo`) has no explicit base — the base is an
Expand Down Expand Up @@ -1196,10 +1221,6 @@ fn translation_rules() -> Vec<Rule<SwiftContext>> {
}
}
),
// Selector expression: `#selector(inner)` -- not yet supported
// (swift-syntax represents `#selector`/`#keyPath` and other macro
// expansions uniformly as a `macroExpansionExpr`).
rule!((macroExpansionExpr) => (unsupported_node)),
// A nominal type's `inheritanceClause` (`: Base, Proto`) becomes a list
// of `base_type`s, one per inherited type. Each declaration keyword
// gets its own rule; the bodies are identical but for the keyword.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
#expect(value == 42)

#custom<Int>(value) {
value
} completion: {
value
}

#Preview {
let previewValue = 42
previewValue
}

---

sourceFile
endOfFileToken: endOfFile
statements:
codeBlockItem
item:
macroExpansionExpr
leftParen: (
rightParen: )
arguments:
labeledExpr
expression:
infixOperatorExpr
operator:
binaryOperatorExpr
operator: binaryOperator "=="
leftOperand:
declReferenceExpr
baseName: identifier "value"
rightOperand:
integerLiteralExpr
literal: integerLiteral "42"
additionalTrailingClosures:
macroName: identifier "expect"
pound: #
codeBlockItem
item:
macroExpansionExpr
leftParen: (
rightParen: )
arguments:
labeledExpr
expression:
declReferenceExpr
baseName: identifier "value"
additionalTrailingClosures:
multipleTrailingClosureElement
colon: :
label: identifier "completion"
closure:
closureExpr
leftBrace: {
rightBrace: }
statements:
codeBlockItem
item:
declReferenceExpr
baseName: identifier "value"
trailingClosure:
closureExpr
leftBrace: {
rightBrace: }
statements:
codeBlockItem
item:
declReferenceExpr
baseName: identifier "value"
genericArgumentClause:
genericArgumentClause
arguments:
genericArgument
argument:
identifierType
name: identifier "Int"
leftAngle: <
rightAngle: >
macroName: identifier "custom"
pound: #
codeBlockItem
item:
macroExpansionExpr
arguments:
additionalTrailingClosures:
trailingClosure:
closureExpr
leftBrace: {
rightBrace: }
statements:
codeBlockItem
item:
variableDecl
attributes:
modifiers:
bindingSpecifier: let
bindings:
patternBinding
initializer:
initializerClause
equal: =
value:
integerLiteralExpr
literal: integerLiteral "42"
pattern:
identifierPattern
identifier: identifier "previewValue"
codeBlockItem
item:
declReferenceExpr
baseName: identifier "previewValue"
macroName: identifier "Preview"
pound: #

---

top_level source="⟨body⟩"
body:
block source="⟨stmt⟩\n\n⟨stmt⟩\n\n⟨stmt⟩"
stmt:
macro_call_expr source="#⟨macro_name_node⟩(⟨argument⟩)"
macro_name_node: identifier "expect" source="expect"
argument:
argument source="⟨value⟩"
value:
binary_expr source="⟨left⟩ ⟨operator⟩ ⟨right⟩"
left: identifier "value" source="value"
operator: infix_operator "==" source="=="
right: int_literal "42" source="42"
macro_call_expr source="#⟨macro_name_node⟩<⟨type_argument⟩>(⟨argument⟩) ⟨argument⟩ ⟨argument⟩"
macro_name_node: identifier "custom" source="custom"
type_argument: identifier "Int" source="Int"
argument:
argument source="⟨value⟩"
value: identifier "value" source="value"
argument source="⟨value⟩"
value:
function_expr source="{\n ⟨body⟩\n}"
body:
block source="⟨stmt⟩"
stmt: identifier "value" source="value"
argument source="⟨name_node⟩: ⟨value⟩"
name_node: identifier "completion" source="completion"
value:
function_expr source="{\n ⟨body⟩\n}"
body:
block source="⟨stmt⟩"
stmt: identifier "value" source="value"
macro_call_expr source="#⟨macro_name_node⟩ ⟨argument⟩"
macro_name_node: identifier "Preview" source="Preview"
argument:
argument source="⟨value⟩"
value:
function_expr source="{\n ⟨body⟩\n}"
body:
block source="⟨stmt⟩\n ⟨stmt⟩"
stmt:
variable_declaration source="⟨modifier⟩ ⟨pattern⟩ = ⟨value⟩"
modifier: modifier "let" source="let"
pattern: identifier "previewValue" source="previewValue"
value: int_literal "42" source="42"
identifier "previewValue" source="previewValue"
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
#expect(value == 42)

#custom<Int>(value) {
value
} completion: {
value
}

#Preview {
let previewValue = 42
previewValue
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,6 @@ top_level source="⟨body⟩"
variable_declaration source="⟨modifier⟩ ⟨pattern⟩ = ⟨value⟩"
modifier: modifier "let" source="let"
pattern: identifier "currentLine" source="currentLine"
value: unsupported_node "#line" source="#line"
value:
macro_call_expr source="#⟨macro_name_node⟩"
macro_name_node: identifier "line" source="line"
36 changes: 36 additions & 0 deletions unified/ql/lib/codeql/unified/internal/Ast.qll
Original file line number Diff line number Diff line change
Expand Up @@ -956,6 +956,34 @@
}
}

/** A class representing `macro_call_expr` nodes. */
class MacroCallExpr extends @unified_macro_call_expr, F::Expr {
/** Gets the name of the primary QL class for this element. */
final override string getAPrimaryQlClass() { result = "MacroCallExpr" }

/** Gets the node corresponding to the field `argument`. */
final F::Argument getArgument(int i) { unified_macro_call_expr_argument(this, i, result) }

/** Gets the node corresponding to the field `argument`. */
final F::Argument getAnArgument() { result = this.getArgument(_) }

/** Gets the node corresponding to the field `macro_name_node`. */
final F::Identifier getMacroNameNode() { unified_macro_call_expr_def(this, result) }

/** Gets the node corresponding to the field `type_argument`. */
final F::Expr getTypeArgument(int i) { unified_macro_call_expr_type_argument(this, i, result) }

/** Gets the node corresponding to the field `type_argument`. */
final F::Expr getATypeArgument() { result = this.getTypeArgument(_) }

/** Gets a field or child node of this node. */
final override F::AstNode getAFieldOrChild() {
unified_macro_call_expr_argument(this, _, result) or
unified_macro_call_expr_def(this, result) or
unified_macro_call_expr_type_argument(this, _, result)
}
}

/** A class representing `map_literal` nodes. */
class MapLiteral extends @unified_map_literal, F::Expr {
/** Gets the name of the primary QL class for this element. */
Expand Down Expand Up @@ -1717,6 +1745,12 @@
or
result = node.(LabeledStmt).getStmt() and i = -1 and name = "getStmt"
or
result = node.(MacroCallExpr).getArgument(i) and name = "getArgument"
or
result = node.(MacroCallExpr).getMacroNameNode() and i = -1 and name = "getMacroNameNode"
or
result = node.(MacroCallExpr).getTypeArgument(i) and name = "getTypeArgument"
or
result = node.(MapLiteral).getElement(i) and name = "getElement"
or
result = node.(MemberAccessExpr).getBase() and i = -1 and name = "getBase"
Expand Down Expand Up @@ -1938,6 +1972,8 @@

final class LabeledStmt = F::LabeledStmt;

final class MacroCallExpr = F::MacroCallExpr;

final class MapLiteral = F::MapLiteral;

final class Member = F::Member;
Expand Down
23 changes: 21 additions & 2 deletions unified/ql/lib/unified.dbscheme
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,7 @@ unified_equality_type_constraint_def(
int right: @unified_expr ref
);

@unified_expr = @unified_array_literal | @unified_binary_expr | @unified_block | @unified_break_expr | @unified_bulk_importing_pattern | @unified_call_expr | @unified_conditional_pattern | @unified_continue_expr | @unified_expr_pattern | @unified_function_expr | @unified_generic_type_expr | @unified_if_expr | @unified_key_value_pair | @unified_map_literal | @unified_member_access_expr | @unified_named_pattern | @unified_or_pattern | @unified_pattern_guard_expr | @unified_return_expr | @unified_string_interpolation_expr | @unified_switch_expr | @unified_throw_expr | @unified_token_boolean_literal | @unified_token_builtin_expr | @unified_token_empty_expr | @unified_token_float_literal | @unified_token_identifier | @unified_token_ignore_pattern | @unified_token_inferred_type_expr | @unified_token_int_literal | @unified_token_regex_literal | @unified_token_string_literal | @unified_token_super_expr | @unified_token_unhandled_node | @unified_token_unsupported_node | @unified_try_expr | @unified_tuple_expr | @unified_type_cast_expr | @unified_type_test_expr | @unified_unary_expr | @unified_unresolved_operator_sequence
@unified_expr = @unified_array_literal | @unified_binary_expr | @unified_block | @unified_break_expr | @unified_bulk_importing_pattern | @unified_call_expr | @unified_conditional_pattern | @unified_continue_expr | @unified_expr_pattern | @unified_function_expr | @unified_generic_type_expr | @unified_if_expr | @unified_key_value_pair | @unified_macro_call_expr | @unified_map_literal | @unified_member_access_expr | @unified_named_pattern | @unified_or_pattern | @unified_pattern_guard_expr | @unified_return_expr | @unified_string_interpolation_expr | @unified_switch_expr | @unified_throw_expr | @unified_token_boolean_literal | @unified_token_builtin_expr | @unified_token_empty_expr | @unified_token_float_literal | @unified_token_identifier | @unified_token_ignore_pattern | @unified_token_inferred_type_expr | @unified_token_int_literal | @unified_token_regex_literal | @unified_token_string_literal | @unified_token_super_expr | @unified_token_unhandled_node | @unified_token_unsupported_node | @unified_try_expr | @unified_tuple_expr | @unified_type_cast_expr | @unified_type_test_expr | @unified_unary_expr | @unified_unresolved_operator_sequence

@unified_expr_or_operator = @unified_expr | @unified_token_infix_operator

Expand Down Expand Up @@ -631,6 +631,25 @@ unified_labeled_stmt_def(
int stmt: @unified_stmt ref
);

#keyset[unified_macro_call_expr, index]
unified_macro_call_expr_argument(
int unified_macro_call_expr: @unified_macro_call_expr ref,
int index: int ref,
unique int argument: @unified_argument ref
);

#keyset[unified_macro_call_expr, index]
unified_macro_call_expr_type_argument(
int unified_macro_call_expr: @unified_macro_call_expr ref,
int index: int ref,
unique int type_argument: @unified_expr ref
);

unified_macro_call_expr_def(
unique int id: @unified_macro_call_expr,
int macro_name_node: @unified_token_identifier ref
);

#keyset[unified_map_literal, index]
unified_map_literal_element(
int unified_map_literal: @unified_map_literal ref,
Expand Down Expand Up @@ -1006,7 +1025,7 @@ unified_trivia_tokeninfo(
string value: string ref
);

@unified_ast_node = @unified_accessor_declaration | @unified_argument | @unified_array_literal | @unified_associated_type_declaration | @unified_base_type | @unified_binary_expr | @unified_block | @unified_bound_type_constraint | @unified_break_expr | @unified_bulk_importing_pattern | @unified_call_expr | @unified_catch_clause | @unified_class_like_declaration | @unified_conditional_pattern | @unified_constructor_declaration | @unified_continue_expr | @unified_defer_stmt | @unified_destructor_declaration | @unified_do_while_stmt | @unified_equality_type_constraint | @unified_expr_pattern | @unified_for_each_stmt | @unified_function_declaration | @unified_function_expr | @unified_generic_type_expr | @unified_guard_if_stmt | @unified_if_expr | @unified_import_declaration | @unified_initializer_declaration | @unified_key_value_pair | @unified_labeled_stmt | @unified_map_literal | @unified_member_access_expr | @unified_named_pattern | @unified_operator_syntax_declaration | @unified_or_pattern | @unified_parameter | @unified_pattern_guard_expr | @unified_return_expr | @unified_string_interpolation_expr | @unified_switch_case | @unified_switch_expr | @unified_throw_expr | @unified_token | @unified_top_level | @unified_trivia_token | @unified_try_expr | @unified_tuple_expr | @unified_type_alias_declaration | @unified_type_cast_expr | @unified_type_parameter | @unified_type_test_expr | @unified_unary_expr | @unified_unresolved_operator_sequence | @unified_variable_declaration | @unified_while_stmt
@unified_ast_node = @unified_accessor_declaration | @unified_argument | @unified_array_literal | @unified_associated_type_declaration | @unified_base_type | @unified_binary_expr | @unified_block | @unified_bound_type_constraint | @unified_break_expr | @unified_bulk_importing_pattern | @unified_call_expr | @unified_catch_clause | @unified_class_like_declaration | @unified_conditional_pattern | @unified_constructor_declaration | @unified_continue_expr | @unified_defer_stmt | @unified_destructor_declaration | @unified_do_while_stmt | @unified_equality_type_constraint | @unified_expr_pattern | @unified_for_each_stmt | @unified_function_declaration | @unified_function_expr | @unified_generic_type_expr | @unified_guard_if_stmt | @unified_if_expr | @unified_import_declaration | @unified_initializer_declaration | @unified_key_value_pair | @unified_labeled_stmt | @unified_macro_call_expr | @unified_map_literal | @unified_member_access_expr | @unified_named_pattern | @unified_operator_syntax_declaration | @unified_or_pattern | @unified_parameter | @unified_pattern_guard_expr | @unified_return_expr | @unified_string_interpolation_expr | @unified_switch_case | @unified_switch_expr | @unified_throw_expr | @unified_token | @unified_top_level | @unified_trivia_token | @unified_try_expr | @unified_tuple_expr | @unified_type_alias_declaration | @unified_type_cast_expr | @unified_type_parameter | @unified_type_test_expr | @unified_unary_expr | @unified_unresolved_operator_sequence | @unified_variable_declaration | @unified_while_stmt

unified_ast_node_location(
unique int node: @unified_ast_node ref,
Expand Down
Loading