Skip to content

Commit 2820e04

Browse files
authored
Merge pull request #22420 from asgerf/unified/instance-members
unified: Build instance-member namespace and resolve unqualified lookups
2 parents 04816f1 + 4185f75 commit 2820e04

10 files changed

Lines changed: 262 additions & 25 deletions

File tree

unified/ql/lib/codeql/Definitions.qll

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ private import codeql.unified.internal.StaticNameBinding
1010
*/
1111
cached
1212
predicate definitionOf(Identifier reference, NameDeclaration definition, string kind) {
13-
reference = trackNameDeclaration(definition).asIdentifier() and
13+
definition = getStaticBindingTarget(reference) and
1414
not reference instanceof NameDeclaration and
1515
kind = "name"
1616
}

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

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,20 +6,50 @@ private import codeql.unified.internal.NameBindingPlugin
66

77
/** Stats about identifiers that static name binding could resolve. */
88
module StaticNameResolutionStats implements EntityStatsSig {
9+
/**
10+
* Holds if `name` has been positively identified as referring to a value, so static name binding
11+
* is not expected to resolve its members.
12+
*/
13+
private predicate resolvesToValue(Identifier name) {
14+
exists(AstNode decl |
15+
decl = getStaticBindingTarget(name).getDeclaration() and
16+
not decl instanceof ClassLikeDeclaration and
17+
not decl instanceof TypeAliasDeclaration and
18+
not decl instanceof TypeParameter and
19+
not decl instanceof AssociatedTypeDeclaration
20+
)
21+
}
22+
23+
/**
24+
* Holds if name-binding for `expr` depends on type inference, and is thus not subject to static name binding.
25+
*
26+
* Usually this holds for qualified instance member accesses (`foo().x`) and leading-dot expressions (`.x`).
27+
*/
28+
private predicate memberAccessDependsOnTypeInference(MemberAccessExpr expr) {
29+
exists(Expr base | base = expr.getBase() |
30+
// Base expression resolves to a value, e.g. a field, variable, or function (for languages where functions are values).
31+
resolvesToValue(getIdentifierFromRef(base))
32+
or
33+
// Base expression is of a kind that is not subject to static name resolution, e.g. `foo().x`
34+
not exists(getIdentifierFromRef(base))
35+
or
36+
// Base expression is a confirmed to depend on type inference
37+
memberAccessDependsOnTypeInference(base)
38+
)
39+
}
40+
941
class Candidate extends Identifier {
1042
Candidate() {
11-
this = getIdentifierFromRef(_) and
43+
exists(AstNode ref |
44+
this = getIdentifierFromRef(ref) and
45+
not memberAccessDependsOnTypeInference(ref)
46+
) and
1247
not this instanceof NameDeclaration
13-
// TODO: exclude names we know are not static references, e.g. unqualified instance-field access,
14-
// currently blocked on getting static name binding to report this information.
1548
}
1649

1750
NameBindingNode getTarget() {
1851
(
19-
exists(NameDeclaration decl |
20-
result.isIdentifier(decl) and
21-
trackNameDeclaration(decl).isIdentifier(this)
22-
)
52+
result.asIdentifier() = getStaticBindingTarget(this)
2353
or
2454
result.isModuleScopeNode(_) and
2555
result.(NamespaceNode).ref().isIdentifier(this)

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ module Unified {
3030
not this instanceof ClassLikeDeclaration and
3131
result = this.getParent().getEnclosingClass()
3232
}
33+
34+
/** Gets the depth of this node in the AST. The root node has a depth of 0. */
35+
int getDepth() {
36+
not exists(this.getParent()) and result = 0
37+
or
38+
result = this.getParent().getDepth() + 1
39+
}
3340
}
3441

3542
/** An expression */

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,14 @@ class NameBindingPlugin extends Unit {
2525
*/
2626
bindingset[member, binding]
2727
predicate isPrivateToLocalScope(Stmt member, AstNode binding) { none() }
28+
29+
/**
30+
* Holds if `member` can be inherited by subclasses of `cls`.
31+
*
32+
* The caller has already restricted `member` to be a member of `cls`.
33+
*/
34+
bindingset[cls, member]
35+
predicate isInheritableMember(ClassLikeDeclaration cls, Member member) { none() }
2836
}
2937

3038
/** Holds if `member` is an instance member. */
@@ -35,6 +43,24 @@ predicate isInstanceMember(Member member) {
3543
)
3644
}
3745

46+
/**
47+
* Holds if `member` is a non-instance member declared in the context of a class or top-level.
48+
*/
49+
predicate isStaticMember(Member member) {
50+
exists(ClassLikeDeclaration cls | cls.getAMember() = member |
51+
not any(NameBindingPlugin p).isInstanceMember(cls, member)
52+
)
53+
or
54+
member = any(TopLevel t).getBody().getAStmt()
55+
}
56+
57+
/** Holds if `member` is an inheritable member. */
58+
predicate isInheritableMember(Member member) {
59+
exists(ClassLikeDeclaration cls | cls.getAMember() = member |
60+
any(NameBindingPlugin p).isInheritableMember(cls, member)
61+
)
62+
}
63+
3864
/** Holds if `binding` is only visible in its local scope. */
3965
pragma[nomagic]
4066
predicate isPrivateToLocalScope(AstNode binding) {

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,12 @@ class NameBindingPluginSwift extends NameBindingPlugin {
2929
// Note: Private class members can be seen within type-extensions in the same file,
3030
// so we can't declare those private to their local scope.
3131
}
32+
33+
bindingset[cls, member]
34+
override predicate isInheritableMember(ClassLikeDeclaration cls, Member member) {
35+
exists(cls) and
36+
not member.hasModifier("private")
37+
}
3238
}
3339

3440
private predicate predefinedSourceFolders(string folder, int ordering) {

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

Lines changed: 159 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,8 @@ private newtype TNameBindingNode =
1010
TIdentifier(Identifier n) or
1111
TBulkImport(BulkImportingPattern p) or
1212
TLocalName(LocalName local) or
13-
TExportedNamespace(ClassLikeDeclaration cls) or
13+
TStaticMemberNamespace(ClassLikeDeclaration cls) or
14+
TInstanceMemberNamespace(ClassLikeDeclaration cls) or
1415
TLocalNamespace(AstNode n) {
1516
n = any(TopLevel t).getBody() or // Imported names come in scope here
1617
n instanceof ClassLikeDeclaration
@@ -31,8 +32,13 @@ class NameBindingNode extends TNameBindingNode {
3132

3233
predicate isLocalName(LocalName local) { this = TLocalName(local) }
3334

34-
/** Holds if this represents the set of static members available in the given namespace. */
35-
predicate isExportedNamespace(ClassLikeDeclaration cls) { this = TExportedNamespace(cls) }
35+
/** Holds if this represents the set of static members available in the given class. */
36+
predicate isStaticMemberNamespace(ClassLikeDeclaration cls) { this = TStaticMemberNamespace(cls) }
37+
38+
/** Holds if this represents the set of instance members available in the given class. */
39+
predicate isInstanceMemberNamespace(ClassLikeDeclaration cls) {
40+
this = TInstanceMemberNamespace(cls)
41+
}
3642

3743
/** Holds if this represents the set of members that can be accessed unqualified within the given scope. */
3844
predicate isLocalNamespace(AstNode n) { this = TLocalNamespace(n) }
@@ -56,7 +62,9 @@ class NameBindingNode extends TNameBindingNode {
5662
or
5763
this.isBulkImport(result)
5864
or
59-
this.isExportedNamespace(result)
65+
this.isStaticMemberNamespace(result)
66+
or
67+
this.isInstanceMemberNamespace(result)
6068
or
6169
this.isLocalNamespace(result)
6270
or
@@ -71,7 +79,11 @@ class NameBindingNode extends TNameBindingNode {
7179
exists(LocalName local | this.isLocalName(local) and result = "LocalName(" + local + ")")
7280
or
7381
exists(ClassLikeDeclaration cls |
74-
this.isExportedNamespace(cls) and result = "ExportedNamespace(" + cls + ")"
82+
this.isStaticMemberNamespace(cls) and result = "StaticMemberNamespace(" + cls + ")"
83+
)
84+
or
85+
exists(ClassLikeDeclaration cls |
86+
this.isInstanceMemberNamespace(cls) and result = "InstanceMemberNamespace(" + cls + ")"
7587
)
7688
or
7789
exists(AstNode n | this.isLocalNamespace(n) and result = "LocalNamespace(" + n + ")")
@@ -92,7 +104,13 @@ class NameBindingNode extends TNameBindingNode {
92104
or
93105
exists(LocalName local | this.isLocalName(local) and result = local.getLocation())
94106
or
95-
exists(ClassLikeDeclaration cls | this.isExportedNamespace(cls) and result = cls.getLocation())
107+
exists(ClassLikeDeclaration cls |
108+
this.isStaticMemberNamespace(cls) and result = cls.getLocation()
109+
)
110+
or
111+
exists(ClassLikeDeclaration cls |
112+
this.isInstanceMemberNamespace(cls) and result = cls.getLocation()
113+
)
96114
or
97115
exists(AstNode n | this.isLocalNamespace(n) and result = n.getLocation())
98116
or
@@ -157,12 +175,13 @@ predicate readStep(NameBindingNode node1, string name, NameBindingNode node2) {
157175
predicate storeStep(NameBindingNode node1, string name, NameBindingNode node2) {
158176
exists(ClassLikeDeclaration cls, Member member, NameDeclaration nameDecl |
159177
member = cls.getAMember() and
160-
not isInstanceMember(member) and
161178
not isPrivateToLocalScope(nameDecl) and
162179
nameDecl.getDeclaration() = member and
163180
node1.isIdentifier(nameDecl) and
164181
name = nameDecl.getName() and
165-
node2.isExportedNamespace(cls)
182+
if isInstanceMember(member)
183+
then node2.isInstanceMemberNamespace(cls)
184+
else node2.isStaticMemberNamespace(cls)
166185
)
167186
or
168187
exists(TopLevel top, Stmt stmt, NameDeclaration nameDecl |
@@ -195,12 +214,12 @@ predicate valueStep(NameBindingNode node1, NameBindingNode node2) {
195214
)
196215
or
197216
exists(ClassLikeDeclaration cls |
198-
node1.isExportedNamespace(cls) and
217+
node1.isStaticMemberNamespace(cls) and
199218
node2.isIdentifier(cls.getName())
200219
)
201220
or
202221
exists(ClassLikeDeclaration cls |
203-
node1.isExportedNamespace(cls) and
222+
node1.isStaticMemberNamespace(cls) and
204223
node2.isLocalNamespace(cls)
205224
)
206225
or
@@ -249,7 +268,7 @@ predicate inheritanceStep(NameBindingNode supertype, NameBindingNode subtype) {
249268
exists(ClassLikeDeclaration cls, BaseType base |
250269
base = cls.getABaseType() and
251270
supertype = getNodeFromRef(base.getType()) and
252-
subtype.isExportedNamespace(cls)
271+
subtype.isStaticMemberNamespace(cls)
253272
)
254273
}
255274

@@ -295,9 +314,23 @@ private predicate derivedStoreReadStep(NameBindingNode node1, NameBindingNode no
295314
)
296315
}
297316

298-
/** A name-binding node that has members. */
317+
/** Holds if the member represented by `node` can be inherited. */
318+
pragma[nomagic]
319+
private predicate isInheritableMemberNode(NameBindingNode node) {
320+
exists(NameDeclaration decl |
321+
node.isIdentifier(decl) and
322+
isInheritableMember(decl.getDeclaration())
323+
)
324+
}
325+
326+
/** A name-binding node that can have members. */
299327
class NamespaceNode extends NameBindingNode {
300-
NamespaceNode() { storeStep(_, _, this) or inheritanceStep(_, this) }
328+
NamespaceNode() {
329+
storeStep(_, _, this) or
330+
inheritanceStep(_, this) or
331+
this.isInstanceMemberNamespace(_) or
332+
this.isStaticMemberNamespace(_)
333+
}
301334

302335
/** Gets a name-binding node that may refer to this namespace. */
303336
NameBindingNode ref() { result = TrackNamespace::track(this) }
@@ -308,8 +341,27 @@ class NamespaceNode extends NameBindingNode {
308341
/** Holds if this namespace has an own-member of the given name */
309342
predicate hasOwnMember(string name) { exists(this.getOwnMember(name)) }
310343

344+
/** If this is the static namespace for a class, gets the corresponding instance namespace. */
345+
NamespaceNode toInstanceNamespace() {
346+
exists(ClassLikeDeclaration cls |
347+
this.isStaticMemberNamespace(cls) and
348+
result.isInstanceMemberNamespace(cls)
349+
)
350+
}
351+
352+
/** If this is the instance namespace for a class, gets the corresponding static namespace. */
353+
NamespaceNode toStaticNamespace() { result.toInstanceNamespace() = this }
354+
355+
private NamespaceNode getAnInheritanceParent1() { inheritanceStep(result.ref(), this) }
356+
311357
/** Gets a namespace from which this namespace inherits directly. */
312-
NamespaceNode getAnInheritanceParent() { inheritanceStep(result.ref(), this) }
358+
NamespaceNode getAnInheritanceParent() {
359+
result = this.getAnInheritanceParent1()
360+
or
361+
// `inheritanceStep` connects the static namespaces of classes.
362+
// Add the corresponding inheritance relation between the instance namespaces.
363+
result = this.toStaticNamespace().getAnInheritanceParent1().toInstanceNamespace()
364+
}
313365

314366
/** Gets a namespace that directly inherits from this one. */
315367
NamespaceNode getAnInheritanceChild() { result.getAnInheritanceParent() = this }
@@ -320,7 +372,8 @@ class NamespaceNode extends NameBindingNode {
320372
result = this.getOwnMember(name)
321373
or
322374
not this.hasOwnMember(name) and
323-
result = this.getAnInheritanceParent().getMember(name)
375+
result = this.getAnInheritanceParent().getMember(name) and
376+
isInheritableMemberNode(result)
324377
}
325378
}
326379

@@ -506,3 +559,94 @@ private module FolderHeuristic {
506559
)
507560
}
508561
}
562+
563+
/**
564+
* Holds if `access` may resolve to `target` through the enclosing `accessingClass`.
565+
*
566+
* `instanceAccess` indicates whether this member should be accessed as an instance of `accessingClass`
567+
* or as a static member.
568+
*/
569+
private predicate unqualifiedMemberAccessCand(
570+
PotentialLocalNameAccess access, boolean instanceAccess, NameDeclaration target,
571+
ClassLikeDeclaration accessingClass
572+
) {
573+
not access instanceof NameDeclaration and
574+
(
575+
// Resolved by local scoping
576+
exists(LocalName local |
577+
target.getLocalName() = local and
578+
access.getLocalName() = local and
579+
target.getDeclaration() = accessingClass.getAMember()
580+
|
581+
instanceAccess = true and
582+
isInstanceMember(target.getDeclaration())
583+
or
584+
instanceAccess = false and
585+
isStaticMember(target.getDeclaration())
586+
)
587+
or
588+
// Resolved in an uncertain scope
589+
exists(NamespaceNode namespace, string name |
590+
name = access.getName() and
591+
accessingClass = LocalNameBindingOutput::getAnUncertainScope(access, name)
592+
|
593+
instanceAccess = true and
594+
namespace.isInstanceMemberNamespace(accessingClass) and
595+
namespace.getMember(name).isIdentifier(target)
596+
or
597+
instanceAccess = false and
598+
namespace.isStaticMemberNamespace(accessingClass) and
599+
namespace.getMember(name).isIdentifier(target)
600+
)
601+
)
602+
}
603+
604+
private int unqualifiedMemberAccessDepth(PotentialLocalNameAccess access) {
605+
result = max(AstNode scope | unqualifiedMemberAccessCand(access, _, _, scope) | scope.getDepth())
606+
}
607+
608+
/**
609+
* Holds if `access` is an unqualified access to `target`.
610+
*
611+
* `accessingClass` is the enclosing class in which the member was found, and
612+
* `instanceAccess` indicates if it is an instance member or static member.
613+
*/
614+
predicate unqualifiedMemberAccess(
615+
PotentialLocalNameAccess access, boolean instanceAccess, NameDeclaration target,
616+
ClassLikeDeclaration accessingClass
617+
) {
618+
unqualifiedMemberAccessCand(access, instanceAccess, target, accessingClass) and
619+
accessingClass.getDepth() = unqualifiedMemberAccessDepth(access)
620+
}
621+
622+
/**
623+
* An identifier appearing in a unqualified position, referring to a member of an enclosing class.
624+
*/
625+
class UnqualifiedMemberAccess extends Identifier {
626+
private boolean instanceAccess;
627+
private NameDeclaration target;
628+
private ClassLikeDeclaration accessingClass;
629+
630+
UnqualifiedMemberAccess() {
631+
unqualifiedMemberAccess(this, instanceAccess, target, accessingClass)
632+
}
633+
634+
/** Gets the name declaration of the member being accessed. */
635+
NameDeclaration getTarget() { result = target }
636+
637+
/** Gets the enclosing class whose (possibly inherited) member is being accessed. */
638+
ClassLikeDeclaration getAccessingClass() { result = accessingClass }
639+
640+
/** Holds if this is an instance access on the accessing class. */
641+
predicate isInstanceAccess() { instanceAccess = true }
642+
}
643+
644+
/** Gets the declaration being accessed by `access`, as determined by static name binding. */
645+
NameDeclaration getStaticBindingTarget(Identifier access) {
646+
// For unqualified accesses, use the shadowing-aware lookup
647+
result = access.(UnqualifiedMemberAccess).getTarget()
648+
or
649+
// For others, just follow the name binding graph
650+
not access instanceof UnqualifiedMemberAccess and
651+
trackNameDeclaration(result).asIdentifier() = access
652+
}

0 commit comments

Comments
 (0)