diff --git a/shared/namebinding/codeql/namebinding/LocalNameBinding.qll b/shared/namebinding/codeql/namebinding/LocalNameBinding.qll index 2dd12bbc8abc..f70ced92fc98 100644 --- a/shared/namebinding/codeql/namebinding/LocalNameBinding.qll +++ b/shared/namebinding/codeql/namebinding/LocalNameBinding.qll @@ -34,8 +34,10 @@ signature module LocalNameBindingInputSig { AstNode getChild(AstNode n, int index); /** - * A conditional where any local declarations in the condition are in scope - * in the then-branch but not the else-branch. + * A conditional with a condition, a then-branch, and an optional else-branch. + * + * Unless `isUnscopedConditional` holds, local declarations in the condition + * are in scope in the then-branch but not the else-branch. * * Example: * @@ -62,6 +64,25 @@ signature module LocalNameBindingInputSig { AstNode getElse(); } + /** + * Holds if local declarations in a branch of `cond` are in scope in the + * remainder of the branch _and_ following the conditional itself. + * + * Example: + * + * ```swift + * #if DEBUG + * let x = def1 + * // x is in scope here and refers to `def1` + * #else + * let x = def2 + * // x is in scope here and refers to `def2` + * #endif + * // x is in scope here and refers to either `def1` or `def2` + * ``` + */ + default predicate isUnscopedConditional(Conditional cond) { none() } + /** * A declaration where all local declarations in the left-hand side are in * scope _after_ the declaration, and where any sibling declarations with @@ -195,17 +216,23 @@ module LocalNameBinding */ private AstNode getChildAdj(AstNode parent, int index) { result = getChild(parent, index) and - not exists(Conditional cond | result = [cond.getElse(), cond.getThen()]) + not exists(Conditional cond | + not isUnscopedConditional(cond) and + result = [cond.getElse(), cond.getThen()] + ) or exists(Conditional cond | - parent = cond and - result = cond.getElse() and - index = -1 - or - exists(int last | - result = cond.getThen() and - last = max(int i | conditionHasChildAt(cond, parent, i)) and - index = last + 1 + not isUnscopedConditional(cond) and + ( + parent = cond and + result = cond.getElse() and + index = -1 + or + exists(int last | + result = cond.getThen() and + last = max(int i | conditionHasChildAt(cond, parent, i)) and + index = last + 1 + ) ) ) } @@ -217,31 +244,132 @@ module LocalNameBinding int getRank(C parent, Ranked child) { child = getChildAdj(parent, result) and - getChildAdj(parent, _) instanceof SiblingShadowingDecl + // Only rank children when necessary, i.e., when there are sibling + // shadowing declarations or unscoped conditionals among the children. + ( + getChildAdj(parent, _) instanceof SiblingShadowingDecl + or + isUnscopedConditional(getChildAdj(parent, _)) + ) } } private predicate getRankedChild = DenseRank1::denseRank/2; + /** Holds if `decl` is a possible final declaration introduced by `node`. */ + private predicate lastDeclAfter(AstNode node, SiblingShadowingDecl decl) { + node = decl + or + node = any(Conditional cond | isUnscopedConditional(cond) and lastDeclInBranch(cond, decl)) + } + + private predicate lastDeclInBranch(Conditional cond, SiblingShadowingDecl decl) { + exists(AstNode branch | branch = [cond.getThen(), cond.getElse()] | + // An `#elseif` is itself a conditional rather than a block of statements. + isUnscopedConditional(branch) and lastDeclAfter(branch, decl) + or + not isUnscopedConditional(branch) and + lastDeclInPrefix(branch, max(int i | exists(getRankedChild(branch, i))), decl) + ) + } + /** - * Holds if `n` is the `i`th child of `parent`, but should instead be considered - * a child of a shadowing sibling declaration `decl` when resolving accesses. + * Holds if the unscoped conditional `cond` might not introduce any shadowing + * declarations. We need this property to determine whether declarations prior + * to the conditional are still in scope following the conditional. * - * This is the case when `decl` is the nearest shadowing sibling declaration - * preceding `n` amongst all the children of `parent`. + * For instance, in + * ```swift + * let x = 1 + * #if DEBUG + * let x = 2 + * #else + * let x = 3 + * #endif + * print(x) // prints 2 or 3 + * ``` + * the initial declaration of `x` is certain to not be shadowed after the + * conditional. * - * Note that `decl` may itself also have to be nested under another shadowing - * sibling declaration. + * If the conditional instead had no else branch or had one of the branches + * not declared any shadowing declarations, then the initial declaration of + * `x` might not have been shadowed after the conditional. */ - private predicate shouldBeShadowingDeclChild( - AstNode parent, SiblingShadowingDecl decl, int i, AstNode n - ) { - n = getRankedChild(parent, i) and + private predicate unscopedConditionalPreservesIncoming(Conditional cond) { + isUnscopedConditional(cond) and ( - decl = getRankedChild(parent, i - 1) + not exists(cond.getElse()) or - shouldBeShadowingDeclChild(parent, decl, i - 1, - any(AstNode prev | not prev instanceof SiblingShadowingDecl)) + branchPreservesIncoming([cond.getThen(), cond.getElse()]) + ) + } + + private predicate branchPreservesIncoming(AstNode branch) { + unscopedConditionalPreservesIncoming(branch) + or + not isUnscopedConditional(branch) and + ( + // If the children are not ranked, then they do not contain any sibling + // shadowing declarations. + not exists(getRankedChild(branch, _)) + or + prefixPreservesIncoming(branch, max(int i | exists(getRankedChild(branch, i)))) + ) + } + + /** + * Holds if `node` might not introduce any shadowing declaration. + * + * For example, the following satisfies the predicate since the declaration + * might be removed at compile time. + * ```swift + * #if DEBUG + * let value = 1 + * #endif + * ``` + */ + private predicate preservesIncoming(AstNode node) { + // A shadowing declaration always introduces a shadowing declaration. + not node instanceof SiblingShadowingDecl and + not isUnscopedConditional(node) + or + unscopedConditionalPreservesIncoming(node) + } + + /** Summarizes the first `i` ranked children without entering ordinary nested scopes. */ + private predicate lastDeclInPrefix(AstNode parent, int i, SiblingShadowingDecl decl) { + exists(AstNode child | child = getRankedChild(parent, i) | + lastDeclAfter(child, decl) + or + preservesIncoming(child) and lastDeclInPrefix(parent, i - 1, decl) + ) + } + + /** + * Holds if the first `i` children might not introduce any shadowing + * declarations. + */ + private predicate prefixPreservesIncoming(AstNode parent, int i) { + i = 0 and exists(getRankedChild(parent, _)) + or + prefixPreservesIncoming(parent, i - 1) and + preservesIncoming(getRankedChild(parent, i)) + } + + /** + * Holds if, instead of its parent, `n` should be considered a child of a + * shadowing sibling declaration `decl` when resolving accesses. + * + * Each alternative through preceding unscoped conditionals contributes its own + * nearest shadowing declaration. + * + * Note that `decl` may itself also have to be nested under another shadowing + * sibling declaration. + */ + private predicate shouldBeShadowingDeclChild(SiblingShadowingDecl decl, AstNode n) { + exists(AstNode parent, int i | + n = getRankedChild(parent, i) and + lastDeclInPrefix(parent, i - 1, decl) ) } @@ -289,11 +417,18 @@ module LocalNameBinding * ``` */ private AstNode getParentForScoping(AstNode n) { - not shouldBeShadowingDeclChild(_, _, _, n) and not exists(SiblingShadowingDecl decl | n = [decl.getRhs(), decl.getElse()]) and - n = getChildAdj(result, _) + n = getChildAdj(result, _) and + ( + not n = getRankedChild(result, _) + or + exists(int i | + n = getRankedChild(result, i) and + prefixPreservesIncoming(result, i - 1) + ) + ) or - shouldBeShadowingDeclChild(_, result, _, n) + shouldBeShadowingDeclChild(result, n) or exists(SiblingShadowingDecl decl | result = getParentForScoping(decl) and diff --git a/unified/extractor/ast_types.yml b/unified/extractor/ast_types.yml index fb9d88333087..5c535c173603 100644 --- a/unified/extractor/ast_types.yml +++ b/unified/extractor/ast_types.yml @@ -68,6 +68,8 @@ supertypes: - do_while_stmt - defer_stmt - labeled_stmt + - associated_type_declaration + - initializer_declaration callable: subtypes: - top_level @@ -93,6 +95,7 @@ supertypes: - class_like_declaration - type_alias_declaration - associated_type_declaration + - if_expr # For conditional compilation - unsupported_node - unhandled_node fields: @@ -261,7 +264,10 @@ named: block: stmt*: stmt + # An if expression or a conditional compilation directive (which we view as a + # compile-time evaluated if expression). if_expr: + modifier*: modifier condition: expr then?: expr else?: expr diff --git a/unified/extractor/src/languages/swift/swift.rs b/unified/extractor/src/languages/swift/swift.rs index af05d93ad172..d4544c2a4712 100644 --- a/unified/extractor/src/languages/swift/swift.rs +++ b/unified/extractor/src/languages/swift/swift.rs @@ -38,6 +38,9 @@ struct SwiftContext { /// True while translating a pattern. Optional chaining in this context is /// represented as an `Optional.some` call rather than being unwrapped. in_pattern: bool, + /// When iterating over the clauses of a conditional compilation block, this + /// holds the tail of the clauses that have already been translated. + conditional_compilation_tail: Option, } impl SwiftContext { @@ -64,11 +67,29 @@ fn implicit_parameterless_initializer( ctx: &mut yeast::build::BuildCtx<'_, SwiftContext>, members: &[yeast::OutputId], ) -> Option { - let has_explicit_initializer = members.iter().any(|member| { - ctx.output - .get_node(*member) - .is_some_and(|node| node.kind_name() == "constructor_declaration") - }); + // Search for an initializer while recursing into nodes for conditional + // compilation blocks. + fn find_initializer(ast: &yeast::OutputAst, id: yeast::OutputId) -> Option { + let node = ast.get_node(id)?; + match node.kind_name() { + "constructor_declaration" => Some(id), + name => { + let fields: &[&str] = match name { + "if_expr" => &["then", "else"], // look into conditional compilation + "block" => &["stmt"], // look into blocks in branches + _ => &[], + }; + fields.iter().find_map(|field| { + node.field_children(ast.field_id_for_name(field)?) + .iter() + .find_map(|child| find_initializer(ast, *child)) + }) + } + } + } + let has_explicit_initializer = members + .iter() + .any(|member| find_initializer(ctx.output, *member).is_some()); (!has_explicit_initializer).then(|| { tree!((constructor_declaration modifier: (modifier "generated") @@ -876,6 +897,53 @@ fn translation_rules() -> Vec> { => (parameter pattern: (identifier #{name})) ), + // ---- Conditioal compilation blocks ---- + // Conditional compilation blocks (`#if … #elseif … #else … #endif`) are + // represented with a `ifConfigDecl` node for the entire block and an + // `ifConfigClause` for each branch. We translate them into nested + // `if_expr` nodes. + + // When a conditional compilation block contains a switch case it + // appears within a `switch` -- we do not support this yet. + rule!( + (ifConfigDecl clauses: (ifConfigClause elements: (switchCase))) + => + (unsupported_node) + ), + // A conditional compilation block is translated into `if_expr` nodes + // with branches occurring nested within the else branch. + rule!( + (ifConfigDecl clauses: _* @@clauses) @@declaration + => + expr { + ctx.conditional_compilation_tail = None; + for clause in clauses.into_iter().rev() { + ctx.conditional_compilation_tail = ctx.translate(clause)?.into_iter().next(); + } + let result = ctx.conditional_compilation_tail.ok_or("conditional compilation has no clauses")?; + ctx.set_node_source_range(result, ctx.source_range(declaration)) + } + ), + // A single clause is mapped to an if expression with the tail of + // already processed clauses (stored in `conditional_compilation_tail`) + // inserted into the else branch. + rule!( + (ifConfigClause condition: @condition elements: _* @items) + => + (if_expr + // Mark this `if_expr` as coming from conditional compilation. + modifier: (modifier "conditional_compilation") + condition: {condition} + then: (block stmt: {items}) + else: {ctx.conditional_compilation_tail}) + ), + // A conditional compilation clause without a condition (a final `#else` + // block). + rule!( + (ifConfigClause elements: _* @items) + => + (block stmt: {items}) + ), // ---- Control flow ---- // An `if`/`else` expression. Conditions are joined via `and_chain`; the // then-body and optional else-body (another block, or an `ifExpr` for an @@ -1510,7 +1578,6 @@ fn translation_rules() -> Vec> { rule!((copyExpr) => (unsupported_node "")), rule!((discardStmt) => (unsupported_node "")), rule!((fallThroughStmt) => (unsupported_node "")), - rule!((ifConfigDecl) => (unsupported_node "")), rule!((implicitlyUnwrappedOptionalType) => (unsupported_node "")), rule!((inlineArrayType) => (unsupported_node "")), rule!((keyPathExpr) => (unsupported_node "")), diff --git a/unified/extractor/tests/corpus/swift/conditional-compilation/conditional-compilation-in-attributes.output b/unified/extractor/tests/corpus/swift/conditional-compilation/conditional-compilation-in-attributes.output new file mode 100644 index 000000000000..c9edeb233870 --- /dev/null +++ b/unified/extractor/tests/corpus/swift/conditional-compilation/conditional-compilation-in-attributes.output @@ -0,0 +1,116 @@ +#if DEBUG + @available(*, deprecated) +#endif +func f() {} + +#if DEBUG + @available(*, deprecated) +#endif +let x = 1 + +--- + +sourceFile + endOfFileToken: endOfFile + statements: + codeBlockItem + item: + functionDecl + attributes: + ifConfigDecl + clauses: + ifConfigClause + elements: + attribute + leftParen: ( + rightParen: ) + arguments: + availabilityArgument + trailingComma: , + argument: binaryOperator "*" + availabilityArgument + argument: deprecated + atSign: @ + attributeName: + identifierType + name: identifier "available" + condition: + declReferenceExpr + baseName: identifier "DEBUG" + poundKeyword: #if + poundEndif: #endif + body: + codeBlock + leftBrace: { + rightBrace: } + statements: + modifiers: + name: identifier "f" + signature: + functionSignature + parameterClause: + functionParameterClause + leftParen: ( + rightParen: ) + parameters: + funcKeyword: func + codeBlockItem + item: + variableDecl + attributes: + ifConfigDecl + clauses: + ifConfigClause + elements: + attribute + leftParen: ( + rightParen: ) + arguments: + availabilityArgument + trailingComma: , + argument: binaryOperator "*" + availabilityArgument + argument: deprecated + atSign: @ + attributeName: + identifierType + name: identifier "available" + condition: + declReferenceExpr + baseName: identifier "DEBUG" + poundKeyword: #if + poundEndif: #endif + modifiers: + bindingSpecifier: let + bindings: + patternBinding + initializer: + initializerClause + equal: = + value: + integerLiteralExpr + literal: integerLiteral "1" + pattern: + identifierPattern + identifier: identifier "x" + +--- + +top_level source="⟨body⟩" + body: + block source="⟨stmt⟩\n\n⟨stmt⟩" + stmt: + function_declaration source="#if DEBUG\n @available(*, deprecated)\n#endif\nfunc ⟨name_node⟩() ⟨body⟩" + name_node: identifier "f" source="f" + body: block "{}" source="{}" + variable_declaration source="⟨modifier⟩\n⟨modifier⟩ ⟨pattern⟩ = ⟨value⟩" + modifier: + modifier "let" source="let" + if_expr source="#if ⟨condition⟩\n ⟨then⟩\n#endif" <-- ERROR: The field variable_declaration.modifier should contain modifier, but got if_expr + modifier: modifier "conditional_compilation" source="" + condition: identifier "DEBUG" source="DEBUG" + then: + block source="⟨stmt⟩" + stmt: modifier "@available(*, deprecated)" source="@available(*, deprecated)" <-- ERROR: The field block.stmt should contain stmt, but got modifier + pattern: identifier "x" source="x" + value: int_literal "1" source="1" diff --git a/unified/extractor/tests/corpus/swift/conditional-compilation/conditional-compilation-in-attributes.swift b/unified/extractor/tests/corpus/swift/conditional-compilation/conditional-compilation-in-attributes.swift new file mode 100644 index 000000000000..00310f5ab5c1 --- /dev/null +++ b/unified/extractor/tests/corpus/swift/conditional-compilation/conditional-compilation-in-attributes.swift @@ -0,0 +1,9 @@ +#if DEBUG + @available(*, deprecated) +#endif +func f() {} + +#if DEBUG + @available(*, deprecated) +#endif +let x = 1 diff --git a/unified/extractor/tests/corpus/swift/types/conditional-compilation-in-class-body.output b/unified/extractor/tests/corpus/swift/conditional-compilation/conditional-compilation-in-class-body.output similarity index 72% rename from unified/extractor/tests/corpus/swift/types/conditional-compilation-in-class-body.output rename to unified/extractor/tests/corpus/swift/conditional-compilation/conditional-compilation-in-class-body.output index 4d6fdc40564a..cbc62d0795ad 100644 --- a/unified/extractor/tests/corpus/swift/types/conditional-compilation-in-class-body.output +++ b/unified/extractor/tests/corpus/swift/conditional-compilation/conditional-compilation-in-class-body.output @@ -1,12 +1,9 @@ -// Conditional compilation is not yet supported: swift-syntax reports a -// structured `ifConfigDecl` (whose branches hold ordinary member items), but -// the mapping has no rule for it, so the whole block becomes one -// `unsupported_node` and its members are not extracted. +// Conditional compilation branches hold ordinary member declarations. class C { -#if DEBUG - init(x: Int) {} - deinit {} -#endif + #if DEBUG + init(x: Int) {} + deinit {} + #endif } --- @@ -81,12 +78,23 @@ top_level source="⟨body⟩" body: block source="⟨stmt⟩" stmt: - class_like_declaration source="⟨modifier⟩ ⟨name_node⟩ {\n⟨member⟩\n}" + class_like_declaration source="⟨modifier⟩ ⟨name_node⟩ {\n ⟨member⟩\n}" modifier: modifier "class" source="class" name_node: identifier "C" source="C" member: - unsupported_node "#if DEBUG\n init(x: Int) {}\n deinit {}\n#endif" source="#if DEBUG\n init(x: Int) {}\n deinit {}\n#endif" - constructor_declaration source="" - modifier: modifier "generated" source="" - name_node: identifier "init" source="" - body: block source="" + if_expr source="#if ⟨condition⟩\n ⟨then⟩\n #endif" + modifier: modifier "conditional_compilation" source="" + condition: identifier "DEBUG" source="DEBUG" + then: + block source="⟨stmt⟩\n ⟨stmt⟩" + stmt: + constructor_declaration source="⟨name_node⟩(⟨parameter⟩) ⟨body⟩" + name_node: identifier "init" source="init" + parameter: + parameter source="⟨external_name_node⟩⟨pattern⟩: ⟨type⟩" + external_name_node: identifier "x" source="x" + type: identifier "Int" source="Int" + pattern: identifier "x" source="x" + body: block "{}" source="{}" + destructor_declaration source="deinit ⟨body⟩" + body: block "{}" source="{}" diff --git a/unified/extractor/tests/corpus/swift/conditional-compilation/conditional-compilation-in-class-body.swift b/unified/extractor/tests/corpus/swift/conditional-compilation/conditional-compilation-in-class-body.swift new file mode 100644 index 000000000000..09e6f7852f4e --- /dev/null +++ b/unified/extractor/tests/corpus/swift/conditional-compilation/conditional-compilation-in-class-body.swift @@ -0,0 +1,7 @@ +// Conditional compilation branches hold ordinary member declarations. +class C { + #if DEBUG + init(x: Int) {} + deinit {} + #endif +} diff --git a/unified/extractor/tests/corpus/swift/conditional-compilation/conditional-compilation-in-switch-cases.output b/unified/extractor/tests/corpus/swift/conditional-compilation/conditional-compilation-in-switch-cases.output new file mode 100644 index 000000000000..658508bcda66 --- /dev/null +++ b/unified/extractor/tests/corpus/swift/conditional-compilation/conditional-compilation-in-switch-cases.output @@ -0,0 +1,133 @@ +func f(_ x: Int) { + switch x { + #if DEBUG + case 0: break + case 1: break + #endif + default: break + } +} + +--- + +sourceFile + endOfFileToken: endOfFile + statements: + codeBlockItem + item: + functionDecl + attributes: + body: + codeBlock + leftBrace: { + rightBrace: } + statements: + codeBlockItem + item: + expressionStmt + expression: + switchExpr + leftBrace: { + rightBrace: } + cases: + ifConfigDecl + clauses: + ifConfigClause + elements: + switchCase + label: + switchCaseLabel + colon: : + caseKeyword: case + caseItems: + switchCaseItem + pattern: + expressionPattern + expression: + integerLiteralExpr + literal: integerLiteral "0" + statements: + codeBlockItem + item: + breakStmt + breakKeyword: break + switchCase + label: + switchCaseLabel + colon: : + caseKeyword: case + caseItems: + switchCaseItem + pattern: + expressionPattern + expression: + integerLiteralExpr + literal: integerLiteral "1" + statements: + codeBlockItem + item: + breakStmt + breakKeyword: break + condition: + declReferenceExpr + baseName: identifier "DEBUG" + poundKeyword: #if + poundEndif: #endif + switchCase + label: + switchDefaultLabel + colon: : + defaultKeyword: default + statements: + codeBlockItem + item: + breakStmt + breakKeyword: break + subject: + declReferenceExpr + baseName: identifier "x" + switchKeyword: switch + modifiers: + name: identifier "f" + signature: + functionSignature + parameterClause: + functionParameterClause + leftParen: ( + rightParen: ) + parameters: + functionParameter + colon: : + attributes: + modifiers: + type: + identifierType + name: identifier "Int" + firstName: _ + secondName: identifier "x" + funcKeyword: func + +--- + +top_level source="⟨body⟩" + body: + block source="⟨stmt⟩" + stmt: + function_declaration source="func ⟨name_node⟩(⟨parameter⟩) ⟨body⟩" + name_node: identifier "f" source="f" + parameter: + parameter source="⟨external_name_node⟩ ⟨pattern⟩: ⟨type⟩" + external_name_node: identifier "_" source="_" + type: identifier "Int" source="Int" + pattern: identifier "x" source="x" + body: + block source="{\n ⟨stmt⟩\n}" + stmt: + switch_expr source="switch ⟨value⟩ {\n ⟨case⟩\n ⟨case⟩\n }" + value: identifier "x" source="x" + case: + unsupported_node "#if DEBUG\n case 0: break\n case 1: break\n #endif" source="#if DEBUG\n case 0: break\n case 1: break\n #endif" <-- ERROR: The field switch_expr.case should contain switch_case, but got unsupported_node + switch_case source="default: ⟨body⟩" + body: + block source="⟨stmt⟩" + stmt: break_expr "break" source="break" diff --git a/unified/extractor/tests/corpus/swift/conditional-compilation/conditional-compilation-in-switch-cases.swift b/unified/extractor/tests/corpus/swift/conditional-compilation/conditional-compilation-in-switch-cases.swift new file mode 100644 index 000000000000..ac09c5660fb9 --- /dev/null +++ b/unified/extractor/tests/corpus/swift/conditional-compilation/conditional-compilation-in-switch-cases.swift @@ -0,0 +1,9 @@ +func f(_ x: Int) { + switch x { + #if DEBUG + case 0: break + case 1: break + #endif + default: break + } +} diff --git a/unified/extractor/tests/corpus/swift/conditional-compilation/conditional-compilation-nested.output b/unified/extractor/tests/corpus/swift/conditional-compilation/conditional-compilation-nested.output new file mode 100644 index 000000000000..d27b4deb5aae --- /dev/null +++ b/unified/extractor/tests/corpus/swift/conditional-compilation/conditional-compilation-nested.output @@ -0,0 +1,354 @@ +#if DEBUG + let first = 1 + let second = 2 + #if EXTRA + func f() {} + #elseif OTHER + #else + let third = 3 + #endif +#else +#endif + +protocol P { + #if DEBUG + associatedtype T + #endif +} + +func use() { + #if DEBUG + let value = 1 + #else + let value = 2 + #endif + if true { + let nested = 3 + } +} + +--- + +sourceFile + endOfFileToken: endOfFile + statements: + codeBlockItem + item: + ifConfigDecl + clauses: + ifConfigClause + elements: + codeBlockItem + item: + variableDecl + attributes: + modifiers: + bindingSpecifier: let + bindings: + patternBinding + initializer: + initializerClause + equal: = + value: + integerLiteralExpr + literal: integerLiteral "1" + pattern: + identifierPattern + identifier: identifier "first" + codeBlockItem + item: + variableDecl + attributes: + modifiers: + bindingSpecifier: let + bindings: + patternBinding + initializer: + initializerClause + equal: = + value: + integerLiteralExpr + literal: integerLiteral "2" + pattern: + identifierPattern + identifier: identifier "second" + codeBlockItem + item: + ifConfigDecl + clauses: + ifConfigClause + elements: + codeBlockItem + item: + functionDecl + attributes: + body: + codeBlock + leftBrace: { + rightBrace: } + statements: + modifiers: + name: identifier "f" + signature: + functionSignature + parameterClause: + functionParameterClause + leftParen: ( + rightParen: ) + parameters: + funcKeyword: func + condition: + declReferenceExpr + baseName: identifier "EXTRA" + poundKeyword: #if + ifConfigClause + elements: + condition: + declReferenceExpr + baseName: identifier "OTHER" + poundKeyword: #elseif + ifConfigClause + elements: + codeBlockItem + item: + variableDecl + attributes: + modifiers: + bindingSpecifier: let + bindings: + patternBinding + initializer: + initializerClause + equal: = + value: + integerLiteralExpr + literal: integerLiteral "3" + pattern: + identifierPattern + identifier: identifier "third" + poundKeyword: #else + poundEndif: #endif + condition: + declReferenceExpr + baseName: identifier "DEBUG" + poundKeyword: #if + ifConfigClause + elements: + poundKeyword: #else + poundEndif: #endif + codeBlockItem + item: + protocolDecl + attributes: + modifiers: + name: identifier "P" + memberBlock: + memberBlock + leftBrace: { + rightBrace: } + members: + memberBlockItem + decl: + ifConfigDecl + clauses: + ifConfigClause + elements: + memberBlockItem + decl: + associatedTypeDecl + attributes: + modifiers: + name: identifier "T" + associatedtypeKeyword: associatedtype + condition: + declReferenceExpr + baseName: identifier "DEBUG" + poundKeyword: #if + poundEndif: #endif + protocolKeyword: protocol + codeBlockItem + item: + functionDecl + attributes: + body: + codeBlock + leftBrace: { + rightBrace: } + statements: + codeBlockItem + item: + ifConfigDecl + clauses: + ifConfigClause + elements: + codeBlockItem + item: + variableDecl + attributes: + modifiers: + bindingSpecifier: let + bindings: + patternBinding + initializer: + initializerClause + equal: = + value: + integerLiteralExpr + literal: integerLiteral "1" + pattern: + identifierPattern + identifier: identifier "value" + condition: + declReferenceExpr + baseName: identifier "DEBUG" + poundKeyword: #if + ifConfigClause + elements: + codeBlockItem + item: + variableDecl + attributes: + modifiers: + bindingSpecifier: let + bindings: + patternBinding + initializer: + initializerClause + equal: = + value: + integerLiteralExpr + literal: integerLiteral "2" + pattern: + identifierPattern + identifier: identifier "value" + poundKeyword: #else + poundEndif: #endif + codeBlockItem + item: + expressionStmt + expression: + ifExpr + body: + codeBlock + leftBrace: { + rightBrace: } + statements: + codeBlockItem + item: + variableDecl + attributes: + modifiers: + bindingSpecifier: let + bindings: + patternBinding + initializer: + initializerClause + equal: = + value: + integerLiteralExpr + literal: integerLiteral "3" + pattern: + identifierPattern + identifier: identifier "nested" + conditions: + conditionElement + condition: + booleanLiteralExpr + literal: true + ifKeyword: if + modifiers: + name: identifier "use" + signature: + functionSignature + parameterClause: + functionParameterClause + leftParen: ( + rightParen: ) + parameters: + funcKeyword: func + +--- + +top_level source="⟨body⟩" + body: + block source="⟨stmt⟩\n\n⟨stmt⟩\n\n⟨stmt⟩" + stmt: + if_expr source="#if ⟨condition⟩\n ⟨then⟩\n⟨else⟩\n#endif" + modifier: modifier "conditional_compilation" source="" + condition: identifier "DEBUG" source="DEBUG" + then: + block source="⟨stmt⟩\n ⟨stmt⟩\n ⟨stmt⟩" + stmt: + variable_declaration source="⟨modifier⟩ ⟨pattern⟩ = ⟨value⟩" + modifier: modifier "let" source="let" + pattern: identifier "first" source="first" + value: int_literal "1" source="1" + variable_declaration source="⟨modifier⟩ ⟨pattern⟩ = ⟨value⟩" + modifier: modifier "let" source="let" + pattern: identifier "second" source="second" + value: int_literal "2" source="2" + if_expr source="#if ⟨condition⟩\n ⟨then⟩\n ⟨else⟩\n #endif" + modifier: modifier "conditional_compilation" source="" + condition: identifier "EXTRA" source="EXTRA" + then: + block source="⟨stmt⟩" + stmt: + function_declaration source="func ⟨name_node⟩() ⟨body⟩" + name_node: identifier "f" source="f" + body: block "{}" source="{}" + else: + if_expr source="#elseif ⟨condition⟩\n ⟨else⟩" + modifier: modifier "conditional_compilation" source="" + condition: identifier "OTHER" source="OTHER" + then: block source="" + else: + block source="#else\n ⟨stmt⟩" + stmt: + variable_declaration source="⟨modifier⟩ ⟨pattern⟩ = ⟨value⟩" + modifier: modifier "let" source="let" + pattern: identifier "third" source="third" + value: int_literal "3" source="3" + else: block "#else" source="#else" + class_like_declaration source="⟨modifier⟩ ⟨name_node⟩ {\n ⟨member⟩\n}" + modifier: modifier "protocol" source="protocol" + name_node: identifier "P" source="P" + member: + if_expr source="#if ⟨condition⟩\n ⟨then⟩\n #endif" + modifier: modifier "conditional_compilation" source="" + condition: identifier "DEBUG" source="DEBUG" + then: + block source="⟨stmt⟩" + stmt: + associated_type_declaration source="associatedtype ⟨name_node⟩" + name_node: identifier "T" source="T" + function_declaration source="func ⟨name_node⟩() ⟨body⟩" + name_node: identifier "use" source="use" + body: + block source="{\n ⟨stmt⟩\n ⟨stmt⟩\n}" + stmt: + if_expr source="#if ⟨condition⟩\n ⟨then⟩\n ⟨else⟩\n #endif" + modifier: modifier "conditional_compilation" source="" + condition: identifier "DEBUG" source="DEBUG" + then: + block source="⟨stmt⟩" + stmt: + variable_declaration source="⟨modifier⟩ ⟨pattern⟩ = ⟨value⟩" + modifier: modifier "let" source="let" + pattern: identifier "value" source="value" + value: int_literal "1" source="1" + else: + block source="#else\n ⟨stmt⟩" + stmt: + variable_declaration source="⟨modifier⟩ ⟨pattern⟩ = ⟨value⟩" + modifier: modifier "let" source="let" + pattern: identifier "value" source="value" + value: int_literal "2" source="2" + if_expr source="if ⟨condition⟩ ⟨then⟩" + condition: boolean_literal "true" source="true" + then: + block source="{\n ⟨stmt⟩\n }" + stmt: + variable_declaration source="⟨modifier⟩ ⟨pattern⟩ = ⟨value⟩" + modifier: modifier "let" source="let" + pattern: identifier "nested" source="nested" + value: int_literal "3" source="3" diff --git a/unified/extractor/tests/corpus/swift/conditional-compilation/conditional-compilation-nested.swift b/unified/extractor/tests/corpus/swift/conditional-compilation/conditional-compilation-nested.swift new file mode 100644 index 000000000000..b65c5b16506e --- /dev/null +++ b/unified/extractor/tests/corpus/swift/conditional-compilation/conditional-compilation-nested.swift @@ -0,0 +1,28 @@ +#if DEBUG + let first = 1 + let second = 2 + #if EXTRA + func f() {} + #elseif OTHER + #else + let third = 3 + #endif +#else +#endif + +protocol P { + #if DEBUG + associatedtype T + #endif +} + +func use() { + #if DEBUG + let value = 1 + #else + let value = 2 + #endif + if true { + let nested = 3 + } +} diff --git a/unified/extractor/tests/corpus/swift/conditional-compilation/conditional-compilation-postfix-expression.output b/unified/extractor/tests/corpus/swift/conditional-compilation/conditional-compilation-postfix-expression.output new file mode 100644 index 000000000000..ba310a0faffe --- /dev/null +++ b/unified/extractor/tests/corpus/swift/conditional-compilation/conditional-compilation-postfix-expression.output @@ -0,0 +1,89 @@ +let result = value + #if DEBUG + .foo(1) + #else + .bar("hi") + #endif + +--- + +sourceFile + endOfFileToken: endOfFile + statements: + codeBlockItem + item: + variableDecl + attributes: + modifiers: + bindingSpecifier: let + bindings: + patternBinding + initializer: + initializerClause + equal: = + value: + postfixIfConfigExpr + base: + declReferenceExpr + baseName: identifier "value" + config: + ifConfigDecl + clauses: + ifConfigClause + elements: + functionCallExpr + leftParen: ( + rightParen: ) + arguments: + labeledExpr + expression: + integerLiteralExpr + literal: integerLiteral "1" + additionalTrailingClosures: + calledExpression: + memberAccessExpr + period: . + declName: + declReferenceExpr + baseName: identifier "foo" + condition: + declReferenceExpr + baseName: identifier "DEBUG" + poundKeyword: #if + ifConfigClause + elements: + functionCallExpr + leftParen: ( + rightParen: ) + arguments: + labeledExpr + expression: + stringLiteralExpr + closingQuote: " + openingQuote: " + segments: + stringSegment + content: stringSegment "hi" + additionalTrailingClosures: + calledExpression: + memberAccessExpr + period: . + declName: + declReferenceExpr + baseName: identifier "bar" + poundKeyword: #else + poundEndif: #endif + pattern: + identifierPattern + identifier: identifier "result" + +--- + +top_level source="⟨body⟩" + body: + block source="⟨stmt⟩" + stmt: + variable_declaration source="⟨modifier⟩ ⟨pattern⟩ = ⟨value⟩" + modifier: modifier "let" source="let" + pattern: identifier "result" source="result" + value: unsupported_node "value\n #if DEBUG\n .foo(1)\n #else\n .bar(\"hi\")\n #endif" source="value\n #if DEBUG\n .foo(1)\n #else\n .bar(\"hi\")\n #endif" diff --git a/unified/extractor/tests/corpus/swift/conditional-compilation/conditional-compilation-postfix-expression.swift b/unified/extractor/tests/corpus/swift/conditional-compilation/conditional-compilation-postfix-expression.swift new file mode 100644 index 000000000000..a947fd60ded9 --- /dev/null +++ b/unified/extractor/tests/corpus/swift/conditional-compilation/conditional-compilation-postfix-expression.swift @@ -0,0 +1,6 @@ +let result = value + #if DEBUG + .foo(1) + #else + .bar("hi") + #endif diff --git a/unified/extractor/tests/corpus/swift/conditional-compilation/conditional-compilation-with-elseif-in-class-body.output b/unified/extractor/tests/corpus/swift/conditional-compilation/conditional-compilation-with-elseif-in-class-body.output new file mode 100644 index 000000000000..1dc544d3b697 --- /dev/null +++ b/unified/extractor/tests/corpus/swift/conditional-compilation/conditional-compilation-with-elseif-in-class-body.output @@ -0,0 +1,163 @@ +class C { + #if DEBUG + init(x: Int) {} + #elseif RELEASE + init(x: String) {} + #else + init() {} + #endif +} + +--- + +sourceFile + endOfFileToken: endOfFile + statements: + codeBlockItem + item: + classDecl + attributes: + modifiers: + name: identifier "C" + memberBlock: + memberBlock + leftBrace: { + rightBrace: } + members: + memberBlockItem + decl: + ifConfigDecl + clauses: + ifConfigClause + elements: + memberBlockItem + decl: + initializerDecl + attributes: + body: + codeBlock + leftBrace: { + rightBrace: } + statements: + modifiers: + signature: + functionSignature + parameterClause: + functionParameterClause + leftParen: ( + rightParen: ) + parameters: + functionParameter + colon: : + attributes: + modifiers: + type: + identifierType + name: identifier "Int" + firstName: identifier "x" + initKeyword: init + condition: + declReferenceExpr + baseName: identifier "DEBUG" + poundKeyword: #if + ifConfigClause + elements: + memberBlockItem + decl: + initializerDecl + attributes: + body: + codeBlock + leftBrace: { + rightBrace: } + statements: + modifiers: + signature: + functionSignature + parameterClause: + functionParameterClause + leftParen: ( + rightParen: ) + parameters: + functionParameter + colon: : + attributes: + modifiers: + type: + identifierType + name: identifier "String" + firstName: identifier "x" + initKeyword: init + condition: + declReferenceExpr + baseName: identifier "RELEASE" + poundKeyword: #elseif + ifConfigClause + elements: + memberBlockItem + decl: + initializerDecl + attributes: + body: + codeBlock + leftBrace: { + rightBrace: } + statements: + modifiers: + signature: + functionSignature + parameterClause: + functionParameterClause + leftParen: ( + rightParen: ) + parameters: + initKeyword: init + poundKeyword: #else + poundEndif: #endif + classKeyword: class + +--- + +top_level source="⟨body⟩" + body: + block source="⟨stmt⟩" + stmt: + class_like_declaration source="⟨modifier⟩ ⟨name_node⟩ {\n ⟨member⟩\n}" + modifier: modifier "class" source="class" + name_node: identifier "C" source="C" + member: + if_expr source="#if ⟨condition⟩\n ⟨then⟩\n ⟨else⟩\n #endif" + modifier: modifier "conditional_compilation" source="" + condition: identifier "DEBUG" source="DEBUG" + then: + block source="⟨stmt⟩" + stmt: + constructor_declaration source="⟨name_node⟩(⟨parameter⟩) ⟨body⟩" + name_node: identifier "init" source="init" + parameter: + parameter source="⟨external_name_node⟩⟨pattern⟩: ⟨type⟩" + external_name_node: identifier "x" source="x" + type: identifier "Int" source="Int" + pattern: identifier "x" source="x" + body: block "{}" source="{}" + else: + if_expr source="#elseif ⟨condition⟩\n ⟨then⟩\n ⟨else⟩" + modifier: modifier "conditional_compilation" source="" + condition: identifier "RELEASE" source="RELEASE" + then: + block source="⟨stmt⟩" + stmt: + constructor_declaration source="⟨name_node⟩(⟨parameter⟩) ⟨body⟩" + name_node: identifier "init" source="init" + parameter: + parameter source="⟨external_name_node⟩⟨pattern⟩: ⟨type⟩" + external_name_node: identifier "x" source="x" + type: identifier "String" source="String" + pattern: identifier "x" source="x" + body: block "{}" source="{}" + else: + block source="#else\n ⟨stmt⟩" + stmt: + constructor_declaration source="⟨name_node⟩() ⟨body⟩" + name_node: identifier "init" source="init" + body: block "{}" source="{}" diff --git a/unified/extractor/tests/corpus/swift/conditional-compilation/conditional-compilation-with-elseif-in-class-body.swift b/unified/extractor/tests/corpus/swift/conditional-compilation/conditional-compilation-with-elseif-in-class-body.swift new file mode 100644 index 000000000000..8422a35d69b6 --- /dev/null +++ b/unified/extractor/tests/corpus/swift/conditional-compilation/conditional-compilation-with-elseif-in-class-body.swift @@ -0,0 +1,9 @@ +class C { + #if DEBUG + init(x: Int) {} + #elseif RELEASE + init(x: String) {} + #else + init() {} + #endif +} diff --git a/unified/extractor/tests/corpus/swift/conditional-compilation/conditional-compilation-with-elseif.output b/unified/extractor/tests/corpus/swift/conditional-compilation/conditional-compilation-with-elseif.output new file mode 100644 index 000000000000..145be9553002 --- /dev/null +++ b/unified/extractor/tests/corpus/swift/conditional-compilation/conditional-compilation-with-elseif.output @@ -0,0 +1,119 @@ +#if DEBUG +let x = 1 +#elseif RELEASE +let x = 2 +#else +let x = 3 +#endif + +--- + +sourceFile + endOfFileToken: endOfFile + statements: + codeBlockItem + item: + ifConfigDecl + clauses: + ifConfigClause + elements: + codeBlockItem + item: + variableDecl + attributes: + modifiers: + bindingSpecifier: let + bindings: + patternBinding + initializer: + initializerClause + equal: = + value: + integerLiteralExpr + literal: integerLiteral "1" + pattern: + identifierPattern + identifier: identifier "x" + condition: + declReferenceExpr + baseName: identifier "DEBUG" + poundKeyword: #if + ifConfigClause + elements: + codeBlockItem + item: + variableDecl + attributes: + modifiers: + bindingSpecifier: let + bindings: + patternBinding + initializer: + initializerClause + equal: = + value: + integerLiteralExpr + literal: integerLiteral "2" + pattern: + identifierPattern + identifier: identifier "x" + condition: + declReferenceExpr + baseName: identifier "RELEASE" + poundKeyword: #elseif + ifConfigClause + elements: + codeBlockItem + item: + variableDecl + attributes: + modifiers: + bindingSpecifier: let + bindings: + patternBinding + initializer: + initializerClause + equal: = + value: + integerLiteralExpr + literal: integerLiteral "3" + pattern: + identifierPattern + identifier: identifier "x" + poundKeyword: #else + poundEndif: #endif + +--- + +top_level source="⟨body⟩" + body: + block source="⟨stmt⟩" + stmt: + if_expr source="#if ⟨condition⟩\n⟨then⟩\n⟨else⟩\n#endif" + modifier: modifier "conditional_compilation" source="" + condition: identifier "DEBUG" source="DEBUG" + then: + block source="⟨stmt⟩" + stmt: + variable_declaration source="⟨modifier⟩ ⟨pattern⟩ = ⟨value⟩" + modifier: modifier "let" source="let" + pattern: identifier "x" source="x" + value: int_literal "1" source="1" + else: + if_expr source="#elseif ⟨condition⟩\n⟨then⟩\n⟨else⟩" + modifier: modifier "conditional_compilation" source="" + condition: identifier "RELEASE" source="RELEASE" + then: + block source="⟨stmt⟩" + stmt: + variable_declaration source="⟨modifier⟩ ⟨pattern⟩ = ⟨value⟩" + modifier: modifier "let" source="let" + pattern: identifier "x" source="x" + value: int_literal "2" source="2" + else: + block source="#else\n⟨stmt⟩" + stmt: + variable_declaration source="⟨modifier⟩ ⟨pattern⟩ = ⟨value⟩" + modifier: modifier "let" source="let" + pattern: identifier "x" source="x" + value: int_literal "3" source="3" diff --git a/unified/extractor/tests/corpus/swift/conditional-compilation/conditional-compilation-with-elseif.swift b/unified/extractor/tests/corpus/swift/conditional-compilation/conditional-compilation-with-elseif.swift new file mode 100644 index 000000000000..f96cda7a9a6c --- /dev/null +++ b/unified/extractor/tests/corpus/swift/conditional-compilation/conditional-compilation-with-elseif.swift @@ -0,0 +1,7 @@ +#if DEBUG +let x = 1 +#elseif RELEASE +let x = 2 +#else +let x = 3 +#endif diff --git a/unified/extractor/tests/corpus/swift/conditional-compilation/conditional-compilation-with-operators.output b/unified/extractor/tests/corpus/swift/conditional-compilation/conditional-compilation-with-operators.output new file mode 100644 index 000000000000..d7268f066d72 --- /dev/null +++ b/unified/extractor/tests/corpus/swift/conditional-compilation/conditional-compilation-with-operators.output @@ -0,0 +1,111 @@ +#if (DEBUG && !RELEASE) || swift(<6.0) + let x = 1 +#endif + +--- + +sourceFile + endOfFileToken: endOfFile + statements: + codeBlockItem + item: + ifConfigDecl + clauses: + ifConfigClause + elements: + codeBlockItem + item: + variableDecl + attributes: + modifiers: + bindingSpecifier: let + bindings: + patternBinding + initializer: + initializerClause + equal: = + value: + integerLiteralExpr + literal: integerLiteral "1" + pattern: + identifierPattern + identifier: identifier "x" + condition: + infixOperatorExpr + operator: + binaryOperatorExpr + operator: binaryOperator "||" + leftOperand: + tupleExpr + leftParen: ( + rightParen: ) + elements: + labeledExpr + expression: + infixOperatorExpr + operator: + binaryOperatorExpr + operator: binaryOperator "&&" + leftOperand: + declReferenceExpr + baseName: identifier "DEBUG" + rightOperand: + prefixOperatorExpr + expression: + declReferenceExpr + baseName: identifier "RELEASE" + operator: prefixOperator "!" + rightOperand: + functionCallExpr + leftParen: ( + rightParen: ) + arguments: + labeledExpr + expression: + prefixOperatorExpr + expression: + floatLiteralExpr + literal: floatLiteral "6.0" + operator: prefixOperator "<" + additionalTrailingClosures: + calledExpression: + declReferenceExpr + baseName: identifier "swift" + poundKeyword: #if + poundEndif: #endif + +--- + +top_level source="⟨body⟩" + body: + block source="⟨stmt⟩" + stmt: + if_expr source="#if ⟨condition⟩\n ⟨then⟩\n#endif" + modifier: modifier "conditional_compilation" source="" + condition: + binary_expr source="(⟨left⟩) ⟨operator⟩ ⟨right⟩" + left: + binary_expr source="⟨left⟩ ⟨operator⟩ ⟨right⟩" + left: identifier "DEBUG" source="DEBUG" + operator: infix_operator "&&" source="&&" + right: + unary_expr source="⟨operator⟩⟨operand⟩" + operand: identifier "RELEASE" source="RELEASE" + operator: prefix_operator "!" source="!" + operator: infix_operator "||" source="||" + right: + call_expr source="⟨callee⟩(⟨argument⟩)" + callee: identifier "swift" source="swift" + argument: + argument source="⟨value⟩" + value: + unary_expr source="⟨operator⟩⟨operand⟩" + operand: float_literal "6.0" source="6.0" + operator: prefix_operator "<" source="<" + then: + block source="⟨stmt⟩" + stmt: + variable_declaration source="⟨modifier⟩ ⟨pattern⟩ = ⟨value⟩" + modifier: modifier "let" source="let" + pattern: identifier "x" source="x" + value: int_literal "1" source="1" diff --git a/unified/extractor/tests/corpus/swift/conditional-compilation/conditional-compilation-with-operators.swift b/unified/extractor/tests/corpus/swift/conditional-compilation/conditional-compilation-with-operators.swift new file mode 100644 index 000000000000..9a8eb0b5eeb6 --- /dev/null +++ b/unified/extractor/tests/corpus/swift/conditional-compilation/conditional-compilation-with-operators.swift @@ -0,0 +1,3 @@ +#if (DEBUG && !RELEASE) || swift(<6.0) + let x = 1 +#endif diff --git a/unified/extractor/tests/corpus/swift/types/conditional-compilation-in-class-body.swift b/unified/extractor/tests/corpus/swift/types/conditional-compilation-in-class-body.swift deleted file mode 100644 index fe50583118c9..000000000000 --- a/unified/extractor/tests/corpus/swift/types/conditional-compilation-in-class-body.swift +++ /dev/null @@ -1,10 +0,0 @@ -// Conditional compilation is not yet supported: swift-syntax reports a -// structured `ifConfigDecl` (whose branches hold ordinary member items), but -// the mapping has no rule for it, so the whole block becomes one -// `unsupported_node` and its members are not extracted. -class C { -#if DEBUG - init(x: Int) {} - deinit {} -#endif -} diff --git a/unified/ql/lib/codeql/unified/internal/Ast.qll b/unified/ql/lib/codeql/unified/internal/Ast.qll index 5f410460536e..6e9631850924 100644 --- a/unified/ql/lib/codeql/unified/internal/Ast.qll +++ b/unified/ql/lib/codeql/unified/internal/Ast.qll @@ -186,7 +186,7 @@ module Unified { } /** A class representing `associated_type_declaration` nodes. */ - class AssociatedTypeDeclaration extends @unified_associated_type_declaration, F::Member { + class AssociatedTypeDeclaration extends @unified_associated_type_declaration, F::Member, F::Stmt { /** Gets the name of the primary QL class for this element. */ final override string getAPrimaryQlClass() { result = "AssociatedTypeDeclaration" } @@ -848,7 +848,7 @@ module Unified { } /** A class representing `if_expr` nodes. */ - class IfExpr extends @unified_if_expr, F::Expr { + class IfExpr extends @unified_if_expr, F::Expr, F::Member { /** Gets the name of the primary QL class for this element. */ final override string getAPrimaryQlClass() { result = "IfExpr" } @@ -858,6 +858,12 @@ module Unified { /** Gets the node corresponding to the field `else`. */ final F::Expr getElse() { unified_if_expr_else(this, result) } + /** Gets the node corresponding to the field `modifier`. */ + final F::Modifier getModifier(int i) { unified_if_expr_modifier(this, i, result) } + + /** Gets the node corresponding to the field `modifier`. */ + final F::Modifier getAModifier() { result = this.getModifier(_) } + /** Gets the node corresponding to the field `then`. */ final F::Expr getThen() { unified_if_expr_then(this, result) } @@ -865,6 +871,7 @@ module Unified { final override F::AstNode getAFieldOrChild() { unified_if_expr_def(this, result) or unified_if_expr_else(this, result) or + unified_if_expr_modifier(this, _, result) or unified_if_expr_then(this, result) } } @@ -915,7 +922,9 @@ module Unified { } /** A class representing `initializer_declaration` nodes. */ - class InitializerDeclaration extends @unified_initializer_declaration, F::Callable, F::Member { + class InitializerDeclaration extends @unified_initializer_declaration, F::Callable, F::Member, + F::Stmt + { /** Gets the name of the primary QL class for this element. */ final override string getAPrimaryQlClass() { result = "InitializerDeclaration" } @@ -1772,6 +1781,8 @@ module Unified { or result = node.(IfExpr).getElse() and i = -1 and name = "getElse" or + result = node.(IfExpr).getModifier(i) and name = "getModifier" + or result = node.(IfExpr).getThen() and i = -1 and name = "getThen" or result = node.(ImportDeclaration).getImportedExpr() and i = -1 and name = "getImportedExpr" diff --git a/unified/ql/lib/codeql/unified/internal/AstExtra.qll b/unified/ql/lib/codeql/unified/internal/AstExtra.qll index f2182698f6d0..ca916e6e56ad 100644 --- a/unified/ql/lib/codeql/unified/internal/AstExtra.qll +++ b/unified/ql/lib/codeql/unified/internal/AstExtra.qll @@ -94,7 +94,7 @@ module Public { /** A `Stmt` at the top-level. */ final class TopLevelStmt extends Stmt { - TopLevelStmt() { this = any(TopLevel t).getBody().getAStmt() } + TopLevelStmt() { this = any(TopLevel t).getBody().getAStmtIncludingConditionalCompilation() } } /** An identifier appearing in the context of a break/continue label, argument/parameter name, or name of a member lookup. */ @@ -139,7 +139,9 @@ module Public { EnumCaseClass() { this.hasModifier("enum_case") } /** Gets the constructor of this algebraic data type. */ - EnumCaseConstructor getConstructor() { result = this.getAMember() } + EnumCaseConstructor getConstructor() { + result = this.getAMemberIncludingConditionalCompilation() + } } /** @@ -164,9 +166,11 @@ module Public { * ``` */ final class EnumCaseConstructor extends ConstructorDeclaration { - EnumCaseConstructor() { this = any(EnumCaseClass cls).getAMember() } + EnumCaseConstructor() { + this = any(EnumCaseClass cls).getAMemberIncludingConditionalCompilation() + } /** Gets the class containing this constructor. */ - EnumCaseClass getEnumCaseClass() { result.getAMember() = this } + EnumCaseClass getEnumCaseClass() { result.getAMemberIncludingConditionalCompilation() = this } } } diff --git a/unified/ql/lib/codeql/unified/internal/AstPluginSwift.qll b/unified/ql/lib/codeql/unified/internal/AstPluginSwift.qll index ff2d03fb9b01..ab8d6053c574 100644 --- a/unified/ql/lib/codeql/unified/internal/AstPluginSwift.qll +++ b/unified/ql/lib/codeql/unified/internal/AstPluginSwift.qll @@ -4,13 +4,26 @@ module; private import unified private import AstPlugin +private Stmt assumeThenCompilationBranches(Stmt stmt) { + // For now we simply assume that the `then` branch is always taken for + // conditional compilation blocks. This means that a parameter index results + // in a unique parameter type. + if stmt.(IfExpr).isConditionalCompilation() + then result = assumeThenCompilationBranches(stmt.(IfExpr).getThen().(Block).getAStmt()) + else result = stmt +} + +/** + * Holds if a constructor declaration `cd` of a struct has a parameter for the + * field `decl` with name `name` and default value `hasDefault`. + */ private predicate structHasParameterForField( - ConstructorDeclaration cd, VariableDeclaration decl, int i, string name, boolean hasDefault + ConstructorDeclaration cd, VariableDeclaration decl, string name, boolean hasDefault ) { exists(ClassLikeDeclaration struct | struct.hasModifier("struct") and - cd = struct.getAMember() and - decl = struct.getMember(i) and + cd = struct.getAMemberIncludingConditionalCompilation() and + decl = assumeThenCompilationBranches(struct.getAMember()) and not decl.hasModifier("static") and name = decl.getPattern().(Identifier).getValue() | @@ -57,15 +70,25 @@ private class AstPluginSwift extends AstPlugin { override predicate defaultConstructorParameter( ConstructorDeclaration cd, int i, string name, boolean hasDefault ) { - name = rank[i](int j, string s | structHasParameterForField(cd, _, j, s, _) | s order by j) and - structHasParameterForField(cd, _, _, name, hasDefault) + name = + rank[i](VariableDeclaration decl, string s | + structHasParameterForField(cd, decl, s, _) + | + // Due to conditional compilation blocks, we cannot simply order + // parameters by their parent index. Instead we order by their location. + s + order by + decl.getLocation().getStartLine(), decl.getLocation().getStartColumn(), + decl.getParentIndex() + ) and + structHasParameterForField(cd, _, name, hasDefault) } bindingset[cd] override predicate defaultConstructorParameterType(ConstructorDeclaration cd, int i, AstNode type) { exists(VariableDeclaration decl, string name | this.defaultConstructorParameter(cd, i, name, _) and - structHasParameterForField(cd, decl, _, name, _) and + structHasParameterForField(cd, decl, name, _) and type = decl.getType() ) } diff --git a/unified/ql/lib/codeql/unified/internal/FacadeAst.qll b/unified/ql/lib/codeql/unified/internal/FacadeAst.qll index 8d40d3f6b099..164ef0764d27 100644 --- a/unified/ql/lib/codeql/unified/internal/FacadeAst.qll +++ b/unified/ql/lib/codeql/unified/internal/FacadeAst.qll @@ -99,6 +99,11 @@ module Unified { /** A block statement. */ class Block extends G::Block { + /** Gets a statement, looking through conditional-compilation branches. */ + Stmt getAStmtIncludingConditionalCompilation() { + result = compilationExpandedStmt(this.getAStmt()) + } + /** Gets the last statement in this block. */ Stmt getLastStmt() { exists(int i | result = this.getStmt(i) and not exists(this.getStmt(i + 1))) @@ -223,10 +228,12 @@ module Unified { } private ClassLikeDeclaration getParentClass(Member m) { - result.getAMemberIncludingExtensions() = m + result.getAMemberIncludingExtensionsAndConditionalCompilation() = m } - private ClassLikeDeclaration getEnclosingClass(Member m) { result.getAMember() = m } + private ClassLikeDeclaration getEnclosingClass(Member m) { + result.getAMemberIncludingConditionalCompilation() = m + } /** * Similar to `Member.getQualifiedName`, but avoids a dependency on name binding, making @@ -236,6 +243,11 @@ module Unified { QualifiedName::getQualifiedName/1; class ClassLikeDeclaration extends G::ClassLikeDeclaration { + /** Gets a member, looking through conditional-compilation branches but not extensions. */ + Member getAMemberIncludingConditionalCompilation() { + result = compilationExpandedStmt(this.getAMember()) + } + /** Gets the name of this declaration. */ string getName() { result = this.getNameNode().getValue() } @@ -259,12 +271,12 @@ module Unified { } /** - * Gets a member of this class, including members from extensions. + * Gets a member of this class or its extensions, looking through conditional-compilation branches. */ - Member getAMemberIncludingExtensions() { + Member getAMemberIncludingExtensionsAndConditionalCompilation() { exists(ClassLikeDeclaration cls | this = cls.resolve() and - result = cls.getAMember() + result = cls.getAMemberIncludingConditionalCompilation() ) } @@ -470,7 +482,24 @@ module Unified { /** An `if` statement or conditional expression. */ class IfExpr extends G::IfExpr { + /** Holds if this expression represents conditional compilation. */ + predicate isConditionalCompilation() { this.hasModifier("conditional_compilation") } + /** Gets then `then` or `else` branch. */ Expr getABranch() { result = [this.getThen(), this.getElse()] } } + + private Stmt compilationExpandedStmt(Stmt stmt) { + not stmt.(IfExpr).isConditionalCompilation() and + result = stmt + or + stmt = + any(IfExpr conditional | + conditional.isConditionalCompilation() and + result = + compilationExpandedStmt([ + conditional.getABranch().(Block).getAStmt(), conditional.getABranch().(IfExpr) + ]) + ) + } } diff --git a/unified/ql/lib/codeql/unified/internal/LocalNameBinding.qll b/unified/ql/lib/codeql/unified/internal/LocalNameBinding.qll index 7f88d1c496bf..191875762610 100644 --- a/unified/ql/lib/codeql/unified/internal/LocalNameBinding.qll +++ b/unified/ql/lib/codeql/unified/internal/LocalNameBinding.qll @@ -87,7 +87,7 @@ private module LocalNameBindingInput implements LocalNameBindingInputSig a -> Argument -V kpGet_mayB_x -^ a(...)' | | 477 | cfg.swift:477:1:504:1 | func testIfConfig | 'func testIfConfig -V testIfConfig' | | 477 | cfg.swift:477:21:504:1 | Block | 'Block' | -| 478 | cfg.swift:478:1:484:6 | | '' | +| 478 | cfg.swift:478:1:484:6 | IfExpr | 'IfExpr -V FOO' | +| 479 | cfg.swift:479:3:480:3 | Block | 'Block -V 1' | +| 480 | cfg.swift:480:3:480:3 | 2 | '2' | +| 481 | cfg.swift:481:1:483:3 | Block | 'Block' | +| 482 | cfg.swift:482:3:482:3 | 3 | '3' | +| 483 | cfg.swift:483:3:483:3 | 4 | '4' | | 486 | cfg.swift:486:3:486:3 | 5 | '5' | -| 488 | cfg.swift:488:1:491:6 | | '' | +| 488 | cfg.swift:488:1:491:6 | IfExpr | 'IfExpr -V BAR' | +| 489 | cfg.swift:489:3:490:3 | Block | 'Block -V 6' | +| 490 | cfg.swift:490:3:490:3 | 7 | '7' | | 493 | cfg.swift:493:3:493:3 | 8 | '8' | -| 495 | cfg.swift:495:1:501:6 | | '' | +| 495 | cfg.swift:495:1:501:6 | IfExpr | 'IfExpr -V FOO' | +| 496 | cfg.swift:496:3:497:4 | Block | 'Block -V 9' | +| 497 | cfg.swift:497:3:497:4 | 10 | '10' | +| 498 | cfg.swift:498:1:500:4 | IfExpr | 'IfExpr -V true' | +| 499 | cfg.swift:499:3:500:4 | Block | 'Block -V 11' | +| 500 | cfg.swift:500:3:500:4 | 12 | '12' | | 503 | cfg.swift:503:3:503:4 | 13 | '13' | | 506 | cfg.swift:506:1:531:1 | func testAvailable | 'func testAvailable -V testAvailable' | | 506 | cfg.swift:506:29:531:1 | Block | 'Block' | diff --git a/unified/ql/test/library-tests/controlflow/cfg.expected b/unified/ql/test/library-tests/controlflow/cfg.expected index 46994ef52c9b..79aeb2956ca0 100644 --- a/unified/ql/test/library-tests/controlflow/cfg.expected +++ b/unified/ql/test/library-tests/controlflow/cfg.expected @@ -114,6 +114,17 @@ bbStep | cfg.swift:369:9:369:14 | ... < ... | '... < ... : true -> Block(+0)' | | cfg.swift:370:5:370:10 | ... += ... | '... += ... : successor -> x(-1)' | | cfg.swift:407:9:407:24 | mightThrow(...) | 'mightThrow(...) : successor -> try ...(+0)' | +| cfg.swift:478:5:478:7 | FOO | 'FOO : false -> Block(+3)' | +| cfg.swift:478:5:478:7 | FOO | 'FOO : true -> Block(+1)' | +| cfg.swift:480:3:480:3 | 2 | '2 : successor -> 5(+6)' | +| cfg.swift:483:3:483:3 | 4 | '4 : successor -> 5(+3)' | +| cfg.swift:488:5:488:7 | BAR | 'BAR : false -> 8(+5)' | +| cfg.swift:488:5:488:7 | BAR | 'BAR : true -> Block(+1)' | +| cfg.swift:490:3:490:3 | 7 | '7 : successor -> 8(+3)' | +| cfg.swift:495:5:495:7 | FOO | 'FOO : false -> IfExpr(+3)' | +| cfg.swift:495:5:495:7 | FOO | 'FOO : true -> Block(+1)' | +| cfg.swift:497:3:497:4 | 10 | '10 : successor -> 13(+6)' | +| cfg.swift:500:3:500:4 | 12 | '12 : successor -> 13(+3)' | | cfg.swift:509:6:509:28 | | ' : false -> IfExpr(+4)' | | cfg.swift:509:6:509:28 | | ' : true -> Block(+0)' | | cfg.swift:510:5:510:10 | ... += ... | '... += ... : successor -> IfExpr(+3)' | diff --git a/unified/ql/test/library-tests/controlflow/cfg.swift b/unified/ql/test/library-tests/controlflow/cfg.swift index f00bea336a97..249832270920 100644 --- a/unified/ql/test/library-tests/controlflow/cfg.swift +++ b/unified/ql/test/library-tests/controlflow/cfg.swift @@ -475,29 +475,29 @@ func test(a : A) { } func testIfConfig() { -#if FOO +#if FOO // $ bbStep='FOO : false -> Block(+3)' bbStep='FOO : true -> Block(+1)' 1 - 2 + 2 // $ bbStep='2 : successor -> 5(+6)' #else 3 - 4 + 4 // $ bbStep='4 : successor -> 5(+3)' #endif 5 -#if BAR +#if BAR // $ bbStep='BAR : false -> 8(+5)' bbStep='BAR : true -> Block(+1)' 6 - 7 + 7 // $ bbStep='7 : successor -> 8(+3)' #endif 8 -#if FOO +#if FOO // $ bbStep='FOO : false -> IfExpr(+3)' bbStep='FOO : true -> Block(+1)' 9 - 10 + 10 // $ bbStep='10 : successor -> 13(+6)' #elseif true 11 - 12 + 12 // $ bbStep='12 : successor -> 13(+3)' #endif 13 diff --git a/unified/ql/test/library-tests/local-name-binding/conditional-compilation.swift b/unified/ql/test/library-tests/local-name-binding/conditional-compilation.swift new file mode 100644 index 000000000000..57ebf09d7aef --- /dev/null +++ b/unified/ql/test/library-tests/local-name-binding/conditional-compilation.swift @@ -0,0 +1,108 @@ +func conditionalIfElse() { + // The same variable is declared in _two_ branches. + #if DEBUG + let inCond = 1 // name=debugValue + #else + let inCond = 2 // name=releaseValue + print("release") + #endif + print(inCond) // $ access=debugValue access=releaseValue +} + +func conditionalIfElseifElse() { + // The same variable is declared in _three_ branches. + #if DEBUG + let alternative = 1 // name=debugValue + #elseif RELEASE + let alternative = 2 // name=releaseValue + print(alternative) // $ access=releaseValue + #else + let alternative = 3 // name=fallbackValue + print(alternative) // $ access=fallbackValue + #endif + print(alternative) // $ access=debugValue access=releaseValue access=fallbackValue +} + +func conditionalNoElse(value: Int) { // name=outerValue + // The variable is declared only in one skipable branch. + #if DEBUG + let value = value + 1 // $ access=outerValue // name=conditionalValue + print(value) // $ access=conditionalValue + #endif + print(value) // $ access=outerValue access=conditionalValue +} + +func conditionalIfNesting(nesting: Int) { // name=nestingFirst + // Nested conditional compilation. + #if DEBUG + #if EXTRA + print(nesting) // $ access=nestingFirst + let nesting = nesting + 1 // $ access=nestingFirst // name=nestingExtra + #else + let nesting = 2 // name=nestingNotExtra + #endif + print(nesting) // $ access=nestingExtra access=nestingNotExtra + #else + let nesting = 3 // name=outerNotDebug + #endif + print(nesting) // $ access=nestingExtra access=nestingNotExtra access=outerNotDebug +} + +func conditionalRealScopes() { + // Conditional compilation, but variables are declared in a real scope. + let value = 0 // name=realOuterValue + #if DEBUG + if true { + let value = 1 // name=runtimeValue + print(value) // $ access=runtimeValue + } + #endif + print(value) // $ access=realOuterValue +} + +func conditionalGuard(opt: Int?) { // name=guardOpt + #if DEBUG + guard let value = opt else { return } // $ access=guardOpt // name=guardValue + print(value) // $ access=guardValue + #else + guard let value = opt else { return } // $ access=guardOpt // name=fallbackGuardValue + #endif + print(value) // $ access=guardValue access=fallbackGuardValue +} + +func conditionalSequentialBindings(value: Int) { // name=sequentialOuter + #if DEBUG + print(value) // $ access=sequentialOuter + let first = 1 // name=sequentialFirst + print(first) // $ access=sequentialFirst + let value = first + 1 // $ access=sequentialFirst // name=sequentialSecond + print(value) // $ access=sequentialSecond + #else + print(value) // $ access=sequentialOuter + #endif + print(value) // $ access=sequentialSecond access=sequentialOuter +} + +func conditionalCapture() { + #if DEBUG + let value = 1 // $ captured=capturedDebug // name=capturedDebug + #else + let value = 2 // $ captured=capturedElse // name=capturedElse + #endif + let closure = { // name=conditionalClosure + print(value) // $ access=capturedDebug access=capturedElse + } + closure() // $ access=conditionalClosure +} + +func conditionalNestedFallback(value: Int) { // name=nestedFallbackOuter + #if DEBUG + #if EXTRA + let value = 1 // name=nestedFallbackExtra + #else + #endif + #else + print(value) // $ access=nestedFallbackOuter + #endif + print(value) // $ access=nestedFallbackOuter access=nestedFallbackExtra +}