Merge pull request #247 from github/hmac-jump-to-def

Jump-to-definition
This commit is contained in:
Harry Maclean
2021-08-31 16:00:43 +01:00
committed by GitHub
6 changed files with 195 additions and 4 deletions

View File

@@ -168,7 +168,19 @@ class InstanceVariableAccess extends VariableAccess, TInstanceVariableAccess {
final override string getAPrimaryQlClass() { result = "InstanceVariableAccess" }
}
/** An access to an instance variable where the value is updated. */
class InstanceVariableWriteAccess extends InstanceVariableAccess, VariableWriteAccess { }
/** An access to an instance variable where the value is read. */
class InstanceVariableReadAccess extends InstanceVariableAccess, VariableReadAccess { }
/** An access to a class variable. */
class ClassVariableAccess extends VariableAccess, TClassVariableAccess {
final override string getAPrimaryQlClass() { result = "ClassVariableAccess" }
}
/** An access to a class variable where the value is updated. */
class ClassVariableWriteAccess extends ClassVariableAccess, VariableWriteAccess { }
/** An access to a class variable where the value is read. */
class ClassVariableReadAccess extends ClassVariableAccess, VariableReadAccess { }

View File

@@ -99,14 +99,22 @@ private module Cached {
}
/**
* Resolve constant read access (typically a scope expression) to a qualified module name.
* Resolve class or module read access to a qualified module name.
*/
cached
TResolved resolveScopeExpr(ConstantReadAccess r) {
exists(string qname | qname = resolveConstant(r) and result = TResolved(qname))
}
/**
* Resolve constant access (class, module or otherwise) to a qualified module name.
* `resolveScopeExpr/1` picks the best (lowest priority number) result of
* `resolveScopeExpr/2` that resolves to a constant definition. If the constant
* definition is a Namespace then it is returned, if it's a constant assignment then
* the right-hand side of the assignment is resolved.
*/
cached
TResolved resolveScopeExpr(ConstantReadAccess r) {
string resolveConstant(ConstantReadAccess r) {
exists(string qname |
qname =
min(string qn, int p |
@@ -122,11 +130,11 @@ private module Cached {
qn order by p
)
|
result = TResolved(qname)
result = qname
or
exists(ConstantAssignment a |
qname = constantDefinition0(a) and
result = resolveScopeExpr(a.getParent().(Assignment).getRightOperand())
result = resolveConstant(a.getParent().(Assignment).getRightOperand())
)
)
}

View File

@@ -0,0 +1,110 @@
/**
* @name Definitions
* @description Jump to definition helper query.
* @kind definitions
* @id rb/jump-to-definition
*/
/*
* TODO:
* - should `Foo.new` point to `Foo#initialize`?
*/
import ruby
import codeql.ruby.ast.internal.Module
import codeql.ruby.dataflow.SSA
import codeql.ruby.dataflow.internal.DataFlowDispatch
from DefLoc loc, Expr src, Expr target, string kind
where
ConstantDefLoc(src, target) = loc and kind = "constant"
or
MethodLoc(src, target) = loc and kind = "method"
or
LocalVariableLoc(src, target) = loc and kind = "variable"
or
InstanceVariableLoc(src, target) = loc and kind = "instance variable"
or
ClassVariableLoc(src, target) = loc and kind = "class variable"
select src, target, kind
/**
* Definition location info for different identifiers.
* Each branch holds two values that are subclasses of `Expr`.
* The first is the "source" - some usage of an identifier.
* The second is the "target" - the definition of that identifier.
*/
newtype DefLoc =
/** A constant, module or class. */
ConstantDefLoc(ConstantReadAccess read, ConstantWriteAccess write) { write = definitionOf(read) } or
/** A method call. */
MethodLoc(MethodCall call, Method meth) {
exists(DataFlowCall c | c.getExpr() = call and c.getTarget() = meth)
} or
/** A local variable. */
LocalVariableLoc(VariableReadAccess read, VariableWriteAccess write) {
exists(Ssa::WriteDefinition w |
write = w.getWriteAccess() and
read = w.getARead().getExpr() and
not read.isSynthesized()
)
} or
/** An instance variable */
InstanceVariableLoc(InstanceVariableReadAccess read, InstanceVariableWriteAccess write) {
/*
* We consider instance variables to be "defined" in the initialize method of their enclosing class.
* If that method doesn't exist, we won't provide any jump-to-def information for the instance variable.
*/
exists(Method m |
m.getAChild+() = write and
m.getName() = "initialize" and
write.getVariable() = read.getVariable()
)
} or
/** A class variable */
ClassVariableLoc(ClassVariableReadAccess read, ClassVariableWriteAccess write) {
read.getVariable() = write.getVariable() and
not exists(MethodBase m | m.getAChild+() = write)
}
/**
* Gets the fully qualified name for a constant, based on the context in which it is defined.
*
* For example, given
* ```ruby
* module Foo
* module Bar
* class Baz
* end
* end
* end
* ```
*
* the constant `Baz` has the fully qualified name `Foo::Bar::Baz`.
*/
string constantQualifiedName(ConstantWriteAccess w) {
/* get the qualified name for the parent module, then append w */
exists(ConstantWriteAccess parent | parent = w.getEnclosingModule() |
result = constantQualifiedName(parent) + "::" + w.getName()
)
or
/* base case - there's no parent module */
not exists(ConstantWriteAccess parent | parent = w.getEnclosingModule()) and
result = w.getName()
}
/**
* Gets the constant write that defines the given constant.
* Modules often don't have a unique definition, as they are opened multiple times in different
* files. In these cases we arbitrarily pick the definition with the lexicographically least
* location.
*/
ConstantWriteAccess definitionOf(ConstantReadAccess r) {
result =
min(ConstantWriteAccess w |
constantQualifiedName(w) = resolveConstant(r)
|
w order by w.getLocation().toString()
)
}

View File

@@ -0,0 +1,14 @@
| Definitions.rb:6:7:6:21 | call to g | Definitions.rb:9:5:11:7 | g | method |
| Definitions.rb:6:9:6:21 | SOME_CONSTANT | Definitions.rb:2:3:2:15 | SOME_CONSTANT | constant |
| Definitions.rb:10:7:10:7 | x | Definitions.rb:9:11:9:11 | x | variable |
| Definitions.rb:14:7:14:7 | call to f | Definitions.rb:5:5:7:7 | f | method |
| Definitions.rb:23:5:23:7 | @@a | Definitions.rb:20:3:20:5 | @@a | class variable |
| Definitions.rb:32:7:32:7 | y | Definitions.rb:31:10:31:10 | y | variable |
| Definitions.rb:36:7:36:7 | A | Definitions.rb:1:1:17:3 | A | constant |
| Definitions.rb:36:7:36:10 | B | Definitions.rb:4:3:16:5 | B | constant |
| Definitions.rb:36:7:36:18 | call to g | Definitions.rb:9:5:11:7 | g | method |
| Definitions.rb:36:18:36:18 | y | Definitions.rb:35:11:35:11 | y | variable |
| Definitions.rb:39:7:39:8 | @e | Definitions.rb:30:7:30:8 | @e | instance variable |
| Definitions.rb:41:7:41:9 | @@b | Definitions.rb:27:5:27:7 | @@b | class variable |
| Definitions.rb:46:1:46:1 | C | Definitions.rb:19:1:44:3 | C | constant |
| Definitions.rb:46:1:46:4 | D | Definitions.rb:26:3:43:5 | D | constant |

View File

@@ -0,0 +1 @@
queries/analysis/Definitions.ql

View File

@@ -0,0 +1,46 @@
module A
SOME_CONSTANT = 1
class B
def f
g SOME_CONSTANT
end
def g x
x
end
def h
f
end
end
end
module C
@@a = 1
def self.a
@@a
end
class D
@@b = 2
def initialize
@e = 1
x, y = [1, 2]
y
end
def h y
A::B.new.g y
UnknownClass.some_method
@f = 2
@e
@f
@@b
end
end
end
C::D.new