Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions shared/namebinding/codeql/namebinding/LocalNameBinding.qll
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,12 @@ module LocalNameBinding<LocationSig Location, LocalNameBindingInputSig<Location>

/** Gets a textual representation of this local entity. */
string toString() { result = this.getName() }

/** Holds if this variable has the given name and scope. */
pragma[nomagic]
predicate hasNameAndScope(string name, AstNode scope) {
this = TExplicitLocal(_, name, scope) or this = TImplicitLocal(name, scope)
}
}

final class Local = LocalImpl;
Expand Down Expand Up @@ -484,10 +490,6 @@ module LocalNameBinding<LocationSig Location, LocalNameBindingInputSig<Location>
override string getName() { result = name }

override Location getLocation() { result = scope.getLocation() }

/** Holds if this variable has the given name and scope. */
pragma[nomagic]
predicate hasNameAndScope(string name_, AstNode scope_) { name = name_ and scope = scope_ }
}

/** A local access. */
Expand Down
51 changes: 33 additions & 18 deletions unified/ql/lib/codeql/unified/internal/LocalNameBinding.qll
Original file line number Diff line number Diff line change
Expand Up @@ -76,28 +76,21 @@
or
result = n.(LogicalAndRoot).getNthLeaf(index)
or
index = 0 and
relocatedClassMember(n, result)
}

/**
* Holds if `member` is moved onto a child of `className` instead of the class itself,
* so the member name is not in scope in the base types and type parameter constraints.
*/
private predicate relocatedClassMember(Identifier className, Member member) {
// Move members under the name node of its class, so the member name is not in scope
// in the base types and type parameter constraints.
exists(ClassLikeDeclaration cls |
className = cls.getNameNode() and
member = cls.getAMember()
n = cls.getNameNode() and
result = cls.getAMember() and
index = 0
)
}

AstNode getChild(AstNode n, int index) {
result = getChild1(n, index)
or
not exists(getChild1(n, _)) and
not result = getChild1(_, _) and
not n instanceof LogicalAndExpr and // also ignore intermediate nodes within a 'logical and' tree
not n instanceof GuardIfStmt and
not relocatedClassMember(_, result) and
index = result.getParentIndex() and
result = n.getAFieldOrChild()
}
Expand Down Expand Up @@ -203,8 +196,17 @@
declaration = func
)
or
exists(Parameter param |
scope = param.getParent() and // TODO: add SourceCallable and use .getParameter() instead
exists(Parameter param, Callable callable |
callable = param.getDeclaringCallable() and
(
// Scope to the body so parameter defaults cannot see the parameter list
// TODO: Use a plugin for languages where parameter defaults can reference earlier parameters
scope = callable.getBody()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems legit.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pushing a test case to document this, but I'd like to leave it as-is.

AFAICT data flow will actually do the right thing (albeit mostly by accident), but this seems like too much of niche compared to how hard it is to fix, and the fix is unlikely to generalize to other languages where quirks like this are likely to behave differently.

or
// For bodyless functions, choose an arbitrary scope
not exists(callable.getBody()) and
scope = param
) and
pattern = param.getPattern() and
declaration = param
)
Expand Down Expand Up @@ -330,7 +332,9 @@

predicate implicitDeclInScope(string name, AstNode scope) { implicitDeclInScope(name, scope, _) }

predicate accessCand(AstNode n, string name) { n.(PotentialLocalNameAccess).getName() = name }
predicate accessCand(AstNode n, string name) {
n.(PotentialLocalNameAccess).getName() = name and not n instanceof NameBinding
}

predicate uncertainScope(AstNode scope) {
// Classes can have uncertain members due to unqualified access to inherited members.
Expand All @@ -347,6 +351,17 @@

module LocalNameBindingOutput = LocalNameBinding<Location, LocalNameBindingInput>;

private LocalNameBindingOutput::Local resolveId(AstNode n) {
// For names in binding position, always resolve to the scope set by the bindingContext predicate
exists(AstNode scope |
bindingContext(n, scope, _) and
result.hasNameAndScope(n.(Identifier).getValue(), scope)
)
or
not bindingContext(n, _, _) and
result = n.(LocalNameBindingOutput::LocalAccess).getLocal()
}

module Public {
/**
* A representative for a lexically scoped entity, such as a local variable, type name, or module name.
Expand Down Expand Up @@ -379,7 +394,7 @@
string getName() { result = this.getValue() }

/** Gets the representative for the local name introduced by this declaration. */
LocalName getLocalName() { result = this.(LocalNameBindingOutput::LocalAccess).getLocal() }
LocalName getLocalName() { result = resolveId(this) }
}

final class LocalVariable = LocalVariableImpl;
Expand Down Expand Up @@ -497,7 +512,7 @@
*/
abstract private class PotentialLocalNameAccessImpl extends AstNode {
/** Gets the representative for the local name being accessed. */
LocalName getLocalName() { result = this.(LocalNameBindingOutput::LocalAccess).getLocal() }
LocalName getLocalName() { result = resolveId(this) }

/** Gets the name being accessed. */
abstract string getName();
Expand Down
Loading
Loading