From 2e8d154f2b00dc7b06c0bc7e8247794195cd7f70 Mon Sep 17 00:00:00 2001 From: Nick Rolfe Date: Wed, 20 Jan 2021 18:14:29 +0000 Subject: [PATCH] Add AST classes and tests for method calls --- Cargo.lock | 2 +- extractor/Cargo.toml | 2 +- generator/Cargo.toml | 2 +- ql/src/codeql_ruby/AST.qll | 1 + ql/src/codeql_ruby/ast/Call.qll | 173 ++++++++ ql/src/codeql_ruby/ast/Expr.qll | 108 ++++- ql/src/codeql_ruby/ast/Method.qll | 2 +- ql/src/codeql_ruby/ast/internal/Call.qll | 208 ++++++++++ ql/src/codeql_ruby/ast/internal/Expr.qll | 140 +++++++ ql/src/codeql_ruby/ast/internal/Method.qll | 6 +- .../codeql_ruby/ast/internal/TreeSitter.qll | 36 +- .../internal/ControlFlowGraphImpl.qll | 6 +- ql/src/ruby.dbscheme | 70 ++-- .../ast/calls/arguments.expected | 10 + ql/test/library-tests/ast/calls/arguments.ql | 15 + .../library-tests/ast/calls/calls.expected | 101 +++++ ql/test/library-tests/ast/calls/calls.ql | 24 ++ ql/test/library-tests/ast/calls/calls.rb | 210 ++++++++++ .../library-tests/ast/params/params.expected | 18 +- .../controlflow/graph/Cfg.expected | 386 ++++++++++-------- .../library-tests/controlflow/graph/cfg.rb | 4 +- 21 files changed, 1275 insertions(+), 249 deletions(-) create mode 100644 ql/src/codeql_ruby/ast/Call.qll create mode 100644 ql/src/codeql_ruby/ast/internal/Call.qll create mode 100644 ql/test/library-tests/ast/calls/arguments.expected create mode 100644 ql/test/library-tests/ast/calls/arguments.ql create mode 100644 ql/test/library-tests/ast/calls/calls.expected create mode 100644 ql/test/library-tests/ast/calls/calls.ql create mode 100644 ql/test/library-tests/ast/calls/calls.rb diff --git a/Cargo.lock b/Cargo.lock index 42e93c82ae2..475d548f1b5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -645,7 +645,7 @@ dependencies = [ [[package]] name = "tree-sitter-ruby" version = "0.17.0" -source = "git+https://github.com/tree-sitter/tree-sitter-ruby.git?rev=5021a6a6eda24e10f954dcfec00e7a7adafba8ba#5021a6a6eda24e10f954dcfec00e7a7adafba8ba" +source = "git+https://github.com/tree-sitter/tree-sitter-ruby.git?rev=454f90628d1d14d0953b3be78bdb9a09f7a5bfd7#454f90628d1d14d0953b3be78bdb9a09f7a5bfd7" dependencies = [ "cc", "tree-sitter", diff --git a/extractor/Cargo.toml b/extractor/Cargo.toml index e21300dc32d..375985080e3 100644 --- a/extractor/Cargo.toml +++ b/extractor/Cargo.toml @@ -10,7 +10,7 @@ edition = "2018" flate2 = "1.0" node-types = { path = "../node-types" } tree-sitter = "0.17" -tree-sitter-ruby = { git = "https://github.com/tree-sitter/tree-sitter-ruby.git", rev = "5021a6a6eda24e10f954dcfec00e7a7adafba8ba" } +tree-sitter-ruby = { git = "https://github.com/tree-sitter/tree-sitter-ruby.git", rev = "454f90628d1d14d0953b3be78bdb9a09f7a5bfd7" } clap = "2.33" tracing = "0.1" tracing-subscriber = { version = "0.2", features = ["env-filter"] } diff --git a/generator/Cargo.toml b/generator/Cargo.toml index a62603a2f75..98415a80828 100644 --- a/generator/Cargo.toml +++ b/generator/Cargo.toml @@ -10,4 +10,4 @@ edition = "2018" node-types = { path = "../node-types" } tracing = "0.1" tracing-subscriber = { version = "0.2", features = ["env-filter"] } -tree-sitter-ruby = { git = "https://github.com/tree-sitter/tree-sitter-ruby.git", rev = "5021a6a6eda24e10f954dcfec00e7a7adafba8ba" } +tree-sitter-ruby = { git = "https://github.com/tree-sitter/tree-sitter-ruby.git", rev = "454f90628d1d14d0953b3be78bdb9a09f7a5bfd7" } diff --git a/ql/src/codeql_ruby/AST.qll b/ql/src/codeql_ruby/AST.qll index 08a5b7a315a..73e55f7a7bd 100644 --- a/ql/src/codeql_ruby/AST.qll +++ b/ql/src/codeql_ruby/AST.qll @@ -1,4 +1,5 @@ import codeql.Locations +import ast.Call import ast.Control import ast.Expr import ast.Method diff --git a/ql/src/codeql_ruby/ast/Call.qll b/ql/src/codeql_ruby/ast/Call.qll new file mode 100644 index 00000000000..5e933415a4d --- /dev/null +++ b/ql/src/codeql_ruby/ast/Call.qll @@ -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() } +} diff --git a/ql/src/codeql_ruby/ast/Expr.qll b/ql/src/codeql_ruby/ast/Expr.qll index 4b51b448ac5..03463cb469e 100644 --- a/ql/src/codeql_ruby/ast/Expr.qll +++ b/ql/src/codeql_ruby/ast/Expr.qll @@ -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() } +} diff --git a/ql/src/codeql_ruby/ast/Method.qll b/ql/src/codeql_ruby/ast/Method.qll index f4780b0bac9..5b8f5c5ccbd 100644 --- a/ql/src/codeql_ruby/ast/Method.qll +++ b/ql/src/codeql_ruby/ast/Method.qll @@ -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" } } /** diff --git a/ql/src/codeql_ruby/ast/internal/Call.qll b/ql/src/codeql_ruby/ast/internal/Call.qll new file mode 100644 index 00000000000..bdc97a46be7 --- /dev/null +++ b/ql/src/codeql_ruby/ast/internal/Call.qll @@ -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() } + } +} diff --git a/ql/src/codeql_ruby/ast/internal/Expr.qll b/ql/src/codeql_ruby/ast/internal/Expr.qll index 5580bc19a26..583fe106905 100644 --- a/ql/src/codeql_ruby/ast/internal/Expr.qll +++ b/ql/src/codeql_ruby/ast/internal/Expr.qll @@ -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() } + } +} diff --git a/ql/src/codeql_ruby/ast/internal/Method.qll b/ql/src/codeql_ruby/ast/internal/Method.qll index 9555dd5d337..9c06ae6386c 100644 --- a/ql/src/codeql_ruby/ast/internal/Method.qll +++ b/ql/src/codeql_ruby/ast/internal/Method.qll @@ -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() + "=" } } diff --git a/ql/src/codeql_ruby/ast/internal/TreeSitter.qll b/ql/src/codeql_ruby/ast/internal/TreeSitter.qll index 5fc45ebfc16..6c0bfe79c55 100644 --- a/ql/src/codeql_ruby/ast/internal/TreeSitter.qll +++ b/ql/src/codeql_ruby/ast/internal/TreeSitter.qll @@ -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" } diff --git a/ql/src/codeql_ruby/controlflow/internal/ControlFlowGraphImpl.qll b/ql/src/codeql_ruby/controlflow/internal/ControlFlowGraphImpl.qll index 39a62fcee08..c25e936b994 100644 --- a/ql/src/codeql_ruby/controlflow/internal/ControlFlowGraphImpl.qll +++ b/ql/src/codeql_ruby/controlflow/internal/ControlFlowGraphImpl.qll @@ -334,13 +334,13 @@ abstract private class PostOrderTree extends ControlFlowTree { private class LeftToRightPostOrderNodes = @argument_list or @array or @bare_string or @bare_symbol or @binary or @block_argument or - @break or @call or @chained_string or @destructured_left_assignment or + @break or @call or @chained_string or @delimited_symbol or @destructured_left_assignment or @destructured_parameter or @element_reference or @exception_variable or @hash or @hash_splat_argument or @heredoc_body or @interpolation or @left_assignment_list or @next or @operator_assignment or @pair or @parenthesized_statements or @range or @redo or @regex or @rest_assignment or @retry or @return or @right_assignment_list or @scope_resolution or - @splat_argument or @splat_parameter or @string__ or @string_array or @subshell or - @superclass or @symbol or @symbol_array or @unary; + @token_simple_symbol or @splat_argument or @splat_parameter or @string__ or @string_array or + @subshell or @superclass or @symbol_array or @token_hash_key_symbol or @unary; private class LeftToRightPostOrderTree extends StandardPostOrderTree, LeftToRightPostOrderNodes { LeftToRightPostOrderTree() { diff --git a/ql/src/ruby.dbscheme b/ql/src/ruby.dbscheme index 235c3e83f29..5df84541a21 100644 --- a/ql/src/ruby.dbscheme +++ b/ql/src/ruby.dbscheme @@ -50,9 +50,9 @@ sourceLocationPrefix( @underscore_lhs = @call | @element_reference | @scope_resolution | @token_false | @token_nil | @token_true | @underscore_variable -@underscore_method_name = @setter | @symbol | @token_class_variable | @token_constant | @token_global_variable | @token_identifier | @token_instance_variable | @token_operator +@underscore_method_name = @delimited_symbol | @setter | @token_class_variable | @token_constant | @token_global_variable | @token_identifier | @token_instance_variable | @token_operator | @token_simple_symbol -@underscore_primary = @array | @begin | @break | @case__ | @chained_string | @class | @for | @hash | @if | @lambda | @method | @module | @next | @parenthesized_statements | @rational | @redo | @regex | @retry | @return | @singleton_class | @singleton_method | @string__ | @string_array | @subshell | @symbol | @symbol_array | @token_character | @token_complex | @token_float | @token_heredoc_beginning | @token_integer | @unary | @underscore_lhs | @unless | @until | @while | @yield +@underscore_primary = @array | @begin | @break | @case__ | @chained_string | @class | @delimited_symbol | @for | @hash | @if | @lambda | @method | @module | @next | @parenthesized_statements | @rational | @redo | @regex | @retry | @return | @singleton_class | @singleton_method | @string__ | @string_array | @subshell | @symbol_array | @token_character | @token_complex | @token_float | @token_heredoc_beginning | @token_integer | @token_simple_symbol | @unary | @underscore_lhs | @unless | @until | @while | @yield @underscore_statement = @alias | @assignment | @begin_block | @binary | @break | @call | @end_block | @if_modifier | @next | @operator_assignment | @rescue_modifier | @return | @unary | @undef | @underscore_arg | @unless_modifier | @until_modifier | @while_modifier | @yield @@ -396,6 +396,23 @@ conditional_def( int loc: @location ref ); +@delimited_symbol_child_type = @interpolation | @token_escape_sequence | @token_string_content + +#keyset[delimited_symbol, index] +delimited_symbol_child( + int delimited_symbol: @delimited_symbol ref, + int index: int ref, + unique int child: @delimited_symbol_child_type ref +); + +#keyset[parent, parent_index] +delimited_symbol_def( + unique int id: @delimited_symbol, + int parent: @ast_node_parent ref, + int parent_index: int ref, + int loc: @location ref +); + @destructured_left_assignment_child_type = @destructured_left_assignment | @rest_assignment | @underscore_lhs #keyset[destructured_left_assignment, index] @@ -886,7 +903,7 @@ optional_parameter_def( int loc: @location ref ); -@pair_key_type = @string__ | @symbol | @underscore_arg +@pair_key_type = @string__ | @token_hash_key_symbol | @underscore_arg #keyset[parent, parent_index] pair_def( @@ -1241,23 +1258,6 @@ superclass_def( int loc: @location ref ); -@symbol_child_type = @interpolation | @token_escape_sequence | @token_string_content - -#keyset[symbol, index] -symbol_child( - int symbol: @symbol ref, - int index: int ref, - unique int child: @symbol_child_type ref -); - -#keyset[parent, parent_index] -symbol_def( - unique int id: @symbol, - int parent: @ast_node_parent ref, - int parent_index: int ref, - int loc: @location ref -); - #keyset[symbol_array, index] symbol_array_child( int symbol_array: @symbol_array ref, @@ -1461,23 +1461,25 @@ case @token.kind of | 8 = @token_false | 9 = @token_float | 10 = @token_global_variable -| 11 = @token_heredoc_beginning -| 12 = @token_heredoc_content -| 13 = @token_heredoc_end -| 14 = @token_identifier -| 15 = @token_instance_variable -| 16 = @token_integer -| 17 = @token_nil -| 18 = @token_operator -| 19 = @token_self -| 20 = @token_string_content -| 21 = @token_super -| 22 = @token_true -| 23 = @token_uninterpreted +| 11 = @token_hash_key_symbol +| 12 = @token_heredoc_beginning +| 13 = @token_heredoc_content +| 14 = @token_heredoc_end +| 15 = @token_identifier +| 16 = @token_instance_variable +| 17 = @token_integer +| 18 = @token_nil +| 19 = @token_operator +| 20 = @token_self +| 21 = @token_simple_symbol +| 22 = @token_string_content +| 23 = @token_super +| 24 = @token_true +| 25 = @token_uninterpreted ; -@ast_node = @alias | @argument_list | @array | @assignment | @bare_string | @bare_symbol | @begin | @begin_block | @binary | @block | @block_argument | @block_parameter | @block_parameters | @break | @call | @case__ | @chained_string | @class | @conditional | @destructured_left_assignment | @destructured_parameter | @do | @do_block | @element_reference | @else | @elsif | @end_block | @ensure | @exception_variable | @exceptions | @for | @hash | @hash_splat_argument | @hash_splat_parameter | @heredoc_body | @if | @if_modifier | @in | @interpolation | @keyword_parameter | @lambda | @lambda_parameters | @left_assignment_list | @method | @method_parameters | @module | @next | @operator_assignment | @optional_parameter | @pair | @parenthesized_statements | @pattern | @program | @range | @rational | @redo | @regex | @rescue | @rescue_modifier | @rest_assignment | @retry | @return | @right_assignment_list | @scope_resolution | @setter | @singleton_class | @singleton_method | @splat_argument | @splat_parameter | @string__ | @string_array | @subshell | @superclass | @symbol | @symbol_array | @then | @token | @unary | @undef | @unless | @unless_modifier | @until | @until_modifier | @when | @while | @while_modifier | @yield +@ast_node = @alias | @argument_list | @array | @assignment | @bare_string | @bare_symbol | @begin | @begin_block | @binary | @block | @block_argument | @block_parameter | @block_parameters | @break | @call | @case__ | @chained_string | @class | @conditional | @delimited_symbol | @destructured_left_assignment | @destructured_parameter | @do | @do_block | @element_reference | @else | @elsif | @end_block | @ensure | @exception_variable | @exceptions | @for | @hash | @hash_splat_argument | @hash_splat_parameter | @heredoc_body | @if | @if_modifier | @in | @interpolation | @keyword_parameter | @lambda | @lambda_parameters | @left_assignment_list | @method | @method_parameters | @module | @next | @operator_assignment | @optional_parameter | @pair | @parenthesized_statements | @pattern | @program | @range | @rational | @redo | @regex | @rescue | @rescue_modifier | @rest_assignment | @retry | @return | @right_assignment_list | @scope_resolution | @setter | @singleton_class | @singleton_method | @splat_argument | @splat_parameter | @string__ | @string_array | @subshell | @superclass | @symbol_array | @then | @token | @unary | @undef | @unless | @unless_modifier | @until | @until_modifier | @when | @while | @while_modifier | @yield @ast_node_parent = @ast_node | @file diff --git a/ql/test/library-tests/ast/calls/arguments.expected b/ql/test/library-tests/ast/calls/arguments.expected new file mode 100644 index 00000000000..39c53ae071d --- /dev/null +++ b/ql/test/library-tests/ast/calls/arguments.expected @@ -0,0 +1,10 @@ +blockArguments +| calls.rb:201:5:201:8 | &... | calls.rb:201:6:201:8 | call to bar | +splatArguments +| calls.rb:204:5:204:8 | *... | calls.rb:204:6:204:8 | call to bar | +hashSplatArguments +| calls.rb:207:5:207:9 | **... | calls.rb:207:7:207:9 | call to bar | +keywordArguments +| calls.rb:210:5:210:13 | Pair | calls.rb:210:5:210:8 | :blah | calls.rb:210:11:210:13 | call to bar | +keywordArgumentsByKeyword +| calls.rb:210:1:210:14 | call to foo | blah | calls.rb:210:11:210:13 | call to bar | diff --git a/ql/test/library-tests/ast/calls/arguments.ql b/ql/test/library-tests/ast/calls/arguments.ql new file mode 100644 index 00000000000..c4086e496ec --- /dev/null +++ b/ql/test/library-tests/ast/calls/arguments.ql @@ -0,0 +1,15 @@ +import ruby + +query predicate blockArguments(BlockArgument a, Expr e) { e = a.getExpr() } + +query predicate splatArguments(SplatArgument a, Expr e) { e = a.getExpr() } + +query predicate hashSplatArguments(HashSplatArgument a, Expr e) { e = a.getExpr() } + +query predicate keywordArguments(Pair a, Expr key, Expr value) { + exists(Call c | c.getAnArgument() = a and key = a.getKey() and value = a.getValue()) +} + +query predicate keywordArgumentsByKeyword(Call c, string keyword, Expr value) { + c.getKeywordArgument(keyword) = value +} diff --git a/ql/test/library-tests/ast/calls/calls.expected b/ql/test/library-tests/ast/calls/calls.expected new file mode 100644 index 00000000000..53a9b998602 --- /dev/null +++ b/ql/test/library-tests/ast/calls/calls.expected @@ -0,0 +1,101 @@ +callsWithNoReceiverArgumentsOrBlock +| calls.rb:2:1:2:5 | call to foo | foo | +| calls.rb:5:1:5:10 | call to bar | bar | +| calls.rb:28:3:28:7 | call to yield | yield | +| calls.rb:42:1:42:3 | call to foo | foo | +| calls.rb:45:2:45:4 | call to foo | foo | +| calls.rb:48:11:48:13 | call to foo | foo | +| calls.rb:51:2:51:4 | call to foo | foo | +| calls.rb:54:8:54:10 | call to foo | foo | +| calls.rb:57:9:57:11 | call to bar | bar | +| calls.rb:60:8:60:10 | call to foo | foo | +| calls.rb:60:13:60:15 | call to bar | bar | +| calls.rb:64:3:64:5 | call to foo | foo | +| calls.rb:68:9:68:11 | call to foo | foo | +| calls.rb:71:7:71:9 | call to foo | foo | +| calls.rb:74:1:74:3 | call to foo | foo | +| calls.rb:74:7:74:9 | call to bar | bar | +| calls.rb:77:2:77:4 | call to foo | foo | +| calls.rb:80:1:80:13 | call to foo | foo | +| calls.rb:80:9:80:11 | call to bar | bar | +| calls.rb:83:1:83:16 | call to foo | foo | +| calls.rb:83:10:83:12 | call to bar | bar | +| calls.rb:86:1:86:3 | call to foo | foo | +| calls.rb:90:6:90:8 | call to foo | foo | +| calls.rb:91:6:91:8 | call to bar | bar | +| calls.rb:92:3:92:5 | call to baz | baz | +| calls.rb:97:3:97:5 | call to foo | foo | +| calls.rb:101:17:101:19 | call to foo | foo | +| calls.rb:105:10:105:12 | call to foo | foo | +| calls.rb:106:3:106:5 | call to bar | bar | +| calls.rb:111:3:111:5 | call to foo | foo | +| calls.rb:115:5:115:7 | call to foo | foo | +| calls.rb:116:3:116:5 | call to bar | bar | +| calls.rb:120:40:120:42 | call to foo | foo | +| calls.rb:124:40:124:42 | call to foo | foo | +| calls.rb:129:3:129:5 | call to foo | foo | +| calls.rb:133:1:133:3 | call to foo | foo | +| calls.rb:133:7:133:9 | call to bar | bar | +| calls.rb:133:13:133:15 | call to baz | baz | +| calls.rb:136:4:136:6 | call to foo | foo | +| calls.rb:137:3:137:8 | call to wibble | wibble | +| calls.rb:138:7:138:9 | call to bar | bar | +| calls.rb:139:3:139:8 | call to wobble | wobble | +| calls.rb:141:3:141:8 | call to wabble | wabble | +| calls.rb:145:1:145:3 | call to bar | bar | +| calls.rb:145:8:145:10 | call to foo | foo | +| calls.rb:148:8:148:10 | call to foo | foo | +| calls.rb:149:3:149:5 | call to bar | bar | +| calls.rb:153:1:153:3 | call to bar | bar | +| calls.rb:153:12:153:14 | call to foo | foo | +| calls.rb:156:7:156:9 | call to foo | foo | +| calls.rb:157:3:157:5 | call to bar | bar | +| calls.rb:161:1:161:3 | call to bar | bar | +| calls.rb:161:11:161:13 | call to foo | foo | +| calls.rb:164:7:164:9 | call to foo | foo | +| calls.rb:165:3:165:5 | call to bar | bar | +| calls.rb:169:1:169:3 | call to bar | bar | +| calls.rb:169:11:169:13 | call to foo | foo | +| calls.rb:172:10:172:12 | call to bar | bar | +| calls.rb:173:3:173:5 | call to baz | baz | +| calls.rb:177:1:177:3 | call to foo | foo | +| calls.rb:177:5:177:7 | call to bar | bar | +| calls.rb:180:8:180:10 | call to bar | bar | +| calls.rb:183:1:183:3 | call to foo | foo | +| calls.rb:186:1:186:3 | call to foo | foo | +| calls.rb:186:6:186:8 | call to bar | bar | +| calls.rb:189:3:189:5 | call to foo | foo | +| calls.rb:189:10:189:12 | call to bar | bar | +| calls.rb:193:8:193:10 | call to foo | foo | +| calls.rb:194:8:194:10 | call to bar | bar | +| calls.rb:198:1:198:3 | call to foo | foo | +| calls.rb:198:12:198:14 | call to bar | bar | +| calls.rb:201:6:201:8 | call to bar | bar | +| calls.rb:204:6:204:8 | call to bar | bar | +| calls.rb:207:7:207:9 | call to bar | bar | +| calls.rb:210:11:210:13 | call to bar | bar | +callsWithScopeResolutionName +| calls.rb:5:1:5:10 | call to bar | calls.rb:5:1:5:8 | ...::bar | +callsWithArguments +| calls.rb:11:1:11:11 | call to foo | foo | 0 | calls.rb:11:5:11:5 | 0 | +| calls.rb:11:1:11:11 | call to foo | foo | 1 | calls.rb:11:8:11:8 | 1 | +| calls.rb:11:1:11:11 | call to foo | foo | 2 | calls.rb:11:11:11:11 | 2 | +| calls.rb:22:1:24:3 | call to bar | bar | 0 | calls.rb:22:9:22:13 | foo | +| calls.rb:33:3:33:16 | call to yield | yield | 0 | calls.rb:33:9:33:11 | 100 | +| calls.rb:33:3:33:16 | call to yield | yield | 1 | calls.rb:33:14:33:16 | 200 | +| calls.rb:48:1:48:14 | call to some_func | some_func | 0 | calls.rb:48:11:48:13 | call to foo | +| calls.rb:201:1:201:9 | call to foo | foo | 0 | calls.rb:201:5:201:8 | &... | +| calls.rb:204:1:204:9 | call to foo | foo | 0 | calls.rb:204:5:204:8 | *... | +| calls.rb:207:1:207:10 | call to foo | foo | 0 | calls.rb:207:5:207:9 | **... | +| calls.rb:210:1:210:14 | call to foo | foo | 0 | calls.rb:210:5:210:13 | Pair | +callsWithReceiver +| calls.rb:8:1:8:7 | call to bar | calls.rb:8:1:8:3 | 123 | +| calls.rb:22:1:24:3 | call to bar | calls.rb:22:1:22:3 | 123 | +| calls.rb:86:1:86:9 | call to bar | calls.rb:86:1:86:3 | call to foo | +callsWithBlock +| calls.rb:14:1:14:17 | call to foo | calls.rb:14:5:14:17 | { ... } | +| calls.rb:17:1:19:3 | call to foo | calls.rb:17:5:19:3 | do ... end | +| calls.rb:22:1:24:3 | call to bar | calls.rb:22:16:24:3 | do ... end | +yieldCalls +| calls.rb:28:3:28:7 | call to yield | +| calls.rb:33:3:33:16 | call to yield | diff --git a/ql/test/library-tests/ast/calls/calls.ql b/ql/test/library-tests/ast/calls/calls.ql new file mode 100644 index 00000000000..7e29d5f7dd6 --- /dev/null +++ b/ql/test/library-tests/ast/calls/calls.ql @@ -0,0 +1,24 @@ +import ruby +import codeql_ruby.ast.internal.TreeSitter + +query predicate callsWithNoReceiverArgumentsOrBlock(Call c, string name) { + name = c.getMethodName() and + not exists(c.getReceiver()) and + not exists(c.getAnArgument()) and + not exists(c.getBlock()) +} + +query predicate callsWithScopeResolutionName(Call c, ScopeResolution sr) { + sr = c.getMethodScopeResolution() +} + +query predicate callsWithArguments(Call c, string name, int n, Expr argN) { + name = c.getMethodName() and + argN = c.getArgument(n) +} + +query predicate callsWithReceiver(Call c, Expr rcv) { rcv = c.getReceiver() } + +query predicate callsWithBlock(Call c, Block b) { b = c.getBlock() } + +query predicate yieldCalls(YieldCall c) { any() } diff --git a/ql/test/library-tests/ast/calls/calls.rb b/ql/test/library-tests/ast/calls/calls.rb new file mode 100644 index 00000000000..7133ee98ff4 --- /dev/null +++ b/ql/test/library-tests/ast/calls/calls.rb @@ -0,0 +1,210 @@ +# call with no receiver, arguments, or block +foo() + +# call whose name is a scope resolution +Foo::bar() + +# call with a receiver, no arguments or block +123.bar + +# call with arguments +foo 0, 1, 2 + +# call with curly brace block +foo { |x| x + 1 } + +# call with do block +foo do |x| + x + 1 +end + +# call with receiver, arguments, and a block +123.bar('foo') do |x| + x + 1 +end + +# a yield call +def method_that_yields + yield +end + +# a yield call with arguments +def another_method_that_yields + yield 100, 200 +end + +# ------------------------------------------------------------------------------ +# Calls without parentheses or arguments are parsed by tree-sitter simply as +# `identifier` nodes, so here we test that our AST library correctly represents +# them as calls in all the following contexts. + +# root level (child of program) +foo + +# in a parenthesized statement +(foo) + +# in an argument list +some_func(foo) + +# in an array +[foo] + +# RHS of an assignment +var1 = foo + +# RHS an operator assignment +var1 += bar + +# RHS assignment list +var1 = foo, bar + +# in a begin-end block +begin + foo +end + +# in a BEGIN block +BEGIN { foo } + +# in an END block +END { foo } + +# both operands of a binary operation +foo + bar + +# unary operand +!foo + +# in a curly brace block +foo() { bar } + +# in a do-end block +foo() do bar end + +# the receiver in a call can itself be a call +foo.bar() + +# the value for a case expr +# and the when pattern and body +case foo +when bar + baz +end + +# in a class definition +class MyClass + foo +end + +# in a superclass +class MyClass < foo +end + +# in a singleton class value or body +class << foo + bar +end + +# in a method body +def some_method + foo +end + +# in a singleton method object or body +def foo.some_method + bar +end + +# in the default value for a keyword parameter +def method_with_keyword_param(keyword: foo) +end + +# in the default value for an optional parameter +def method_with_optional_param(param = foo) +end + +# in a module +module SomeModule + foo +end + +# ternary if: condition, consequence, and alternative can all be calls +foo ? bar : baz + +# if/elsif/else conditions and bodies +if foo + wibble +elsif bar + wobble +else + wabble +end + +# if-modifier condition/body +bar if foo + +# unless condition/body +unless foo + bar +end + +# unless-modifier condition/body +bar unless foo + +# while loop condition/body +while foo do + bar +end + +# while-modifier loop condition/body +bar while foo + +# until loop condition/body +until foo do + bar +end + +# until-modifier loop condition/body +bar until foo + +# the collection being iterated over in a for loop, and the body +for x in bar + baz +end + +# in an array indexing operation, both the object and the index can be calls +foo[bar] + +# interpolation +"foo-#{bar}" + +# the scope in a scope resolution +foo::Bar + +# in a range +foo..bar + +# the key/value in a hash pair +{ foo => bar } + +# rescue exceptions and ensure +begin +rescue foo +ensure bar +end + +# rescue-modifier body and handler +foo rescue bar + +# block argument +foo(&bar) + +# splat argument +foo(*bar) + +# hash-splat argument +foo(**bar) + +# the value in a keyword argument +foo(blah: bar) \ No newline at end of file diff --git a/ql/test/library-tests/ast/params/params.expected b/ql/test/library-tests/ast/params/params.expected index dfb59506714..b86c4482826 100644 --- a/ql/test/library-tests/ast/params/params.expected +++ b/ql/test/library-tests/ast/params/params.expected @@ -84,16 +84,16 @@ paramsInMethods | params.rb:58:1:59:3 | method_with_optional_params | 2 | params.rb:58:49:58:58 | val3 | OptionalParameter | | params.rb:62:1:64:3 | use_block_with_optional | 0 | params.rb:62:29:62:34 | &block | BlockParameter | paramsInBlocks -| params.rb:9:11:11:3 | \| ... \| | 0 | params.rb:9:15:9:17 | key | SimpleParameter | -| params.rb:9:11:11:3 | \| ... \| | 1 | params.rb:9:20:9:24 | value | SimpleParameter | +| params.rb:9:11:11:3 | do ... end | 0 | params.rb:9:15:9:17 | key | SimpleParameter | +| params.rb:9:11:11:3 | do ... end | 1 | params.rb:9:20:9:24 | value | SimpleParameter | | params.rb:22:12:22:32 | { ... } | 0 | params.rb:22:15:22:20 | (..., ...) | TuplePatternParameter | -| params.rb:34:12:35:3 | \| ... \| | 0 | params.rb:34:16:34:18 | val | SimpleParameter | -| params.rb:34:12:35:3 | \| ... \| | 1 | params.rb:34:21:34:26 | *splat | SplatParameter | -| params.rb:34:12:35:3 | \| ... \| | 2 | params.rb:34:29:34:42 | **double_splat | HashSplatParameter | -| params.rb:49:24:51:3 | \| ... \| | 0 | params.rb:49:28:49:30 | xx | KeywordParameter | -| params.rb:49:24:51:3 | \| ... \| | 1 | params.rb:49:33:49:39 | yy | KeywordParameter | -| params.rb:65:25:67:3 | \| ... \| | 0 | params.rb:65:29:65:32 | name | SimpleParameter | -| params.rb:65:25:67:3 | \| ... \| | 1 | params.rb:65:35:65:42 | age | OptionalParameter | +| params.rb:34:12:35:3 | do ... end | 0 | params.rb:34:16:34:18 | val | SimpleParameter | +| params.rb:34:12:35:3 | do ... end | 1 | params.rb:34:21:34:26 | *splat | SplatParameter | +| params.rb:34:12:35:3 | do ... end | 2 | params.rb:34:29:34:42 | **double_splat | HashSplatParameter | +| params.rb:49:24:51:3 | do ... end | 0 | params.rb:49:28:49:30 | xx | KeywordParameter | +| params.rb:49:24:51:3 | do ... end | 1 | params.rb:49:33:49:39 | yy | KeywordParameter | +| params.rb:65:25:67:3 | do ... end | 0 | params.rb:65:29:65:32 | name | SimpleParameter | +| params.rb:65:25:67:3 | do ... end | 1 | params.rb:65:35:65:42 | age | OptionalParameter | paramsInLambdas | params.rb:14:7:14:33 | -> { ... } | 0 | params.rb:14:11:14:13 | foo | SimpleParameter | | params.rb:14:7:14:33 | -> { ... } | 1 | params.rb:14:16:14:18 | bar | SimpleParameter | diff --git a/ql/test/library-tests/controlflow/graph/Cfg.expected b/ql/test/library-tests/controlflow/graph/Cfg.expected index 43788edcec7..27112a8de97 100644 --- a/ql/test/library-tests/controlflow/graph/Cfg.expected +++ b/ql/test/library-tests/controlflow/graph/Cfg.expected @@ -58,10 +58,10 @@ cfg.rb: # 153| enter two_parameters #-----| -> a -# 185| enter run_block +# 187| enter run_block #-----| -> Yield -# 189| enter block +# 191| enter block #-----| -> x exit.rb: @@ -674,12 +674,12 @@ cfg.rb: #-----| -> Proc # 27| puts -#-----| -> Symbol +#-----| -> :puts # 27| BlockArgument #-----| -> Call -# 27| Symbol +# 27| :puts #-----| -> BlockArgument # 29| Call @@ -1202,7 +1202,7 @@ cfg.rb: #-----| -> Pair # 97| Pair -#-----| -> Hash +#-----| -> e # 97| String #-----| -> String @@ -1210,6 +1210,15 @@ cfg.rb: # 97| String #-----| -> Pair +# 97| Pair +#-----| -> Hash + +# 97| e +#-----| -> String + +# 97| String +#-----| -> Pair + # 98| Assignment #-----| -> parameters @@ -1700,7 +1709,7 @@ cfg.rb: #-----| -> Array # 157| Assignment -#-----| -> Symbol +#-----| -> :hello # 157| scriptfile #-----| -> Assignment @@ -1715,89 +1724,89 @@ cfg.rb: #-----| -> Interpolation # 159| Assignment -#-----| -> true +#-----| -> 12 # 159| symbol #-----| -> Assignment -# 159| Symbol +# 159| :hello #-----| -> symbol # 161| Assignment #-----| -> true -# 161| x +# 161| delimited_symbol #-----| -> Assignment -# 161| true -#-----| -> x +# 161| DelimitedSymbol +#-----| -> delimited_symbol -# 162| Assignment -#-----| -> 42 +# 161| Interpolation +#-----| -> DelimitedSymbol -# 162| x -#-----| -> Assignment +# 161| Binary +#-----| -> Interpolation -# 162| Unary -#-----| -> x +# 161| 12 +#-----| -> 13 -# 162| true -#-----| -> Unary +# 161| 13 +#-----| -> Binary # 163| Assignment -#-----| -> Undef +#-----| -> true # 163| x #-----| -> Assignment -# 163| Unary +# 163| true #-----| -> x -# 163| 42 +# 164| Assignment +#-----| -> 42 + +# 164| x +#-----| -> Assignment + +# 164| Unary +#-----| -> x + +# 164| true #-----| -> Unary -# 165| Undef +# 165| Assignment +#-----| -> Undef + +# 165| x +#-----| -> Assignment + +# 165| Unary +#-----| -> x + +# 165| 42 +#-----| -> Unary + +# 167| Undef #-----| -> two_parameters -# 165| two_parameters +# 167| two_parameters #-----| -> x -# 167| Unless +# 169| Unless #-----| -> x -# 167| Binary +# 169| Binary #-----| false -> puts #-----| true -> puts -# 167| x +# 169| x #-----| -> 10 -# 167| 10 +# 169| 10 #-----| -> Binary -# 167| Call -#-----| -> Unless - -# 167| puts -#-----| -> String - -# 167| String -#-----| -> Call - -# 167| Call -#-----| -> Unless - -# 167| puts -#-----| -> String - -# 167| String -#-----| -> Call - -# 169| UnlessModifier -#-----| -> Until - # 169| Call -#-----| -> UnlessModifier +#-----| -> Unless # 169| puts #-----| -> String @@ -1805,40 +1814,20 @@ cfg.rb: # 169| String #-----| -> Call -# 169| Binary -#-----| true -> UnlessModifier -#-----| false -> puts +# 169| Call +#-----| -> Unless -# 169| x -#-----| -> 0 +# 169| puts +#-----| -> String -# 169| 0 -#-----| -> Binary +# 169| String +#-----| -> Call -# 171| Until -#-----| -> x - -# 171| Binary -#-----| false -> x -#-----| true -> 0 - -# 171| x -#-----| -> 10 - -# 171| 10 -#-----| -> Binary - -# 171| OperatorAssignment -#-----| -> puts - -# 171| x -#-----| -> 10 - -# 171| 10 -#-----| -> OperatorAssignment +# 171| UnlessModifier +#-----| -> Until # 171| Call -#-----| -> x +#-----| -> UnlessModifier # 171| puts #-----| -> String @@ -1846,169 +1835,210 @@ cfg.rb: # 171| String #-----| -> Call -# 173| Assignment -#-----| -> UntilModifier +# 171| Binary +#-----| true -> UnlessModifier +#-----| false -> puts -# 173| i -#-----| -> Assignment +# 171| x +#-----| -> 0 -# 173| 0 -#-----| -> i +# 171| 0 +#-----| -> Binary -# 174| UntilModifier -#-----| -> i +# 173| Until +#-----| -> x -# 174| ParenthesizedStatements -#-----| -> i +# 173| Binary +#-----| false -> x +#-----| true -> 0 -# 174| Call -#-----| -> i +# 173| x +#-----| -> 10 -# 174| puts -#-----| -> String +# 173| 10 +#-----| -> Binary -# 174| String -#-----| -> Call +# 173| OperatorAssignment +#-----| -> puts -# 174| OperatorAssignment -#-----| -> ParenthesizedStatements +# 173| x +#-----| -> 10 -# 174| i -#-----| -> 1 - -# 174| 1 +# 173| 10 #-----| -> OperatorAssignment -# 174| Binary +# 173| Call +#-----| -> x + +# 173| puts +#-----| -> String + +# 173| String +#-----| -> Call + +# 175| Assignment +#-----| -> UntilModifier + +# 175| i +#-----| -> Assignment + +# 175| 0 +#-----| -> i + +# 176| UntilModifier +#-----| -> i + +# 176| ParenthesizedStatements +#-----| -> i + +# 176| Call +#-----| -> i + +# 176| puts +#-----| -> String + +# 176| String +#-----| -> Call + +# 176| OperatorAssignment +#-----| -> ParenthesizedStatements + +# 176| i +#-----| -> 1 + +# 176| 1 +#-----| -> OperatorAssignment + +# 176| Binary #-----| false -> puts #-----| true -> 0 -# 174| i +# 176| i #-----| -> 10 -# 174| 10 +# 176| 10 #-----| -> Binary -# 176| Assignment +# 178| Assignment #-----| -> While -# 176| x +# 178| x #-----| -> Assignment -# 176| 0 +# 178| 0 #-----| -> x -# 177| While +# 179| While #-----| -> x -# 177| Binary +# 179| Binary #-----| true -> x #-----| false -> WhileModifier -# 177| x +# 179| x #-----| -> 10 -# 177| 10 +# 179| 10 #-----| -> Binary -# 178| OperatorAssignment -#-----| -> x - -# 178| x -#-----| -> 1 - -# 178| 1 -#-----| -> OperatorAssignment - -# 179| If -#-----| -> puts - -# 179| Binary -#-----| false -> If -#-----| true -> Redo - -# 179| x -#-----| -> 5 - -# 179| 5 -#-----| -> Binary - -# 179| Redo -#-----| redo -> x - -# 180| Call -#-----| -> x - -# 180| puts +# 180| OperatorAssignment #-----| -> x # 180| x -#-----| -> Call - -# 183| WhileModifier -#-----| -> i - -# 183| ParenthesizedStatements -#-----| -> i - -# 183| Call -#-----| -> i - -# 183| puts -#-----| -> String - -# 183| String -#-----| -> Call - -# 183| OperatorAssignment -#-----| -> ParenthesizedStatements - -# 183| i #-----| -> 1 -# 183| 1 +# 180| 1 #-----| -> OperatorAssignment -# 183| Binary +# 181| If +#-----| -> puts + +# 181| Binary +#-----| false -> If +#-----| true -> Redo + +# 181| x +#-----| -> 5 + +# 181| 5 +#-----| -> Binary + +# 181| Redo +#-----| redo -> x + +# 182| Call +#-----| -> x + +# 182| puts +#-----| -> x + +# 182| x +#-----| -> Call + +# 185| WhileModifier +#-----| -> i + +# 185| ParenthesizedStatements +#-----| -> i + +# 185| Call +#-----| -> i + +# 185| puts +#-----| -> String + +# 185| String +#-----| -> Call + +# 185| OperatorAssignment +#-----| -> ParenthesizedStatements + +# 185| i +#-----| -> 1 + +# 185| 1 +#-----| -> OperatorAssignment + +# 185| Binary #-----| true -> puts #-----| false -> run_block -# 183| i +# 185| i #-----| -> 0 -# 183| 0 +# 185| 0 #-----| -> Binary -# 185| Method +# 187| Method #-----| -> run_block -# 185| run_block +# 187| run_block #-----| -> Method -# 186| Yield +# 188| Yield #-----| -> 42 -# 186| 42 +# 188| 42 -# 189| Call +# 191| Call #-----| -> exit top-level (normal) -# 189| run_block +# 191| run_block #-----| -> Block -# 189| Block +# 191| Block #-----| -> Call -# 189| x +# 191| x #-----| -> puts -# 189| Call +# 191| Call #-----| -> exit block (normal) -# 189| puts +# 191| puts #-----| -> x -# 189| x +# 191| x #-----| -> Call exit.rb: @@ -3760,7 +3790,7 @@ cfg.rb: # 153| exit two_parameters -# 189| exit block +# 191| exit block exit.rb: # 1| exit top-level @@ -3898,7 +3928,7 @@ cfg.rb: # 153| exit two_parameters (normal) #-----| -> exit two_parameters -# 189| exit block (normal) +# 191| exit block (normal) #-----| -> exit block exit.rb: diff --git a/ql/test/library-tests/controlflow/graph/cfg.rb b/ql/test/library-tests/controlflow/graph/cfg.rb index 40ffc598f7e..ff3305df60a 100644 --- a/ql/test/library-tests/controlflow/graph/cfg.rb +++ b/ql/test/library-tests/controlflow/graph/cfg.rb @@ -94,7 +94,7 @@ end $global = 42 -map1 = { 'a' => 'b', 'c': 'd', } +map1 = { 'a' => 'b', 'c': 'd', e: 'f' } map2 = { **map1, 'x' => 'y', **map1} @@ -158,6 +158,8 @@ scriptfile = `cat "#{__FILE__}"` symbol = :hello +delimited_symbol = :"goodbye-#{ 12 + 13 }" + x = true x = ! true x = - 42