mirror of
https://github.com/github/codeql.git
synced 2026-07-09 05:25:32 +02:00
unified: Add local name binding and tests
As documented in the test, 'guard if' statements are still not properly supported.
This commit is contained in:
244
unified/ql/lib/codeql/unified/internal/Variables.qll
Normal file
244
unified/ql/lib/codeql/unified/internal/Variables.qll
Normal file
@@ -0,0 +1,244 @@
|
||||
/**
|
||||
* Provides classes for reasoning about lexically scoped variables and references to these.
|
||||
*/
|
||||
|
||||
private import unified
|
||||
private import unified as U
|
||||
private import codeql.namebinding.LocalNameBinding
|
||||
|
||||
private module LocalNameBindingInput implements LocalNameBindingInputSig<Location> {
|
||||
class AstNode = U::AstNode;
|
||||
|
||||
private class LogicalAndRoot extends LogicalAndExpr {
|
||||
LogicalAndRoot() { not this = any(LogicalAndExpr e).getAnOperand() }
|
||||
|
||||
private Expr getDescendent(string path) {
|
||||
path = "" and result = this
|
||||
or
|
||||
exists(LogicalAndExpr mid, string midPath | mid = this.getDescendent(midPath) |
|
||||
result = mid.getLeft() and path = midPath + "A"
|
||||
or
|
||||
result = mid.getRight() and path = midPath + "B"
|
||||
)
|
||||
}
|
||||
|
||||
Expr getNthLeaf(int n) {
|
||||
result =
|
||||
rank[n](Expr e, string path |
|
||||
e = this.getDescendent(path) and not e instanceof LogicalAndExpr
|
||||
|
|
||||
e order by path
|
||||
)
|
||||
}
|
||||
|
||||
Expr getLastLeaf() { result = max(int n | | this.getNthLeaf(n) order by n) }
|
||||
}
|
||||
|
||||
private AstNode getChild1(AstNode n, int index) {
|
||||
result = n.(Block).getStmt(index)
|
||||
or
|
||||
result = n.(LogicalAndRoot).getNthLeaf(index)
|
||||
or
|
||||
exists(PatternGuardExpr guard | n = guard |
|
||||
index = 0 and result = guard.getPattern()
|
||||
or
|
||||
index = 1 and result = guard.getValue()
|
||||
)
|
||||
or
|
||||
exists(IfExpr expr | n = expr |
|
||||
index = 0 and result = expr.getCondition()
|
||||
or
|
||||
index = 1 and result = expr.getThen()
|
||||
or
|
||||
index = 2 and result = expr.getElse()
|
||||
)
|
||||
or
|
||||
exists(VariableDeclaration decl | n = decl |
|
||||
index = 0 and result = decl.getPattern()
|
||||
or
|
||||
index = 1 and result = decl.getType()
|
||||
or
|
||||
index = 2 and result = decl.getValue()
|
||||
)
|
||||
}
|
||||
|
||||
AstNode getChild(AstNode n, int index) {
|
||||
result = getChild1(n, index)
|
||||
or
|
||||
not exists(getChild1(n, _)) and
|
||||
not n instanceof LogicalAndExpr and // also ignore intermediate nodes within a 'logical and' tree
|
||||
index = 0 and
|
||||
result = n.getAFieldOrChild()
|
||||
}
|
||||
|
||||
abstract class Conditional extends AstNode {
|
||||
/** Gets the condition of this conditional. */
|
||||
abstract AstNode getCondition();
|
||||
|
||||
/** Gets the then-branch of this conditional. */
|
||||
abstract AstNode getThen();
|
||||
|
||||
/** Gets the else-branch of this conditional. */
|
||||
abstract AstNode getElse();
|
||||
}
|
||||
|
||||
private class IfExprConditional extends Conditional instanceof IfExpr {
|
||||
override AstNode getCondition() { result = IfExpr.super.getCondition() }
|
||||
|
||||
override AstNode getThen() { result = IfExpr.super.getThen() }
|
||||
|
||||
override AstNode getElse() { result = IfExpr.super.getElse() }
|
||||
}
|
||||
|
||||
abstract class SiblingShadowingDecl extends AstNode {
|
||||
/** Gets the left-hand side of this declaration. */
|
||||
abstract AstNode getLhs();
|
||||
|
||||
/**
|
||||
* Gets the right-hand side of this declaration.
|
||||
*
|
||||
* Any local declared in the left-hand side of this declaration is _not_ in scope
|
||||
* in the right-hand side.
|
||||
*/
|
||||
abstract AstNode getRhs();
|
||||
|
||||
/**
|
||||
* Gets the else-branch of this declaration, if any.
|
||||
*
|
||||
* Any local declared in the left-hand side of this declaration is _not_ in scope
|
||||
* in the else-branch.
|
||||
*/
|
||||
abstract AstNode getElse();
|
||||
}
|
||||
|
||||
private class LocalVariableDeclarationSiblingShadowingDecl extends SiblingShadowingDecl instanceof LocalVariableDeclaration
|
||||
{
|
||||
override AstNode getLhs() { result = LocalVariableDeclaration.super.getPattern() }
|
||||
|
||||
override AstNode getRhs() { result = LocalVariableDeclaration.super.getValue() }
|
||||
|
||||
override AstNode getElse() { none() }
|
||||
}
|
||||
|
||||
private class PatternGuardExprSiblingShadowingDecl extends SiblingShadowingDecl instanceof PatternGuardExpr
|
||||
{
|
||||
override AstNode getLhs() { result = PatternGuardExpr.super.getPattern() }
|
||||
|
||||
override AstNode getRhs() { result = PatternGuardExpr.super.getValue() }
|
||||
|
||||
override AstNode getElse() { none() }
|
||||
}
|
||||
|
||||
private class GuardIfStmtSiblingShadowingDecl extends SiblingShadowingDecl instanceof GuardIfStmt {
|
||||
override AstNode getLhs() { result = GuardIfStmt.super.getCondition() }
|
||||
|
||||
override AstNode getRhs() { none() }
|
||||
|
||||
override AstNode getElse() { result = GuardIfStmt.super.getElse() }
|
||||
}
|
||||
|
||||
private predicate bindingContext(AstNode pattern, AstNode scope) {
|
||||
exists(LocalVariableDeclaration decl |
|
||||
scope = decl and // LocalVariableDeclaration is a ShadowingSiblingDecl, it must use itself as the scope
|
||||
pattern = decl.getPattern()
|
||||
)
|
||||
or
|
||||
exists(LocalFunctionDeclaration func |
|
||||
scope = func.getDeclaringBlock() and
|
||||
pattern = func.getName()
|
||||
)
|
||||
or
|
||||
exists(Parameter param |
|
||||
scope = param.getParent() and // TODO: add SourceCallable and use .getParameter() instead
|
||||
pattern = param.getPattern()
|
||||
)
|
||||
or
|
||||
exists(CatchClause catch |
|
||||
scope = catch and // ensure both 'body' and 'guard' clause are in scope
|
||||
pattern = catch.getPattern()
|
||||
)
|
||||
or
|
||||
exists(SwitchCase case |
|
||||
scope = case and // ensure both 'body' and 'guard' clause are in scope (TODO: merge CatchClause and SwitchCase?)
|
||||
pattern = case.getPattern()
|
||||
)
|
||||
or
|
||||
exists(ForEachStmt stmt |
|
||||
scope = stmt and // ensure both 'body' and 'guard' are in scope
|
||||
pattern = stmt.getPattern()
|
||||
)
|
||||
or
|
||||
exists(TuplePattern pat |
|
||||
bindingContext(pat, scope) and
|
||||
pattern = pat.getElement(_).getPattern()
|
||||
)
|
||||
or
|
||||
exists(ConstructorPattern pat |
|
||||
bindingContext(pat, scope) and
|
||||
pattern = pat.getElement(_).getPattern()
|
||||
)
|
||||
or
|
||||
exists(OrPattern pat |
|
||||
bindingContext(pat, scope) and
|
||||
pattern = pat.getPattern(_)
|
||||
)
|
||||
or
|
||||
exists(PatternGuardExpr expr |
|
||||
pattern = expr.getPattern() and
|
||||
scope = expr
|
||||
)
|
||||
}
|
||||
|
||||
predicate declInScope(AstNode definingNode, string name, AstNode scope) {
|
||||
bindingContext(definingNode, scope) and
|
||||
(
|
||||
definingNode.(NamePattern).getIdentifier().getValue() = name
|
||||
or
|
||||
definingNode.(Identifier).getValue() = name
|
||||
)
|
||||
}
|
||||
|
||||
predicate implicitDeclInScope(string name, AstNode scope) {
|
||||
none()
|
||||
// TODO: self
|
||||
}
|
||||
|
||||
predicate accessCand(AstNode n, string name) {
|
||||
n.(NameExpr).getIdentifier().getValue() = name
|
||||
or
|
||||
n.(NamePattern).getIdentifier().getValue() = name
|
||||
or
|
||||
n = any(LocalFunctionDeclaration f).getName() and
|
||||
n.(Identifier).getValue() = name
|
||||
}
|
||||
|
||||
predicate lookupStartsAt(AstNode n, AstNode scope) { none() }
|
||||
}
|
||||
|
||||
module LocalNameBindingOutput = LocalNameBinding<Location, LocalNameBindingInput>;
|
||||
|
||||
module Public {
|
||||
/**
|
||||
* A local variable.
|
||||
*/
|
||||
class Variable extends LocalNameBindingOutput::Local {
|
||||
VariableAccess getAnAccess() { result.getVariable() = this }
|
||||
}
|
||||
|
||||
/**
|
||||
* An AST node that is a reference to a local variable.
|
||||
*/
|
||||
class VariableAccess extends AstNode instanceof LocalNameBindingOutput::LocalAccess {
|
||||
Variable getVariable() { result = super.getLocal() }
|
||||
|
||||
Identifier getIdentifier() {
|
||||
result = this.(NameExpr).getIdentifier()
|
||||
or
|
||||
result = this.(NamePattern).getIdentifier()
|
||||
or
|
||||
result = this
|
||||
}
|
||||
|
||||
string getName() { result = this.getIdentifier().getValue() }
|
||||
}
|
||||
}
|
||||
@@ -7,5 +7,6 @@ library: true
|
||||
upgrades: upgrades
|
||||
dependencies:
|
||||
codeql/util: ${workspace}
|
||||
codeql/namebinding: ${workspace}
|
||||
warnOnImplicitThis: true
|
||||
compileForOverlayEval: true
|
||||
|
||||
@@ -6,3 +6,4 @@ import codeql.Locations
|
||||
import codeql.files.FileSystem
|
||||
import codeql.unified.Ast::Unified
|
||||
import codeql.unified.internal.AstExtra::Public
|
||||
import codeql.unified.internal.Variables::Public
|
||||
|
||||
309
unified/ql/test/library-tests/variables/test.swift
Normal file
309
unified/ql/test/library-tests/variables/test.swift
Normal file
@@ -0,0 +1,309 @@
|
||||
func t1() -> Int {
|
||||
let x = 42 // name=x1
|
||||
return x // $ access=x1
|
||||
}
|
||||
|
||||
func t2() -> Int {
|
||||
let x = 42 // name=x1
|
||||
|
||||
if case let x = x + 10 { // $ access=x1 // name=x2
|
||||
print(x) // $ access=x2
|
||||
}
|
||||
print(x) // $ access=x1
|
||||
}
|
||||
|
||||
func t3() -> Int {
|
||||
guard let x = foo() else { // name=x1
|
||||
return 0
|
||||
}
|
||||
print(x) // $ MISSING: access=x1
|
||||
}
|
||||
|
||||
// Function parameters
|
||||
func t4(x: Int) -> Int { // name=x1
|
||||
return x // $ access=x1
|
||||
}
|
||||
|
||||
// Multiple parameters
|
||||
func t5(x: Int, // name=x1
|
||||
y: Int) -> Int { // name=y1
|
||||
let z = x + // $ access=x1 // name=z1
|
||||
y // $ access=y1
|
||||
return z // $ access=z1
|
||||
}
|
||||
|
||||
// Parameter shadowed by local variable
|
||||
func t6(x: Int) -> Int { // name=x1
|
||||
let x = x * 2 // $ access=x1 // name=x2
|
||||
return x // $ access=x2
|
||||
}
|
||||
|
||||
// Nested blocks
|
||||
func t7() {
|
||||
let x = 1 // name=x1
|
||||
{
|
||||
print(x) // $ MISSING: access=x1
|
||||
let x = 2 // name=x2
|
||||
print(x) // $ access=x2
|
||||
}
|
||||
print(x) // $ access=x1
|
||||
}
|
||||
|
||||
// For-in loop
|
||||
func t8() {
|
||||
let array = [1, 2, 3] // name=array1
|
||||
for x in array { // $ access=array1 // name=x1
|
||||
print(x) // $ access=x1
|
||||
}
|
||||
}
|
||||
|
||||
// For-in loop with shadowing
|
||||
func t9() {
|
||||
let x = 0 // name=x1
|
||||
let array = [1, 2, 3] // name=array1
|
||||
for x in array { // $ access=array1 // name=x2
|
||||
print(x) // $ access=x2
|
||||
}
|
||||
print(x) // $ access=x1
|
||||
}
|
||||
|
||||
// Switch statement with case bindings
|
||||
func t10(value: Int) { // name=value1
|
||||
switch value { // $ access=value1
|
||||
case let x: // name=x1
|
||||
print(x) // $ access=x1
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Switch with multiple cases
|
||||
func t11(value: Int) { // name=value1
|
||||
switch value { // $ access=value1
|
||||
case let x where x > 0: // name=x1
|
||||
print(x) // $ access=x1
|
||||
case let x: // name=x2
|
||||
print(x) // $ access=x2
|
||||
}
|
||||
}
|
||||
|
||||
// Tuple unpacking
|
||||
func t12() {
|
||||
let tuple = (1, 2) // name=tuple1
|
||||
let (x, // name=x1
|
||||
y) = tuple // $ access=tuple1 // name=y1
|
||||
print(x) // $ MISSING: access=x1 // because x,y are incorrectly mapped to expr_equality_pattern
|
||||
print(y) // $ MISSING: access=y1
|
||||
}
|
||||
|
||||
// Tuple unpacking with underscore
|
||||
func t13() {
|
||||
let tuple = (1, 2, 3) // name=tuple1
|
||||
let (x, // name=x1
|
||||
_,
|
||||
y) = tuple // $ access=tuple1 // name=y1
|
||||
print(x) // $ MISSING: access=x1
|
||||
print(y) // $ MISSING: access=y1
|
||||
}
|
||||
|
||||
// Optional binding (if let)
|
||||
func t14(optional: Int?) { // name=optional1
|
||||
if let x = optional { // $ access=optional1 // name=x1
|
||||
print(x) // $ access=x1
|
||||
}
|
||||
}
|
||||
|
||||
// Multiple optional bindings
|
||||
func t15(opt1: Int?, // name=opt11
|
||||
opt2: String?) { // name=opt21
|
||||
if let x = opt1, // $ access=opt11 // name=x1
|
||||
let y = opt2 { // $ access=opt21 // name=y1
|
||||
print(x) // $ access=x1
|
||||
print(y) // $ access=y1
|
||||
}
|
||||
}
|
||||
|
||||
// Do-catch blocks
|
||||
func t16() throws {
|
||||
do {
|
||||
let x = try foo() // name=x1
|
||||
print(x) // $ access=x1
|
||||
} catch let error { // name=error1
|
||||
print(error) // $ access=error1
|
||||
}
|
||||
}
|
||||
|
||||
// Closure captures
|
||||
func t17() {
|
||||
let x = 1 // name=x1
|
||||
let closure = { // name=closure1
|
||||
print(x) // $ access=x1
|
||||
}
|
||||
closure() // $ access=closure1
|
||||
}
|
||||
|
||||
// Closure with parameter shadowing
|
||||
func t18() {
|
||||
let x = 1 // name=x1
|
||||
let closure =
|
||||
{ (x: Int) -> Void in // name=x2
|
||||
print(x) // $ access=x2
|
||||
}
|
||||
closure(2) // $ access=closure
|
||||
print(x) // $ access=x1
|
||||
}
|
||||
|
||||
// While loop
|
||||
func t19() {
|
||||
var x = 0 // name=x1
|
||||
while x < 10 { // $ access=x1
|
||||
x = x + 1 // $ access=x1
|
||||
print(x) // $ access=x1
|
||||
}
|
||||
}
|
||||
|
||||
// Repeat-while loop
|
||||
func t20() {
|
||||
var x = 0 // name=x1
|
||||
repeat {
|
||||
x = x + 1 // $ access=x1
|
||||
print(x) // $ access=x1
|
||||
} while x < 10 // $ access=x1
|
||||
}
|
||||
|
||||
// Property shadowing (var)
|
||||
func t21() {
|
||||
var x = 1 // name=x1
|
||||
var x = 2 // name=x2
|
||||
print(x) // $ access=x2
|
||||
}
|
||||
|
||||
// Nested functions
|
||||
func t22() {
|
||||
let x = 1 // name=x1
|
||||
func inner() { // name=inner1
|
||||
let x = 2 // name=x2
|
||||
print(x) // $ access=x2
|
||||
}
|
||||
inner() // $ access=inner1
|
||||
print(x) // $ access=x1
|
||||
}
|
||||
|
||||
// Three levels of shadowing
|
||||
func t23() {
|
||||
let x = 1 // name=x1
|
||||
{
|
||||
let x = 2 // name=x2
|
||||
{
|
||||
let x = 3 // name=x3
|
||||
print(x) // $ access=x3
|
||||
}
|
||||
print(x) // $ access=x2
|
||||
}
|
||||
print(x) // $ access=x1
|
||||
}
|
||||
|
||||
// If-let followed by regular if
|
||||
func t24(optional: Int?) { // name=optional1
|
||||
if let x = optional { // $ access=optional1 // name=x1
|
||||
print(x) // $ access=x1
|
||||
}
|
||||
if true {
|
||||
let x = 5 // name=x2
|
||||
print(x) // $ access=x2
|
||||
}
|
||||
}
|
||||
|
||||
// Switch with same variable name in different cases
|
||||
func t25(value: Int) { // name=value1
|
||||
switch value { // $ access=value1
|
||||
case let x: // name=x1
|
||||
print(x) // $ access=x1
|
||||
let x = 10 // name=x2
|
||||
print(x) // $ access=x2
|
||||
}
|
||||
}
|
||||
|
||||
func t26() -> Int {
|
||||
let x = 42 // name=x1
|
||||
guard let x = foo() else { // name=x2
|
||||
return x // $ access=x1
|
||||
}
|
||||
print(x) // $ MISSING: access=x2 SPURIOUS: access=x1
|
||||
}
|
||||
|
||||
// if with multiple conditions, mixing boolean and optional binding
|
||||
func t27(opt: Int?) { // name=opt1
|
||||
if opt != nil, // $ access=opt1
|
||||
let x = opt { // $ access=opt1
|
||||
print(x) // $ access=x
|
||||
}
|
||||
}
|
||||
|
||||
// if with multiple let bindings and a boolean condition
|
||||
func t28(a: Int?, b: Int?) { // name=a1 // name=b1
|
||||
if let x = a, // $ access=a1 SPURIOUS: access=b1
|
||||
let y = b, // $ access=b1 SPURIOUS: access=a1
|
||||
x < y { // $ $ access=x access=y
|
||||
print(x) // $ access=x
|
||||
print(y) // $ access=y
|
||||
}
|
||||
}
|
||||
|
||||
// if with multiple conditions where a later binding shadows an outer variable
|
||||
func t29(opt: Int?) { // name=opt1
|
||||
let x = 0 // name=x1
|
||||
if opt != nil, // $ access=opt1
|
||||
let x = opt { // $ access=opt1
|
||||
print(x) // $ access=x
|
||||
}
|
||||
print(x) // $ access=x1
|
||||
}
|
||||
|
||||
// guard with multiple conditions, mixing boolean and optional binding
|
||||
func t30(opt: Int?) { // name=opt1
|
||||
guard opt != nil, // $ access=opt1
|
||||
let x = opt else { // $ access=opt1
|
||||
return
|
||||
}
|
||||
print(x) // $ MISSING: access=x
|
||||
}
|
||||
|
||||
// guard with multiple let bindings and a boolean condition
|
||||
func t31(a: Int?, b: Int?) { // name=a1 // name=b1
|
||||
guard let x = a, // $ access=a1 SPURIOUS: access=b1
|
||||
let y = b, // $ access=b1 SPURIOUS: access=a1
|
||||
x < y else { // $ $ access=x access=y
|
||||
return
|
||||
}
|
||||
print(x) // $ MISSING: access=x
|
||||
print(y) // $ MISSING: access=y
|
||||
}
|
||||
|
||||
// guard with multiple conditions where bound variable is used in later condition
|
||||
func t32(opt: Int?) { // name=opt1
|
||||
guard let x = opt, // $ access=opt1
|
||||
x > 0 else { // $ access=x
|
||||
return
|
||||
}
|
||||
print(x) // $ MISSING: access=x
|
||||
}
|
||||
|
||||
func t33() {
|
||||
let x = 1 // name=x1
|
||||
guard x > 0, // $ access=x1
|
||||
let x = x, // $ MISSING: access=x2 SPURIOUS: access=x1 // name=x2
|
||||
x > 0 else { // $ access=x2
|
||||
return
|
||||
}
|
||||
print(x) // $ MISSING: access=x2 SPURIOUS: access=x1
|
||||
}
|
||||
|
||||
func t34() {
|
||||
let x = 1 // name=x1
|
||||
if x > 0, // $ access=x1
|
||||
let x = x, // $ access=x1 // name=x2
|
||||
x > 0 else { // $ access=x2
|
||||
print(x) // $ access=x2
|
||||
}
|
||||
}
|
||||
51
unified/ql/test/library-tests/variables/variables.ql
Normal file
51
unified/ql/test/library-tests/variables/variables.ql
Normal file
@@ -0,0 +1,51 @@
|
||||
import unified
|
||||
import utils.test.InlineExpectationsTest
|
||||
|
||||
/** 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" }
|
||||
|
||||
private predicate declAt(Variable v, string filepath, int line) {
|
||||
v.getLocation().hasLocationInfo(filepath, _, _, line, _)
|
||||
}
|
||||
|
||||
private predicate decl(Variable 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 = v.getName()
|
||||
)
|
||||
}
|
||||
|
||||
predicate hasActualResult(Location location, string element, string tag, string value) {
|
||||
exists(VariableAccess va, Variable v |
|
||||
v = va.getVariable() and
|
||||
not va = v.getDefiningNode() and
|
||||
location = va.getLocation() and
|
||||
element = va.toString() and
|
||||
decl(v, value) and
|
||||
tag = "access"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
import MakeTest<VariableAccessTest>
|
||||
Reference in New Issue
Block a user