Skip to content

Commit bc141a7

Browse files
authored
Merge pull request #22558 from asgerf/unified/extensions
Unified: Basic support for extensions
2 parents 3ac67c5 + 432f8c0 commit bc141a7

10 files changed

Lines changed: 156 additions & 10 deletions

File tree

unified/extractor/ast_types.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,7 @@ named:
478478
class_like_declaration:
479479
modifier*: modifier
480480
name_node?: identifier
481+
extension_target?: expr
481482
type_parameter*: type_parameter
482483
type_constraint*: type_constraint
483484
base_type*: base_type

unified/extractor/src/languages/swift/swift.rs

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1266,22 +1266,19 @@ fn translation_rules() -> Vec<Rule<SwiftContext>> {
12661266
base_type: {bases.into_iter().map(|ty| tree!((base_type type: {ty})))}
12671267
member: {members})
12681268
),
1269-
// An `extension Foo { … }` is likewise a `class_like_declaration`, named
1270-
// by the extended type. The extended type is captured opaquely (as its
1271-
// source text) so that qualified names (`extension String.Interpolation`,
1272-
// a `memberType`) name the declaration just like simple ones.
1269+
// An `extension Foo.Bar { … }` is likewise a `class_like_declaration`.
12731270
rule!(
12741271
(extensionDecl
12751272
extensionKeyword: @kind
12761273
modifiers: _* @mods
1277-
extendedType: @@name
1274+
extendedType: @extendedType
12781275
inheritanceClause: (inheritanceClause inheritedTypes: (inheritedType type: @bases)*)?
12791276
memberBlock: (memberBlock members: _* @members))
12801277
=>
12811278
(class_like_declaration
12821279
modifier: (modifier #{kind})
12831280
modifier: {mods}
1284-
name_node: (identifier #{name})
1281+
extension_target: {extendedType}
12851282
base_type: {bases.into_iter().map(|ty| tree!((base_type type: {ty})))}
12861283
member: {members})
12871284
),

unified/extractor/tests/corpus/swift/types/extension.output

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ top_level
7070
stmt:
7171
class_like_declaration
7272
modifier: modifier "extension"
73-
name_node: identifier "Int"
73+
extension_target: identifier "Int"
7474
member:
7575
function_declaration
7676
name_node: identifier "squared"

unified/ql/lib/codeql/unified/internal/Ast.qll

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -418,6 +418,11 @@ module Unified {
418418
/** Gets the node corresponding to the field `base_type`. */
419419
final F::BaseType getABaseType() { result = this.getBaseType(_) }
420420

421+
/** Gets the node corresponding to the field `extension_target`. */
422+
final F::Expr getExtensionTarget() {
423+
unified_class_like_declaration_extension_target(this, result)
424+
}
425+
421426
/** Gets the node corresponding to the field `member`. */
422427
final F::Member getMember(int i) { unified_class_like_declaration_member(this, i, result) }
423428

@@ -454,6 +459,7 @@ module Unified {
454459
/** Gets a field or child node of this node. */
455460
final override F::AstNode getAFieldOrChild() {
456461
unified_class_like_declaration_base_type(this, _, result) or
462+
unified_class_like_declaration_extension_target(this, result) or
457463
unified_class_like_declaration_member(this, _, result) or
458464
unified_class_like_declaration_modifier(this, _, result) or
459465
unified_class_like_declaration_name_node(this, result) or
@@ -1614,6 +1620,10 @@ module Unified {
16141620
or
16151621
result = node.(ClassLikeDeclaration).getBaseType(i) and name = "getBaseType"
16161622
or
1623+
result = node.(ClassLikeDeclaration).getExtensionTarget() and
1624+
i = -1 and
1625+
name = "getExtensionTarget"
1626+
or
16171627
result = node.(ClassLikeDeclaration).getMember(i) and name = "getMember"
16181628
or
16191629
result = node.(ClassLikeDeclaration).getModifier(i) and name = "getModifier"

unified/ql/lib/codeql/unified/internal/StaticNameBinding.qll

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,13 @@ predicate valueStep(NameBindingNode node1, NameBindingNode node2) {
262262
node2 = getNodeFromRef(p.getSubPattern())
263263
)
264264
or
265+
// Extensions have access to the members of the entity they extend.
266+
// TODO: The type parameters of the target type should also be in the local scope (for Swift).
267+
exists(ClassLikeDeclaration extension |
268+
node1 = getNodeFromRef(extension.getExtensionTarget()) and
269+
node2.isLocalNamespace(extension)
270+
)
271+
or
265272
FolderHeuristic::valueStep(node1, node2)
266273
or
267274
exists(ClassLikeDeclaration cls, LocalNameBindingOutput::ImplicitLocal self |
@@ -288,6 +295,13 @@ predicate inheritanceStep(NameBindingNode supertype, NameBindingNode subtype) {
288295
)
289296
}
290297

298+
predicate extensionStep(NameBindingNode extension, NameBindingNode targetClass) {
299+
exists(ClassLikeDeclaration cls |
300+
targetClass = getNodeFromRef(cls.getExtensionTarget()) and
301+
extension.isStaticMemberNamespace(cls)
302+
)
303+
}
304+
291305
signature module TrackInputSig {
292306
/** Holds if the forward-flow of `node` should be tracked. */
293307
predicate shouldTrack(NameBindingNode node);
@@ -368,6 +382,17 @@ class NamespaceNode extends NameBindingNode {
368382
/** If this is the instance namespace for a class, gets the corresponding static namespace. */
369383
NamespaceNode toStaticNamespace() { result.toInstanceNamespace() = this }
370384

385+
private NamespaceNode getAnExtension1() { extensionStep(result, this.ref()) }
386+
387+
/** Gets a namespace that is an extension (i.e. containing extension methods) of this node. */
388+
NamespaceNode getAnExtension() {
389+
result = this.getAnExtension1()
390+
or
391+
// `extensionStep` connects the static namespaces of classes.
392+
// Add the corresponding extension relation between the instance namespaces.
393+
result = this.toStaticNamespace().getAnExtension1().toInstanceNamespace()
394+
}
395+
371396
private NamespaceNode getAnInheritanceParent1() { inheritanceStep(result.ref(), this) }
372397

373398
/** Gets a namespace from which this namespace inherits directly. */
@@ -390,6 +415,8 @@ class NamespaceNode extends NameBindingNode {
390415
not this.hasOwnMember(name) and
391416
result = this.getAnInheritanceParent().getMember(name) and
392417
isInheritableMemberNode(result)
418+
or
419+
result = this.getAnExtension().getMember(name)
393420
}
394421
}
395422

@@ -486,6 +513,9 @@ module DebugGraph<relevantNodeSig/1 relevantNode> {
486513
or
487514
inheritanceStep(node1, node2) and
488515
value = "inheritedBy"
516+
or
517+
extensionStep(node1, node2) and
518+
value = "extensionOf"
489519
)
490520
}
491521
}
@@ -582,6 +612,17 @@ private module FolderHeuristic {
582612
}
583613
}
584614

615+
private ClassLikeDeclaration resolveExtensionTarget(ClassLikeDeclaration cls) {
616+
trackNameBinding(result.getNameNode()) = getNodeFromRef(cls.getExtensionTarget())
617+
}
618+
619+
private ClassLikeDeclaration tryResolveExtensionTarget(ClassLikeDeclaration cls) {
620+
result = resolveExtensionTarget(cls)
621+
or
622+
not exists(resolveExtensionTarget(cls)) and
623+
result = cls
624+
}
625+
585626
/**
586627
* Holds if `access` may resolve to `target` through the enclosing `accessingClass`.
587628
*
@@ -610,7 +651,8 @@ private predicate unqualifiedMemberAccessCand(
610651
// Resolved in an uncertain scope
611652
exists(NamespaceNode namespace, string name |
612653
name = access.getName() and
613-
accessingClass = LocalNameBindingOutput::getAnUncertainScope(access, name)
654+
accessingClass =
655+
tryResolveExtensionTarget(LocalNameBindingOutput::getAnUncertainScope(access, name))
614656
|
615657
instanceAccess = true and
616658
namespace.isInstanceMemberNamespace(accessingClass) and

unified/ql/lib/unified.dbscheme

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,11 @@ unified_class_like_declaration_base_type(
318318
unique int base_type: @unified_base_type ref
319319
);
320320

321+
unified_class_like_declaration_extension_target(
322+
unique int unified_class_like_declaration: @unified_class_like_declaration ref,
323+
unique int extension_target: @unified_expr ref
324+
);
325+
321326
#keyset[unified_class_like_declaration, index]
322327
unified_class_like_declaration_member(
323328
int unified_class_like_declaration: @unified_class_like_declaration ref,
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
class A {
2+
func ownMethod() {
3+
ownMethod() // $ access=A.ownMethod
4+
extensionMethod1() // $ access=A.extensionMethod1
5+
extensionMethod2() // $ access=A.extensionMethod2
6+
}
7+
}
8+
9+
extension A { // $ access=A
10+
func extensionMethod1() { // name=A.extensionMethod1
11+
ownMethod() // $ access=A.ownMethod
12+
extensionMethod1() // $ access=A.extensionMethod1
13+
extensionMethod2() // $ access=A.extensionMethod2
14+
}
15+
}
16+
17+
extension A { // $ access=A
18+
func extensionMethod2() { // name=A.extensionMethod2
19+
ownMethod() // $ access=A.ownMethod
20+
extensionMethod1() // $ access=A.extensionMethod1
21+
extensionMethod2() // $ access=A.extensionMethod2
22+
}
23+
}
24+
25+
class B {
26+
}
27+
28+
extension B { // $ access=B
29+
class C { // name=B.C
30+
class D {} // name=B.C.D
31+
}
32+
}
33+
extension B { // $ access=B
34+
class Nested : C { // $ access=B.C
35+
let x : D // $ access=B.C.D
36+
}
37+
}
38+
39+
// Protocol conformance through extension
40+
protocol Base {
41+
func baseMethod();
42+
func baseMethodNoImpl();
43+
func baseMethodDefaultImpl();
44+
}
45+
extension Base { // $ access=Base
46+
func baseMethodExt() {} // name=BaseImpl.baseMethodExt
47+
func baseMethodDefaultImpl() {} // name=BaseImpl.baseMethodDefaultImpl
48+
}
49+
class X {
50+
func xMethod() {
51+
baseMethod() // $ access=X.baseMethod
52+
baseMethodNoImpl() // $ access=Base.baseMethodNoImpl // with no visible implementation, just resolve to the signature
53+
baseMethodExt() // $ access=BaseImpl.baseMethodExt
54+
55+
// Static name binding may find multiple targets. Type inference should disambiguate.
56+
baseMethodDefaultImpl() // $ access=Base.baseMethodDefaultImpl access=BaseImpl.baseMethodDefaultImpl
57+
}
58+
}
59+
60+
extension X : Base { // $ access=X access=Base
61+
func baseMethod() {} // name=X.baseMethod
62+
}
63+
64+
class Y {
65+
func yMethod() {
66+
baseMethod() // $ access=Y.baseMethod
67+
baseMethodDefaultImpl() // $ access=Y.baseMethodDefaultImpl
68+
}
69+
}
70+
extension Y : Base { // $ access=Y access=Base
71+
func baseMethod() {} // name=Y.baseMethod
72+
func baseMethodDefaultImpl() {} // name=Y.baseMethodDefaultImpl
73+
}
74+
75+
// Type parameters of the extended type should be in scope in the extension.
76+
class GenericExtensionTarget<ExtensionTypeParameter> {}
77+
extension GenericExtensionTarget { // $ access=GenericExtensionTarget
78+
func useTypeParameter(_: ExtensionTypeParameter) {} // $ MISSING: access=ExtensionTypeParameter
79+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,8 @@
11
let x: A; // $ access=Target1.A
22
let y: Target2.A; // not a valid reference
3+
4+
public class ScopedExtensionTarget {
5+
func useExtensionFromUnimportedModule() {
6+
target2ExtensionMethod() // $ SPURIOUS: access=Target1.ScopedExtensionTarget.target2ExtensionMethod
7+
}
8+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
1+
import Target1
2+
13
public class A {} // name=Target2.A
24

35
public class B { // name=Target2.B
46
public class C {} // name=Target2.B.C
57
}
8+
9+
extension ScopedExtensionTarget { // $ access=ScopedExtensionTarget
10+
func target2ExtensionMethod() {} // name=Target1.ScopedExtensionTarget.target2ExtensionMethod
11+
}

unified/ql/test/library-tests/static-name-binding/test.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,5 +68,5 @@ protocol P { }
6868
extension H // $ access=H1
6969
: P { } // $ access=P
7070

71-
extension A.B.C // $ MISSING: access=A access=A.B access=A.B.C (`A.B.C` is currently parsed as a single identifier)
72-
: P { } // $ access=P
71+
extension A.B.C // $ access=A access=A.B access=A.B.C
72+
: P { } // $ access=P

0 commit comments

Comments
 (0)