From 37a3ae54b1f56eace274953168e873942cbf180c Mon Sep 17 00:00:00 2001 From: Taus Date: Thu, 24 Sep 2026 13:14:21 +0000 Subject: [PATCH 1/4] Unified: Use raw captures for all tokens These were inadvertently being translated into `unsupported_node`s. --- .../extractor/src/languages/swift/swift.rs | 60 +++++++++---------- 1 file changed, 30 insertions(+), 30 deletions(-) diff --git a/unified/extractor/src/languages/swift/swift.rs b/unified/extractor/src/languages/swift/swift.rs index 62561c6a1620..a8afb13cdbfb 100644 --- a/unified/extractor/src/languages/swift/swift.rs +++ b/unified/extractor/src/languages/swift/swift.rs @@ -198,13 +198,13 @@ fn translation_rules() -> Vec> { => (unsupported_node) ), - rule!((declReferenceExpr baseName: (identifier) @name) => expr { + rule!((declReferenceExpr baseName: (identifier) @@name) => expr { tree!((identifier #{name})) }), // A bare name reference (`x`), and an operator used as a value (`+` in // `reduce(0, +)`), are both `declReferenceExpr`; its `baseName` is the // referenced identifier / operator symbol. - rule!((declReferenceExpr baseName: @name) => (identifier #{name})), + rule!((declReferenceExpr baseName: @@name) => (identifier #{name})), // A discard `_` used as an expression — e.g. the target of a discarding // assignment `_ = x`. swift-syntax models it as a `discardAssignmentExpr`. rule!((discardAssignmentExpr wildcard: @@w) => (ignore_pattern #{w})), @@ -215,7 +215,7 @@ fn translation_rules() -> Vec> { // `generic_type_expr`, so we map it directly to that shape. rule!( (genericSpecializationExpr - expression: (declReferenceExpr baseName: @name) + expression: (declReferenceExpr baseName: @@name) genericArgumentClause: (genericArgumentClause arguments: (genericArgument argument: @args)*)) => (generic_type_expr @@ -230,7 +230,7 @@ fn translation_rules() -> Vec> { // A `binaryOperatorExpr` wraps the operator token; unwrap it to the // operator leaf. Used by `infixOperatorExpr` (folded) and `sequenceExpr` // (unresolved). - rule!((binaryOperatorExpr operator: @op) => (infix_operator #{op})), + rule!((binaryOperatorExpr operator: @@op) => (infix_operator #{op})), // A `binaryOperator`-based `infixOperatorExpr` represents both ordinary // binary applications (`a + b`) and compound assignments (`x += y`). // Both have the same target AST shape; the QL library distinguishes @@ -276,7 +276,7 @@ fn translation_rules() -> Vec> { // infix operators, rather than guessing a structure. rule!((sequenceExpr elements: _* @els) => (unresolved_operator_sequence element: {els})), // Prefix unary operators (`!a`, `-x`). - rule!((prefixOperatorExpr operator: @op expression: @operand) => (unary_expr operator: (prefix_operator #{op}) operand: {operand})), + rule!((prefixOperatorExpr operator: @@op expression: @operand) => (unary_expr operator: (prefix_operator #{op}) operand: {operand})), // A parenthesised expression has a single tuple element; elide the // grouping and preserve the expression itself. Actual tuple literals // retain their translated labeled elements as `argument` children. @@ -468,7 +468,7 @@ fn translation_rules() -> Vec> { // `chained_declaration` tag. rule!( (enumCaseElement - name: @name + name: @@name parameterClause: (enumCaseParameterClause parameters: _* @params) @@clause) => class_like_declaration { @@ -486,7 +486,7 @@ fn translation_rules() -> Vec> { } ), rule!( - (enumCaseElement name: @name rawValue: (initializerClause value: @val)) + (enumCaseElement name: @@name rawValue: (initializerClause value: @val)) => (variable_declaration modifier: {ctx.outer_modifiers.clone()} @@ -496,7 +496,7 @@ fn translation_rules() -> Vec> { value: {val}) ), rule!( - (enumCaseElement name: @name) + (enumCaseElement name: @@name) => (variable_declaration modifier: {ctx.outer_modifiers.clone()} @@ -526,7 +526,7 @@ fn translation_rules() -> Vec> { ), // `identifierPattern` wraps a single identifier token. rule!( - (identifierPattern identifier: @name) + (identifierPattern identifier: @@name) => (identifier #{name}) ), @@ -574,7 +574,7 @@ fn translation_rules() -> Vec> { rule!( (functionDecl modifiers: _* @mods - name: @name + name: @@name genericParameterClause: (genericParameterClause parameters: _* @type_params)? signature: (functionSignature parameterClause: (functionParameterClause parameters: _* @params) @@ -592,7 +592,7 @@ fn translation_rules() -> Vec> { rule!( (functionDecl modifiers: _* @mods - name: @name + name: @@name genericParameterClause: (genericParameterClause parameters: _* @type_params)? signature: (functionSignature parameterClause: (functionParameterClause parameters: _* @params) @@ -715,7 +715,7 @@ fn translation_rules() -> Vec> { (memberAccessExpr base: (arrayExpr elements: (arrayElement expression: (genericSpecializationExpr) @element)) @@array - declName: (declReferenceExpr baseName: @member)) + declName: (declReferenceExpr baseName: @@member)) => member_access_expr { let base = tree_at!( @@ -731,12 +731,12 @@ fn translation_rules() -> Vec> { } ), rule!( - (memberAccessExpr base: @base declName: (declReferenceExpr baseName: @member)) + (memberAccessExpr base: @base declName: (declReferenceExpr baseName: @@member)) => (member_access_expr base: {base} member_name_node: (identifier #{member})) ), rule!( - (memberAccessExpr period: @dot declName: (declReferenceExpr baseName: @member)) + (memberAccessExpr period: @@dot declName: (declReferenceExpr baseName: @@member)) => (member_access_expr base: (inferred_type_expr #{dot}) member_name_node: (identifier #{member})) ), @@ -790,14 +790,14 @@ fn translation_rules() -> Vec> { // A closure parameter (`x: Int`, or just `x`). Unlike a function // parameter it has no external label; the type is optional. rule!( - (closureParameter firstName: @name type: _? @ty) + (closureParameter firstName: @@name type: _? @ty) => (parameter pattern: (identifier #{name}) type: {ty}) ), // A shorthand closure parameter (`x` in `{ x, y in … }`): a bare name // with no parentheses and no type. rule!( - (closureShorthandParameter name: @name) + (closureShorthandParameter name: @@name) => (parameter pattern: (identifier #{name})) ), @@ -872,7 +872,7 @@ fn translation_rules() -> Vec> { rule!( (optionalBindingCondition bindingSpecifier: @@spec - pattern: (identifierPattern identifier: @name) + pattern: (identifierPattern identifier: @@name) initializer: (initializerClause value: @val)) => (pattern_guard_expr @@ -886,7 +886,7 @@ fn translation_rules() -> Vec> { rule!( (optionalBindingCondition bindingSpecifier: @@spec - pattern: (identifierPattern identifier: @name)) + pattern: (identifierPattern identifier: @@name)) => (pattern_guard_expr value: (identifier #{name}) @@ -1091,7 +1091,7 @@ fn translation_rules() -> Vec> { rule!((declModifier) @m => (modifier #{m})), // Preserve the `super` keyword as a dedicated expression, normally used // as the base of a member access (`super.foo`). - rule!((superExpr superKeyword: @keyword) => (super_expr #{keyword})), + rule!((superExpr superKeyword: @@keyword) => (super_expr #{keyword})), // Type expressions. A generic type applied with explicit arguments // (`Set`) becomes a `generic_type_expr` whose `base` is the type // name and whose `type_argument`s are the (structured) arguments — the @@ -1213,9 +1213,9 @@ fn translation_rules() -> Vec> { // Class declaration with body containing members rule!( (classDecl - classKeyword: @kind + classKeyword: @@kind modifiers: _* @mods - name: @name + name: @@name genericParameterClause: (genericParameterClause parameters: _* @params genericWhereClause: (genericWhereClause requirements: _* @parameter_constraints)?)? @@ -1236,9 +1236,9 @@ fn translation_rules() -> Vec> { // Enum class declaration: same as a regular class but with an enum body. rule!( (enumDecl - enumKeyword: @kind + enumKeyword: @@kind modifiers: _* @mods - name: @name + name: @@name genericParameterClause: (genericParameterClause parameters: _* @params genericWhereClause: (genericWhereClause requirements: _* @parameter_constraints)?)? @@ -1259,9 +1259,9 @@ fn translation_rules() -> Vec> { // A `struct` declaration. rule!( (structDecl - structKeyword: @kind + structKeyword: @@kind modifiers: _* @mods - name: @name + name: @@name genericParameterClause: (genericParameterClause parameters: _* @params genericWhereClause: (genericWhereClause requirements: _* @parameter_constraints)?)? @@ -1282,9 +1282,9 @@ fn translation_rules() -> Vec> { // Protocol declaration rule!( (protocolDecl - protocolKeyword: @kind + protocolKeyword: @@kind modifiers: _* @mods - name: @name + name: @@name genericParameterClause: (genericParameterClause parameters: _* @params)? inheritanceClause: (inheritanceClause inheritedTypes: (inheritedType type: @bases)*)? genericWhereClause: (genericWhereClause requirements: _* @declaration_constraints)? @@ -1302,7 +1302,7 @@ fn translation_rules() -> Vec> { // An `extension Foo.Bar { … }` is likewise a `class_like_declaration`. rule!( (extensionDecl - extensionKeyword: @kind + extensionKeyword: @@kind modifiers: _* @mods extendedType: @extendedType inheritanceClause: (inheritanceClause inheritedTypes: (inheritedType type: @bases)*)? @@ -1322,7 +1322,7 @@ fn translation_rules() -> Vec> { // nest under `signature` (as for `functionDecl`). rule!( (initializerDecl - initKeyword: @initK + initKeyword: @@initK modifiers: _* @mods signature: (functionSignature parameterClause: (functionParameterClause parameters: _* @params)) @@ -1336,7 +1336,7 @@ fn translation_rules() -> Vec> { ), rule!( (initializerDecl - initKeyword: @initK + initKeyword: @@initK modifiers: _* @mods signature: (functionSignature parameterClause: (functionParameterClause parameters: _* @params))) From 67b126e364f18312b01b2ccfd1bf2e6c19e85f8e Mon Sep 17 00:00:00 2001 From: Taus Date: Thu, 24 Sep 2026 13:21:10 +0000 Subject: [PATCH 2/4] Unified: Add `unhandled_node` node type This node type will be used to represent nodes that -- during translation -- manage to reach the catch-all rule at the end. This almost certainly indicates something went wrong. In contrast, `unsupported_node` will now be used for nodes that we're aware of, but which have not proper translation just yet. --- unified/extractor/ast_types.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/unified/extractor/ast_types.yml b/unified/extractor/ast_types.yml index 1d8500447696..5c97b12b2ab0 100644 --- a/unified/extractor/ast_types.yml +++ b/unified/extractor/ast_types.yml @@ -34,6 +34,7 @@ supertypes: - switch_expr - unresolved_operator_sequence - unsupported_node + - unhandled_node - or_pattern - conditional_pattern - bulk_importing_pattern @@ -84,6 +85,7 @@ supertypes: - type_alias_declaration - associated_type_declaration - unsupported_node + - unhandled_node type_constraint: - equality_type_constraint - bound_type_constraint @@ -421,6 +423,9 @@ named: # A node that we don't yet translate unsupported_node: + # A node kind that has no explicit translation or unsupported classification + unhandled_node: + infix_operator: prefix_operator: From d802fe20f9b39e90aa00a3bc8dbd9fa4ce51d644 Mon Sep 17 00:00:00 2001 From: Taus Date: Thu, 24 Sep 2026 13:24:57 +0000 Subject: [PATCH 3/4] Unified: Regenerate AST/dbscheme --- unified/ql/lib/codeql/unified/internal/Ast.qll | 8 ++++++++ unified/ql/lib/unified.dbscheme | 7 ++++--- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/unified/ql/lib/codeql/unified/internal/Ast.qll b/unified/ql/lib/codeql/unified/internal/Ast.qll index 0c4c9f65a3c6..cfc917b85580 100644 --- a/unified/ql/lib/codeql/unified/internal/Ast.qll +++ b/unified/ql/lib/codeql/unified/internal/Ast.qll @@ -1440,6 +1440,12 @@ module Unified { } } + /** A class representing `unhandled_node` tokens. */ + class UnhandledNode extends @unified_token_unhandled_node, F::Expr, F::Member, F::Token { + /** Gets the name of the primary QL class for this element. */ + final override string getAPrimaryQlClass() { result = "UnhandledNode" } + } + /** A class representing `unresolved_operator_sequence` nodes. */ class UnresolvedOperatorSequence extends @unified_unresolved_operator_sequence, F::Expr { /** Gets the name of the primary QL class for this element. */ @@ -1976,6 +1982,8 @@ module UnifiedFinal { final class UnaryExpr = F::UnaryExpr; + final class UnhandledNode = F::UnhandledNode; + final class UnresolvedOperatorSequence = F::UnresolvedOperatorSequence; final class UnsupportedNode = F::UnsupportedNode; diff --git a/unified/ql/lib/unified.dbscheme b/unified/ql/lib/unified.dbscheme index 8916955b5408..fda85ffc96e3 100644 --- a/unified/ql/lib/unified.dbscheme +++ b/unified/ql/lib/unified.dbscheme @@ -435,7 +435,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_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_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 @@ -637,7 +637,7 @@ unified_map_literal_def( unique int id: @unified_map_literal ); -@unified_member = @unified_accessor_declaration | @unified_associated_type_declaration | @unified_class_like_declaration | @unified_constructor_declaration | @unified_destructor_declaration | @unified_function_declaration | @unified_initializer_declaration | @unified_token_unsupported_node | @unified_type_alias_declaration | @unified_variable_declaration +@unified_member = @unified_accessor_declaration | @unified_associated_type_declaration | @unified_class_like_declaration | @unified_constructor_declaration | @unified_destructor_declaration | @unified_function_declaration | @unified_initializer_declaration | @unified_token_unhandled_node | @unified_token_unsupported_node | @unified_type_alias_declaration | @unified_variable_declaration unified_member_access_expr_def( unique int id: @unified_member_access_expr, @@ -990,7 +990,8 @@ case @unified_token.kind of | 15 = @unified_token_regex_literal | 16 = @unified_token_string_literal | 17 = @unified_token_super_expr -| 18 = @unified_token_unsupported_node +| 18 = @unified_token_unhandled_node +| 19 = @unified_token_unsupported_node ; From ebac209b61651320fd1bc20fd76ad10a58a52f53 Mon Sep 17 00:00:00 2001 From: Taus Date: Thu, 24 Sep 2026 14:39:24 +0000 Subject: [PATCH 4/4] Unified: Add explicit rules for unsupported nodes Adds rules for each of the node types that we currently do not support. This means if the fallback clause actually fires, then we've encountered a truly new node type (or something that shouldn't have been translated, like a token). In this case, we now emit an error message (but this does not affect extraction -- it'll succeed with an `unhandled_node` in the output). --- .../extractor/src/languages/swift/swift.rs | 70 +++++++++++++++---- 1 file changed, 55 insertions(+), 15 deletions(-) diff --git a/unified/extractor/src/languages/swift/swift.rs b/unified/extractor/src/languages/swift/swift.rs index a8afb13cdbfb..d7026b19092d 100644 --- a/unified/extractor/src/languages/swift/swift.rs +++ b/unified/extractor/src/languages/swift/swift.rs @@ -129,9 +129,9 @@ fn translation_rules() -> Vec> { vec![ // ---- Top-level ---- // These rules translate the swift-syntax AST (camelCase kind names), - // produced by the sibling `adapter` module from the `swift-syntax-parse` - // binary's JSON. Anything unmatched falls through to the - // `unsupported_node` fallback at the end. + // produced by the sibling `adapter` module from swift-syntax JSON. + // Known kinds without dedicated rules become `unsupported_node`; + // genuinely unknown kinds become `unhandled_node`. // // `sourceFile` holds its top-level statements in an (elided) // `statements` collection; each element is a `codeBlockItem` wrapping @@ -1383,18 +1383,58 @@ fn translation_rules() -> Vec> { name_node: (identifier #{name}) bound: {bound}) ), - // ---- Fallbacks ---- - // Bare `_` (rather than `(_)`) so this matches both named nodes - // and unnamed tokens. Any unnamed token that escapes the - // input-schema-specific rules (e.g. captured operators in - // `additive_expression op: @op`) has its auto-translated value - // replaced with an `unsupported_node` whose source range is - // inherited from the original token, so `#{op}` still reads the - // original text. - rule!( - _ - => - (unsupported_node) + // ---- Explicitly unsupported roots ---- + // These kinds can reach translation independently, but we do not + // currently map them to the unified AST. Syntax nested inside one of + // these roots is discarded with its parent and needs no separate rule. + rule!((actorDecl) => (unsupported_node)), + rule!((attributedType) => (unsupported_node)), + rule!((borrowExpr) => (unsupported_node)), + rule!((classRestrictionType) => (unsupported_node)), + rule!((compositionType) => (unsupported_node)), + rule!((consumeExpr) => (unsupported_node)), + rule!((copyExpr) => (unsupported_node)), + rule!((deferStmt) => (unsupported_node)), + rule!((discardStmt) => (unsupported_node)), + rule!((fallThroughStmt) => (unsupported_node)), + rule!((ifConfigDecl) => (unsupported_node)), + rule!((implicitlyUnwrappedOptionalType) => (unsupported_node)), + rule!((inOutExpr) => (unsupported_node)), + rule!((inlineArrayType) => (unsupported_node)), + rule!((keyPathExpr) => (unsupported_node)), + rule!((macroDecl) => (unsupported_node)), + rule!((metatypeType) => (unsupported_node)), + rule!((namedOpaqueReturnType) => (unsupported_node)), + rule!((operatorDecl) => (unsupported_node)), + rule!((packElementExpr) => (unsupported_node)), + rule!((packElementType) => (unsupported_node)), + rule!((packExpansionExpr) => (unsupported_node)), + rule!((packExpansionType) => (unsupported_node)), + rule!((postfixIfConfigExpr) => (unsupported_node)), + rule!((postfixOperatorExpr) => (unsupported_node)), + rule!((poundSourceLocation) => (unsupported_node)), + rule!((precedenceGroupDecl) => (unsupported_node)), + rule!((someOrAnyType) => (unsupported_node)), + rule!((subscriptDecl) => (unsupported_node)), + rule!((suppressedType) => (unsupported_node)), + rule!((typeExpr) => (unsupported_node)), + rule!((unsafeExpr) => (unsupported_node)), + rule!((yieldStmt) => (unsupported_node)), + // Anything reaching this final generic handler has neither a + // dedicated rule nor an explicit unsupported entry. + rule!( + _ @@node + => + unhandled_node { + let input = ctx.ast.get_node(node).expect("matched node must exist"); + tracing::error!( + target: "unified_extractor", + node_kind = input.kind_name(), + source_range = ?input.source_range(), + "Unhandled Swift syntax node reached translation" + ); + tree!((unhandled_node)) + } ), ] }