diff --git a/unified/ql/lib/codeql/unified/internal/AstPluginSwift.qll b/unified/ql/lib/codeql/unified/internal/AstPluginSwift.qll index 801055188218..e277e4fec074 100644 --- a/unified/ql/lib/codeql/unified/internal/AstPluginSwift.qll +++ b/unified/ql/lib/codeql/unified/internal/AstPluginSwift.qll @@ -10,6 +10,12 @@ private class AstPluginSwift extends AstPlugin { exists(f) and result = "func" } + bindingset[c] + override string getConstructorDeclarationKeyword(ConstructorDeclaration c) { + c.hasModifier(result) and + result = "convenience" + } + override string getClassLikeDeclarationKeyword(ClassLikeDeclaration cls) { cls.hasModifier(result) and result in ["class", "struct", "enum", "actor", "extension", "protocol"] diff --git a/unified/ql/lib/codeql/unified/internal/CallableEx.qll b/unified/ql/lib/codeql/unified/internal/CallableEx.qll new file mode 100644 index 000000000000..4e6a9607b31d --- /dev/null +++ b/unified/ql/lib/codeql/unified/internal/CallableEx.qll @@ -0,0 +1,255 @@ +/** + * Provides logic for synthesizing callables and parameters. + */ + +private import unified +private import CallableExPlugin +private import NameBinding + +private newtype TCallableEx = + TAstCallableEx(Callable c) or + TDefaultConstructor(ClassLikeDeclaration cls, string name) { + mayHaveConstructor(cls, name) and + not cls.getAMember() instanceof ConstructorDeclaration and + not exists(ClassLikeDeclaration base | + base.getNameNode() = getStaticBindingTargetFromRef(cls.getABaseType().getType()) and + mayHaveConstructor(base, _) + ) + } + +private newtype TParameterEx = + TAstParameter(Parameter p) or + TImplicitReceiverParameter(Callable c) { + exists(LocalVariable v | v.isImplicitReceiverParameter(c)) + } or + TDefaultConstructorParameter(ClassLikeDeclaration cls, int i, string name) { + defaultConstructorParameter(cls, i, name) + } + +final class CallableEx = CallableExImpl; + +/** + * A callable from source code or a synthesized callable. + */ +abstract private class CallableExImpl extends TCallableEx { + /** + * Gets the source code callable that this entity represents, if any. + */ + Callable asCallable() { this = TAstCallableEx(result) } + + /** + * Holds if this entity represents a default constructor for `cls`, for example a + * parameterless Swift `class` constructor or a Swift `struct` constructor + * with parameters for relevant fields. + */ + predicate isDefaultConstructor(ClassLikeDeclaration cls) { this = TDefaultConstructor(cls, _) } + + /** + * Holds if this entity represents a constructor for `cls`. + */ + predicate isConstructor(ClassLikeDeclaration cls, boolean inheritable) { + exists(ConstructorDeclaration c | + c = this.asCallable() and + c = cls.getAMember() and + if isInheritableMember(c) then inheritable = true else inheritable = false + ) + or + this.isDefaultConstructor(cls) and inheritable = true + } + + /** + * Holds if this entity is a direct member of `cls`. + */ + predicate isMemberOf(ClassLikeDeclaration cls) { + this.asCallable() = cls.getAMember() + or + this.isConstructor(cls, _) + } + + /** Gets the `i`-th type parameter of this callable. */ + abstract TypeParameter getTypeParameter(int i); + + /** + * Gets the `i`-th parameter of this callable. Implicit receiver parameters are + * at index `0`, and all other parameters start at index `1`. + */ + abstract ParameterEx getParameter(int i); + + /** Gets a parameter of this callable. */ + final ParameterEx getAParameter() { result = this.getParameter(_) } + + /** Gets the declared return type of this callable, if any. */ + abstract Expr getReturnType(); + + /** Gets the body of this callable, if any. */ + abstract AstNode getBody(); + + /** Gets the name node of this callable, if any. */ + abstract Identifier getNameNode(); + + /** Gets the name of this callable. */ + abstract string getName(); + + /** Gets a textual representation of this callable. */ + abstract string toString(); + + /** Gets the location of this callable. */ + abstract Location getLocation(); +} + +private class AstCallableEx extends CallableExImpl, TAstCallableEx { + Callable c; + + AstCallableEx() { this = TAstCallableEx(c) } + + override TypeParameter getTypeParameter(int i) { + result = c.(FunctionDeclaration).getTypeParameter(i) + } + + override ParameterEx getParameter(int i) { + result.isImplicitReceiverParameter(c) and + i = 0 + or + exists(int j, Parameter p | + result.asParameter() = p and + i = j + 1 + | + p = c.(FunctionDeclaration).getParameter(j) + or + p = c.(ConstructorDeclaration).getParameter(j) + or + p = c.(FunctionExpr).getParameter(j) + or + p = c.(AccessorDeclaration).getParameter(j) + ) + } + + override Expr getReturnType() { + result = c.(FunctionDeclaration).getReturnType() + or + result = c.(FunctionExpr).getReturnType() + } + + override AstNode getBody() { result = c.getBody() } + + override Identifier getNameNode() { + result = c.(AccessorDeclaration).getNameNode() + or + result = c.(ConstructorDeclaration).getNameNode() + or + result = c.(FunctionDeclaration).getNameNode() + } + + override string getName() { + result = this.getNameNode().getValue() + or + c instanceof DestructorDeclaration and + result = "" + or + c instanceof InitializerDeclaration and + result = "" + } + + override string toString() { result = c.toString() } + + override Location getLocation() { result = c.getLocation() } +} + +private class DefaultConstructor extends CallableExImpl, TDefaultConstructor { + ClassLikeDeclaration c; + string name; + + DefaultConstructor() { this = TDefaultConstructor(c, name) } + + override TypeParameter getTypeParameter(int i) { none() } + + override ParameterEx getParameter(int i) { result = TDefaultConstructorParameter(c, i, _) } + + override Expr getReturnType() { none() } + + override AstNode getBody() { none() } + + override Identifier getNameNode() { none() } + + override string getName() { result = name } + + override string toString() { result = c.getName() + " [default constructor]" } + + override Location getLocation() { result = c.getLocation() } +} + +final class ParameterEx = ParameterExImpl; + +/** + * A parameter from source code or a synthesized parameter. + */ +abstract private class ParameterExImpl extends TParameterEx { + /** + * Gets the source code parameter that this entity represents, if any. + */ + Parameter asParameter() { this = TAstParameter(result) } + + /** + * Holds if this entity represents a regular parameter from source code. + */ + predicate isParameter() { exists(this.asParameter()) } + + /** + * Holds if this entity represents an implicit receiver parameter of `c`. + */ + predicate isImplicitReceiverParameter(Callable c) { this = TImplicitReceiverParameter(c) } + + /** + * Holds if this entity represents the `i`th parameter of the default + * constructor of `cls` with name `name`. + */ + predicate isDefaultConstructorParameter(ClassLikeDeclaration cls, int i, string name) { + this = TDefaultConstructorParameter(cls, i, name) + } + + /** Gets the callable that this parameter belongs to. */ + CallableEx getCallable() { this = result.getAParameter() } + + /** Gets a textual representation of this parameter. */ + abstract string toString(); + + /** Gets the location of this parameter. */ + abstract Location getLocation(); +} + +private class AstParameterEx extends ParameterExImpl, TAstParameter { + Parameter p; + + AstParameterEx() { this = TAstParameter(p) } + + override string toString() { result = p.toString() } + + override Location getLocation() { result = p.getLocation() } +} + +private class ImplicitReceiverParameterEx extends ParameterExImpl, TImplicitReceiverParameter { + Callable c; + + ImplicitReceiverParameterEx() { this = TImplicitReceiverParameter(c) } + + override string toString() { + exists(LocalVariable v | + v.isImplicitReceiverParameter(c) and + result = v.getName() + ) + } + + override Location getLocation() { result = c.getLocation() } +} + +private class DefaultConstructorParameterEx extends ParameterExImpl, TDefaultConstructorParameter { + ClassLikeDeclaration c; + int i; + string name; + + DefaultConstructorParameterEx() { this = TDefaultConstructorParameter(c, i, name) } + + override string toString() { result = name + " [" + c.getName() + " default constructor]" } + + override Location getLocation() { result = c.getLocation() } +} diff --git a/unified/ql/lib/codeql/unified/internal/CallableExPlugin.qll b/unified/ql/lib/codeql/unified/internal/CallableExPlugin.qll new file mode 100644 index 000000000000..91d0c16c3dbc --- /dev/null +++ b/unified/ql/lib/codeql/unified/internal/CallableExPlugin.qll @@ -0,0 +1,32 @@ +private import unified +private import codeql.util.Unit + +private module Plugins { + private import codeql.unified.internal.CallableExPluginSwift +} + +/** Extension point for language-specific inputs to `CallableEx.qll`. */ +class CallableExPlugin extends Unit { + /** + * Holds if the class-like declaration `cls` may have a constructor with the + * given name. + */ + predicate mayHaveConstructor(ClassLikeDeclaration cls, string name) { none() } + + /** + * Holds if a default constructor for `cls` would need to have a parameter + * at index `i` with the given name. `i = 0` should be the implicit receiver + * parameter. + */ + bindingset[cls] + predicate defaultConstructorParameter(ClassLikeDeclaration cls, int i, string name) { none() } +} + +predicate mayHaveConstructor(ClassLikeDeclaration cls, string name) { + any(CallableExPlugin p).mayHaveConstructor(cls, name) +} + +bindingset[cls] +predicate defaultConstructorParameter(ClassLikeDeclaration cls, int i, string name) { + any(CallableExPlugin p).defaultConstructorParameter(cls, i, name) +} diff --git a/unified/ql/lib/codeql/unified/internal/CallableExPluginSwift.qll b/unified/ql/lib/codeql/unified/internal/CallableExPluginSwift.qll new file mode 100644 index 000000000000..56225ff62711 --- /dev/null +++ b/unified/ql/lib/codeql/unified/internal/CallableExPluginSwift.qll @@ -0,0 +1,32 @@ +private import unified +private import codeql.unified.internal.CallableExPlugin + +class CallableExPluginSwift extends CallableExPlugin { + override predicate mayHaveConstructor(ClassLikeDeclaration cls, string name) { + cls.hasModifier(["actor", "class", "struct"]) and + name = "init" + } + + bindingset[cls] + override predicate defaultConstructorParameter(ClassLikeDeclaration cls, int i, string name) { + i = 0 and + name = "self" + or + cls.hasModifier("struct") and + name = + rank[i](VariableDeclaration decl, int j, string s | + decl = cls.getMember(j) and + not decl.hasModifier("static") and + ( + // if `decl` has an initializer then this parameter has that initializer as a default value + decl.hasModifier("var") + or + decl.hasModifier("let") and + not exists(decl.getValue()) + ) and + s = decl.getPattern().(Identifier).getValue() + | + s order by j + ) + } +} diff --git a/unified/ql/test/library-tests/constructors/constructors.swift b/unified/ql/test/library-tests/constructors/constructors.swift new file mode 100644 index 000000000000..2638bbd91d9d --- /dev/null +++ b/unified/ql/test/library-tests/constructors/constructors.swift @@ -0,0 +1,45 @@ +class C1 {} // implicit parameterless `init` + +class C2: C1 {} // inherits `init` + +class C3 { + init() {} +} + +class C4: C3 { + init(_ x: Int) { + super.init() + } + + override convenience init() { + self.init(0) + } +} + +class C5: C4 {} // inherits `init` and `convenience init` + +class C6 { + init(x: T1, y: T2) {} + + private init(s: String) {} + + convenience init(x: T1) { + fatalError("Convenience initializer not implemented") + } +} + +class C7: C6 {} // inherits `init(x: T4, y: T3)` and `convenience init(x: T4)` + +class C8: C7 { // inherits `init(x: String, y: Int)` + convenience init(x: String) { + self.init(x: x, y: 0) + } +} + +struct S1 { // implicit `init(f1: Int = 0, f2: String, f4: Double)` + var f1: Int = 0 + var f2: String + let f3: Bool = false + let f4: Double + static var f5: Float = 0.0 +} diff --git a/unified/ql/test/library-tests/constructors/test.expected b/unified/ql/test/library-tests/constructors/test.expected new file mode 100644 index 000000000000..79070fac587c --- /dev/null +++ b/unified/ql/test/library-tests/constructors/test.expected @@ -0,0 +1,9 @@ +callableExDefaultConstructor +| constructors.swift:1:1:1:11 | C1 [default constructor] | +| constructors.swift:39:1:45:1 | S1 [default constructor] | +callableExDefaultConstructorParameter +| constructors.swift:1:1:1:11 | C1 [default constructor] | 0 | constructors.swift:1:1:1:11 | self [C1 default constructor] | +| constructors.swift:39:1:45:1 | S1 [default constructor] | 0 | constructors.swift:39:1:45:1 | self [S1 default constructor] | +| constructors.swift:39:1:45:1 | S1 [default constructor] | 1 | constructors.swift:39:1:45:1 | f1 [S1 default constructor] | +| constructors.swift:39:1:45:1 | S1 [default constructor] | 2 | constructors.swift:39:1:45:1 | f2 [S1 default constructor] | +| constructors.swift:39:1:45:1 | S1 [default constructor] | 3 | constructors.swift:39:1:45:1 | f4 [S1 default constructor] | diff --git a/unified/ql/test/library-tests/constructors/test.ql b/unified/ql/test/library-tests/constructors/test.ql new file mode 100644 index 000000000000..f5400e6e1db5 --- /dev/null +++ b/unified/ql/test/library-tests/constructors/test.ql @@ -0,0 +1,11 @@ +import unified +import codeql.unified.internal.CallableEx + +query predicate callableExDefaultConstructor(CallableEx c) { + c.isDefaultConstructor(any(ClassLikeDeclaration cls | cls.fromSource())) +} + +query predicate callableExDefaultConstructorParameter(CallableEx c, int i, ParameterEx p) { + callableExDefaultConstructor(c) and + p = c.getParameter(i) +}