diff --git a/shared/namebinding/codeql/namebinding/LocalNameBinding.qll b/shared/namebinding/codeql/namebinding/LocalNameBinding.qll index 57c792027ba8..c4d4abdead45 100644 --- a/shared/namebinding/codeql/namebinding/LocalNameBinding.qll +++ b/shared/namebinding/codeql/namebinding/LocalNameBinding.qll @@ -136,6 +136,12 @@ signature module LocalNameBindingInputSig { * full control of scope resolution for specific types of references. */ default predicate lookupStartsAt(AstNode n, AstNode scope) { none() } + + /** + * Holds if the set of names available in `scope` is not known ahead of time, + * and thus any lookup chain that goes through `scope` may need to be reconciled at a later stage. + */ + default predicate uncertainScope(AstNode scope) { none() } } /** @@ -154,6 +160,8 @@ module LocalNameBinding implicitDeclInScope(_, this) or isTopScope(this) + or + uncertainScope(this) } } @@ -353,6 +361,27 @@ module LocalNameBinding ) } + /** + * Holds if `name`, when resolved from `lookup`, may resolve to one of the uncertain members of `scope`. + */ + pragma[nomagic] + private predicate lookupInUncertainScope(string name, Scope lookup, Scope scope) { + lookupInScope(name, lookup, scope) and + uncertainScope(scope) and + not declInScope(_, name, scope) and + not implicitDeclInScope(name, scope) + } + + /** + * Gets an uncertain scope that the given `accessCand` pair may resolve to. + */ + AstNode getAnUncertainScope(AstNode access, string name) { + exists(Scope lookup | + accessCandInLookupScope(access, name, lookup) and + lookupInUncertainScope(name, lookup, result) + ) + } + cached private newtype TLocal = TExplicitLocal(AstNode definingNode, string name, AstNode scope) { diff --git a/unified/extractor/ast_types.yml b/unified/extractor/ast_types.yml index ca5f6d45edc6..854829a17cb4 100644 --- a/unified/extractor/ast_types.yml +++ b/unified/extractor/ast_types.yml @@ -413,6 +413,7 @@ named: name_pattern: modifier*: modifier identifier: identifier + sub_pattern?: pattern # A pattern matching anything, binding no variables, usually using the syntax "_" ignore_pattern: diff --git a/unified/extractor/src/languages/swift/swift.rs b/unified/extractor/src/languages/swift/swift.rs index ccc1cc7f725d..0762cc66b291 100644 --- a/unified/extractor/src/languages/swift/swift.rs +++ b/unified/extractor/src/languages/swift/swift.rs @@ -158,12 +158,30 @@ fn translation_rules() -> Vec> { // swift-syntax does not distinguish the lexical integer/string forms // (hex/binary/octal, single- vs multi-line, raw): each is a single // `*LiteralExpr` kind, so one rule per literal type suffices. - rule!((integerLiteralExpr) => (int_literal)), - rule!((floatLiteralExpr) => (float_literal)), - rule!((booleanLiteralExpr) => (boolean_literal)), - rule!((nilLiteralExpr) => (builtin_expr)), - rule!((stringLiteralExpr) => (string_literal)), - rule!((regexLiteralExpr) => (regex_literal)), + rule!((integerLiteralExpr) @@node => expr { + let value = tree!((int_literal #{node})); + if ctx.in_pattern { tree!((expr_equality_pattern expr: {value})) } else { value } + }), + rule!((floatLiteralExpr) @@node => expr { + let value = tree!((float_literal #{node})); + if ctx.in_pattern { tree!((expr_equality_pattern expr: {value})) } else { value } + }), + rule!((booleanLiteralExpr) @@node => expr { + let value = tree!((boolean_literal #{node})); + if ctx.in_pattern { tree!((expr_equality_pattern expr: {value})) } else { value } + }), + rule!((nilLiteralExpr) @@node => expr { + let value = tree!((builtin_expr #{node})); + if ctx.in_pattern { tree!((expr_equality_pattern expr: {value})) } else { value } + }), + rule!((stringLiteralExpr) @@node => expr { + let value = tree!((string_literal #{node})); + if ctx.in_pattern { tree!((expr_equality_pattern expr: {value})) } else { value } + }), + rule!((regexLiteralExpr) @@node => expr { + let value = tree!((regex_literal #{node})); + if ctx.in_pattern { tree!((expr_equality_pattern expr: {value})) } else { value } + }), // ---- Names ---- // A function reference spelled with argument labels (`f(x:y:z:)`) is a // `declReferenceExpr` carrying `argumentNames`. Mark it unsupported for @@ -176,6 +194,14 @@ fn translation_rules() -> Vec> { => (unsupported_node) ), + rule!((declReferenceExpr baseName: (identifier) @name) => expr { + let name = tree!((name_expr identifier: (identifier #{name}))); + if ctx.in_pattern { + tree!((expr_equality_pattern expr: {name})) + } else { + 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. @@ -507,29 +533,6 @@ fn translation_rules() -> Vec> { // introduces a new binding; it unwraps to its inner pattern (a // `name_pattern`). rule!((valueBindingPattern pattern: @p) => pattern { p }), - // An enum-case pattern with associated values (`case .foo(let x)`, - // `case Color.foo(let x)`) is an expression pattern wrapping a call of a - // member access. It becomes a `constructor_pattern`; its arguments are - // translated as pattern elements (see the `labeledExpr` rules, gated by - // `ctx.in_pattern`). Matched before the generic `expressionPattern` rule. - // The base is optional: a leading-dot form (`.foo`) has none, so the - // constructor's base is an `inferred_type_expr`. - rule!( - (expressionPattern expression: (functionCallExpr - calledExpression: (memberAccessExpr base: _? @base period: @dot declName: (declReferenceExpr baseName: @name)) - arguments: _* @@args)) - => - constructor_pattern { - ctx.in_pattern = true; - let elements = ctx.translate(args)?; - let base = base.unwrap_or_else(|| tree!((inferred_type_expr #{dot}))); - tree!((constructor_pattern - constructor: (member_access_expr - base: {base} - member: (identifier #{name})) - element: {elements})) - } - ), // A tuple destructuring pattern (`let (a, b) = …`). A labelled element // (`let (x: a) = …`) carries its label through as the `pattern_element` // key; unlabelled elements have no key. @@ -544,36 +547,16 @@ fn translation_rules() -> Vec> { // handling in the future. (Redundant with the catch-all fallback, but // kept as a signpost.) rule!((isTypePattern) => (unsupported_node)), - // A standalone wildcard pattern (`case _:`, `if case _`): swift-syntax - // models the bare `_` as an `expressionPattern` wrapping a - // `discardAssignmentExpr`. Matched before the generic `expressionPattern` - // rule so `_` becomes an `ignore_pattern` rather than an equality match. - // (Wildcards *inside* an enum-case argument list are handled by the - // `labeledExpr`/`discardAssignmentExpr` rules.) - rule!((expressionPattern expression: (discardAssignmentExpr)) => (ignore_pattern)), // A wildcard *binding* pattern (`let _ = x`, `for _ in xs`). swift-syntax - // models this as a `wildcardPattern` — distinct from the `_` *match* - // pattern above, which is an `expressionPattern` over a - // `discardAssignmentExpr`. + // models this as a `wildcardPattern`, distinct from the `_` match form + // handled by the context-aware `discardAssignmentExpr` rule. rule!((wildcardPattern) => (ignore_pattern)), - // A tuple pattern in a match position (`case (let a, 3):`) is parsed by - // swift-syntax as an `expressionPattern` wrapping a `tupleExpr` — unlike a - // binding tuple (`let (a, b)`), which is a real `tuplePattern`. Recognise - // it as a `tuple_pattern`; its `labeledExpr` elements translate to - // `pattern_element`s under `ctx.in_pattern` (a binding element becomes a - // `name_pattern`, any other expression an `expr_equality_pattern`). - rule!( - (expressionPattern expression: (tupleExpr elements: _* @@els)) - => - tuple_pattern { - ctx.in_pattern = true; - let elements = ctx.translate(els)?; - tree!((tuple_pattern element: {elements})) - } - ), - // A bare expression pattern (`case 1:`, `case someConstant:`) matches by - // equality. - rule!((expressionPattern expression: @e) => (expr_equality_pattern expr: {e})), + // An expression pattern only establishes pattern context; its child + // determines the concrete pattern shape. + rule!((expressionPattern expression: @@e) => expr { + ctx.in_pattern = true; + ctx.translate(e)?.into_iter().next().ok_or("expression pattern has no child")? + }), // ---- Functions ---- // A function declaration (parameters/return type/body optional). The // parameters and return type nest under `signature`; the body is a @@ -633,6 +616,22 @@ fn translation_rules() -> Vec> { default: {val})) } ), + // Swift's `[T](...)` array-type constructor syntax is parsed as a call + // whose callee is an `arrayExpr` containing `T`. For a generic `T`, + // translating that callee as an array literal would place a type + // expression in an expression-only element field. Normalize it to an + // `Array` generic type constructor instead. + rule!( + (functionCallExpr + calledExpression: (arrayExpr elements: (arrayElement expression: (genericSpecializationExpr) @element)) + arguments: _* @args) + => + (call_expr + callee: (generic_type_expr + base: (named_type_expr name: (identifier "Array")) + type_argument: {element}) + argument: {args}) + ), // A function/method call (`foo(1, 2)`). `calledExpression` is the callee // and `arguments` is an (elided) list of `labeledExpr`, each translated // to an `argument` below. A trailing closure (`xs.map { … }`) becomes a @@ -645,7 +644,13 @@ fn translation_rules() -> Vec> { rule!( (functionCallExpr calledExpression: @callee arguments: _* @args) => - (call_expr callee: {callee} argument: {args}) + expr { + if ctx.in_pattern { + tree!((constructor_pattern constructor: {callee} element: {args})) + } else { + tree!((call_expr callee: {callee} argument: {args})) + } + } ), // A call argument or an enum-case pattern argument. When translating an // enum-case `constructor_pattern`'s arguments (`ctx.in_pattern`), a @@ -656,6 +661,27 @@ fn translation_rules() -> Vec> { // Otherwise the argument keeps its label as the `name` and its value. // The pattern-only shapes (`patternExpr`, `discardAssignmentExpr`) are // matched first; they never occur as ordinary call arguments. + rule!( + (labeledExpr + label: _? @@lbl + expression: (functionCallExpr + calledExpression: @constructor + arguments: _* @elements)) + => + argument { + if ctx.in_pattern { + tree!((pattern_element + key: (identifier #{lbl})? + pattern: (constructor_pattern + constructor: {constructor} + element: {elements}))) + } else { + tree!((argument + name: (identifier #{lbl})? + value: (call_expr callee: {constructor} argument: {elements}))) + } + } + ), rule!( (labeledExpr label: _? @@lbl expression: (patternExpr pattern: @p)) => @@ -673,7 +699,7 @@ fn translation_rules() -> Vec> { if ctx.in_pattern { tree!((pattern_element key: (identifier #{lbl})? - pattern: (expr_equality_pattern expr: {val}))) + pattern: {val})) } else { tree!((argument name: (identifier #{lbl})? value: {val})) } @@ -683,15 +709,29 @@ fn translation_rules() -> Vec> { // `declReferenceExpr`; pull its `baseName` out as the member identifier. // A leading-dot access (`.foo`) has no explicit base — the base is an // `inferred_type_expr`. The base-ful form is matched first. + // A bracketed generic array type used as a metatype or static-member + // base (`[T].self`) is parsed as an `arrayExpr`; preserve its type + // meaning as `Array` rather than an array literal. + rule!( + (memberAccessExpr + base: (arrayExpr elements: (arrayElement expression: (genericSpecializationExpr) @element)) + declName: (declReferenceExpr baseName: @member)) + => + (member_access_expr + base: (generic_type_expr + base: (named_type_expr name: (identifier "Array")) + type_argument: {element}) + member: (identifier #{member})) + ), rule!( (memberAccessExpr base: @base declName: (declReferenceExpr baseName: @member)) => (member_access_expr base: {base} member: (identifier #{member})) ), rule!( - (memberAccessExpr declName: (declReferenceExpr baseName: @member)) + (memberAccessExpr period: @dot declName: (declReferenceExpr baseName: @member)) => - (member_access_expr base: (inferred_type_expr) member: (identifier #{member})) + (member_access_expr base: (inferred_type_expr #{dot}) member: (identifier #{member})) ), // Control transfer, one rule per keyword. `return` carries an optional // value; `break` / `continue` an optional target label; `throw` its @@ -903,7 +943,18 @@ fn translation_rules() -> Vec> { ), // ---- Optionals and errors ---- // Optional chaining — unwrap the marker - rule!((optionalChainingExpr expression: @inner) => expr { inner }), + rule!((optionalChainingExpr expression: @@inner) => expr { + let inner = ctx.translate(inner)?.into_iter().next().ok_or("optional chaining expression has no child")?; + if ctx.in_pattern { + tree!((constructor_pattern + constructor: (member_access_expr + base: (named_type_expr name: (identifier "Optional")) + member: (identifier "some")) + element: (pattern_element pattern: {inner}))) + } else { + inner + } + }), // try/try?/try! expr → unary_expr with operator "try", "try?" or "try!" rule!( (tryExpr questionOrExclamationMark: _? @@m expression: @e) @@ -972,13 +1023,12 @@ fn translation_rules() -> Vec> { path: (importPathComponent name: @@parts)*) => import_declaration { - let pattern = match kind { - Some(_) => { - let last = *parts.last().ok_or("import has no path")?; - tree!((name_pattern identifier: (identifier #{last}))) - } - None => tree!((bulk_importing_pattern)), + let bulk_import = match kind { + None => Some(tree!((bulk_importing_pattern))), + Some(_) => None, // scoped import, no bulk import }; + let last = *parts.last().ok_or("import has no path")?; + let pattern = tree!((name_pattern identifier: (identifier #{last}) sub_pattern: {bulk_import})); tree!((import_declaration modifier: (modifier #{kind})? modifier: {attrs} @@ -993,8 +1043,7 @@ fn translation_rules() -> Vec> { // becomes a `modifier`; its source text is the modifier spelling. rule!((attribute) @m => (modifier #{m})), rule!((declModifier) @m => (modifier #{m})), - // A `super` expression. (`self` needs no rule: swift-syntax models it as - // an ordinary `declReferenceExpr`, already mapped to a `name_expr`.) + // A `super` expression. rule!((superExpr) => (super_expr)), // Type expressions. A generic type applied with explicit arguments // (`Set`) becomes a `generic_type_expr` whose `base` is the type @@ -1014,9 +1063,13 @@ fn translation_rules() -> Vec> { // A named type (`Int`). `identifierType.name` is the type-name token. rule!((identifierType name: @@n) => (named_type_expr name: (identifier #{n}))), // A qualified type (`Outer.Inner`, `NSString.CompareOptions`). swift-syntax - // nests these as `memberType` nodes; we keep the whole dotted path as the - // opaque `named_type_expr` name. - rule!((memberType) @ty => (named_type_expr name: (identifier #{ty}))), + // nests these as `memberType` nodes; preserve the nesting in the + // named_type_expr qualifier field. + rule!( + (memberType baseType: @base name: @@name) + => + (named_type_expr qualifier: {base} name: (identifier #{name})) + ), // Sugared types desugar to `generic_type_expr`: `T?` -> Optional, // `[T]` -> Array, `[K: V]` -> Dictionary. rule!( 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 new file mode 100644 index 000000000000..22d9b86bf996 --- /dev/null +++ b/unified/extractor/tests/corpus/swift/control-flow/nested-enum-case-pattern.output @@ -0,0 +1,160 @@ +switch event { +case let .received(.some(value), timestamp): + print(value, timestamp) +default: + break +} + +--- + +sourceFile + endOfFileToken: endOfFile + statements: + codeBlockItem + item: + expressionStmt + expression: + switchExpr + leftBrace: { + rightBrace: } + cases: + switchCase + label: + switchCaseLabel + colon: : + caseKeyword: case + caseItems: + switchCaseItem + pattern: + valueBindingPattern + pattern: + expressionPattern + expression: + functionCallExpr + leftParen: ( + rightParen: ) + arguments: + labeledExpr + expression: + functionCallExpr + leftParen: ( + rightParen: ) + arguments: + labeledExpr + expression: + patternExpr + pattern: + identifierPattern + identifier: identifier "value" + additionalTrailingClosures: + calledExpression: + memberAccessExpr + period: . + declName: + declReferenceExpr + baseName: identifier "some" + trailingComma: , + labeledExpr + expression: + patternExpr + pattern: + identifierPattern + identifier: identifier "timestamp" + additionalTrailingClosures: + calledExpression: + memberAccessExpr + period: . + declName: + declReferenceExpr + baseName: identifier "received" + bindingSpecifier: let + statements: + codeBlockItem + item: + functionCallExpr + leftParen: ( + rightParen: ) + arguments: + labeledExpr + expression: + declReferenceExpr + baseName: identifier "value" + trailingComma: , + labeledExpr + expression: + declReferenceExpr + baseName: identifier "timestamp" + additionalTrailingClosures: + calledExpression: + declReferenceExpr + baseName: identifier "print" + switchCase + label: + switchDefaultLabel + colon: : + defaultKeyword: default + statements: + codeBlockItem + item: + breakStmt + breakKeyword: break + subject: + declReferenceExpr + baseName: identifier "event" + switchKeyword: switch + +--- + +top_level + body: + block + stmt: + switch_expr + value: + name_expr + identifier: identifier "event" + case: + switch_case + pattern: + constructor_pattern + constructor: + member_access_expr + base: inferred_type_expr "." + member: identifier "received" + element: + pattern_element + pattern: + constructor_pattern + constructor: + member_access_expr + base: inferred_type_expr "." + member: identifier "some" + element: + pattern_element + pattern: + name_pattern + identifier: identifier "value" + pattern_element + pattern: + name_pattern + identifier: identifier "timestamp" + body: + block + stmt: + call_expr + callee: + name_expr + identifier: identifier "print" + argument: + argument + value: + name_expr + identifier: identifier "value" + argument + value: + name_expr + identifier: identifier "timestamp" + switch_case + body: + block + stmt: break_expr "break" diff --git a/unified/extractor/tests/corpus/swift/control-flow/nested-enum-case-pattern.swift b/unified/extractor/tests/corpus/swift/control-flow/nested-enum-case-pattern.swift new file mode 100644 index 000000000000..876d9995aee6 --- /dev/null +++ b/unified/extractor/tests/corpus/swift/control-flow/nested-enum-case-pattern.swift @@ -0,0 +1,6 @@ +switch event { +case let .received(.some(value), timestamp): + print(value, timestamp) +default: + break +} diff --git a/unified/extractor/tests/corpus/swift/expressions/array-type-constructor.output b/unified/extractor/tests/corpus/swift/expressions/array-type-constructor.output new file mode 100644 index 000000000000..e721ba9cfde2 --- /dev/null +++ b/unified/extractor/tests/corpus/swift/expressions/array-type-constructor.output @@ -0,0 +1,74 @@ +let values = [Result]() + +--- + +sourceFile + endOfFileToken: endOfFile + statements: + codeBlockItem + item: + variableDecl + attributes: + modifiers: + bindingSpecifier: let + bindings: + patternBinding + initializer: + initializerClause + equal: = + value: + functionCallExpr + leftParen: ( + rightParen: ) + arguments: + additionalTrailingClosures: + calledExpression: + arrayExpr + elements: + arrayElement + expression: + genericSpecializationExpr + expression: + declReferenceExpr + baseName: identifier "Result" + genericArgumentClause: + genericArgumentClause + arguments: + genericArgument + argument: + identifierType + name: identifier "Void" + leftAngle: < + rightAngle: > + leftSquare: [ + rightSquare: ] + pattern: + identifierPattern + identifier: identifier "values" + +--- + +top_level + body: + block + stmt: + variable_declaration + modifier: modifier "let" + pattern: + name_pattern + identifier: identifier "values" + value: + call_expr + callee: + generic_type_expr + base: + named_type_expr + name: identifier "Array" + type_argument: + generic_type_expr + base: + named_type_expr + name: identifier "Result" + type_argument: + named_type_expr + name: identifier "Void" diff --git a/unified/extractor/tests/corpus/swift/expressions/array-type-constructor.swift b/unified/extractor/tests/corpus/swift/expressions/array-type-constructor.swift new file mode 100644 index 000000000000..49191aee851d --- /dev/null +++ b/unified/extractor/tests/corpus/swift/expressions/array-type-constructor.swift @@ -0,0 +1 @@ +let values = [Result]() diff --git a/unified/extractor/tests/corpus/swift/expressions/array-type-metatype.output b/unified/extractor/tests/corpus/swift/expressions/array-type-metatype.output new file mode 100644 index 000000000000..f06917190b3a --- /dev/null +++ b/unified/extractor/tests/corpus/swift/expressions/array-type-metatype.output @@ -0,0 +1,75 @@ +let type = [Result].self + +--- + +sourceFile + endOfFileToken: endOfFile + statements: + codeBlockItem + item: + variableDecl + attributes: + modifiers: + bindingSpecifier: let + bindings: + patternBinding + initializer: + initializerClause + equal: = + value: + memberAccessExpr + period: . + declName: + declReferenceExpr + baseName: self + base: + arrayExpr + elements: + arrayElement + expression: + genericSpecializationExpr + expression: + declReferenceExpr + baseName: identifier "Result" + genericArgumentClause: + genericArgumentClause + arguments: + genericArgument + argument: + identifierType + name: identifier "Void" + leftAngle: < + rightAngle: > + leftSquare: [ + rightSquare: ] + pattern: + identifierPattern + identifier: identifier "type" + +--- + +top_level + body: + block + stmt: + variable_declaration + modifier: modifier "let" + pattern: + name_pattern + identifier: identifier "type" + value: + member_access_expr + base: + generic_type_expr + base: + named_type_expr + name: identifier "Array" + type_argument: + generic_type_expr + base: + named_type_expr + name: identifier "Result" + type_argument: + named_type_expr + name: identifier "Void" + member: identifier "self" diff --git a/unified/extractor/tests/corpus/swift/expressions/array-type-metatype.swift b/unified/extractor/tests/corpus/swift/expressions/array-type-metatype.swift new file mode 100644 index 000000000000..85d770be76cb --- /dev/null +++ b/unified/extractor/tests/corpus/swift/expressions/array-type-metatype.swift @@ -0,0 +1 @@ +let type = [Result].self diff --git a/unified/extractor/tests/corpus/swift/functions/leading-dot-expression-call.output b/unified/extractor/tests/corpus/swift/functions/leading-dot-expression-call.output index d0a844d02afa..a5b38e1e2bea 100644 --- a/unified/extractor/tests/corpus/swift/functions/leading-dot-expression-call.output +++ b/unified/extractor/tests/corpus/swift/functions/leading-dot-expression-call.output @@ -51,7 +51,7 @@ top_level call_expr callee: member_access_expr - base: inferred_type_expr ".some" + base: inferred_type_expr "." member: identifier "some" argument: argument diff --git a/unified/extractor/tests/corpus/swift/functions/leading-dot-expression-value.output b/unified/extractor/tests/corpus/swift/functions/leading-dot-expression-value.output index bec014593d91..41128b61818e 100644 --- a/unified/extractor/tests/corpus/swift/functions/leading-dot-expression-value.output +++ b/unified/extractor/tests/corpus/swift/functions/leading-dot-expression-value.output @@ -39,5 +39,5 @@ top_level identifier: identifier "x" value: member_access_expr - base: inferred_type_expr ".foo" + base: inferred_type_expr "." member: identifier "foo" diff --git a/unified/extractor/tests/corpus/swift/optionals-and-errors/optional-enum-case-binding.output b/unified/extractor/tests/corpus/swift/optionals-and-errors/optional-enum-case-binding.output new file mode 100644 index 000000000000..7b1eb1eea3b4 --- /dev/null +++ b/unified/extractor/tests/corpus/swift/optionals-and-errors/optional-enum-case-binding.output @@ -0,0 +1,117 @@ +if case .some(let value)? = input { + print(value) +} + +--- + +sourceFile + endOfFileToken: endOfFile + statements: + codeBlockItem + item: + expressionStmt + expression: + ifExpr + body: + codeBlock + leftBrace: { + rightBrace: } + statements: + codeBlockItem + item: + functionCallExpr + leftParen: ( + rightParen: ) + arguments: + labeledExpr + expression: + declReferenceExpr + baseName: identifier "value" + additionalTrailingClosures: + calledExpression: + declReferenceExpr + baseName: identifier "print" + conditions: + conditionElement + condition: + matchingPatternCondition + initializer: + initializerClause + equal: = + value: + declReferenceExpr + baseName: identifier "input" + pattern: + expressionPattern + expression: + optionalChainingExpr + expression: + functionCallExpr + leftParen: ( + rightParen: ) + arguments: + labeledExpr + expression: + patternExpr + pattern: + valueBindingPattern + pattern: + identifierPattern + identifier: identifier "value" + bindingSpecifier: let + additionalTrailingClosures: + calledExpression: + memberAccessExpr + period: . + declName: + declReferenceExpr + baseName: identifier "some" + questionMark: ? + caseKeyword: case + ifKeyword: if + +--- + +top_level + body: + block + stmt: + if_expr + condition: + pattern_guard_expr + pattern: + constructor_pattern + constructor: + member_access_expr + base: + named_type_expr + name: identifier "Optional" + member: identifier "some" + element: + pattern_element + pattern: + constructor_pattern + constructor: + member_access_expr + base: inferred_type_expr "." + member: identifier "some" + element: + pattern_element + pattern: + name_pattern + identifier: identifier "value" + value: + name_expr + identifier: identifier "input" + then: + block + stmt: + call_expr + callee: + name_expr + identifier: identifier "print" + argument: + argument + value: + name_expr + identifier: identifier "value" diff --git a/unified/extractor/tests/corpus/swift/optionals-and-errors/optional-enum-case-binding.swift b/unified/extractor/tests/corpus/swift/optionals-and-errors/optional-enum-case-binding.swift new file mode 100644 index 000000000000..c0e58852bef1 --- /dev/null +++ b/unified/extractor/tests/corpus/swift/optionals-and-errors/optional-enum-case-binding.swift @@ -0,0 +1,3 @@ +if case .some(let value)? = input { + print(value) +} diff --git a/unified/extractor/tests/corpus/swift/types/qualified-type.output b/unified/extractor/tests/corpus/swift/types/qualified-type.output new file mode 100644 index 000000000000..f561dc6ab2dc --- /dev/null +++ b/unified/extractor/tests/corpus/swift/types/qualified-type.output @@ -0,0 +1,139 @@ +struct Outer { + struct Inner { + struct Deep {} + } +} + +let value: Outer.Inner +let nested: Outer.Inner.Deep + +--- + +sourceFile + endOfFileToken: endOfFile + statements: + codeBlockItem + item: + structDecl + attributes: + name: identifier "Outer" + memberBlock: + memberBlock + leftBrace: { + rightBrace: } + members: + memberBlockItem + decl: + structDecl + attributes: + name: identifier "Inner" + memberBlock: + memberBlock + leftBrace: { + rightBrace: } + members: + memberBlockItem + decl: + structDecl + attributes: + name: identifier "Deep" + memberBlock: + memberBlock + leftBrace: { + rightBrace: } + members: + modifiers: + structKeyword: struct + modifiers: + structKeyword: struct + modifiers: + structKeyword: struct + codeBlockItem + item: + variableDecl + attributes: + modifiers: + bindingSpecifier: let + bindings: + patternBinding + pattern: + identifierPattern + identifier: identifier "value" + typeAnnotation: + typeAnnotation + colon: : + type: + memberType + name: identifier "Inner" + baseType: + identifierType + name: identifier "Outer" + period: . + codeBlockItem + item: + variableDecl + attributes: + modifiers: + bindingSpecifier: let + bindings: + patternBinding + pattern: + identifierPattern + identifier: identifier "nested" + typeAnnotation: + typeAnnotation + colon: : + type: + memberType + name: identifier "Deep" + baseType: + memberType + name: identifier "Inner" + baseType: + identifierType + name: identifier "Outer" + period: . + period: . + +--- + +top_level + body: + block + stmt: + class_like_declaration + modifier: modifier "struct" + name: identifier "Outer" + member: + class_like_declaration + modifier: modifier "struct" + name: identifier "Inner" + member: + class_like_declaration + modifier: modifier "struct" + name: identifier "Deep" + variable_declaration + modifier: modifier "let" + pattern: + name_pattern + identifier: identifier "value" + type: + named_type_expr + qualifier: + named_type_expr + name: identifier "Outer" + name: identifier "Inner" + variable_declaration + modifier: modifier "let" + pattern: + name_pattern + identifier: identifier "nested" + type: + named_type_expr + qualifier: + named_type_expr + qualifier: + named_type_expr + name: identifier "Outer" + name: identifier "Inner" + name: identifier "Deep" diff --git a/unified/extractor/tests/corpus/swift/types/qualified-type.swift b/unified/extractor/tests/corpus/swift/types/qualified-type.swift new file mode 100644 index 000000000000..9c5fbae3cf7b --- /dev/null +++ b/unified/extractor/tests/corpus/swift/types/qualified-type.swift @@ -0,0 +1,8 @@ +struct Outer { + struct Inner { + struct Deep {} + } +} + +let value: Outer.Inner +let nested: Outer.Inner.Deep diff --git a/unified/ql/lib/codeql/files/FileSystem.qll b/unified/ql/lib/codeql/files/FileSystem.qll index 6cc771fad9d2..a50c1cd3432a 100644 --- a/unified/ql/lib/codeql/files/FileSystem.qll +++ b/unified/ql/lib/codeql/files/FileSystem.qll @@ -31,6 +31,8 @@ class Container = Impl::Container; class Folder = Impl::Folder; +module Folder = Impl::Folder; + /** A file. */ class File extends Container, Impl::File { /** Holds if this file was extracted from ordinary source code. */ diff --git a/unified/ql/lib/codeql/unified/internal/Ast.qll b/unified/ql/lib/codeql/unified/internal/Ast.qll index 20ff74e6eaf7..d5ffa0218523 100644 --- a/unified/ql/lib/codeql/unified/internal/Ast.qll +++ b/unified/ql/lib/codeql/unified/internal/Ast.qll @@ -1078,9 +1078,14 @@ module Unified { /** Gets the node corresponding to the field `modifier`. */ final F::Modifier getAModifier() { result = this.getModifier(_) } + /** Gets the node corresponding to the field `sub_pattern`. */ + final F::Pattern getSubPattern() { unified_name_pattern_sub_pattern(this, result) } + /** Gets a field or child node of this node. */ final override F::AstNode getAFieldOrChild() { - unified_name_pattern_def(this, result) or unified_name_pattern_modifier(this, _, result) + unified_name_pattern_def(this, result) or + unified_name_pattern_modifier(this, _, result) or + unified_name_pattern_sub_pattern(this, result) } } @@ -1895,6 +1900,8 @@ module Unified { or result = node.(NamePattern).getModifier(i) and name = "getModifier" or + result = node.(NamePattern).getSubPattern() and i = -1 and name = "getSubPattern" + or result = node.(NamedTypeExpr).getName() and i = -1 and name = "getName" or result = node.(NamedTypeExpr).getQualifier() and i = -1 and name = "getQualifier" diff --git a/unified/ql/lib/codeql/unified/internal/FacadeAst.qll b/unified/ql/lib/codeql/unified/internal/FacadeAst.qll index a25fb3447c1e..cb00bae7eeca 100644 --- a/unified/ql/lib/codeql/unified/internal/FacadeAst.qll +++ b/unified/ql/lib/codeql/unified/internal/FacadeAst.qll @@ -4,12 +4,17 @@ overlay[local?] module; +private import codeql.files.FileSystem + module Unified { private import Ast::Unified as G import G /** The base class for all AST nodes. */ class AstNode extends G::AstNode { + /** Gets the file containing this AST node. */ + File getFile() { result = this.getLocation().getFile() } + /** Holds if this AST node has a modifier with the given text. */ predicate hasModifier(string text) { exists(Modifier mod | @@ -17,6 +22,36 @@ module Unified { mod.getValue() = text ) } + + /** Gets the nearest enclosing class declaration, possibly this node itself. */ + ClassLikeDeclaration getEnclosingClass() { + result = this + or + not this instanceof ClassLikeDeclaration and + result = this.getParent().getEnclosingClass() + } + } + + /** An expression */ + class Expr extends G::Expr { + /** Gets the string value of this expression, if it is a known string constant. */ + string getStringValue() { + // TODO: we'll want to cook the string literals extractor-side, but for now + // just strip the quotes here and ignore escape sequences. + result = this.(StringLiteral).getValue().regexpCapture("\"(.*)\"", 1) + } + } + + /** A function call */ + class CallExpr extends G::CallExpr { + /** Gets the named argument with the given `name`. */ + Expr getNamedArgument(string name) { + exists(Argument arg | + arg = this.getAnArgument() and + arg.getName().getValue() = name and + result = arg.getValue() + ) + } } /** The base class for all patterns. */ diff --git a/unified/ql/lib/codeql/unified/internal/LocalNameBinding.qll b/unified/ql/lib/codeql/unified/internal/LocalNameBinding.qll index 0df235d521fd..d7020c153465 100644 --- a/unified/ql/lib/codeql/unified/internal/LocalNameBinding.qll +++ b/unified/ql/lib/codeql/unified/internal/LocalNameBinding.qll @@ -184,79 +184,92 @@ private module LocalNameBindingInput implements LocalNameBindingInputSig; module Public { @@ -319,6 +348,20 @@ module Public { /** Gets the name of this local, as a string. */ string getName() { result = super.getName() } } + + /** An identifier that appears as the declaration site of a name, such as the `x` in `let x = 123`. */ + class NameDeclaration extends Identifier { + NameDeclaration() { LocalNameBindingInput::bindingContext(this, _, _) } + + /** Gets the statement-like node declaring this name, such as a `VariableDeclaration` or `CatchClause`. */ + AstNode getDeclaration() { LocalNameBindingInput::bindingContext(this, _, result) } + + /** Gets the name being declared. */ + string getName() { result = this.getValue() } + + /** Gets the representative for the local name introduced by this declaration. */ + LocalName getLocalName() { result = this.(LocalNameBindingOutput::LocalAccess).getLocal() } + } } /** @@ -342,7 +385,7 @@ class PotentialLocalNameAccess extends Identifier { or this = any(NamedTypeExpr e | not exists(e.getQualifier())).getName() or - LocalNameBindingInput::bindingContext(this, _) + this instanceof NameDeclaration } LocalName getLocalName() { result = this.(LocalNameBindingOutput::LocalAccess).getLocal() } @@ -350,5 +393,5 @@ class PotentialLocalNameAccess extends Identifier { string getName() { result = this.getValue() } /** Holds if this is one of the declaration sites for a name, such as the `x` in `let x = 123`. */ - predicate isDeclarationSite() { LocalNameBindingInput::bindingContext(this, _) } + predicate isDeclarationSite() { this instanceof NameDeclaration } } diff --git a/unified/ql/lib/codeql/unified/internal/NameBindingPlugin.qll b/unified/ql/lib/codeql/unified/internal/NameBindingPlugin.qll new file mode 100644 index 000000000000..de9c09f5bb70 --- /dev/null +++ b/unified/ql/lib/codeql/unified/internal/NameBindingPlugin.qll @@ -0,0 +1,79 @@ +private import unified +private import codeql.util.Unit +private import codeql.unified.internal.NameBindingPluginSwift // ensure overrides are seen + +/** Extension point for language-specific inputs to name binding. */ +class NameBindingPlugin extends Unit { + /** + * Holds if `member` is an instance member. + * + * The caller has already restricted `member` to be a member of `cls`, and + * ensured that `member` is a `VariableDeclaration` or `FunctionDeclaration`. + */ + bindingset[cls, member] + predicate isInstanceMember(ClassLikeDeclaration cls, Member member) { none() } + + /** + * Holds if `member` is only visible in its local scope, and can thus be entirely resolved + * by local name-binding, suppressing any store-steps that would otherwise be induced from the member. + * + * Need only be implemented for members that occur in the context of class or top-level, as other + * contexts are considered local already. + */ + predicate isPrivateToLocalScope(Stmt member) { none() } +} + +/** Holds if `member` is an instance member. */ +predicate isInstanceMember(Member member) { + (member instanceof VariableDeclaration or member instanceof FunctionDeclaration) and + exists(ClassLikeDeclaration cls | cls.getAMember() = member | + any(NameBindingPlugin p).isInstanceMember(cls, member) + ) +} + +/** Holds if `member` is only visible in its local scope. */ +predicate isPrivateToLocalScope(Stmt member) { + any(NameBindingPlugin p).isPrivateToLocalScope(member) +} + +/** + * Representative for a module scope. + * + * Module scopes can encompass a set of files, and is the canonical representative + * for the top-level members collectively exported from those files. + */ +abstract class ModuleScopeRepr extends AstNode { + /** + * Holds if files matched by `path` should be part of this module; + * `path` is resolved relative to `c` and may use globs. + * + * For each file in the module: + * - Top-level exported members become members of this module, and + * - This module is implicitly imported at the top-level + */ + predicate shouldInclude(Container c, string path) { none() } + + /** + * Holds if this module scope can be referenced by an identifier `name` + * appearing as the leading identifier of an import path. + */ + predicate hasImportableName(string name) { none() } + + /** Gets one of the files included due to the `shouldInclude` predicate. */ + final File getAnIncludedFile() { + exists(Container c, string path | + this.shouldInclude(c, path) and + result = FileResolver::resolve(c, path) + ) + } +} + +private module FileResolverInput implements Folder::ResolveSig { + predicate shouldResolve(Container base, string path) { + any(ModuleScopeRepr r).shouldInclude(base, path) + } + + predicate allowGlobs() { any() } +} + +private module FileResolver = Folder::Resolve; diff --git a/unified/ql/lib/codeql/unified/internal/NameBindingPluginSwift.qll b/unified/ql/lib/codeql/unified/internal/NameBindingPluginSwift.qll new file mode 100644 index 000000000000..fb47c577b79e --- /dev/null +++ b/unified/ql/lib/codeql/unified/internal/NameBindingPluginSwift.qll @@ -0,0 +1,103 @@ +/** + * Provides Swift-specific name binding rules. + */ + +private import unified +private import codeql.unified.internal.NameBindingPlugin + +class NameBindingPluginSwift extends NameBindingPlugin { + // Note: For now we assume all code is Swift, but in the future we must restrict these rules to Swift-files + bindingset[cls, member] + override predicate isInstanceMember(ClassLikeDeclaration cls, Member member) { + exists(cls) and + not member.hasModifier(["static", "class", "enum_case"]) + } + + override predicate isPrivateToLocalScope(Stmt member) { + // Private top-level members + member = any(TopLevel top).getBody().getAStmt() and + member.hasModifier(["private", "fileprivate"]) + or + // Imports are always file-local + member instanceof ImportDeclaration + // + // Note: Private class members can be seen within type-extensions in the same file, + // so we can't declare those private to their local scope. + } +} + +private predicate predefinedSourceFolders(string folder, int ordering) { + folder = "Sources,Source,src,srcs".splitAt(",", ordering) +} + +bindingset[targetKind] +private predicate predefinedSourceFoldersByTarget(string targetKind, string folder, int ordering) { + predefinedSourceFolders(folder, ordering) + or + ordering = -1 and + ( + targetKind = "testTarget" and + folder = "Tests" + or + targetKind = "plugin" and + folder = "Plugins" + ) +} + +/** + * A call to `.target()` or similar target spec, in a `Package.swift` file. + */ +class SwiftPackageTarget extends ModuleScopeRepr, CallExpr { + private string targetKind; + + SwiftPackageTarget() { + this.getFile().getBaseName() = "Package.swift" and + this.getCallee().(MemberAccessExpr).getMember().getValue() = targetKind and + targetKind = + [ + "target", "executableTarget", "testTarget", "systemLibrary", "binaryTarget", "plugin", + "macro" + ] + } + + Folder getFolder() { result = this.getFile().getParentContainer() } + + /** Gets the intermediate folder such as `Sources/` containing the sources, but without the target name. */ + Folder getSourceMidFolder() { + result = + min(int i, string name, Folder subfolder | + predefinedSourceFoldersByTarget(targetKind, name, i) and + subfolder = this.getFolder().getFolder(name) + | + subfolder order by i + ) + } + + /** Gets the source folder to use if no explicit `path:` if given, typically `Sources/` */ + Folder getDefaultSourceFolder() { + exists(Folder subfolder | subfolder = this.getSourceMidFolder() | + result = subfolder.getFolder(this.getName()) + or + not exists(subfolder.getFolder(this.getName())) and + result = subfolder + ) + } + + string getName() { result = this.getNamedArgument("name").getStringValue() } + + string getExplicitPath() { result = this.getNamedArgument("path").getStringValue() } + + override predicate shouldInclude(Container c, string path) { + c = this.getFolder() and + path = this.getExplicitPath() + "/**/*.swift" + or + not exists(this.getExplicitPath()) and + c = this.getDefaultSourceFolder() and + path = "**/*.swift" + } + + override predicate hasImportableName(string name) { + targetKind = "target" and + name = this.getName() + } +} diff --git a/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll b/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll new file mode 100644 index 000000000000..4a677769f5c0 --- /dev/null +++ b/unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll @@ -0,0 +1,383 @@ +/** + * Provides classes for reasoning about static references to the members of classes and top-levels. + */ + +private import unified +private import codeql.unified.internal.LocalNameBinding +private import codeql.unified.internal.NameBindingPlugin + +private newtype TNameBindingNode = + TIdentifier(Identifier n) or + TBulkImport(BulkImportingPattern p) or + TLocalName(LocalName local) or + TExportedNamespace(ClassLikeDeclaration cls) or + TLocalNamespace(AstNode n) { + n = any(TopLevel t).getBody() or // Imported names come in scope here + n instanceof ClassLikeDeclaration + } or + TModuleScope(ModuleScopeRepr repr) or + TModuleRoot() + +/** + * A node in a graph, in which name-binding rules are represented as edges between nodes. + */ +class NameBindingNode extends TNameBindingNode { + predicate isIdentifier(Identifier n) { this = TIdentifier(n) } + + Identifier asIdentifier() { this.isIdentifier(result) } + + predicate isBulkImport(BulkImportingPattern p) { this = TBulkImport(p) } + + predicate isLocalName(LocalName local) { this = TLocalName(local) } + + /** Holds if this represents the set of static members available in the given namespace. */ + predicate isExportedNamespace(ClassLikeDeclaration cls) { this = TExportedNamespace(cls) } + + /** Holds if this represents the set of members that can be accessed unqualified within the given scope. */ + predicate isLocalNamespace(AstNode n) { this = TLocalNamespace(n) } + + /** Holds if this represents the given module scope. */ + predicate isModuleScopeNode(ModuleScopeRepr repr) { this = TModuleScope(repr) } + + /** Holds if this represents the root namespace in which all named modules are members. */ + predicate isModuleRoot() { this = TModuleRoot() } + + string toString() { + exists(Identifier n | this.isIdentifier(n) and result = "Identifier(" + n + ")") + or + exists(BulkImportingPattern p | this.isBulkImport(p) and result = "BulkImport(" + p + ")") + or + exists(LocalName local | this.isLocalName(local) and result = "LocalName(" + local + ")") + or + exists(ClassLikeDeclaration cls | + this.isExportedNamespace(cls) and result = "ExportedNamespace(" + cls + ")" + ) + or + exists(AstNode n | this.isLocalNamespace(n) and result = "LocalNamespace(" + n + ")") + or + exists(ModuleScopeRepr repr | + this.isModuleScopeNode(repr) and result = "ModuleScope(" + repr + ")" + ) + or + this.isModuleRoot() and result = "ModuleRoot" + } + + Location getLocation() { + exists(Identifier n | this.isIdentifier(n) and result = n.getLocation()) + or + exists(BulkImportingPattern p | this.isBulkImport(p) and result = p.getLocation()) + or + exists(LocalName local | this.isLocalName(local) and result = local.getLocation()) + or + exists(ClassLikeDeclaration cls | this.isExportedNamespace(cls) and result = cls.getLocation()) + or + exists(AstNode n | this.isLocalNamespace(n) and result = n.getLocation()) + or + exists(ModuleScopeRepr repr | this.isModuleScopeNode(repr) and result = repr.getLocation()) + } +} + +Identifier getIdentifierFromRef(AstNode n) { + result = n.(NameExpr).getIdentifier() + or + result = n.(NamePattern).getIdentifier() + or + result = n.(MemberAccessExpr).getMember() + or + result = n.(NamedTypeExpr).getName() +} + +NameBindingNode getNodeFromRef(AstNode n) { + result.isIdentifier(getIdentifierFromRef(n)) + or + result.isBulkImport(n) +} + +NameBindingNode getModuleNodeFromFile(File f) { + exists(ModuleScopeRepr mod | + mod.getAnIncludedFile() = f and + result.isModuleScopeNode(mod) + ) +} + +/** Gets the name-binding node associated with the given uncertain scope node. */ +private NameBindingNode getNodeFromUncertainScope(AstNode n) { + exists(ClassLikeDeclaration cls | + n = cls.getAMember() and // note: must align with LocalNameBindingInput::uncertainScope + result.isLocalNamespace(cls) + ) + or + result.isLocalNamespace(n) +} + +predicate readStep(NameBindingNode node1, string name, NameBindingNode node2) { + exists(MemberAccessExpr expr | + node1 = getNodeFromRef(expr.getBase()) and + name = expr.getMember().getValue() and + node2 = getNodeFromRef(expr) + ) + or + exists(NamedTypeExpr expr | + node1 = getNodeFromRef(expr.getQualifier()) and + name = expr.getName().getValue() and + node2 = getNodeFromRef(expr) + ) + or + exists(PotentialLocalNameAccess access | + name = access.getName() and + node1 = getNodeFromUncertainScope(LocalNameBindingOutput::getAnUncertainScope(access, name)) and + node2.isIdentifier(access) + ) + or + exists(NameExpr expr | + isImportPrefix(expr) and + node1.isModuleRoot() and + name = expr.getIdentifier().getValue() and + node2 = getNodeFromRef(expr) + ) +} + +predicate storeStep(NameBindingNode node1, string name, NameBindingNode node2) { + exists(ClassLikeDeclaration cls, Member member, NameDeclaration nameDecl | + member = cls.getAMember() and + not isInstanceMember(member) and + not isPrivateToLocalScope(member) and + nameDecl.getDeclaration() = member + | + node1.isIdentifier(nameDecl) and + name = nameDecl.getName() and + node2.isExportedNamespace(cls) + ) + or + exists(TopLevel top, Stmt stmt, NameDeclaration nameDecl | + stmt = top.getBody().getAStmt() and + not isPrivateToLocalScope(stmt) and + nameDecl.getDeclaration() = stmt + | + node1.isIdentifier(nameDecl) and + name = nameDecl.getName() and + node2 = getModuleNodeFromFile(top.getFile()) + ) + or + exists(ModuleScopeRepr mod | + node1.isModuleScopeNode(mod) and + mod.hasImportableName(name) and + node2.isModuleRoot() + ) +} + +predicate valueStep(NameBindingNode node1, NameBindingNode node2) { + exists(PotentialLocalNameAccess access | + access.isDeclarationSite() and + node1.isIdentifier(access) and + node2.isLocalName(access.getLocalName()) + or + node1.isLocalName(access.getLocalName()) and + node2.isIdentifier(access) + ) + or + exists(ClassLikeDeclaration cls | + node1.isExportedNamespace(cls) and + node2.isIdentifier(cls.getName()) + ) + or + exists(ClassLikeDeclaration cls | + node1.isExportedNamespace(cls) and + node2.isLocalNamespace(cls) + ) + or + exists(TopLevel top | + node1 = getModuleNodeFromFile(top.getFile()) and + node2.isLocalNamespace(top.getBody()) // implicitly import own module + ) + or + exists(ImportDeclaration imprt | + node1 = getNodeFromRef(imprt.getImportedExpr()) and + node2 = getNodeFromRef(imprt.getPattern()) + ) + or + exists(BulkImportingPattern p, AstNode scope, AstNode declaration | + bindingContext(p, scope, declaration) and + node1 = getNodeFromRef(p) + | + node2 = getNodeFromUncertainScope(scope) + or + // Bulk re-exporting declarations + exists(TopLevel top | + declaration = top.getBody().getAStmt() and + not isPrivateToLocalScope(declaration) and + node2 = getModuleNodeFromFile(top.getFile()) + ) + ) + or + exists(NamePattern p | + node1 = getNodeFromRef(p) and + node2 = getNodeFromRef(p.getSubPattern()) + ) +} + +private predicate isImportPrefix(Expr e) { + e = any(ImportDeclaration impr).getImportedExpr() + or + exists(MemberAccessExpr member | + isImportPrefix(member) and + e = member.getBase() + ) +} + +predicate inheritanceStep(NameBindingNode supertype, NameBindingNode subtype) { + exists(ClassLikeDeclaration cls, BaseType base | + base = cls.getABaseType() and + supertype = getNodeFromRef(base.getType()) and + subtype.isExportedNamespace(cls) + ) +} + +signature module TrackInputSig { + /** Holds if the forward-flow of `node` should be tracked. */ + predicate shouldTrack(NameBindingNode node); + + default predicate additionalValueStep(NameBindingNode node1, NameBindingNode node2) { none() } +} + +/** Creates a module for tracking flow through the name-binding graph. */ +module Track { + private import Input + + /** Gets a name-binding node to which `node` can flow. */ + NameBindingNode track(NameBindingNode node) { + shouldTrack(node) and + result = node + or + exists(NameBindingNode prev | prev = track(node) | valueStepEx(prev, result)) + } + + /** Holds if there is an effective value step `node1 -> node2`. */ + pragma[inline] + private predicate valueStepEx(NameBindingNode node1, NameBindingNode node2) { + valueStep(node1, node2) + or + derivedStoreReadStep(node1, node2) + or + additionalValueStep(node1, node2) + } +} + +/** + * Holds if `node1 -> node2` is derived by combining a store and a read step. + */ +pragma[nomagic] +private predicate derivedStoreReadStep(NameBindingNode node1, NameBindingNode node2) { + exists(NamespaceNode namespace, string name | + node1 = namespace.getMember(name) and + readStep(namespace.ref(), name, node2) + ) +} + +/** A name-binding node that has members. */ +class NamespaceNode extends NameBindingNode { + NamespaceNode() { storeStep(_, _, this) or inheritanceStep(_, this) } + + /** Gets a name-binding node that may refer to this namespace. */ + NameBindingNode ref() { result = TrackNamespace::track(this) } + + /** Gets an own (non-inherited) member of this namespace of the given name. */ + NameBindingNode getOwnMember(string name) { storeStep(result, name, this) } + + /** Holds if this namespace has an own-member of the given name */ + predicate hasOwnMember(string name) { exists(this.getOwnMember(name)) } + + /** Gets a namespace from which this namespace inherits directly. */ + NamespaceNode getAnInheritanceParent() { inheritanceStep(result.ref(), this) } + + /** Gets a namespace that directly inherits from this one. */ + NamespaceNode getAnInheritanceChild() { result.getAnInheritanceParent() = this } + + /** Gets a member of this namespace of the given name. */ + pragma[nomagic] + NameBindingNode getMember(string name) { + result = this.getOwnMember(name) + or + not this.hasOwnMember(name) and + result = this.getAnInheritanceParent().getMember(name) + } +} + +private module TrackNamespaceInput implements TrackInputSig { + predicate shouldTrack(NameBindingNode node) { node instanceof NamespaceNode } + + predicate additionalValueStep(NameBindingNode node1, NameBindingNode node2) { + // Namespace-tracking goes through aliases, but declaration-tracking does not + exists(TypeAliasDeclaration decl | + node1 = getNodeFromRef(decl.getType()) and + node2.isIdentifier(decl.getName()) + ) + } +} + +private module TrackNamespace = Track; + +/** + * Holds if `decl` is a trivial local alias for an imported name. + * + * Declaration-tracking usually stops at type-aliases, but trivial aliases + * will be passed through. + */ +predicate isTrivialNameAlias(NameDeclaration decl) { + exists(ImportDeclaration imprt | + decl = getIdentifierFromRef(imprt.getPattern()) and + decl.getName() = getIdentifierFromRef(imprt.getImportedExpr()).getValue() + ) +} + +private module TrackNameDeclarationInput implements TrackInputSig { + predicate shouldTrack(NameBindingNode node) { + exists(NameDeclaration decl | + node.isIdentifier(decl) and + not isTrivialNameAlias(decl) + ) + } +} + +private module TrackNameDeclaration = Track; + +/** Gets a name-binding node that may refer to the given declaration. */ +NameBindingNode trackNameDeclaration(NameDeclaration decl) { + exists(NameBindingNode start | + start.isIdentifier(decl) and + result = TrackNameDeclaration::track(start) + ) +} + +/** Holds if `node` should be included in the debug view. */ +private signature predicate relevantFileSig(File node); + +module DebugGraph { + private predicate relevantNode(NameBindingNode node) { + relevantFile(node.getLocation().getFile()) + } + + query predicate nodes(NameBindingNode node, string key, string value) { + relevantNode(node) and + key = "semmle.label" and + value = node.toString() + } + + query predicate edges(NameBindingNode node1, NameBindingNode node2, string key, string value) { + key = "semmle.label" and + ( + valueStep(node1, node2) and value = "" + or + exists(string name | + readStep(node1, name, node2) and + value = "read(" + name + ")" + or + storeStep(node1, name, node2) and + value = "store(" + name + ")" + ) + or + inheritanceStep(node1, node2) and + value = "inheritedBy" + ) + } +} diff --git a/unified/ql/lib/codeql/unified/internal/dev/debugScopeGraph.ql b/unified/ql/lib/codeql/unified/internal/dev/debugLocalNameBindingGraph.ql similarity index 72% rename from unified/ql/lib/codeql/unified/internal/dev/debugScopeGraph.ql rename to unified/ql/lib/codeql/unified/internal/dev/debugLocalNameBindingGraph.ql index a77f8912d6fa..71887f476f16 100644 --- a/unified/ql/lib/codeql/unified/internal/dev/debugScopeGraph.ql +++ b/unified/ql/lib/codeql/unified/internal/dev/debugLocalNameBindingGraph.ql @@ -1,8 +1,8 @@ /** - * @name Debug scope graph - * @description Renders the graph used to perform local variable lookups + * @name Debug local name-binding graph + * @description Renders the graph used to perform local name lookups * @kind graph - * @id unified/debug-scope-graph + * @id unified/debug-local-name-binding-graph */ private import unified diff --git a/unified/ql/lib/codeql/unified/internal/dev/debugStaticNameBindingGraph.ql b/unified/ql/lib/codeql/unified/internal/dev/debugStaticNameBindingGraph.ql new file mode 100644 index 000000000000..740943fc66db --- /dev/null +++ b/unified/ql/lib/codeql/unified/internal/dev/debugStaticNameBindingGraph.ql @@ -0,0 +1,16 @@ +/** + * @name Debug static name-binding graph + * @description Renders the graph used to perform static name lookups + * @kind graph + * @id unified/debug-static-name-binding-graph + */ + +private import unified +private import codeql.unified.internal.StaticNameBinding + +/** + * Holds if graphs related to `file` should be shown in the graph. + */ +predicate relevantFile(File file) { file.getBaseName() = "test.swift" } + +import DebugGraph diff --git a/unified/ql/lib/unified.dbscheme b/unified/ql/lib/unified.dbscheme index 8306c3fcf0c7..c36721515d5c 100644 --- a/unified/ql/lib/unified.dbscheme +++ b/unified/ql/lib/unified.dbscheme @@ -687,6 +687,11 @@ unified_name_pattern_modifier( unique int modifier: @unified_token_modifier ref ); +unified_name_pattern_sub_pattern( + unique int unified_name_pattern: @unified_name_pattern ref, + unique int sub_pattern: @unified_pattern ref +); + unified_name_pattern_def( unique int id: @unified_name_pattern, int identifier: @unified_token_identifier ref diff --git a/unified/ql/lib/utils/test/CommentUtil.qll b/unified/ql/lib/utils/test/CommentUtil.qll new file mode 100644 index 000000000000..bd6f887a4010 --- /dev/null +++ b/unified/ql/lib/utils/test/CommentUtil.qll @@ -0,0 +1,20 @@ +private import unified + +/** Holds if a comment with `text` appears at `filepath:line`, excluding the text in a `$` section. */ +predicate plainCommentAt(string filepath, int line, string text) { + exists(Comment comment | + comment.getLocation().hasLocationInfo(filepath, line, _, _, _) and + text = comment.getCommentText().regexpReplaceAll("\\$([^/]|/[^/])*", "") + ) +} + +/** Holds if a `key=value` comment appears on `filepath:line` (not in the `$` section). */ +predicate keyValueCommentAt(string filepath, int line, string key, string value) { + exists(string text, string regexp, string match | + plainCommentAt(filepath, line, text) and + regexp = "(\\w+)=([\\w.0-9]+)" and + match = text.regexpFind(regexp, _, _) and + key = match.regexpCapture(regexp, 1) and + value = match.regexpCapture(regexp, 2) + ) +} diff --git a/unified/ql/test/library-tests/BasicTest/strings.swift b/unified/ql/test/library-tests/BasicTest/strings.swift new file mode 100644 index 000000000000..db9ae84e980b --- /dev/null +++ b/unified/ql/test/library-tests/BasicTest/strings.swift @@ -0,0 +1 @@ +let x = "hello" diff --git a/unified/ql/test/library-tests/BasicTest/test.expected b/unified/ql/test/library-tests/BasicTest/test.expected index b9f4eafe8653..301cd90ca2a5 100644 --- a/unified/ql/test/library-tests/BasicTest/test.expected +++ b/unified/ql/test/library-tests/BasicTest/test.expected @@ -35,3 +35,5 @@ nameExpr | test.swift:87:38:87:43 | NameExpr | values | | test.swift:87:49:87:57 | NameExpr | transform | unsupported +stringValue +| strings.swift:1:9:1:15 | "hello" | "hello" | diff --git a/unified/ql/test/library-tests/BasicTest/test.ql b/unified/ql/test/library-tests/BasicTest/test.ql index 5e70f9303687..8e30af381d61 100644 --- a/unified/ql/test/library-tests/BasicTest/test.ql +++ b/unified/ql/test/library-tests/BasicTest/test.ql @@ -3,3 +3,5 @@ import unified query predicate nameExpr(NameExpr node, string value) { value = node.getIdentifier().getValue() } query predicate unsupported(UnsupportedNode node, string value) { value = node.getValue() } + +query predicate stringValue(StringLiteral e, string value) { value = e.getValue() } diff --git a/unified/ql/test/library-tests/local-name-binding/test.ql b/unified/ql/test/library-tests/local-name-binding/test.ql index 0fcae36dd31e..e430f16ba48d 100644 --- a/unified/ql/test/library-tests/local-name-binding/test.ql +++ b/unified/ql/test/library-tests/local-name-binding/test.ql @@ -1,26 +1,8 @@ import unified import utils.test.InlineExpectationsTest +import utils.test.CommentUtil import codeql.unified.internal.LocalNameBinding -/** Holds if a comment with `text` appears at `filepath:line`, excluding the text in a `$` section. */ -predicate plainCommentAt(string filepath, int line, string text) { - exists(Comment comment | - comment.getLocation().hasLocationInfo(filepath, line, _, _, _) and - text = comment.getCommentText().regexpReplaceAll("\\$([^/]|/[^/])*", "") - ) -} - -/** Holds if a `key=value` comment appears on `filepath:line` (not in the `$` section). */ -predicate keyValueCommentAt(string filepath, int line, string key, string value) { - exists(string text, string regexp, string match | - plainCommentAt(filepath, line, text) and - regexp = "(\\w+)=([\\w.]+)" and - match = text.regexpFind(regexp, _, _) and - key = match.regexpCapture(regexp, 1) and - value = match.regexpCapture(regexp, 2) - ) -} - module VariableAccessTest implements TestSig { string getARelevantTag() { result = "access" } diff --git a/unified/ql/test/library-tests/static-name-binding/inheritance.swift b/unified/ql/test/library-tests/static-name-binding/inheritance.swift new file mode 100644 index 000000000000..fcf1967d4842 --- /dev/null +++ b/unified/ql/test/library-tests/static-name-binding/inheritance.swift @@ -0,0 +1,19 @@ +class A { + class B { + class C {} + } +} + +class D: A {} // $ access=A +class E: D.B {} // $ access=D access=A.B + +// Members of base classes can be accessed through derived classes +func t1() { + let x1: D = nil; // $ access=D + let x2: D.B = nil; // $ access=D access=A.B + let x3: D.B.C = nil; // $ access=D access=A.B access=A.B.C + + // The base class of 'E' is itself resolved through inheritance + let x4: E = nil; // $ access=E + let x5: E.C = nil; // $ access=E access=A.B.C +} diff --git a/unified/ql/test/library-tests/static-name-binding/package1/Package.swift b/unified/ql/test/library-tests/static-name-binding/package1/Package.swift new file mode 100644 index 000000000000..0789887c4b4c --- /dev/null +++ b/unified/ql/test/library-tests/static-name-binding/package1/Package.swift @@ -0,0 +1,11 @@ +// swift-tools-version: 5.9 + +import PackageDescription + +let package = Package( + name: "Package1", + targets: [ + .target(name: "Target1"), + .target(name: "Target2"), + ] +) diff --git a/unified/ql/test/library-tests/static-name-binding/package1/Sources/Target1/File1.swift b/unified/ql/test/library-tests/static-name-binding/package1/Sources/Target1/File1.swift new file mode 100644 index 000000000000..6e807bcf5dba --- /dev/null +++ b/unified/ql/test/library-tests/static-name-binding/package1/Sources/Target1/File1.swift @@ -0,0 +1,2 @@ +let x: A; // $ access=Target1.A +let y: Target2.A; // not a valid reference diff --git a/unified/ql/test/library-tests/static-name-binding/package1/Sources/Target1/File2.swift b/unified/ql/test/library-tests/static-name-binding/package1/Sources/Target1/File2.swift new file mode 100644 index 000000000000..62dbfcbf5c04 --- /dev/null +++ b/unified/ql/test/library-tests/static-name-binding/package1/Sources/Target1/File2.swift @@ -0,0 +1,5 @@ +class A {} // name=Target1.A + +private import class Target2.B // $ access=Target2.B + +private let x: B.C; // $ access=Target2.B access=Target2.B.C diff --git a/unified/ql/test/library-tests/static-name-binding/package1/Sources/Target1/File3.swift b/unified/ql/test/library-tests/static-name-binding/package1/Sources/Target1/File3.swift new file mode 100644 index 000000000000..7d06b0852ef8 --- /dev/null +++ b/unified/ql/test/library-tests/static-name-binding/package1/Sources/Target1/File3.swift @@ -0,0 +1,4 @@ +import Target2 + +private let x1: B.C; // $ access=Target2.B access=Target2.B.C +private let x2: Target2.B.C; // $ access=Target2.B access=Target2.B.C diff --git a/unified/ql/test/library-tests/static-name-binding/package1/Sources/Target2/File3.swift b/unified/ql/test/library-tests/static-name-binding/package1/Sources/Target2/File3.swift new file mode 100644 index 000000000000..8309118a867b --- /dev/null +++ b/unified/ql/test/library-tests/static-name-binding/package1/Sources/Target2/File3.swift @@ -0,0 +1,5 @@ +public class A {} // name=Target2.A + +public class B { // name=Target2.B + public class C {} // name=Target2.B.C +} diff --git a/unified/ql/test/library-tests/static-name-binding/test.expected b/unified/ql/test/library-tests/static-name-binding/test.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/unified/ql/test/library-tests/static-name-binding/test.ql b/unified/ql/test/library-tests/static-name-binding/test.ql new file mode 100644 index 000000000000..272606e2bd20 --- /dev/null +++ b/unified/ql/test/library-tests/static-name-binding/test.ql @@ -0,0 +1,51 @@ +import unified +import utils.test.InlineExpectationsTest +import utils.test.CommentUtil +import codeql.unified.internal.StaticNameBinding + +module StaticDeclAccess implements TestSig { + string getARelevantTag() { result = "access" } + + private string deriveClassName(ClassLikeDeclaration cls) { + not exists(cls.getParent().getEnclosingClass()) and + result = cls.getName().getValue() + or + result = deriveClassName(cls.getParent().getEnclosingClass()) + "." + cls.getName().getValue() + } + + private string defaultName(NameDeclaration decl) { + exists(ClassLikeDeclaration cls | + decl.getDeclaration() = cls.getAMember() and + result = deriveClassName(cls) + "." + decl.getName() + ) + or + not decl.getDeclaration() = any(ClassLikeDeclaration cls).getAMember() and + result = decl.getName() + } + + additional predicate declAt(NameDeclaration v, string filepath, int line) { + v.getLocation().hasLocationInfo(filepath, line, _, _, _) + } + + private predicate decl(NameDeclaration v, string alias) { + exists(string filepath, int line | declAt(v, filepath, line) | + keyValueCommentAt(filepath, line, "name", alias) + or + not keyValueCommentAt(filepath, line, "name", _) and + alias = defaultName(v) + ) + } + + predicate hasActualResult(Location location, string element, string tag, string value) { + exists(NameDeclaration decl, Identifier access | + access = trackNameDeclaration(decl).asIdentifier() and + not access instanceof NameDeclaration and + location = access.getLocation() and + element = access.toString() and + decl(decl, value) and + tag = "access" + ) + } +} + +import MakeTest diff --git a/unified/ql/test/library-tests/static-name-binding/test.swift b/unified/ql/test/library-tests/static-name-binding/test.swift new file mode 100644 index 000000000000..27453ec34351 --- /dev/null +++ b/unified/ql/test/library-tests/static-name-binding/test.swift @@ -0,0 +1,53 @@ +class B {} // name=top.B + +class A { + class B { + class C {} + } +} + +let x1: B = nil; // $ access=top.B +let x2: A.B = nil; // $ access=A access=A.B +let x3: A.B.C = nil; // $ access=A access=A.B access=A.B.C + +class D { + let x1: B = nil; // $ access=top.B + let x2: A.B = nil; // $ access=A access=A.B + let x3: A.B.C = nil; // $ access=A access=A.B access=A.B.C + + func member() { + let x1: B = nil; // $ access=top.B + let x2: A.B = nil; // $ access=A access=A.B + let x3: A.B.C = nil; // $ access=A access=A.B access=A.B.C + } +} + +class E { + class A { + class B { + } + } + + let x1: A = nil; // $ access=E.A + let x2: A.B = nil; // $ access=E.A access=E.A.B +} + +class F { + static let field = 1 + static let (a,b) = (1,2) + + static func foo() { + F.field // $ access=F access=F.field + F.a // $ access=F access=F.a + F.b // $ access=F access=F.b + } +} + +typealias G = A // $ access=A + +// Members can be accessed through aliases, but references to the alias itself do not bypass the alias. +class H { + let x1: G = nil; // $ access=G + let x2: G.B = nil; // $ access=G access=A.B + let x3: G.B.C = nil; // $ access=G access=A.B access=A.B.C +} diff --git a/unified/ql/test/library-tests/static-name-binding/unqualified-access.swift b/unified/ql/test/library-tests/static-name-binding/unqualified-access.swift new file mode 100644 index 000000000000..732e65563b3b --- /dev/null +++ b/unified/ql/test/library-tests/static-name-binding/unqualified-access.swift @@ -0,0 +1,20 @@ +class A { + class B { + class C {} + } +} + +class ASub : A { // $ access=A + let x1: B = nil; // $ access=A.B + let x2: B.C = nil; // $ access=A.B access=A.B.C + + class BSub : B { // $ access=A.B + let x3: B = nil; // $ access=A.B + let x4: C = nil; // $ access=A.B.C + } + + class BSub2 : B { // $ access=A.B + class C {} // shadow the inherited C + let x5: C = nil; // $ access=ASub.BSub2.C + } +}