Skip to content
Open
Show file tree
Hide file tree
Changes from all 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}))
}
),
// 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