Merge pull request #64 from github/hvitved/cfg/rescue

Implement CFG logic for `rescue-ensure`
This commit is contained in:
Arthur Baars
2020-12-15 11:43:14 +01:00
committed by GitHub
10 changed files with 4474 additions and 1562 deletions

View File

@@ -186,12 +186,17 @@ module SuccessorTypes {
/**
* A conditional control flow successor. Either a Boolean successor (`BooleanSuccessor`),
* or an emptiness successor (`EmptinessSuccessor`).
* an emptiness successor (`EmptinessSuccessor`), or a matching successor
* (`MatchingSuccessor`)
*/
class ConditionalSuccessor extends SuccessorType {
boolean value;
ConditionalSuccessor() { this = TBooleanSuccessor(value) or this = TEmptinessSuccessor(value) }
ConditionalSuccessor() {
this = TBooleanSuccessor(value) or
this = TEmptinessSuccessor(value) or
this = TMatchingSuccessor(value)
}
/** Gets the Boolean value of this successor. */
final boolean getValue() { result = value }
@@ -248,12 +253,38 @@ module SuccessorTypes {
* ```
*/
class EmptinessSuccessor extends ConditionalSuccessor, TEmptinessSuccessor {
/** Holds if this is an empty successor. */
predicate isEmpty() { value = true }
override string toString() { if value = true then result = "empty" else result = "non-empty" }
}
final override string toString() {
if this.isEmpty() then result = "empty" else result = "non-empty"
}
/**
* A matching control flow successor.
*
* For example, this program fragment:
*
* ```rb
* case x
* when 1 then puts "one"
* else puts "not one"
* end
* ```
*
* has a control flow graph containing matching successors:
*
* ```
* x
* |
* 1
* / \
* / \
* / \
* / \
* match non-match
* | |
* puts "one" puts "not one"
* ```
*/
class MatchingSuccessor extends ConditionalSuccessor, TMatchingSuccessor {
override string toString() { if value = true then result = "match" else result = "no-match" }
}
/**

View File

@@ -1,3 +1,7 @@
/**
* @kind graph
*/
import codeql_ruby.controlflow.ControlFlowGraph
query predicate nodes(CfgNode n) { any() }
@@ -5,6 +9,6 @@ query predicate nodes(CfgNode n) { any() }
query predicate edges(CfgNode pred, CfgNode succ, string attr, string val) {
exists(SuccessorType t | succ = pred.getASuccessor(t) |
attr = "semmle.label" and
val = t.toString()
if t instanceof SuccessorTypes::NormalSuccessor then val = "" else val = t.toString()
)
}

View File

@@ -7,13 +7,15 @@
private import codeql_ruby.ast.internal.TreeSitter::Generated
private import codeql_ruby.controlflow.ControlFlowGraph
private import AstNodes
private import ControlFlowGraphImpl
private import NonReturning
private import SuccessorTypes
private newtype TCompletion =
TSimpleCompletion() or
TBooleanCompletion(boolean b) { b = true or b = false } or
TEmptinessCompletion(boolean isEmpty) { isEmpty = true or isEmpty = false } or
TBooleanCompletion(boolean b) { b in [false, true] } or
TEmptinessCompletion(boolean isEmpty) { isEmpty in [false, true] } or
TMatchingCompletion(boolean isMatch) { isMatch in [false, true] } or
TReturnCompletion() or
TBreakCompletion() or
TNextCompletion() or
@@ -21,11 +23,35 @@ private newtype TCompletion =
TRetryCompletion() or
TRaiseCompletion() or // TODO: Add exception type?
TExitCompletion() or
TNestedCompletion(Completion inner, Completion outer) {
outer = TSimpleCompletion() and
inner = TBreakCompletion()
TNestedCompletion(Completion inner, Completion outer, int nestLevel) {
inner = TBreakCompletion() and
outer instanceof NonNestedNormalCompletion and
nestLevel = 0
or
inner instanceof NormalCompletion and
nestedEnsureCompletion(outer, nestLevel)
}
pragma[noinline]
private predicate nestedEnsureCompletion(Completion outer, int nestLevel) {
(
outer = TReturnCompletion()
or
outer = TBreakCompletion()
or
outer = TNextCompletion()
or
outer = TRedoCompletion()
or
outer = TRetryCompletion()
or
outer = TRaiseCompletion()
or
outer = TExitCompletion()
) and
nestLevel = any(Trees::RescueEnsureBlockTree t).nestLevel()
}
pragma[noinline]
private predicate completionIsValidForStmt(AstNode n, Completion c) {
n instanceof Break and
@@ -57,11 +83,15 @@ abstract class Completion extends TCompletion {
this = TBooleanCompletion(_)
)
or
mustHaveMatchingCompletion(n) and
this = TMatchingCompletion(_)
or
n = any(RescueModifier parent).getBody() and this = TRaiseCompletion()
or
not n instanceof NonReturningCall and
not completionIsValidForStmt(n, _) and
not mustHaveBooleanCompletion(n) and
not mustHaveMatchingCompletion(n) and
this = TSimpleCompletion()
}
@@ -140,7 +170,35 @@ private predicate inBooleanContext(AstNode n) {
or
n = any(ParenthesizedStatement parent | inBooleanContext(parent)).getChild()
or
n instanceof Pattern
exists(Case c, When w |
not exists(c.getValue()) and
c.getChild(_) = w and
w.getPattern(_).getChild() = n
)
}
/**
* Holds if a normal completion of `n` must be a matching completion.
*/
private predicate mustHaveMatchingCompletion(AstNode n) {
inMatchingContext(n) and
not n instanceof NonReturningCall
}
/**
* Holds if `n` is used in a matching context. That is, whether or
* not the value of `n` matches, determines the successor.
*/
private predicate inMatchingContext(AstNode n) {
n = any(Rescue r).getExceptions().getChild(_)
or
exists(Case c, When w |
exists(c.getValue()) and
c.getChild(_) = w and
w.getPattern(_).getChild() = n
)
or
n.(Trees::DefaultValueParameterTree).hasDefaultValue()
}
/**
@@ -149,8 +207,10 @@ private predicate inBooleanContext(AstNode n) {
*/
abstract class NormalCompletion extends Completion { }
abstract private class NonNestedNormalCompletion extends NormalCompletion { }
/** A simple (normal) completion. */
class SimpleCompletion extends NormalCompletion, TSimpleCompletion {
class SimpleCompletion extends NonNestedNormalCompletion, TSimpleCompletion {
override NormalSuccessor getAMatchingSuccessorType() { any() }
override string toString() { result = "simple" }
@@ -158,23 +218,26 @@ class SimpleCompletion extends NormalCompletion, TSimpleCompletion {
/**
* A completion that represents evaluation of an expression, whose value determines
* the successor. Either a Boolean completion (`BooleanCompletion`)
* or an emptiness completion (`EmptinessCompletion`).
* the successor. Either a Boolean completion (`BooleanCompletion`), an emptiness
* completion (`EmptinessCompletion`), or a matching completion (`MatchingCompletion`).
*/
abstract class ConditionalCompletion extends NormalCompletion { }
abstract class ConditionalCompletion extends NonNestedNormalCompletion {
boolean value;
bindingset[value]
ConditionalCompletion() { any() }
/** Gets the Boolean value of this conditional completion. */
final boolean getValue() { result = value }
}
/**
* A completion that represents evaluation of an expression
* with a Boolean value.
*/
class BooleanCompletion extends ConditionalCompletion, TBooleanCompletion {
private boolean value;
BooleanCompletion() { this = TBooleanCompletion(value) }
/** Gets the Boolean value of this completion. */
boolean getValue() { result = value }
/** Gets the dual Boolean completion. */
BooleanCompletion getDual() { result = TBooleanCompletion(value.booleanNot()) }
@@ -198,84 +261,149 @@ class FalseCompletion extends BooleanCompletion {
* a test in a `for in` statement.
*/
class EmptinessCompletion extends ConditionalCompletion, TEmptinessCompletion {
/** Holds if the emptiness test evaluates to `true`. */
predicate isEmpty() { this = TEmptinessCompletion(true) }
EmptinessCompletion() { this = TEmptinessCompletion(value) }
override EmptinessSuccessor getAMatchingSuccessorType() {
if isEmpty() then result.getValue() = true else result.getValue() = false
}
override EmptinessSuccessor getAMatchingSuccessorType() { result.getValue() = value }
override string toString() { if this.isEmpty() then result = "empty" else result = "non-empty" }
override string toString() { if value = true then result = "empty" else result = "non-empty" }
}
/**
* A completion that represents evaluation of a matching test, for example
* a test in a `rescue` statement.
*/
class MatchingCompletion extends ConditionalCompletion, TMatchingCompletion {
MatchingCompletion() { this = TMatchingCompletion(value) }
override MatchingSuccessor getAMatchingSuccessorType() { result.getValue() = value }
override string toString() { if value = true then result = "match" else result = "no-match" }
}
/**
* A completion that represents evaluation of a statement or an
* expression resulting in a return.
*/
class ReturnCompletion extends Completion, TReturnCompletion {
class ReturnCompletion extends Completion {
ReturnCompletion() {
this = TReturnCompletion() or
this = TNestedCompletion(_, TReturnCompletion(), _)
}
override ReturnSuccessor getAMatchingSuccessorType() { any() }
override string toString() { result = "return" }
override string toString() {
// `NestedCompletion` defines `toString()` for the other case
this = TReturnCompletion() and result = "return"
}
}
/**
* A completion that represents evaluation of a statement or an
* expression resulting in a break from a loop.
*/
class BreakCompletion extends Completion, TBreakCompletion {
class BreakCompletion extends Completion {
BreakCompletion() {
this = TBreakCompletion() or
this = TNestedCompletion(_, TBreakCompletion(), _)
}
override BreakSuccessor getAMatchingSuccessorType() { any() }
override string toString() { result = "break" }
override string toString() {
// `NestedCompletion` defines `toString()` for the other case
this = TBreakCompletion() and result = "break"
}
}
/**
* A completion that represents evaluation of a statement or an
* expression resulting in a continuation of a loop.
*/
class NextCompletion extends Completion, TNextCompletion {
class NextCompletion extends Completion {
NextCompletion() {
this = TNextCompletion() or
this = TNestedCompletion(_, TNextCompletion(), _)
}
override NextSuccessor getAMatchingSuccessorType() { any() }
override string toString() { result = "next" }
override string toString() {
// `NestedCompletion` defines `toString()` for the other case
this = TNextCompletion() and result = "next"
}
}
/**
* A completion that represents evaluation of a statement or an
* expression resulting in a redo of a loop iteration.
*/
class RedoCompletion extends Completion, TRedoCompletion {
class RedoCompletion extends Completion {
RedoCompletion() {
this = TRedoCompletion() or
this = TNestedCompletion(_, TRedoCompletion(), _)
}
override RedoSuccessor getAMatchingSuccessorType() { any() }
override string toString() { result = "redo" }
override string toString() {
// `NestedCompletion` defines `toString()` for the other case
this = TRedoCompletion() and result = "redo"
}
}
/**
* A completion that represents evaluation of a statement or an
* expression resulting in a retry.
*/
class RetryCompletion extends Completion, TRetryCompletion {
class RetryCompletion extends Completion {
RetryCompletion() {
this = TRetryCompletion() or
this = TNestedCompletion(_, TRetryCompletion(), _)
}
override RetrySuccessor getAMatchingSuccessorType() { any() }
override string toString() { result = "retry" }
override string toString() {
// `NestedCompletion` defines `toString()` for the other case
this = TRetryCompletion() and result = "retry"
}
}
/**
* A completion that represents evaluation of a statement or an
* expression resulting in a thrown exception.
*/
class RaiseCompletion extends Completion, TRaiseCompletion {
class RaiseCompletion extends Completion {
RaiseCompletion() {
this = TRaiseCompletion() or
this = TNestedCompletion(_, TRaiseCompletion(), _)
}
override RaiseSuccessor getAMatchingSuccessorType() { any() }
override string toString() { result = "raise" }
override string toString() {
// `NestedCompletion` defines `toString()` for the other case
this = TRaiseCompletion() and result = "raise"
}
}
/**
* A completion that represents evaluation of a statement or an
* expression resulting in an abort/exit.
*/
class ExitCompletion extends Completion, TExitCompletion {
class ExitCompletion extends Completion {
ExitCompletion() {
this = TExitCompletion() or
this = TNestedCompletion(_, TExitCompletion(), _)
}
override ExitSuccessor getAMatchingSuccessorType() { any() }
override string toString() { result = "exit" }
override string toString() {
// `NestedCompletion` defines `toString()` for the other case
this = TExitCompletion() and result = "exit"
}
}
/**
@@ -296,25 +424,60 @@ class ExitCompletion extends Completion, TExitCompletion {
* the `while` loop can have a nested completion where the inner completion
* is a `break` and the outer completion is a simple successor.
*/
class NestedCompletion extends Completion, TNestedCompletion {
abstract class NestedCompletion extends Completion, TNestedCompletion {
Completion inner;
Completion outer;
int nestLevel;
NestedCompletion() { this = TNestedCompletion(inner, outer) }
NestedCompletion() { this = TNestedCompletion(inner, outer, nestLevel) }
override Completion getInnerCompletion() { result = inner }
/** Gets a completion that is compatible with the inner completion. */
abstract Completion getAnInnerCompatibleCompletion();
/** Gets the level of this nested completion. */
final int getNestLevel() { result = nestLevel }
override string toString() { result = outer + " [" + inner + "] (" + nestLevel + ")" }
}
class NestedBreakCompletion extends NormalCompletion, NestedCompletion {
NestedBreakCompletion() {
inner = TBreakCompletion() and
outer instanceof NonNestedNormalCompletion
}
override BreakCompletion getInnerCompletion() { result = inner }
override NonNestedNormalCompletion getOuterCompletion() { result = outer }
override Completion getAnInnerCompatibleCompletion() {
result = inner and
outer = TSimpleCompletion()
or
result = TNestedCompletion(outer, inner, _)
}
override SuccessorType getAMatchingSuccessorType() {
outer instanceof SimpleCompletion and
result instanceof BreakSuccessor
or
result = outer.(ConditionalCompletion).getAMatchingSuccessorType()
}
}
class NestedEnsureCompletion extends NestedCompletion {
NestedEnsureCompletion() {
inner instanceof NormalCompletion and
nestedEnsureCompletion(outer, nestLevel)
}
override NormalCompletion getInnerCompletion() { result = inner }
override Completion getOuterCompletion() { result = outer }
override SuccessorType getAMatchingSuccessorType() {
inner = TBreakCompletion() and
outer = TSimpleCompletion() and
result instanceof BreakSuccessor
override Completion getAnInnerCompatibleCompletion() {
result.getOuterCompletion() = this.getInnerCompletion()
}
override string toString() { result = outer + " [" + inner + "]" }
}
private class NestedNormalCompletion extends NormalCompletion, NestedCompletion {
NestedNormalCompletion() { outer instanceof NormalCompletion }
override SuccessorType getAMatchingSuccessorType() { none() }
}

View File

@@ -38,6 +38,14 @@ private import Completion
private import SuccessorTypes
private import Splitting
private AstNode parent(AstNode n) {
result.getAFieldOrChild() = n and
not n instanceof CfgScope
}
/** Gets the CFG scope of node `n`. */
CfgScope getScope(AstNode n) { result = parent+(n) }
abstract private class ControlFlowTree extends AstNode {
/**
* Holds if `first` is the first element executed within this AST node.
@@ -227,7 +235,7 @@ abstract private class LeafTree extends PreOrderTree, PostOrderTree {
}
/** Defines the CFG by dispatch on the various AST types. */
private module Trees {
module Trees {
private class AliasTree extends StandardPreOrderTree, Alias {
final override AstNode getChildNode(int i) {
i = 0 and result = this.getName()
@@ -262,12 +270,9 @@ private module Trees {
final override Interpolation getChildNode(int i) { result = this.getChild(i) }
}
private class BeginTree extends StandardPreOrderTree, Begin {
final override AstNode getChildNode(int i) {
result = this.getChild(i) and
not result instanceof Rescue and
not result instanceof Else and
not result instanceof Ensure
private class BeginTree extends RescueEnsureBlockTree, Begin {
final override AstNode getChildNode(int i, boolean rescuable) {
result = this.getChild(i) and rescuable = true
}
override predicate isHidden() { any() }
@@ -357,7 +362,7 @@ private module Trees {
exists(int i, WhenTree branch | branch = this.getChild(i) |
last(branch.getLastPattern(), pred, c) and
first(this.getChild(i + 1), succ) and
c instanceof FalseCompletion
c.(ConditionalCompletion).getValue() = false
)
}
}
@@ -370,11 +375,11 @@ private module Trees {
private class CharacterTree extends LeafTree, Character { }
private class ClassTree extends StandardPreOrderTree, Class {
final override AstNode getChildNode(int i) {
result = this.getName() and i = 0
private class ClassTree extends RescueEnsureBlockTree, Class {
final override AstNode getChildNode(int i, boolean rescuable) {
result = this.getName() and i = 0 and rescuable = false
or
result = this.getChild(i - 1)
result = this.getChild(i - 1) and rescuable = true
}
}
@@ -384,6 +389,33 @@ private module Trees {
private class ConstantTree extends LeafTree, Constant { }
/** A parameter that may have a default value. */
abstract class DefaultValueParameterTree extends PreOrderTree {
abstract AstNode getDefaultValue();
predicate hasDefaultValue() { exists(this.getDefaultValue()) }
final override predicate propagatesAbnormal(AstNode child) { child = this.getDefaultValue() }
final override predicate last(AstNode last, Completion c) {
last = this and
exists(this.getDefaultValue()) and
c.(MatchingCompletion).getValue() = true
or
last(this.getDefaultValue(), last, c)
or
last = this and
not exists(this.getDefaultValue()) and
c instanceof SimpleCompletion
}
final override predicate succ(AstNode pred, AstNode succ, Completion c) {
pred = this and
first(this.getDefaultValue(), succ) and
c.(MatchingCompletion).getValue() = false
}
}
private class DestructuredLeftAssignmentTree extends StandardPostOrderTree,
DestructuredLeftAssignment {
final override AstNode getChildNode(int i) { result = this.getChild(i) }
@@ -399,11 +431,11 @@ private module Trees {
override predicate isHidden() { any() }
}
private class DoBlockTree extends StandardPreOrderTree, DoBlock {
final override AstNode getChildNode(int i) {
result = this.getParameters() and i = 0
private class DoBlockTree extends RescueEnsureBlockTree, DoBlock {
final override AstNode getChildNode(int i, boolean rescuable) {
result = this.getParameters() and i = 0 and rescuable = false
or
result = this.getChild(i - 1)
result = this.getChild(i - 1) and rescuable = true
}
override predicate isHidden() { any() }
@@ -431,8 +463,6 @@ private module Trees {
private class EnsureTree extends StandardPreOrderTree, Ensure {
final override AstNode getChildNode(int i) { result = this.getChild(i) }
override predicate isHidden() { any() }
}
private class EscapeSequenceTree extends LeafTree, EscapeSequence { }
@@ -443,8 +473,30 @@ private module Trees {
override predicate isHidden() { any() }
}
private class ExceptionsTree extends StandardPostOrderTree, Exceptions {
final override AstNode getChildNode(int i) { result = this.getChild(i) }
private class ExceptionsTree extends PreOrderTree, Exceptions {
final override predicate propagatesAbnormal(AstNode child) { none() }
final override predicate last(AstNode last, Completion c) {
last(this.getChild(_), last, c) and
c.(MatchingCompletion).getValue() = true
or
exists(int lst |
last(this.getChild(lst), last, c) and
not exists(this.getChild(lst + 1))
)
}
final override predicate succ(AstNode pred, AstNode succ, Completion c) {
pred = this and
first(this.getChild(0), succ) and
c instanceof SimpleCompletion
or
exists(int i |
last(this.getChild(i), pred, c) and
c.(MatchingCompletion).getValue() = false and
first(this.getChild(i + 1), succ)
)
}
override predicate isHidden() { any() }
}
@@ -493,18 +545,14 @@ private module Trees {
final override predicate last(AstNode last, Completion c) {
last = this and
c.(EmptinessCompletion).isEmpty()
c.(EmptinessCompletion).getValue() = true
or
last(this.getBody(), last, c) and
not c.continuesLoop() and
not c instanceof BreakCompletion and
not c instanceof RedoCompletion
or
c =
any(NestedCompletion nc |
last(this.getBody(), last, nc.getInnerCompletion().(BreakCompletion)) and
nc.getOuterCompletion() instanceof SimpleCompletion
)
last(this.getBody(), last, c.(NestedBreakCompletion).getAnInnerCompatibleCompletion())
}
/**
@@ -521,8 +569,7 @@ private module Trees {
or
pred = this and
first(this.getPattern(0), succ) and
c instanceof EmptinessCompletion and
not c.(EmptinessCompletion).isEmpty()
c.(EmptinessCompletion).getValue() = false
or
exists(int i, ControlFlowTree next |
last(this.getPattern(i), pred, c) and
@@ -637,8 +684,8 @@ private module Trees {
final override AstNode getChildNode(int i) { result = this.getChild() and i = 0 }
}
private class KeywordParameterTree extends StandardPostOrderTree, KeywordParameter {
final override AstNode getChildNode(int i) { result = this.getValue() and i = 0 }
private class KeywordParameterTree extends DefaultValueParameterTree, KeywordParameter {
final override AstNode getDefaultValue() { result = this.getValue() }
}
private class LambdaTree extends StandardPreOrderTree, Lambda {
@@ -719,11 +766,11 @@ private module Trees {
}
}
private class MethodTree extends StandardPreOrderTree, Method {
final override AstNode getChildNode(int i) {
result = this.getParameters() and i = 0
private class MethodTree extends RescueEnsureBlockTree, Method {
final override AstNode getChildNode(int i, boolean rescuable) {
result = this.getParameters() and i = 0 and rescuable = false
or
result = this.getChild(i - 1)
result = this.getChild(i - 1) and rescuable = true
}
override predicate isHidden() { any() }
@@ -744,11 +791,11 @@ private module Trees {
override predicate isHidden() { any() }
}
private class ModuleTree extends StandardPreOrderTree, Module {
final override AstNode getChildNode(int i) {
result = this.getName() and i = 0
private class ModuleTree extends RescueEnsureBlockTree, Module {
final override AstNode getChildNode(int i, boolean rescuable) {
result = this.getName() and i = 0 and rescuable = false
or
result = this.getChild(i - 1)
result = this.getChild(i - 1) and rescuable = true
}
}
@@ -766,8 +813,8 @@ private module Trees {
}
}
private class OptionalParameterTree extends StandardPostOrderTree, OptionalParameter {
final override AstNode getChildNode(int i) { result = this.getValue() and i = 0 }
private class OptionalParameterTree extends DefaultValueParameterTree, OptionalParameter {
final override AstNode getDefaultValue() { result = this.getValue() }
}
private class PairTree extends StandardPostOrderTree, Pair {
@@ -782,8 +829,10 @@ private module Trees {
final override AstNode getChildNode(int i) { result = this.getChild(i) }
}
private class PatternTree extends StandardPostOrderTree, Pattern {
private class PatternTree extends StandardPreOrderTree, Pattern {
final override AstNode getChildNode(int i) { result = this.getChild() and i = 0 }
final override predicate isHidden() { any() }
}
private class ProgramTree extends StandardPreOrderTree, Program {
@@ -809,13 +858,282 @@ private module Trees {
final override Interpolation getChildNode(int i) { result = this.getChild(i) }
}
private class RescueTree extends StandardPreOrderTree, Rescue {
final override AstNode getChildNode(int i) {
result = this.getExceptions() and i = 0
private class RescueTree extends PreOrderTree, Rescue {
final override predicate propagatesAbnormal(AstNode child) { child = this.getExceptions() }
predicate lastMatch(AstNode last, Completion c) {
last(this.getBody(), last, c)
or
result = this.getVariable() and i = 1
not exists(this.getBody()) and
(
last(this.getVariable(), last, c)
or
not exists(this.getVariable()) and
(
last(this.getExceptions(), last, c) and
c.(MatchingCompletion).getValue() = true
or
not exists(this.getExceptions()) and
last = this and
c.isValidFor(this)
)
)
}
predicate lastNoMatch(AstNode last, Completion c) {
last(this.getExceptions(), last, c) and
c.(MatchingCompletion).getValue() = false
}
final override predicate last(AstNode last, Completion c) {
this.lastNoMatch(last, c)
or
result = this.getBody() and i = 2
this.lastMatch(last, c)
}
final override predicate succ(AstNode pred, AstNode succ, Completion c) {
exists(AstNode next |
pred = this and
first(next, succ) and
c instanceof SimpleCompletion
|
next = this.getExceptions()
or
not exists(this.getExceptions()) and
(
next = this.getVariable()
or
not exists(this.getVariable()) and
next = this.getBody()
)
)
or
exists(AstNode next |
last(this.getExceptions(), pred, c) and
first(next, succ) and
c.(MatchingCompletion).getValue() = true
|
next = this.getVariable()
or
not exists(this.getVariable()) and
next = this.getBody()
)
or
last(this.getVariable(), pred, c) and
first(this.getBody(), succ) and
c instanceof NormalCompletion
}
}
/** Gets a child of `n` that is in CFG scope `scope`. */
pragma[noinline]
private AstNode getAChildInScope(AstNode n, CfgScope scope) {
result.getParent() = n and
scope = getScope(result)
}
/** A block that may contain `rescue`/`ensure`. */
abstract class RescueEnsureBlockTree extends PreOrderTree {
/**
* Gets the `i`th child of this block. `rescuable` indicates whether exceptional
* execution of the child can be caught by `rescue`/`ensure`.
*/
abstract AstNode getChildNode(int i, boolean rescuable);
/** Gets the `i`th child in the body of this block. */
final private AstNode getBodyChild(int i, boolean rescuable) {
result = this.getChildNode(_, rescuable) and
result =
rank[i + 1](AstNode child, int j |
child = this.getChildNode(j, _) and
not result instanceof Rescue and
not result instanceof Ensure and
not result instanceof Else and
not child instanceof CfgScope
|
child order by j
)
}
/** Gets the `i`th `rescue` block in this block. */
final Rescue getRescue(int i) {
result = rank[i + 1](Rescue s | s = this.getAFieldOrChild() | s order by s.getParentIndex())
}
/** Gets the `else` block in this block, if any. */
final private Else getElse() { result = unique(Else s | s = this.getAFieldOrChild()) }
/** Gets the `ensure` block in this block, if any. */
final Ensure getEnsure() { result = unique(Ensure s | s = this.getAFieldOrChild()) }
final private predicate hasEnsure() { exists(this.getEnsure()) }
final override predicate propagatesAbnormal(AstNode child) {
child = this.getEnsure()
or
child = this.getBodyChild(_, false)
}
/**
* Gets a descendant that belongs to the `ensure` block of this block, if any.
* Nested `ensure` blocks are not included.
*/
AstNode getAnEnsureDescendant() {
result = this.getEnsure()
or
exists(AstNode mid |
mid = this.getAnEnsureDescendant() and
result = getAChildInScope(mid, getScope(mid)) and
not exists(RescueEnsureBlockTree nestedBlock |
result = nestedBlock.getEnsure() and
nestedBlock != this
)
)
}
/**
* Holds if `innerBlock` has an `ensure` block and is immediately nested inside the
* `ensure` block of this block.
*/
private predicate nestedEnsure(RescueEnsureBlockTree innerBlock) {
exists(Ensure innerEnsure |
innerEnsure = getAChildInScope(this.getAnEnsureDescendant(), getScope(this)) and
innerEnsure = innerBlock.getEnsure()
)
}
/**
* Gets the `ensure`-nesting level of this block. That is, the number of `ensure`
* blocks that this block is nested under.
*/
int nestLevel() { result = count(RescueEnsureBlockTree outer | outer.nestedEnsure+(this)) }
/**
* Holds if `last` is a last element in the body of this block. `ensurable`
* indicates whether `last` may be a predecessor of an `ensure` block.
*/
pragma[nomagic]
private predicate lastBody(AstNode last, Completion c, boolean ensurable) {
exists(boolean rescuable |
if c instanceof RaiseCompletion then ensurable = rescuable else ensurable = true
|
last(this.getBodyChild(_, rescuable), last, c) and
not c instanceof NormalCompletion
or
exists(int lst |
last(this.getBodyChild(lst, rescuable), last, c) and
not exists(this.getBodyChild(lst + 1, _))
)
)
}
/**
* Gets a last element from this block that may finish with completion `c`, such
* that control may be transferred to the `ensure` block (if it exists), but only
* if `ensurable = true`.
*/
pragma[nomagic]
private AstNode getAnEnsurePredecessor(Completion c, boolean ensurable) {
this.lastBody(result, c, ensurable) and
(
// Any non-throw completion will always continue directly to the `ensure` block,
// unless there is an `else` block
not c instanceof RaiseCompletion and
not exists(this.getElse())
or
// Any completion will continue to the `ensure` block when there are no `rescue`
// blocks
not exists(this.getRescue(_))
)
or
// Last element from any matching `rescue` block continues to the `ensure` block
this.getRescue(_).(RescueTree).lastMatch(result, c) and
ensurable = true
or
// If the last `rescue` block does not match, continue to the `ensure` block
exists(int lst, MatchingCompletion mc |
this.getRescue(lst).(RescueTree).lastNoMatch(result, mc) and
mc.getValue() = false and
not exists(this.getRescue(lst + 1)) and
c =
any(NestedEnsureCompletion nec |
nec.getOuterCompletion() instanceof RaiseCompletion and
nec.getInnerCompletion() = mc and
nec.getNestLevel() = 0
) and
ensurable = true
)
or
// Last element of `else` block continues to the `ensure` block
last(this.getElse(), result, c) and
ensurable = true
}
pragma[nomagic]
private predicate lastEnsure0(AstNode last, Completion c) { last(this.getEnsure(), last, c) }
pragma[nomagic]
private predicate lastEnsure(
AstNode last, NormalCompletion ensure, Completion outer, int nestLevel
) {
this.lastEnsure0(last, ensure) and
exists(
this.getAnEnsurePredecessor(any(Completion c0 | outer = c0.getOuterCompletion()), true)
) and
nestLevel = this.nestLevel()
}
override predicate last(AstNode last, Completion c) {
exists(boolean ensurable | last = this.getAnEnsurePredecessor(c, ensurable) |
not this.hasEnsure()
or
ensurable = false
)
or
// If the body completes normally, take the completion from the `ensure` block
this.lastEnsure(last, c, any(NormalCompletion nc), _)
or
// If the `ensure` block completes normally, it inherits any non-normal
// completion from the body
c =
any(NestedEnsureCompletion nec |
this
.lastEnsure(last, nec.getAnInnerCompatibleCompletion(), nec.getOuterCompletion(),
nec.getNestLevel())
)
}
final override predicate succ(AstNode pred, AstNode succ, Completion c) {
pred = this and
first(this.getBodyChild(0, _), succ) and
c instanceof SimpleCompletion
or
// Normal left-to-right evaluation in the body
exists(int i |
last(this.getBodyChild(i, _), pred, c) and
first(this.getBodyChild(i + 1, _), succ) and
c instanceof NormalCompletion
)
or
// Exceptional flow from body to first `rescue`
this.lastBody(pred, c, true) and
first(this.getRescue(0), succ) and
c instanceof RaiseCompletion
or
// Flow from one `rescue` clause to the next when there is no match
exists(RescueTree rescue, int i | rescue = this.getRescue(i) |
rescue.lastNoMatch(pred, c) and
first(this.getRescue(i + 1), succ)
)
or
// Flow from body to `else` block when no exception
this.lastBody(pred, c, _) and
first(this.getElse(), succ) and
c instanceof NormalCompletion
or
// Flow into `ensure` block
pred = getAnEnsurePredecessor(c, true) and
first(this.getEnsure(), succ)
}
}
@@ -870,23 +1188,31 @@ private module Trees {
private class SetterTree extends LeafTree, Setter { }
private class SingletonClassTree extends StandardPreOrderTree, SingletonClass {
final override AstNode getChildNode(int i) {
result = this.getValue() and i = 0
or
result = this.getChild(i - 1)
private class SingletonClassTree extends RescueEnsureBlockTree, SingletonClass {
final override AstNode getChildNode(int i, boolean rescuable) {
rescuable = true and
(
result = this.getValue() and i = 0
or
result = this.getChild(i - 1)
)
}
override predicate isHidden() { any() }
}
private class SingletonMethodTree extends StandardPreOrderTree, SingletonMethod {
final override AstNode getChildNode(int i) {
result = this.getObject() and i = 0
private class SingletonMethodTree extends RescueEnsureBlockTree, SingletonMethod {
final override AstNode getChildNode(int i, boolean rescuable) {
result = this.getObject() and
i = 0 and
rescuable = false
or
result = this.getParameters() and i = 1
result = this.getParameters() and
i = 1 and
rescuable = false
or
result = this.getChild(i - 2)
result = this.getChild(i - 2) and
rescuable = true
}
override predicate isHidden() { any() }
@@ -956,7 +1282,7 @@ private module Trees {
final override predicate last(AstNode last, Completion c) {
last(this.getLastPattern(), last, c) and
c instanceof FalseCompletion
c.(ConditionalCompletion).getValue() = false
or
last(this.getBody(), last, c)
}
@@ -966,13 +1292,15 @@ private module Trees {
first(this.getPattern(0), succ) and
c instanceof SimpleCompletion
or
exists(int i, Pattern p |
exists(int i, Pattern p, boolean b |
p = this.getPattern(i) and
last(p, pred, c)
last(p, pred, c) and
b = c.(ConditionalCompletion).getValue()
|
c instanceof TrueCompletion and first(this.getBody(), succ)
b = true and
first(this.getBody(), succ)
or
c instanceof FalseCompletion and
b = false and
first(this.getPattern(i + 1), succ)
)
}
@@ -990,11 +1318,7 @@ private module Trees {
not c instanceof BreakCompletion and
not c instanceof RedoCompletion
or
c =
any(NestedCompletion nc |
last(this.getBodyNode(), last, nc.getInnerCompletion().(BreakCompletion)) and
nc.getOuterCompletion() instanceof SimpleCompletion
)
last(this.getBodyNode(), last, c.(NestedBreakCompletion).getAnInnerCompatibleCompletion())
}
final override predicate succ(AstNode pred, AstNode succ, Completion c) {
@@ -1010,9 +1334,9 @@ private module Trees {
first(this.getConditionNode(), succ) and
c.continuesLoop()
or
last(this.getBodyNode(), pred, any(RedoCompletion rc)) and
last(this.getBodyNode(), pred, c) and
first(this.getBodyNode(), succ) and
c instanceof SimpleCompletion
c instanceof RedoCompletion
}
}
}
@@ -1048,8 +1372,9 @@ private module Cached {
cached
newtype TSuccessorType =
TSuccessorSuccessor() or
TBooleanSuccessor(boolean b) { b = true or b = false } or
TEmptinessSuccessor(boolean isEmpty) { isEmpty = true or isEmpty = false } or
TBooleanSuccessor(boolean b) { b in [false, true] } or
TEmptinessSuccessor(boolean isEmpty) { isEmpty in [false, true] } or
TMatchingSuccessor(boolean isMatch) { isMatch in [false, true] } or
TReturnSuccessor() or
TBreakSuccessor() or
TNextSuccessor() or

View File

@@ -12,11 +12,11 @@ abstract class NonReturningCall extends AstNode {
private class RaiseCall extends NonReturningCall, MethodCall {
RaiseCall() { this.getMethod().toString() = "raise" }
override RaiseCompletion getACompletion() { any() }
override RaiseCompletion getACompletion() { not result instanceof NestedCompletion }
}
private class ExitCall extends NonReturningCall, MethodCall {
ExitCall() { this.getMethod().toString() in ["abort", "exit"] }
override ExitCompletion getACompletion() { any() }
override ExitCompletion getACompletion() { not result instanceof NestedCompletion }
}

View File

@@ -15,10 +15,16 @@ private int maxSplits() { result = 5 }
cached
private module Cached {
cached
newtype TSplitKind = TConditionalCompletionSplitKind()
newtype TSplitKind =
TConditionalCompletionSplitKind() or
TEnsureSplitKind(int nestLevel) { nestLevel = any(Trees::RescueEnsureBlockTree t).nestLevel() }
cached
newtype TSplit = TConditionalCompletionSplit(BooleanCompletion c)
newtype TSplit =
TConditionalCompletionSplit(ConditionalCompletion c) or
TEnsureSplit(EnsureSplitting::EnsureSplitType type, int nestLevel) {
nestLevel = any(Trees::RescueEnsureBlockTree t).nestLevel()
}
cached
newtype TSplits =
@@ -60,13 +66,6 @@ class Split extends TSplit {
string toString() { none() }
}
private AstNode parent(AstNode n) {
result.getAFieldOrChild() = n and
not n instanceof CfgScope
}
private CfgScope getScope(AstNode n) { result = parent+(n) }
/**
* Holds if split kinds `sk1` and `sk2` may overlap. That is, they may apply
* to at least one common AST node inside `scope`.
@@ -195,7 +194,7 @@ private module ConditionalCompletionSplitting {
* restrict the edges out of `x < 2 and x > 0` accordingly.
*/
class ConditionalCompletionSplit extends Split, TConditionalCompletionSplit {
BooleanCompletion completion;
ConditionalCompletion completion;
ConditionalCompletionSplit() { this = TConditionalCompletionSplit(completion) }
@@ -238,19 +237,258 @@ private module ConditionalCompletionSplitting {
override predicate hasExit(AstNode pred, AstNode succ, Completion c) {
this.appliesTo(pred) and
succ(pred, succ, c) and
if c instanceof BooleanCompletion then completion = c else any()
if c instanceof ConditionalCompletion then completion = c else any()
}
override predicate hasExitScope(AstNode last, CfgScope scope, Completion c) {
this.appliesTo(last) and
succExit(last, scope, c) and
if c instanceof BooleanCompletion then completion = c else any()
if c instanceof ConditionalCompletion then completion = c else any()
}
override predicate hasSuccessor(AstNode pred, AstNode succ, Completion c) { none() }
}
}
module EnsureSplitting {
/**
* The type of a split `ensure` node.
*
* The type represents one of the possible ways of entering an `ensure`
* block. For example, if a block ends with a `return` statement, then
* the `ensure` block must end with a `return` as well (provided that
* the `ensure` block executes normally).
*/
class EnsureSplitType extends SuccessorType {
EnsureSplitType() { not this instanceof ConditionalSuccessor }
/** Holds if this split type matches entry into an `ensure` block with completion `c`. */
predicate isSplitForEntryCompletion(Completion c) {
if c instanceof NormalCompletion
then
// If the entry into the `ensure` block completes with any normal completion,
// it simply means normal execution after the `ensure` block
this instanceof NormalSuccessor
else this = c.getAMatchingSuccessorType()
}
}
/** A node that belongs to an `ensure` block. */
private class EnsureNode extends AstNode {
private Trees::RescueEnsureBlockTree block;
EnsureNode() { this = block.getAnEnsureDescendant() }
/** Gets the immediate block that this node belongs to. */
Trees::RescueEnsureBlockTree getBlock() { result = block }
/** Holds if this node is the entry node in the `ensure` block it belongs to. */
predicate isEntryNode() { first(block.getEnsure(), this) }
}
/** A node that does not belong to an `ensure` block. */
private class NonEnsureNode extends EnsureNode {
NonEnsureNode() { not this = any(Trees::RescueEnsureBlockTree t).getAnEnsureDescendant() }
}
/**
* A split for nodes belonging to an `ensure` block, which determines how to
* continue execution after leaving the `ensure` block. For example, in
*
* ```rb
* begin
* if x
* raise "Exception"
* end
* ensure
* puts "Ensure"
* end
* ```
*
* all control flow nodes in the `ensure` block have two splits: one representing
* normal execution of the body (when `x` evaluates to `true`), and one representing
* exceptional execution of the body (when `x` evaluates to `false`).
*/
class EnsureSplit extends Split, TEnsureSplit {
private EnsureSplitType type;
private int nestLevel;
EnsureSplit() { this = TEnsureSplit(type, nestLevel) }
/**
* Gets the type of this `ensure` split, that is, how to continue execution after the
* `ensure` block.
*/
EnsureSplitType getType() { result = type }
/** Gets the nesting level. */
int getNestLevel() { result = nestLevel }
override string toString() {
if type instanceof NormalSuccessor
then result = ""
else
if nestLevel > 0
then result = "ensure(" + nestLevel + "): " + type.toString()
else result = "ensure: " + type.toString()
}
}
private int getListOrder(EnsureSplitKind kind) {
result = ConditionalCompletionSplitting::getNextListOrder() + kind.getNestLevel()
}
int getNextListOrder() {
result = max([getListOrder(_) + 1, ConditionalCompletionSplitting::getNextListOrder()])
}
private class EnsureSplitKind extends SplitKind, TEnsureSplitKind {
private int nestLevel;
EnsureSplitKind() { this = TEnsureSplitKind(nestLevel) }
/** Gets the nesting level. */
int getNestLevel() { result = nestLevel }
override int getListOrder() { result = getListOrder(this) }
override string toString() { result = "ensure (" + nestLevel + ")" }
}
pragma[noinline]
private predicate hasEntry0(AstNode pred, EnsureNode succ, int nestLevel, Completion c) {
succ.isEntryNode() and
nestLevel = succ.getBlock().nestLevel() and
succ(pred, succ, c)
}
private class EnsureSplitImpl extends SplitImpl, EnsureSplit {
override EnsureSplitKind getKind() { result.getNestLevel() = this.getNestLevel() }
override predicate hasEntry(AstNode pred, AstNode succ, Completion c) {
hasEntry0(pred, succ, this.getNestLevel(), c) and
this.getType().isSplitForEntryCompletion(c)
}
override predicate hasEntryScope(CfgScope scope, AstNode first) { none() }
/**
* Holds if this split applies to `pred`, where `pred` is a valid predecessor.
*/
private predicate appliesToPredecessor(AstNode pred) {
this.appliesTo(pred) and
(succ(pred, _, _) or succExit(pred, _, _))
}
pragma[noinline]
private predicate exit0(
AstNode pred, Trees::RescueEnsureBlockTree block, int nestLevel, Completion c
) {
this.appliesToPredecessor(pred) and
nestLevel = block.nestLevel() and
last(block, pred, c)
}
/**
* Holds if `pred` may exit this split with completion `c`. The Boolean
* `inherited` indicates whether `c` is an inherited completion from the
* body.
*/
private predicate exit(
Trees::RescueEnsureBlockTree block, AstNode pred, Completion c, boolean inherited
) {
exists(EnsureSplitType type |
exit0(pred, block, this.getNestLevel(), c) and
type = this.getType()
|
if last(block.getEnsure(), pred, c)
then
// `ensure` block can itself exit with completion `c`: either `c` must
// match this split, `c` must be an abnormal completion, or this split
// does not require another completion to be recovered
inherited = false and
(
type = c.getAMatchingSuccessorType()
or
not c instanceof NormalCompletion
or
type instanceof NormalSuccessor
)
else (
// `ensure` block can exit with inherited completion `c`, which must
// match this split
inherited = true and
type = c.getAMatchingSuccessorType() and
not type instanceof NormalSuccessor
)
)
or
// If this split is normal, and an outer split can exit based on an inherited
// completion, we need to exit this split as well. For example, in
//
// ```rb
// def m(b1, b2)
// if b1
// return
// end
// ensure
// begin
// if b2
// raise "Exception"
// end
// ensure
// puts "inner ensure"
// end
// end
// ```
//
// if the outer split for `puts "inner ensure"` is `return` and the inner split
// is "normal" (corresponding to `b1 = true` and `b2 = false`), then the inner
// split must be able to exit with a `return` completion.
this.appliesToPredecessor(pred) and
exists(EnsureSplitImpl outer |
outer.getNestLevel() = this.getNestLevel() - 1 and
outer.exit(_, pred, c, inherited) and
this.getType() instanceof NormalSuccessor and
inherited = true
)
}
override predicate hasExit(AstNode pred, AstNode succ, Completion c) {
succ(pred, succ, c) and
(
exit(_, pred, c, _)
or
exit(_, pred, c.(NestedBreakCompletion).getAnInnerCompatibleCompletion(), _)
)
}
override predicate hasExitScope(AstNode last, CfgScope scope, Completion c) {
succExit(last, scope, c) and
(
exit(_, last, c, _)
or
exit(_, last, c.(NestedBreakCompletion).getAnInnerCompatibleCompletion(), _)
)
}
override predicate hasSuccessor(AstNode pred, AstNode succ, Completion c) {
this.appliesToPredecessor(pred) and
succ(pred, succ, c) and
succ =
any(EnsureNode en |
if en.isEntryNode()
then
// entering a nested `ensure` block
en.getBlock().nestLevel() > this.getNestLevel()
else
// staying in the same (possibly nested) `ensure` block as `pred`
en.getBlock().nestLevel() >= this.getNestLevel()
)
}
}
}
/**
* A set of control flow node splits. The set is represented by a list of splits,
* ordered by ascending rank.

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,42 @@
def m1 elements
for element in elements do
if x > 0 then
break
end
end
ensure
if elements.nil? then
puts "elements nil"
end
end
def m2 elements
for element in elements do
begin
if x > 0 then
break
end
ensure
if elements.nil? then
puts "elements nil"
end
end
end
end
def m3 elements
begin
if elements.nil? then
return
end
ensure
for element in elements do
begin
if x > 0 then
break
end
end
end
end
puts "Done"
end

View File

@@ -24,3 +24,7 @@ def m3 x
end
puts x
end
def m4 (b1, b2, b3)
return (b1 ? b2 : b3) ? "b2 || b3" : "!b2 || !b3"
end

View File

@@ -1,6 +1,140 @@
class ExceptionA < Exception
end
class ExceptionB < Exception
end
def m1 x
if x > 2
raise "x > 2"
end
puts "x <= 2"
end
def m2 b
begin
if b
raise ExceptionA
end
rescue ExceptionA
puts "Rescued"
end
puts "End m2"
end
def m3 b
begin
if b
raise ExceptionA
end
rescue
puts "Rescued"
end
puts "End m3"
end
def m4 b
begin
if b
raise ExceptionA
end
rescue => e
puts "Rescued {e}"
end
puts "End m4"
end
def m5 b
begin
if b
raise ExceptionA
end
rescue => e
end
puts "End m5"
end
def m6 b
begin
if b
raise ExceptionA
end
rescue ExceptionA, ExceptionB => e
puts "Rescued {e}"
end
puts "End m6"
end
def m7 x
if x > 2
raise "x > 2"
elsif x < 0
return "x < 0"
end
puts "0 <= x <= 2"
ensure
puts "ensure"
end
def m8 x
puts "Begin m8"
begin
if x > 2
raise "x > 2"
elsif x < 0
return "x < 0"
end
puts "0 <= x <= 2"
ensure
puts "ensure"
end
puts "End m8"
end
def m9(x, b1, b2)
puts "Begin m9"
begin
if x > 2
raise "x > 2"
elsif x < 0
return "x < 0"
end
puts "0 <= x <= 2"
ensure
puts "outer ensure"
begin
if b1
raise "b1 is true"
end
ensure
puts "inner ensure"
end
end
puts "End m9"
ensure
puts "method ensure"
if b2
raise "b2 is true"
end
end
def m10(p = (raise "Exception"))
rescue
puts "Will not get executed if p is not supplied"
ensure
puts "Will not get executed if p is not supplied"
end
def m11 b
begin
if b
raise ExceptionA
end
rescue ExceptionA
rescue ExceptionB
puts "ExceptionB"
ensure
puts "Ensure"
end
puts "End m5"
end