From 318bc889c85f93d179b8287f97cb4570d73e7005 Mon Sep 17 00:00:00 2001 From: Simon Friis Vindum Date: Fri, 30 Oct 2026 13:35:35 +0100 Subject: [PATCH] Unified: Handle Swift `default` with an ignore pattern --- unified/extractor/ast_types.yml | 4 ++-- unified/extractor/src/languages/swift/swift.rs | 8 +++++--- .../binding-modifier-does-not-leak-to-sibling.output | 3 ++- .../tests/corpus/swift/control-flow/fallthrough.output | 3 ++- .../swift/control-flow/nested-enum-case-pattern.output | 3 ++- .../control-flow/switch-case-item-where-clauses.output | 3 ++- .../swift/control-flow/switch-expression-pattern.output | 3 ++- .../corpus/swift/control-flow/switch-statement.output | 3 ++- ...ding-modifier-does-not-leak-into-accessor-body.output | 3 ++- ...inding-modifier-does-not-leak-into-initializer.output | 3 ++- unified/extractor/tests/location_tests.rs | 9 +++++++++ unified/ql/lib/codeql/unified/internal/Ast.qll | 8 ++++---- .../ql/lib/codeql/unified/internal/ControlFlowGraph.qll | 2 +- unified/ql/lib/unified.dbscheme | 8 ++------ .../library-tests/controlflow/basicblock-slices.expected | 4 ++-- 15 files changed, 41 insertions(+), 26 deletions(-) diff --git a/unified/extractor/ast_types.yml b/unified/extractor/ast_types.yml index fb9d88333087..75ef6549b6f7 100644 --- a/unified/extractor/ast_types.yml +++ b/unified/extractor/ast_types.yml @@ -387,10 +387,10 @@ named: # A single `case ...:` (or `default:`) entry in a switch. # An entry with multiple `case p1, p2:` patterns uses an `or_pattern`. - # A `default:` entry has no pattern. + # A `default:` entry uses an `ignore_pattern`. switch_case: modifier*: modifier - pattern?: expr + pattern: expr body: block # Evaluate 'expr' and match its result against 'pattern', and return true if it matches. diff --git a/unified/extractor/src/languages/swift/swift.rs b/unified/extractor/src/languages/swift/swift.rs index a1dab86b626e..77ab8ab53e59 100644 --- a/unified/extractor/src/languages/swift/swift.rs +++ b/unified/extractor/src/languages/swift/swift.rs @@ -918,7 +918,7 @@ fn translation_rules() -> Vec> { ), // A `switch` statement. Each `switchCase` becomes a `switch_case` with a // pattern (or an `or_pattern` for comma-separated `case a, b:`) and a - // body; a `default:` case has a body but no pattern. The case items and + // body; a `default:` case uses an `ignore_pattern`. The case items and // body are auto-translated; the Rust block only picks the pattern shape // by arity (the query engine can't branch on list length). rule!( @@ -934,9 +934,11 @@ fn translation_rules() -> Vec> { body: (block stmt: {body})) ), rule!( - (switchCase label: (switchDefaultLabel) statements: _* @body) + (switchCase label: (switchDefaultLabel defaultKeyword: @@keyword) statements: _* @body) => - (switch_case body: (block stmt: {body})) + (switch_case + pattern: (ignore_pattern #{keyword}) + body: (block stmt: {body})) ), // A single case item unwraps to its pattern, possibly boxed in conditional_pattern rule!( diff --git a/unified/extractor/tests/corpus/swift/control-flow/binding-modifier-does-not-leak-to-sibling.output b/unified/extractor/tests/corpus/swift/control-flow/binding-modifier-does-not-leak-to-sibling.output index 73c3312ea09e..962b4fefde19 100644 --- a/unified/extractor/tests/corpus/swift/control-flow/binding-modifier-does-not-leak-to-sibling.output +++ b/unified/extractor/tests/corpus/swift/control-flow/binding-modifier-does-not-leak-to-sibling.output @@ -105,7 +105,8 @@ top_level source="⟨body⟩" argument: argument source="⟨value⟩" value: string_literal "\"matched\"" source="\"matched\"" - switch_case source="default:\n ⟨body⟩" + switch_case source="⟨pattern⟩:\n ⟨body⟩" + pattern: ignore_pattern "default" source="default" body: block source="⟨stmt⟩" stmt: break_expr "break" source="break" diff --git a/unified/extractor/tests/corpus/swift/control-flow/fallthrough.output b/unified/extractor/tests/corpus/swift/control-flow/fallthrough.output index 080c3b0089d3..b949bc57072d 100644 --- a/unified/extractor/tests/corpus/swift/control-flow/fallthrough.output +++ b/unified/extractor/tests/corpus/swift/control-flow/fallthrough.output @@ -104,7 +104,8 @@ top_level source="⟨body⟩" body: block source="⟨stmt⟩" stmt: unsupported_node "fallthrough" source="fallthrough" - switch_case source="default:\n ⟨body⟩" + switch_case source="⟨pattern⟩:\n ⟨body⟩" + pattern: ignore_pattern "default" source="default" body: block source="⟨stmt⟩" stmt: break_expr "break" source="break" diff --git a/unified/extractor/tests/corpus/swift/control-flow/nested-enum-case-pattern.output b/unified/extractor/tests/corpus/swift/control-flow/nested-enum-case-pattern.output index ea814c00d53b..dc024f4f2cff 100644 --- a/unified/extractor/tests/corpus/swift/control-flow/nested-enum-case-pattern.output +++ b/unified/extractor/tests/corpus/swift/control-flow/nested-enum-case-pattern.output @@ -286,7 +286,8 @@ top_level source="⟨body⟩" body: block source="⟨stmt⟩" stmt: break_expr "break" source="break" - switch_case source="default:\n ⟨body⟩" + switch_case source="⟨pattern⟩:\n ⟨body⟩" + pattern: ignore_pattern "default" source="default" body: block source="⟨stmt⟩" stmt: break_expr "break" source="break" diff --git a/unified/extractor/tests/corpus/swift/control-flow/switch-case-item-where-clauses.output b/unified/extractor/tests/corpus/swift/control-flow/switch-case-item-where-clauses.output index ddd959679201..73f836a620bc 100644 --- a/unified/extractor/tests/corpus/swift/control-flow/switch-case-item-where-clauses.output +++ b/unified/extractor/tests/corpus/swift/control-flow/switch-case-item-where-clauses.output @@ -200,7 +200,8 @@ top_level source="⟨body⟩" argument: argument source="⟨value⟩" value: string_literal "\"non-positive\"" source="\"non-positive\"" - switch_case source="default:\n ⟨body⟩" + switch_case source="⟨pattern⟩:\n ⟨body⟩" + pattern: ignore_pattern "default" source="default" body: block source="⟨stmt⟩" stmt: diff --git a/unified/extractor/tests/corpus/swift/control-flow/switch-expression-pattern.output b/unified/extractor/tests/corpus/swift/control-flow/switch-expression-pattern.output index bd170eeb80a4..fa24016c0bd8 100644 --- a/unified/extractor/tests/corpus/swift/control-flow/switch-expression-pattern.output +++ b/unified/extractor/tests/corpus/swift/control-flow/switch-expression-pattern.output @@ -393,7 +393,8 @@ top_level source="⟨body⟩" argument: argument source="⟨value⟩" value: identifier "fallback" source="fallback" - switch_case source="default:\n ⟨body⟩" + switch_case source="⟨pattern⟩:\n ⟨body⟩" + pattern: ignore_pattern "default" source="default" body: block source="⟨stmt⟩" stmt: break_expr "break" source="break" diff --git a/unified/extractor/tests/corpus/swift/control-flow/switch-statement.output b/unified/extractor/tests/corpus/swift/control-flow/switch-statement.output index 7ff90754ac7a..176aaa249044 100644 --- a/unified/extractor/tests/corpus/swift/control-flow/switch-statement.output +++ b/unified/extractor/tests/corpus/swift/control-flow/switch-statement.output @@ -151,7 +151,8 @@ top_level source="⟨body⟩" argument: argument source="⟨value⟩" value: string_literal "\"two or three\"" source="\"two or three\"" - switch_case source="default:\n ⟨body⟩" + switch_case source="⟨pattern⟩:\n ⟨body⟩" + pattern: ignore_pattern "default" source="default" body: block source="⟨stmt⟩" stmt: diff --git a/unified/extractor/tests/corpus/swift/types/binding-modifier-does-not-leak-into-accessor-body.output b/unified/extractor/tests/corpus/swift/types/binding-modifier-does-not-leak-into-accessor-body.output index f3ce25b05608..6d8bd6f990ba 100644 --- a/unified/extractor/tests/corpus/swift/types/binding-modifier-does-not-leak-into-accessor-body.output +++ b/unified/extractor/tests/corpus/swift/types/binding-modifier-does-not-leak-into-accessor-body.output @@ -114,7 +114,8 @@ top_level source="var p: Int {\n ⟨body⟩\n}" stmt: return_expr source="return ⟨value⟩" value: int_literal "1" source="1" - switch_case source="default:\n ⟨body⟩" + switch_case source="⟨pattern⟩:\n ⟨body⟩" + pattern: ignore_pattern "default" source="default" body: block source="⟨stmt⟩" stmt: diff --git a/unified/extractor/tests/corpus/swift/variables/binding-modifier-does-not-leak-into-initializer.output b/unified/extractor/tests/corpus/swift/variables/binding-modifier-does-not-leak-into-initializer.output index d42155011399..1eed0c4f40be 100644 --- a/unified/extractor/tests/corpus/swift/variables/binding-modifier-does-not-leak-into-initializer.output +++ b/unified/extractor/tests/corpus/swift/variables/binding-modifier-does-not-leak-into-initializer.output @@ -77,7 +77,8 @@ top_level source="⟨body⟩" body: block source="⟨stmt⟩" stmt: int_literal "1" source="1" - switch_case source="default: ⟨body⟩" + switch_case source="⟨pattern⟩: ⟨body⟩" + pattern: ignore_pattern "default" source="default" body: block source="⟨stmt⟩" stmt: int_literal "2" source="2" diff --git a/unified/extractor/tests/location_tests.rs b/unified/extractor/tests/location_tests.rs index 66e24473993c..734d3bd62a98 100644 --- a/unified/extractor/tests/location_tests.rs +++ b/unified/extractor/tests/location_tests.rs @@ -104,6 +104,15 @@ fn synthesized_condition_and_switch_nodes_use_child_ranges() { assert_has_span(&ast, source, "block", None, "c"); } +#[test] +fn switch_default_patterns_span_the_default_keyword() { + let source = "switch x { case _: break; @unknown default: break }"; + let ast = desugar(source); + + assert_has_span(&ast, source, "ignore_pattern", Some("_"), "_"); + assert_has_span(&ast, source, "ignore_pattern", Some("default"), "default"); +} + #[test] fn declaration_and_operator_tokens_keep_precise_ranges() { let source = "func f() { return x }\nlet y = try? await value! as? T\nlet z = value is T"; diff --git a/unified/ql/lib/codeql/unified/internal/Ast.qll b/unified/ql/lib/codeql/unified/internal/Ast.qll index 5f410460536e..442ac24f6919 100644 --- a/unified/ql/lib/codeql/unified/internal/Ast.qll +++ b/unified/ql/lib/codeql/unified/internal/Ast.qll @@ -1278,7 +1278,7 @@ module Unified { final override string getAPrimaryQlClass() { result = "SwitchCase" } /** Gets the node corresponding to the field `body`. */ - final F::Block getBody() { unified_switch_case_def(this, result) } + final F::Block getBody() { unified_switch_case_def(this, result, _) } /** Gets the node corresponding to the field `modifier`. */ final F::Modifier getModifier(int i) { unified_switch_case_modifier(this, i, result) } @@ -1287,13 +1287,13 @@ module Unified { final F::Modifier getAModifier() { result = this.getModifier(_) } /** Gets the node corresponding to the field `pattern`. */ - final F::Expr getPattern() { unified_switch_case_pattern(this, result) } + final F::Expr getPattern() { unified_switch_case_def(this, _, result) } /** Gets a field or child node of this node. */ final override F::AstNode getAFieldOrChild() { - unified_switch_case_def(this, result) or + unified_switch_case_def(this, result, _) or unified_switch_case_modifier(this, _, result) or - unified_switch_case_pattern(this, result) + unified_switch_case_def(this, _, result) } } diff --git a/unified/ql/lib/codeql/unified/internal/ControlFlowGraph.qll b/unified/ql/lib/codeql/unified/internal/ControlFlowGraph.qll index 374afa4a6c82..b7f2d3397d6e 100644 --- a/unified/ql/lib/codeql/unified/internal/ControlFlowGraph.qll +++ b/unified/ql/lib/codeql/unified/internal/ControlFlowGraph.qll @@ -247,7 +247,7 @@ module Ast implements AstSig { } class DefaultCase extends Case { - DefaultCase() { not exists(super.getPattern()) } + DefaultCase() { super.getPattern() instanceof U::IgnorePattern } } class ConditionalExpr = U::IfExpr; diff --git a/unified/ql/lib/unified.dbscheme b/unified/ql/lib/unified.dbscheme index 54ede4d9d45c..5a426ea58922 100644 --- a/unified/ql/lib/unified.dbscheme +++ b/unified/ql/lib/unified.dbscheme @@ -809,14 +809,10 @@ unified_switch_case_modifier( unique int modifier: @unified_token_modifier ref ); -unified_switch_case_pattern( - unique int unified_switch_case: @unified_switch_case ref, - unique int pattern: @unified_expr ref -); - unified_switch_case_def( unique int id: @unified_switch_case, - int body: @unified_block ref + int body: @unified_block ref, + int pattern: @unified_expr ref ); #keyset[unified_switch_expr, index] diff --git a/unified/ql/test/library-tests/controlflow/basicblock-slices.expected b/unified/ql/test/library-tests/controlflow/basicblock-slices.expected index 75209a2d826b..7b9289fffa2e 100644 --- a/unified/ql/test/library-tests/controlflow/basicblock-slices.expected +++ b/unified/ql/test/library-tests/controlflow/basicblock-slices.expected @@ -132,7 +132,7 @@ | 155 | cfg.swift:155:5:157:17 | SwitchCase | 'SwitchCase -V 42' | | 156 | cfg.swift:156:7:157:17 | Block | 'Block -V print -> Argument -V "42!" -^ print(...)' | | 157 | cfg.swift:157:7:157:17 | | '' | -| 158 | cfg.swift:158:5:159:34 | SwitchCase | 'SwitchCase' | +| 158 | cfg.swift:158:5:159:34 | SwitchCase | 'SwitchCase -V default' | | 159 | cfg.swift:159:7:159:34 | Block | 'Block -V IfExpr -V x -> 10 -^ ... > ...' | | 159 | cfg.swift:159:19:159:34 | Block | 'Block -V false -^ ReturnExpr' | | 162 | cfg.swift:162:3:162:31 | var obj | 'var obj -V obj -> AnyObject -> C -> Argument -V x -^ C(...)' | @@ -482,7 +482,7 @@ | 595 | cfg.swift:595:3:598:3 | let a | 'let a -V a -> SwitchExpr -V x' | | 596 | cfg.swift:596:5:596:17 | SwitchCase | 'SwitchCase -V 0 -> 5 -^ ... ..< ...' | | 596 | cfg.swift:596:17:596:17 | Block | 'Block -V 1' | -| 597 | cfg.swift:597:5:597:14 | SwitchCase | 'SwitchCase -V Block -V 2' | +| 597 | cfg.swift:597:5:597:14 | SwitchCase | 'SwitchCase -V default -> Block -V 2' | | 599 | cfg.swift:599:3:601:18 | let b | 'let b -V b' | | 600 | cfg.swift:600:9:601:18 | IfExpr | 'IfExpr -V x -> 42 -^ ... < ...' | | 600 | cfg.swift:600:21:600:25 | Block | 'Block -V 1' |