Skip to content

Commit a9f6d20

Browse files
authored
Merge pull request #8971 from aibaars/safe-nagivation
Ruby: add safe navigation operator
2 parents cee7aed + e1e13b5 commit a9f6d20

File tree

18 files changed

+396
-34
lines changed

18 files changed

+396
-34
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
Support of the safe navigation operator (`&.`) has been added; there is a new predicate `MethodCall.isSafeNavigation()`.

ruby/ql/lib/codeql/ruby/ast/Call.qll

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,14 @@ class MethodCall extends Call instanceof MethodCallImpl {
105105
*/
106106
final Block getBlock() { result = super.getBlockImpl() }
107107

108+
/**
109+
* Holds if the safe nagivation operator (`&.`) is used in this call.
110+
* ```rb
111+
* foo&.empty?
112+
* ```
113+
*/
114+
final predicate isSafeNavigation() { super.isSafeNavigationImpl() }
115+
108116
override string toString() { result = "call to " + this.getMethodName() }
109117

110118
override AstNode getAChild(string pred) {

ruby/ql/lib/codeql/ruby/ast/Control.qll

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,10 +111,10 @@ class IfExpr extends ConditionalExpr, TIfExpr {
111111
}
112112
}
113113

114-
private class If extends IfExpr, TIf {
114+
private class IfReal extends IfExpr, TIfReal {
115115
private Ruby::If g;
116116

117-
If() { this = TIf(g) }
117+
IfReal() { this = TIfReal(g) }
118118

119119
final override Expr getCondition() { toGenerated(result) = g.getCondition() }
120120

@@ -125,6 +125,18 @@ private class If extends IfExpr, TIf {
125125
final override string toString() { result = "if ..." }
126126
}
127127

128+
private class IfSynth extends IfExpr, TIfSynth {
129+
IfSynth() { this = TIfSynth(_, _) }
130+
131+
final override Expr getCondition() { synthChild(this, 0, result) }
132+
133+
final override Stmt getThen() { synthChild(this, 1, result) }
134+
135+
final override Stmt getElse() { synthChild(this, 2, result) }
136+
137+
final override string toString() { result = "if ..." }
138+
}
139+
128140
private class Elsif extends IfExpr, TElsif {
129141
private Ruby::Elsif g;
130142

ruby/ql/lib/codeql/ruby/ast/internal/AST.qll

Lines changed: 38 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -162,7 +162,8 @@ private module Cached {
162162
} or
163163
THereDoc(Ruby::HeredocBeginning g) or
164164
TIdentifierMethodCall(Ruby::Identifier g) { isIdentifierMethodCall(g) } or
165-
TIf(Ruby::If g) or
165+
TIfReal(Ruby::If g) or
166+
TIfSynth(AST::AstNode parent, int i) { mkSynthChild(IfKind(), parent, i) } or
166167
TIfModifierExpr(Ruby::IfModifier g) or
167168
TInClause(Ruby::InClause g) or
168169
TInstanceVariableAccessReal(Ruby::InstanceVariable g, AST::InstanceVariable v) {
@@ -214,7 +215,8 @@ private module Cached {
214215
TMulExprSynth(AST::AstNode parent, int i) { mkSynthChild(MulExprKind(), parent, i) } or
215216
TNEExpr(Ruby::Binary g) { g instanceof @ruby_binary_bangequal } or
216217
TNextStmt(Ruby::Next g) or
217-
TNilLiteral(Ruby::Nil g) or
218+
TNilLiteralReal(Ruby::Nil g) or
219+
TNilLiteralSynth(AST::AstNode parent, int i) { mkSynthChild(NilLiteralKind(), parent, i) } or
218220
TNoRegExpMatchExpr(Ruby::Binary g) { g instanceof @ruby_binary_bangtilde } or
219221
TNotExpr(Ruby::Unary g) { g instanceof @ruby_unary_bang or g instanceof @ruby_unary_not } or
220222
TOptionalParameter(Ruby::OptionalParameter g) or
@@ -347,35 +349,36 @@ private module Cached {
347349
TFalseLiteral or TFile or TFindPattern or TFloatLiteral or TForExpr or TForwardParameter or
348350
TForwardArgument or TGEExpr or TGTExpr or TGlobalVariableAccessReal or
349351
THashKeySymbolLiteral or THashLiteral or THashPattern or THashSplatExpr or
350-
THashSplatNilParameter or THashSplatParameter or THereDoc or TIdentifierMethodCall or TIf or
351-
TIfModifierExpr or TInClause or TInstanceVariableAccessReal or TIntegerLiteralReal or
352-
TKeywordParameter or TLEExpr or TLShiftExprReal or TLTExpr or TLambda or
353-
TLeftAssignmentList or TLine or TLocalVariableAccessReal or TLogicalAndExprReal or
354-
TLogicalOrExprReal or TMethod or TModuleDeclaration or TModuloExprReal or TMulExprReal or
355-
TNEExpr or TNextStmt or TNilLiteral or TNoRegExpMatchExpr or TNotExpr or
356-
TOptionalParameter or TPair or TParenthesizedExpr or TParenthesizedPattern or
357-
TRShiftExprReal or TRangeLiteralReal or TRationalLiteral or TRedoStmt or TRegExpLiteral or
358-
TRegExpMatchExpr or TRegularArrayLiteral or TRegularMethodCall or TRegularStringLiteral or
359-
TRegularSuperCall or TRescueClause or TRescueModifierExpr or TRetryStmt or TReturnStmt or
360-
TScopeResolutionConstantAccess or TSelfReal or TSimpleParameterReal or
361-
TSimpleSymbolLiteral or TSingletonClass or TSingletonMethod or TSpaceshipExpr or
362-
TSplatExprReal or TSplatParameter or TStringArrayLiteral or TStringConcatenation or
363-
TStringEscapeSequenceComponent or TStringInterpolationComponent or TStringTextComponent or
364-
TSubExprReal or TSubshellLiteral or TSymbolArrayLiteral or TTernaryIfExpr or TThen or
365-
TTokenConstantAccess or TTokenMethodName or TTokenSuperCall or TToplevel or TTrueLiteral or
366-
TUnaryMinusExpr or TUnaryPlusExpr or TUndefStmt or TUnlessExpr or TUnlessModifierExpr or
367-
TUntilExpr or TUntilModifierExpr or TReferencePattern or TWhenClause or TWhileExpr or
352+
THashSplatNilParameter or THashSplatParameter or THereDoc or TIdentifierMethodCall or
353+
TIfReal or TIfModifierExpr or TInClause or TInstanceVariableAccessReal or
354+
TIntegerLiteralReal or TKeywordParameter or TLEExpr or TLShiftExprReal or TLTExpr or
355+
TLambda or TLeftAssignmentList or TLine or TLocalVariableAccessReal or
356+
TLogicalAndExprReal or TLogicalOrExprReal or TMethod or TModuleDeclaration or
357+
TModuloExprReal or TMulExprReal or TNEExpr or TNextStmt or TNilLiteralReal or
358+
TNoRegExpMatchExpr or TNotExpr or TOptionalParameter or TPair or TParenthesizedExpr or
359+
TParenthesizedPattern or TRShiftExprReal or TRangeLiteralReal or TRationalLiteral or
360+
TRedoStmt or TRegExpLiteral or TRegExpMatchExpr or TRegularArrayLiteral or
361+
TRegularMethodCall or TRegularStringLiteral or TRegularSuperCall or TRescueClause or
362+
TRescueModifierExpr or TRetryStmt or TReturnStmt or TScopeResolutionConstantAccess or
363+
TSelfReal or TSimpleParameterReal or TSimpleSymbolLiteral or TSingletonClass or
364+
TSingletonMethod or TSpaceshipExpr or TSplatExprReal or TSplatParameter or
365+
TStringArrayLiteral or TStringConcatenation or TStringEscapeSequenceComponent or
366+
TStringInterpolationComponent or TStringTextComponent or TSubExprReal or TSubshellLiteral or
367+
TSymbolArrayLiteral or TTernaryIfExpr or TThen or TTokenConstantAccess or
368+
TTokenMethodName or TTokenSuperCall or TToplevel or TTrueLiteral or TUnaryMinusExpr or
369+
TUnaryPlusExpr or TUndefStmt or TUnlessExpr or TUnlessModifierExpr or TUntilExpr or
370+
TUntilModifierExpr or TReferencePattern or TWhenClause or TWhileExpr or
368371
TWhileModifierExpr or TYieldCall;
369372

370373
class TAstNodeSynth =
371374
TAddExprSynth or TAssignExprSynth or TBitwiseAndExprSynth or TBitwiseOrExprSynth or
372375
TBitwiseXorExprSynth or TBraceBlockSynth or TClassVariableAccessSynth or
373376
TConstantReadAccessSynth or TDivExprSynth or TExponentExprSynth or
374-
TGlobalVariableAccessSynth or TInstanceVariableAccessSynth or TIntegerLiteralSynth or
375-
TLShiftExprSynth or TLocalVariableAccessSynth or TLogicalAndExprSynth or
376-
TLogicalOrExprSynth or TMethodCallSynth or TModuloExprSynth or TMulExprSynth or
377-
TRShiftExprSynth or TRangeLiteralSynth or TSelfSynth or TSimpleParameterSynth or
378-
TSplatExprSynth or TStmtSequenceSynth or TSubExprSynth;
377+
TGlobalVariableAccessSynth or TIfSynth or TInstanceVariableAccessSynth or
378+
TIntegerLiteralSynth or TLShiftExprSynth or TLocalVariableAccessSynth or
379+
TLogicalAndExprSynth or TLogicalOrExprSynth or TMethodCallSynth or TModuloExprSynth or
380+
TMulExprSynth or TNilLiteralSynth or TRShiftExprSynth or TRangeLiteralSynth or TSelfSynth or
381+
TSimpleParameterSynth or TSplatExprSynth or TStmtSequenceSynth or TSubExprSynth;
379382

380383
/**
381384
* Gets the underlying TreeSitter entity for a given AST node. This does not
@@ -457,7 +460,7 @@ private module Cached {
457460
n = THereDoc(result) or
458461
n = TIdentifierMethodCall(result) or
459462
n = TIfModifierExpr(result) or
460-
n = TIf(result) or
463+
n = TIfReal(result) or
461464
n = TInClause(result) or
462465
n = TInstanceVariableAccessReal(result, _) or
463466
n = TIntegerLiteralReal(result) or
@@ -477,7 +480,7 @@ private module Cached {
477480
n = TMulExprReal(result) or
478481
n = TNEExpr(result) or
479482
n = TNextStmt(result) or
480-
n = TNilLiteral(result) or
483+
n = TNilLiteralReal(result) or
481484
n = TNoRegExpMatchExpr(result) or
482485
n = TNotExpr(result) or
483486
n = TOptionalParameter(result) or
@@ -568,6 +571,8 @@ private module Cached {
568571
or
569572
result = TGlobalVariableAccessSynth(parent, i, _)
570573
or
574+
result = TIfSynth(parent, i)
575+
or
571576
result = TInstanceVariableAccessSynth(parent, i, _)
572577
or
573578
result = TIntegerLiteralSynth(parent, i, _)
@@ -586,6 +591,8 @@ private module Cached {
586591
or
587592
result = TMulExprSynth(parent, i)
588593
or
594+
result = TNilLiteralSynth(parent, i)
595+
or
589596
result = TRangeLiteralSynth(parent, i, _)
590597
or
591598
result = TRShiftExprSynth(parent, i)
@@ -672,6 +679,8 @@ class TControlExpr = TConditionalExpr or TCaseExpr or TCaseMatch or TLoop;
672679
class TConditionalExpr =
673680
TIfExpr or TUnlessExpr or TIfModifierExpr or TUnlessModifierExpr or TTernaryIfExpr;
674681

682+
class TIf = TIfReal or TIfSynth;
683+
675684
class TIfExpr = TIf or TElsif;
676685

677686
class TConditionalLoop = TWhileExpr or TUntilExpr or TWhileModifierExpr or TUntilModifierExpr;
@@ -695,6 +704,8 @@ class TStmtSequence =
695704

696705
class TBodyStmt = TBeginExpr or TModuleBase or TMethod or TLambda or TDoBlock or TSingletonMethod;
697706

707+
class TNilLiteral = TNilLiteralReal or TNilLiteralSynth;
708+
698709
class TLiteral =
699710
TEncoding or TFile or TLine or TNumericLiteral or TNilLiteral or TBooleanLiteral or
700711
TStringlikeLiteral or TCharacterLiteral or TArrayLiteral or THashLiteral or TRangeLiteral or

ruby/ql/lib/codeql/ruby/ast/internal/Call.qll

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ abstract class MethodCallImpl extends CallImpl, TMethodCall {
2828
abstract string getMethodNameImpl();
2929

3030
abstract Block getBlockImpl();
31+
32+
predicate isSafeNavigationImpl() { none() }
3133
}
3234

3335
class MethodCallSynth extends MethodCallImpl, TMethodCallSynth {
@@ -89,6 +91,10 @@ class RegularMethodCall extends MethodCallImpl, TRegularMethodCall {
8991
final override int getNumberOfArgumentsImpl() { result = count(g.getArguments().getChild(_)) }
9092

9193
final override Block getBlockImpl() { toGenerated(result) = g.getBlock() }
94+
95+
final override predicate isSafeNavigationImpl() {
96+
g.getOperator().(Ruby::Token).getValue() = "&."
97+
}
9298
}
9399

94100
class ElementReferenceImpl extends MethodCallImpl, TElementReference {

ruby/ql/lib/codeql/ruby/ast/internal/Literal.qll

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -111,12 +111,18 @@ class ComplexLiteralImpl extends Expr, TComplexLiteral {
111111
}
112112
}
113113

114-
class NilLiteralImpl extends Expr, TNilLiteral {
114+
abstract class NilLiteralImpl extends Expr, TNilLiteral {
115+
final override string toString() { result = "nil" }
116+
}
117+
118+
class NilLiteralReal extends NilLiteralImpl, TNilLiteralReal {
115119
private Ruby::Nil g;
116120

117-
NilLiteralImpl() { this = TNilLiteral(g) }
121+
NilLiteralReal() { this = TNilLiteralReal(g) }
122+
}
118123

119-
final override string toString() { result = g.getValue() }
124+
class NilLiteralSynth extends NilLiteralImpl, TNilLiteralSynth {
125+
NilLiteralSynth() { this = TNilLiteralSynth(_, _) }
120126
}
121127

122128
abstract class BooleanLiteralImpl extends Expr, TBooleanLiteral {

ruby/ql/lib/codeql/ruby/ast/internal/Synthesis.qll

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ newtype SynthKind =
2121
DivExprKind() or
2222
ExponentExprKind() or
2323
GlobalVariableAccessKind(GlobalVariable v) or
24+
IfKind() or
2425
InstanceVariableAccessKind(InstanceVariable v) or
2526
IntegerLiteralKind(int i) { i in [-1000 .. 1000] } or
2627
LShiftExprKind() or
@@ -33,6 +34,7 @@ newtype SynthKind =
3334
} or
3435
ModuloExprKind() or
3536
MulExprKind() or
37+
NilLiteralKind() or
3638
RangeLiteralKind(boolean inclusive) { inclusive in [false, true] } or
3739
RShiftExprKind() or
3840
SimpleParameterKind() or
@@ -1083,3 +1085,123 @@ private module AnonymousBlockParameterSynth {
10831085
}
10841086
}
10851087
}
1088+
1089+
private module SafeNavigationCallDesugar {
1090+
/**
1091+
* ```rb
1092+
* receiver&.method(args) { ... }
1093+
* ```
1094+
* desugars to
1095+
*
1096+
* ```rb
1097+
* __synth__0 = receiver
1098+
* if nil == __synth__0 then nil else __synth__0.method(args) {...} end
1099+
* ```
1100+
*/
1101+
pragma[nomagic]
1102+
private predicate safeNavigationCallSynthesis(AstNode parent, int i, Child child) {
1103+
exists(RegularMethodCall call, LocalVariableAccessSynthKind local |
1104+
call.isSafeNavigationImpl() and
1105+
local = LocalVariableAccessSynthKind(TLocalVariableSynth(call.getReceiverImpl(), 0))
1106+
|
1107+
parent = call and
1108+
i = -1 and
1109+
child = SynthChild(StmtSequenceKind())
1110+
or
1111+
exists(TStmtSequenceSynth seq | seq = TStmtSequenceSynth(call, -1) |
1112+
parent = seq and
1113+
(
1114+
child = SynthChild(AssignExprKind()) and i = 0
1115+
or
1116+
child = SynthChild(IfKind()) and i = 1
1117+
)
1118+
or
1119+
parent = TAssignExprSynth(seq, 0) and
1120+
(
1121+
child = SynthChild(local) and
1122+
i = 0
1123+
or
1124+
child = childRef(call.getReceiverImpl()) and i = 1
1125+
)
1126+
or
1127+
exists(TIfSynth ifExpr | ifExpr = TIfSynth(seq, 1) |
1128+
parent = ifExpr and
1129+
(
1130+
child = SynthChild(MethodCallKind("==", false, 2)) and
1131+
i = 0
1132+
or
1133+
child = SynthChild(NilLiteralKind()) and i = 1
1134+
or
1135+
child =
1136+
SynthChild(MethodCallKind(call.getMethodNameImpl(), false,
1137+
call.getNumberOfArgumentsImpl())) and
1138+
i = 2
1139+
)
1140+
or
1141+
parent = TMethodCallSynth(ifExpr, 0, _, _, _) and
1142+
(
1143+
child = SynthChild(NilLiteralKind()) and i = 0
1144+
or
1145+
child = SynthChild(local) and
1146+
i = 1
1147+
)
1148+
or
1149+
parent = TMethodCallSynth(ifExpr, 2, _, _, _) and
1150+
(
1151+
i = 0 and
1152+
child = SynthChild(local)
1153+
or
1154+
child = childRef(call.getArgumentImpl(i - 1))
1155+
or
1156+
child = childRef(call.getBlockImpl()) and i = -2
1157+
)
1158+
)
1159+
)
1160+
)
1161+
}
1162+
1163+
private class SafeNavigationCallSynthesis extends Synthesis {
1164+
final override predicate child(AstNode parent, int i, Child child) {
1165+
safeNavigationCallSynthesis(parent, i, child)
1166+
}
1167+
1168+
final override predicate methodCall(string name, boolean setter, int arity) {
1169+
exists(RegularMethodCall call |
1170+
call.isSafeNavigationImpl() and
1171+
name = call.getMethodNameImpl() and
1172+
setter = false and
1173+
arity = call.getNumberOfArgumentsImpl()
1174+
)
1175+
or
1176+
name = "==" and setter = false and arity = 2
1177+
}
1178+
1179+
final override predicate localVariable(AstNode n, int i) {
1180+
i = 0 and n = any(RegularMethodCall c | c.isSafeNavigationImpl()).getReceiverImpl()
1181+
}
1182+
1183+
override predicate location(AstNode n, Location l) {
1184+
exists(RegularMethodCall call, StmtSequence seq |
1185+
call.isSafeNavigationImpl() and seq = call.getDesugared()
1186+
|
1187+
n = seq.getStmt(0) and
1188+
hasLocation(call.getReceiverImpl(), l)
1189+
or
1190+
n = seq.getStmt(1) and
1191+
l = toGenerated(call).(Ruby::Call).getOperator().getLocation()
1192+
or
1193+
n = seq.getStmt(1).(IfExpr).getCondition().(MethodCall).getArgument(0) and
1194+
hasLocation(call.getReceiverImpl(), l)
1195+
or
1196+
n = seq.getStmt(1).(IfExpr).getThen() and
1197+
hasLocation(call.getReceiverImpl(), l)
1198+
or
1199+
n = seq.getStmt(1).(IfExpr).getElse() and
1200+
hasLocation(call, l)
1201+
or
1202+
n = seq.getStmt(1).(IfExpr).getElse().(MethodCall).getReceiver() and
1203+
hasLocation(call.getReceiverImpl(), l)
1204+
)
1205+
}
1206+
}
1207+
}

ruby/ql/lib/codeql/ruby/controlflow/internal/ControlFlowGraphImpl.qll

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -371,7 +371,9 @@ module Trees {
371371
CallTree() {
372372
// Logical operations are handled separately
373373
not this instanceof UnaryLogicalOperation and
374-
not this instanceof BinaryLogicalOperation
374+
not this instanceof BinaryLogicalOperation and
375+
// Calls with the `&.` operator are desugared
376+
not this.(MethodCall).isSafeNavigation()
375377
}
376378

377379
override ControlFlowTree getChildElement(int i) { result = this.getArgument(i) }

0 commit comments

Comments
 (0)