Add AST classes and tests for method calls

This commit is contained in:
Nick Rolfe
2021-01-20 18:14:29 +00:00
parent 78771ba4c2
commit 2e8d154f2b
21 changed files with 1275 additions and 249 deletions

View File

@@ -0,0 +1,173 @@
private import codeql_ruby.AST
private import internal.Call
/**
* A method call.
*/
class Call extends Expr {
override Call::Range range;
override string getAPrimaryQlClass() { result = "Call" }
final override string toString() { result = "call to " + this.getMethodName() }
/**
* Gets the receiver of the call, if any. For example:
* ```rb
* foo.bar
* baz()
* ```
* The result for the call to `bar` is the `Expr` for `foo`, while the call
* to `baz` has no result.
*/
final Expr getReceiver() { result = range.getReceiver() }
/**
* Gets the name of the method being called. For example, in:
* ```rb
* foo.bar x, y
* ```
* the result is `"bar"`.
*
* N.B. in the following example, where the method name is a scope
* resolution, the result is the name being resolved, i.e. `"bar"`. Use
* `getMethodScopeResolution` to get the complete `ScopeResolution`.
* ```rb
* Foo::bar x, y
* ```
*/
final string getMethodName() { result = range.getMethodName() }
/**
* Gets the method name if it is a `ScopeResolution`.
*/
final ScopeResolution getMethodScopeResolution() { result = range.getMethodScopeResolution() }
/**
* Gets the `n`th argument of this method call. In the following example, the
* result for n=0 is the `IntegerLiteral` 0, while for n=1 the result is a
* `Pair` (whose `getKey` returns the `SymbolLiteral` for `bar`, and
* `getValue` returns the `IntegerLiteral` 1). Keyword arguments like this
* can be accessed more naturally using the
* `getKeywordArgument(string keyword)` predicate.
* ```rb
* foo(0, bar: 1)
* ```
*/
final Expr getArgument(int n) { result = range.getArgument(n) }
/**
* Gets an argument of this method call.
*/
final Expr getAnArgument() { result = this.getArgument(_) }
/**
* Gets the value of the keyword argument whose key is `keyword`, if any. For
* example, the result for `getKeywordArgument("qux")` in the following
* example is the `IntegerLiteral` 123.
* ```rb
* foo :bar "baz", qux: 123
* ```
*/
final Expr getKeywordArgument(string keyword) {
exists(Pair p |
p = getAnArgument() and
p.getKey().(SymbolLiteral).getValueText() = keyword and
result = p.getValue()
)
}
/**
* Gets the number of arguments of this method call.
*/
final int getNumberOfArguments() { result = count(this.getAnArgument()) }
/**
* Gets the block of this method call, if any.
* ```rb
* foo.each { |x| puts x }
* ```
*/
final Block getBlock() { result = range.getBlock() }
}
/**
* A call to `yield`.
* ```rb
* yield x, y
* ```
*/
class YieldCall extends Call, @yield {
final override YieldCall::Range range;
final override string getAPrimaryQlClass() { result = "YieldCall" }
}
/**
* A block argument in a method call.
* ```rb
* foo(&block)
* ```
*/
class BlockArgument extends Expr, @block_argument {
final override BlockArgument::Range range;
final override string getAPrimaryQlClass() { result = "BlockArgument" }
final override string toString() { result = "&..." }
/**
* Gets the underlying expression representing the block. In the following
* example, the result is the `Expr` for `bar`:
* ```rb
* foo(&bar)
* ```
*/
final Expr getExpr() { result = range.getExpr() }
}
/**
* A splat argument in a method call.
* ```rb
* foo(*args)
* ```
*/
class SplatArgument extends Expr, @splat_argument {
final override SplatArgument::Range range;
final override string getAPrimaryQlClass() { result = "SplatArgument" }
final override string toString() { result = "*..." }
/**
* Gets the underlying expression. In the following example, the result is
* the `Expr` for `bar`:
* ```rb
* foo(*bar)
* ```
*/
final Expr getExpr() { result = range.getExpr() }
}
/**
* A hash-splat (or 'double-splat') argument in a method call.
* ```rb
* foo(**options)
* ```
*/
class HashSplatArgument extends Expr, @hash_splat_argument {
final override HashSplatArgument::Range range;
final override string getAPrimaryQlClass() { result = "HashSplatArgument" }
final override string toString() { result = "**..." }
/**
* Gets the underlying expression. In the following example, the result is
* the `Expr` for `bar`:
* ```rb
* foo(**bar)
* ```
*/
final Expr getExpr() { result = range.getExpr() }
}

View File

@@ -20,9 +20,9 @@ class Expr extends AstNode {
class Literal extends Expr {
override Literal::Range range;
override string toString() { result = this.getValueText() }
override string toString() { result = range.toString() }
/** Gets the source text for this literal. */
/** Gets the source text for this literal, if it is constant. */
final string getValueText() { result = range.getValueText() }
}
@@ -74,6 +74,35 @@ class RegexLiteral extends Literal, @regex {
final override string getAPrimaryQlClass() { result = "RegexLiteral" }
}
/**
* A string literal.
* ```rb
* 'hello'
* "hello, #{name}"
* ```
* TODO: expand this minimal placeholder.
*/
class StringLiteral extends Literal, @string__ {
final override StringLiteral::Range range;
final override string getAPrimaryQlClass() { result = "StringLiteral" }
}
/**
* A symbol literal.
* ```rb
* :foo
* :"foo bar"
* :"foo bar #{baz}"
* ```
* TODO: expand this minimal placeholder.
*/
class SymbolLiteral extends Literal {
final override SymbolLiteral::Range range;
final override string getAPrimaryQlClass() { result = "SymbolLiteral" }
}
/** A sequence of expressions. */
class ExprSequence extends Expr {
override ExprSequence::Range range;
@@ -97,3 +126,78 @@ class ExprSequence extends Expr {
/** Holds if this sequence has no expressions. */
final predicate isEmpty() { this.getNumberOfExpressions() = 0 }
}
/**
* A scope resolution, typically used to access constants defined in a class or
* module.
* ```rb
* Foo::Bar
* ```
*/
class ScopeResolution extends Expr, @scope_resolution {
final override ScopeResolution::Range range;
final override string getAPrimaryQlClass() { result = "ScopeResolution" }
final override string toString() { result = "...::" + this.getName() }
/**
* Gets the expression representing the scope, if any. In the following
* example, the scope is the `Expr` for `Foo`:
* ```rb
* Foo::Bar
* ```
* However, in the following example, accessing the `Bar` constant in the
* `Object` class, there is no result:
* ```rb
* ::Bar
* ```
*/
final Expr getScope() { result = range.getScope() }
/**
* Gets the name being resolved. For example, in `Foo::Bar`, the result is
* `"Bar"`.
*/
final string getName() { result = range.getName() }
}
/**
* A pair expression. For example, in a hash:
* ```rb
* { foo: bar }
* ```
* Or a keyword argument:
* ```rb
* baz(qux: 1)
* ```
*/
class Pair extends Expr, @pair {
final override Pair::Range range;
final override string getAPrimaryQlClass() { result = "Pair" }
final override string toString() { result = "Pair" }
/**
* Gets the key expression of this pair. For example, the `SymbolLiteral`
* representing the keyword `foo` in the following example:
* ```rb
* bar(foo: 123)
* ```
* Or the `StringLiteral` for `'foo'` in the following hash pair:
* ```rb
* { 'foo' => 123 }
* ```
*/
final Expr getKey() { result = range.getKey() }
/**
* Gets the value expression of this pair. For example, the `InteralLiteral`
* 123 in the following hash pair:
* ```rb
* { 'foo' => 123 }
* ```
*/
final Expr getValue() { result = range.getValue() }
}

View File

@@ -80,7 +80,7 @@ class DoBlock extends Block, @do_block {
final override string getAPrimaryQlClass() { result = "DoBlock" }
final override string toString() { result = "| ... |" }
final override string toString() { result = "do ... end" }
}
/**

View File

@@ -0,0 +1,208 @@
private import codeql_ruby.AST
private import codeql_ruby.ast.internal.Expr
private import codeql_ruby.ast.internal.TreeSitter
private import codeql_ruby.ast.internal.Variable
module Call {
abstract class Range extends Expr::Range {
abstract Expr getReceiver();
abstract string getMethodName();
abstract ScopeResolution getMethodScopeResolution();
abstract Expr getArgument(int n);
abstract Block getBlock();
}
/**
* Holds if `i` is an `identifier` node occurring in the context where it
* should be considered a VCALL. VCALL is the term that MRI/Ripper uses
* internally when there's an identifier without arguments or parentheses,
* i.e. it *might* be a method call, but it might also be a variable access,
* depending on the bindings in the current scope.
* ```rb
* foo # in MRI this is a VCALL, and the predicate should hold for this
* bar() # in MRI this would be an FCALL. Tree-sitter gives us a `call` node,
* # and the `method` field will be an `identifier`, but this predicate
* # will not hold for that identifier.
* ```
*/
private predicate vcall(Generated::Identifier i) {
exists(Generated::ArgumentList x | x.getChild(_) = i)
or
exists(Generated::Array x | x.getChild(_) = i)
or
exists(Generated::Assignment x | x.getRight() = i)
or
exists(Generated::Begin x | x.getChild(_) = i)
or
exists(Generated::BeginBlock x | x.getChild(_) = i)
or
exists(Generated::Binary x | x.getLeft() = i or x.getRight() = i)
or
exists(Generated::Block x | x.getChild(_) = i)
or
exists(Generated::BlockArgument x | x.getChild() = i)
or
exists(Generated::Call x | x.getReceiver() = i)
or
exists(Generated::Case x | x.getValue() = i)
or
exists(Generated::Class x | x.getChild(_) = i)
or
exists(Generated::Conditional x |
x.getCondition() = i or x.getConsequence() = i or x.getAlternative() = i
)
or
exists(Generated::Do x | x.getChild(_) = i)
or
exists(Generated::DoBlock x | x.getChild(_) = i)
or
exists(Generated::ElementReference x | x.getChild(_) = i or x.getObject() = i)
or
exists(Generated::Else x | x.getChild(_) = i)
or
exists(Generated::Elsif x | x.getCondition() = i)
or
exists(Generated::EndBlock x | x.getChild(_) = i)
or
exists(Generated::Ensure x | x.getChild(_) = i)
or
exists(Generated::Exceptions x | x.getChild(_) = i)
or
exists(Generated::HashSplatArgument x | x.getChild() = i)
or
exists(Generated::If x | x.getCondition() = i)
or
exists(Generated::IfModifier x | x.getCondition() = i or x.getBody() = i)
or
exists(Generated::In x | x.getChild() = i)
or
exists(Generated::Interpolation x | x.getChild() = i)
or
exists(Generated::KeywordParameter x | x.getValue() = i)
or
exists(Generated::Method x | x.getChild(_) = i)
or
exists(Generated::Module x | x.getChild(_) = i)
or
exists(Generated::OperatorAssignment x | x.getRight() = i)
or
exists(Generated::OptionalParameter x | x.getValue() = i)
or
exists(Generated::Pair x | x.getKey() = i or x.getValue() = i)
or
exists(Generated::ParenthesizedStatements x | x.getChild(_) = i)
or
exists(Generated::Pattern x | x.getChild() = i)
or
exists(Generated::Program x | x.getChild(_) = i)
or
exists(Generated::Range x | x.getChild(_) = i)
or
exists(Generated::RescueModifier x | x.getBody() = i or x.getHandler() = i)
or
exists(Generated::RightAssignmentList x | x.getChild(_) = i)
or
exists(Generated::ScopeResolution x | x.getScope() = i)
or
exists(Generated::SingletonClass x | x.getValue() = i or x.getChild(_) = i)
or
exists(Generated::SingletonMethod x | x.getChild(_) = i or x.getObject() = i)
or
exists(Generated::SplatArgument x | x.getChild() = i)
or
exists(Generated::Superclass x | x.getChild() = i)
or
exists(Generated::Then x | x.getChild(_) = i)
or
exists(Generated::Unary x | x.getOperand() = i)
or
exists(Generated::Unless x | x.getCondition() = i)
or
exists(Generated::UnlessModifier x | x.getCondition() = i or x.getBody() = i)
or
exists(Generated::Until x | x.getCondition() = i)
or
exists(Generated::UntilModifier x | x.getCondition() = i or x.getBody() = i)
or
exists(Generated::While x | x.getCondition() = i)
or
exists(Generated::WhileModifier x | x.getCondition() = i or x.getBody() = i)
}
private class IdentifierCallRange extends Call::Range, @token_identifier {
final override Generated::Identifier generated;
IdentifierCallRange() { vcall(this) and not access(this, _) }
final override Expr getReceiver() { none() }
final override string getMethodName() { result = generated.getValue() }
final override ScopeResolution getMethodScopeResolution() { none() }
final override Expr getArgument(int n) { none() }
final override Block getBlock() { none() }
}
private class RegularCallRange extends Call::Range, @call {
final override Generated::Call generated;
final override Expr getReceiver() { result = generated.getReceiver() }
final override string getMethodName() {
result = generated.getMethod().(Generated::Token).getValue() or
result = getMethodScopeResolution().getName()
}
final override ScopeResolution getMethodScopeResolution() { result = generated.getMethod() }
final override Expr getArgument(int n) { result = generated.getArguments().getChild(n) }
final override Block getBlock() { result = generated.getBlock() }
}
}
module YieldCall {
class Range extends Call::Range, @yield {
final override Generated::Yield generated;
final override Expr getReceiver() { none() }
final override string getMethodName() { result = "yield" }
final override ScopeResolution getMethodScopeResolution() { none() }
final override Expr getArgument(int n) { result = generated.getChild().getChild(n) }
final override Block getBlock() { none() }
}
}
module BlockArgument {
class Range extends Expr::Range, @block_argument {
final override Generated::BlockArgument generated;
final Expr getExpr() { result = generated.getChild() }
}
}
module SplatArgument {
class Range extends Expr::Range, @splat_argument {
final override Generated::SplatArgument generated;
final Expr getExpr() { result = generated.getChild() }
}
}
module HashSplatArgument {
class Range extends Expr::Range, @hash_splat_argument {
final override Generated::HashSplatArgument generated;
final Expr getExpr() { result = generated.getChild() }
}
}

View File

@@ -1,5 +1,6 @@
private import codeql_ruby.AST
private import codeql_ruby.ast.internal.TreeSitter
private import codeql_ruby.ast.internal.Variable
module Expr {
abstract class Range extends AstNode { }
@@ -16,6 +17,8 @@ module IntegerLiteral {
final override Generated::Integer generated;
final override string getValueText() { result = generated.getValue() }
final override string toString() { result = getValueText() }
}
}
@@ -24,6 +27,8 @@ module NilLiteral {
final override Generated::Nil generated;
final override string getValueText() { result = generated.getValue() }
final override string toString() { result = getValueText() }
}
}
@@ -35,6 +40,8 @@ module BooleanLiteral {
final override string getValueText() { result = generated.getValue() }
final override string toString() { result = getValueText() }
predicate isTrue() { this instanceof @token_true }
predicate isFalse() { this instanceof @token_false }
@@ -47,6 +54,16 @@ module RegexLiteral {
final override Generated::Regex generated;
final override string getValueText() {
forall(AstNode n | n = generated.getChild(_) | n instanceof Generated::Token) and
result =
concat(int i, string s |
s = generated.getChild(i).(Generated::Token).getValue()
|
s order by i
)
}
final override string toString() {
result =
concat(AstNode c, int i, string s |
c = generated.getChild(i) and
@@ -60,6 +77,109 @@ module RegexLiteral {
}
}
// TODO: expand this minimal placeholder.
module StringLiteral {
class Range extends Literal::Range, @string__ {
final override Generated::String generated;
final override string getValueText() {
count(generated.getChild(_)) = 1 and
generated.getChild(0) instanceof Generated::Token and
result = generated.getChild(0).(Generated::Token).getValue()
}
final override string toString() {
result =
concat(AstNode c, int i, string s |
c = generated.getChild(i) and
if c instanceof Generated::Token
then s = c.(Generated::Token).getValue()
else s = "#{...}"
|
s order by i
)
}
}
}
// TODO: expand this minimal placeholder.
module SymbolLiteral {
abstract class Range extends Literal::Range { }
class SimpleSymbolRange extends SymbolLiteral::Range {
final override Generated::SimpleSymbol generated;
// Tree-sitter gives us value text including the colon, which we skip.
final override string getValueText() { result = generated.getValue().suffix(1) }
final override string toString() { result = generated.getValue() }
}
class DelimitedSymbolRange extends SymbolLiteral::Range, @delimited_symbol {
final override Generated::DelimitedSymbol generated;
final override string getValueText() {
count(generated.getChild(_)) = 1 and
generated.getChild(0) instanceof Generated::Token and
result = generated.getChild(0).(Generated::Token).getValue()
}
private string summaryString() {
result =
concat(AstNode c, int i, string s |
c = generated.getChild(i) and
if c instanceof Generated::Token
then s = c.(Generated::Token).getValue()
else s = "#{...}"
|
s order by i
)
}
final override string toString() {
if summaryString().regexpMatch("[a-zA-z_][a-zA-Z_0-9]*")
then result = ":" + summaryString()
else result = ":\"" + summaryString() + "\""
}
}
class BareSymbolRange extends SymbolLiteral::Range, @bare_symbol {
final override Generated::BareSymbol generated;
final override string getValueText() {
count(generated.getChild(_)) = 1 and
generated.getChild(0) instanceof Generated::Token and
result = generated.getChild(0).(Generated::Token).getValue()
}
private string summaryString() {
result =
concat(AstNode c, int i, string s |
c = generated.getChild(i) and
if c instanceof Generated::Token
then s = c.(Generated::Token).getValue()
else s = "#{...}"
|
s order by i
)
}
final override string toString() {
if summaryString().regexpMatch("[a-zA-z_][a-zA-Z_0-9]*")
then result = ":" + summaryString()
else result = ":\"" + summaryString() + "\""
}
}
class HashKeySymbolRange extends SymbolLiteral::Range, @token_hash_key_symbol {
final override Generated::HashKeySymbol generated;
final override string getValueText() { result = generated.getValue() }
final override string toString() { result = ":" + getValueText() }
}
}
module ExprSequence {
abstract class Range extends Expr::Range {
abstract Expr getExpr(int n);
@@ -89,3 +209,23 @@ module DoExpr {
final override Expr getExpr(int n) { result = generated.getChild(n) }
}
}
module ScopeResolution {
class Range extends Expr::Range, @scope_resolution {
final override Generated::ScopeResolution generated;
final Expr getScope() { result = generated.getScope() }
final string getName() { result = generated.getName().(Generated::Token).getValue() }
}
}
module Pair {
class Range extends Expr::Range, @pair {
final override Generated::Pair generated;
final Expr getKey() { result = generated.getKey() }
final Expr getValue() { result = generated.getValue() }
}
}

View File

@@ -15,8 +15,7 @@ module Method {
string getName() {
result = generated.getName().(Generated::Token).getValue() or
// TODO: use hand-written Symbol class
result = generated.getName().(Generated::Symbol).toString() or
result = generated.getName().(SymbolLiteral).getValueText() or
result = generated.getName().(Generated::Setter).getName().getValue() + "="
}
}
@@ -30,8 +29,7 @@ module SingletonMethod {
string getName() {
result = generated.getName().(Generated::Token).getValue() or
// TODO: use hand-written Symbol class
result = generated.getName().(Generated::Symbol).toString() or
result = generated.getName().(SymbolLiteral).getValueText() or
result = generated.getName().(Generated::Setter).getName().getValue() + "="
}
}

View File

@@ -432,6 +432,20 @@ module Generated {
override string getAPrimaryQlClass() { result = "Constant" }
}
class DelimitedSymbol extends @delimited_symbol, AstNode {
override string getAPrimaryQlClass() { result = "DelimitedSymbol" }
override Location getLocation() { delimited_symbol_def(this, _, _, result) }
AstNode getChild(int i) { delimited_symbol_child(this, i, result) }
override AstNode getParent() { delimited_symbol_def(this, result, _, _) }
override int getParentIndex() { delimited_symbol_def(this, _, result, _) }
override AstNode getAFieldOrChild() { delimited_symbol_child(this, _, result) }
}
class DestructuredLeftAssignment extends @destructured_left_assignment, AstNode {
override string getAPrimaryQlClass() { result = "DestructuredLeftAssignment" }
@@ -658,6 +672,10 @@ module Generated {
override AstNode getAFieldOrChild() { hash_child(this, _, result) }
}
class HashKeySymbol extends @token_hash_key_symbol, Token {
override string getAPrimaryQlClass() { result = "HashKeySymbol" }
}
class HashSplatArgument extends @hash_splat_argument, AstNode {
override string getAPrimaryQlClass() { result = "HashSplatArgument" }
@@ -1244,6 +1262,10 @@ module Generated {
override AstNode getAFieldOrChild() { setter_def(this, _, _, result, _) }
}
class SimpleSymbol extends @token_simple_symbol, Token {
override string getAPrimaryQlClass() { result = "SimpleSymbol" }
}
class SingletonClass extends @singleton_class, AstNode {
override string getAPrimaryQlClass() { result = "SingletonClass" }
@@ -1379,20 +1401,6 @@ module Generated {
override AstNode getAFieldOrChild() { superclass_def(this, _, _, result, _) }
}
class Symbol extends @symbol, AstNode {
override string getAPrimaryQlClass() { result = "Symbol" }
override Location getLocation() { symbol_def(this, _, _, result) }
AstNode getChild(int i) { symbol_child(this, i, result) }
override AstNode getParent() { symbol_def(this, result, _, _) }
override int getParentIndex() { symbol_def(this, _, result, _) }
override AstNode getAFieldOrChild() { symbol_child(this, _, result) }
}
class SymbolArray extends @symbol_array, AstNode {
override string getAPrimaryQlClass() { result = "SymbolArray" }