Skip to content

Commit 47051ec

Browse files
authored
Merge pull request github#9320 from hvitved/ruby/hash-splat-flow
Ruby: Flow through hash-splat parameters
2 parents d5c8188 + ce49592 commit 47051ec

File tree

8 files changed

+171
-2
lines changed

8 files changed

+171
-2
lines changed

ruby/ql/consistency-queries/DataFlowConsistency.ql

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,7 @@ private class MyConsistencyConfiguration extends ConsistencyConfiguration {
99
n instanceof BlockArgumentNode
1010
or
1111
n instanceof SummaryNode
12+
or
13+
n instanceof HashSplatArgumentsNode
1214
}
1315
}

ruby/ql/lib/codeql/ruby/dataflow/internal/DataFlowDispatch.qll

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -259,7 +259,8 @@ private module Cached {
259259
exists(any(Call c).getKeywordArgument(name))
260260
or
261261
FlowSummaryImplSpecific::ParsePositions::isParsedKeywordParameterPosition(_, name)
262-
}
262+
} or
263+
THashSplatArgumentPosition()
263264

264265
cached
265266
newtype TParameterPosition =
@@ -278,6 +279,7 @@ private module Cached {
278279
or
279280
FlowSummaryImplSpecific::ParsePositions::isParsedKeywordArgumentPosition(_, name)
280281
} or
282+
THashSplatParameterPosition() or
281283
TAnyParameterPosition()
282284
}
283285

@@ -476,6 +478,9 @@ class ParameterPosition extends TParameterPosition {
476478
/** Holds if this position represents a keyword parameter named `name`. */
477479
predicate isKeyword(string name) { this = TKeywordParameterPosition(name) }
478480

481+
/** Holds if this position represents a hash-splat parameter. */
482+
predicate isHashSplat() { this = THashSplatParameterPosition() }
483+
479484
/**
480485
* Holds if this position represents any parameter. This includes both positional
481486
* and named parameters.
@@ -494,6 +499,8 @@ class ParameterPosition extends TParameterPosition {
494499
or
495500
exists(string name | this.isKeyword(name) and result = "keyword " + name)
496501
or
502+
this.isHashSplat() and result = "**"
503+
or
497504
this.isAny() and result = "any"
498505
}
499506
}
@@ -512,6 +519,12 @@ class ArgumentPosition extends TArgumentPosition {
512519
/** Holds if this position represents a keyword argument named `name`. */
513520
predicate isKeyword(string name) { this = TKeywordArgumentPosition(name) }
514521

522+
/**
523+
* Holds if this position represents a synthesized argument containing all keyword
524+
* arguments wrapped in a hash.
525+
*/
526+
predicate isHashSplat() { this = THashSplatArgumentPosition() }
527+
515528
/** Gets a textual representation of this position. */
516529
string toString() {
517530
this.isSelf() and result = "self"
@@ -521,6 +534,8 @@ class ArgumentPosition extends TArgumentPosition {
521534
exists(int pos | this.isPositional(pos) and result = "position " + pos)
522535
or
523536
exists(string name | this.isKeyword(name) and result = "keyword " + name)
537+
or
538+
this.isHashSplat() and result = "**"
524539
}
525540
}
526541

@@ -539,5 +554,7 @@ predicate parameterMatch(ParameterPosition ppos, ArgumentPosition apos) {
539554
or
540555
exists(string name | ppos.isKeyword(name) and apos.isKeyword(name))
541556
or
557+
ppos.isHashSplat() and apos.isHashSplat()
558+
or
542559
ppos.isAny() and exists(apos)
543560
}

ruby/ql/lib/codeql/ruby/dataflow/internal/DataFlowPrivate.qll

Lines changed: 77 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,7 @@ private class Argument extends CfgNodes::ExprCfgNode {
179179
this = call.getArgument(i) and
180180
not this.getExpr() instanceof BlockArgument and
181181
not this.getExpr().(Pair).getKey().getConstantValue().isSymbol(_) and
182+
not this.getExpr() instanceof HashSplatExpr and
182183
arg.isPositional(i)
183184
)
184185
or
@@ -189,6 +190,10 @@ private class Argument extends CfgNodes::ExprCfgNode {
189190
)
190191
or
191192
this = call.getReceiver() and arg.isSelf()
193+
or
194+
this = call.getAnArgument() and
195+
this.getExpr() instanceof HashSplatExpr and
196+
arg.isHashSplat()
192197
}
193198

194199
/** Holds if this expression is the `i`th argument of `c`. */
@@ -216,7 +221,8 @@ private module Cached {
216221
TNormalParameterNode(Parameter p) {
217222
p instanceof SimpleParameter or
218223
p instanceof OptionalParameter or
219-
p instanceof KeywordParameter
224+
p instanceof KeywordParameter or
225+
p instanceof HashSplatParameter
220226
} or
221227
TSelfParameterNode(MethodBase m) or
222228
TBlockParameterNode(MethodBase m) or
@@ -232,6 +238,9 @@ private module Cached {
232238
} or
233239
TSummaryParameterNode(FlowSummaryImpl::Public::SummarizedCallable c, ParameterPosition pos) {
234240
FlowSummaryImpl::Private::summaryParameterNodeRange(c, pos)
241+
} or
242+
THashSplatArgumentsNode(CfgNodes::ExprNodes::CallCfgNode c) {
243+
exists(Argument arg | arg.isArgumentOf(c, any(ArgumentPosition pos | pos.isKeyword(_))))
235244
}
236245

237246
class TParameterNode =
@@ -389,6 +398,8 @@ predicate nodeIsHidden(Node n) {
389398
n instanceof SummaryParameterNode
390399
or
391400
n instanceof SynthReturnNode
401+
or
402+
n instanceof HashSplatArgumentsNode
392403
}
393404

394405
/** An SSA definition, viewed as a node in a data flow graph. */
@@ -473,6 +484,9 @@ private module ParameterNodes {
473484
c.getAParameter() = kp and
474485
pos.isKeyword(kp.getName())
475486
)
487+
or
488+
parameter = c.getAParameter().(HashSplatParameter) and
489+
pos.isHashSplat()
476490
}
477491

478492
override CfgScope getCfgScope() { result = parameter.getCallable() }
@@ -651,6 +665,40 @@ private module ArgumentNodes {
651665
FlowSummaryImpl::Private::summaryArgumentNode(call, this, pos)
652666
}
653667
}
668+
669+
/**
670+
* A data-flow node that represents all keyword arguments wrapped in a hash.
671+
*
672+
* The callee is responsible for filtering out the keyword arguments that are
673+
* part of the method signature, such that those cannot end up in the hash-splat
674+
* parameter.
675+
*/
676+
class HashSplatArgumentsNode extends ArgumentNode, THashSplatArgumentsNode {
677+
CfgNodes::ExprNodes::CallCfgNode c;
678+
679+
HashSplatArgumentsNode() { this = THashSplatArgumentsNode(c) }
680+
681+
override predicate argumentOf(DataFlowCall call, ArgumentPosition pos) {
682+
this.sourceArgumentOf(call.asCall(), pos)
683+
}
684+
685+
override predicate sourceArgumentOf(CfgNodes::ExprNodes::CallCfgNode call, ArgumentPosition pos) {
686+
call = c and
687+
pos.isHashSplat()
688+
}
689+
}
690+
691+
private class HashSplatArgumentsNodeImpl extends NodeImpl, THashSplatArgumentsNode {
692+
CfgNodes::ExprNodes::CallCfgNode c;
693+
694+
HashSplatArgumentsNodeImpl() { this = THashSplatArgumentsNode(c) }
695+
696+
override CfgScope getCfgScope() { result = c.getExpr().getCfgScope() }
697+
698+
override Location getLocationImpl() { result = c.getLocation() }
699+
700+
override string toStringImpl() { result = "**" }
701+
}
654702
}
655703

656704
import ArgumentNodes
@@ -807,6 +855,13 @@ predicate jumpStep(Node pred, Node succ) {
807855
succ.asExpr().getExpr().(ConstantReadAccess).getValue() = pred.asExpr().getExpr()
808856
}
809857

858+
private ContentSet getKeywordContent(string name) {
859+
exists(ConstantValue::ConstantSymbolValue key |
860+
result.isSingleton(TKnownElementContent(key)) and
861+
key.isSymbol(name)
862+
)
863+
}
864+
810865
/**
811866
* Holds if data can flow from `node1` to `node2` via an assignment to
812867
* content `c`.
@@ -845,6 +900,14 @@ predicate storeStep(Node node1, ContentSet c, Node node2) {
845900
c.isSingleton(TUnknownPairValueContent())
846901
)
847902
)
903+
or
904+
// Wrap all keyword arguments in a synthesized hash-splat argument node
905+
exists(CfgNodes::ExprNodes::CallCfgNode call, ArgumentPosition keywordPos, string name |
906+
node2 = THashSplatArgumentsNode(call) and
907+
node1.asExpr().(Argument).isArgumentOf(call, keywordPos) and
908+
keywordPos.isKeyword(name) and
909+
c = getKeywordContent(name)
910+
)
848911
}
849912

850913
/**
@@ -870,6 +933,19 @@ predicate readStep(Node node1, ContentSet c, Node node2) {
870933
*/
871934
predicate clearsContent(Node n, ContentSet c) {
872935
FlowSummaryImpl::Private::Steps::summaryClearsContent(n, c)
936+
or
937+
// Filter out keyword arguments that are part of the method signature from
938+
// the hash-splat parameter
939+
exists(
940+
DataFlowCallable callable, ParameterPosition hashSplatPos, ParameterNodeImpl keywordParam,
941+
ParameterPosition keywordPos, string name
942+
|
943+
n.(ParameterNodes::NormalParameterNode).isParameterOf(callable, hashSplatPos) and
944+
hashSplatPos.isHashSplat() and
945+
keywordParam.isParameterOf(callable, keywordPos) and
946+
keywordPos.isKeyword(name) and
947+
c = getKeywordContent(name)
948+
)
873949
}
874950

875951
/**

ruby/ql/lib/codeql/ruby/frameworks/Core.qll

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,3 +73,15 @@ private class SplatSummary extends SummarizedCallable {
7373
preservesValue = true
7474
}
7575
}
76+
77+
private class HashSplatSummary extends SummarizedCallable {
78+
HashSplatSummary() { this = "**(hash-splat)" }
79+
80+
override HashSplatExpr getACall() { any() }
81+
82+
override predicate propagatesFlowExt(string input, string output, boolean preservesValue) {
83+
input = "Argument[self].WithElement[any]" and
84+
output = "ReturnValue" and
85+
preservesValue = true
86+
}
87+
}

ruby/ql/test/library-tests/dataflow/params/params-flow.expected

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,20 @@ edges
1212
| params_flow.rb:22:27:22:34 | call to taint : | params_flow.rb:16:13:16:14 | p1 : |
1313
| params_flow.rb:23:16:23:23 | call to taint : | params_flow.rb:16:18:16:19 | p2 : |
1414
| params_flow.rb:23:33:23:40 | call to taint : | params_flow.rb:16:13:16:14 | p1 : |
15+
| params_flow.rb:25:12:25:13 | p1 : | params_flow.rb:26:10:26:11 | p1 |
16+
| params_flow.rb:25:17:25:24 | **kwargs [element :p2] : | params_flow.rb:28:11:28:16 | kwargs [element :p2] : |
17+
| params_flow.rb:25:17:25:24 | **kwargs [element :p3] : | params_flow.rb:29:11:29:16 | kwargs [element :p3] : |
18+
| params_flow.rb:28:11:28:16 | kwargs [element :p2] : | params_flow.rb:28:11:28:21 | ...[...] : |
19+
| params_flow.rb:28:11:28:21 | ...[...] : | params_flow.rb:28:10:28:22 | ( ... ) |
20+
| params_flow.rb:29:11:29:16 | kwargs [element :p3] : | params_flow.rb:29:11:29:21 | ...[...] : |
21+
| params_flow.rb:29:11:29:21 | ...[...] : | params_flow.rb:29:10:29:22 | ( ... ) |
22+
| params_flow.rb:33:12:33:19 | call to taint : | params_flow.rb:25:12:25:13 | p1 : |
23+
| params_flow.rb:33:26:33:34 | call to taint : | params_flow.rb:25:17:25:24 | **kwargs [element :p2] : |
24+
| params_flow.rb:33:41:33:49 | call to taint : | params_flow.rb:25:17:25:24 | **kwargs [element :p3] : |
25+
| params_flow.rb:34:14:34:22 | call to taint : | params_flow.rb:35:25:35:28 | args [element :p3] : |
26+
| params_flow.rb:35:12:35:20 | call to taint : | params_flow.rb:25:12:25:13 | p1 : |
27+
| params_flow.rb:35:23:35:28 | ** ... [element :p3] : | params_flow.rb:25:17:25:24 | **kwargs [element :p3] : |
28+
| params_flow.rb:35:25:35:28 | args [element :p3] : | params_flow.rb:35:23:35:28 | ** ... [element :p3] : |
1529
nodes
1630
| params_flow.rb:9:16:9:17 | p1 : | semmle.label | p1 : |
1731
| params_flow.rb:9:20:9:21 | p2 : | semmle.label | p2 : |
@@ -29,6 +43,23 @@ nodes
2943
| params_flow.rb:22:27:22:34 | call to taint : | semmle.label | call to taint : |
3044
| params_flow.rb:23:16:23:23 | call to taint : | semmle.label | call to taint : |
3145
| params_flow.rb:23:33:23:40 | call to taint : | semmle.label | call to taint : |
46+
| params_flow.rb:25:12:25:13 | p1 : | semmle.label | p1 : |
47+
| params_flow.rb:25:17:25:24 | **kwargs [element :p2] : | semmle.label | **kwargs [element :p2] : |
48+
| params_flow.rb:25:17:25:24 | **kwargs [element :p3] : | semmle.label | **kwargs [element :p3] : |
49+
| params_flow.rb:26:10:26:11 | p1 | semmle.label | p1 |
50+
| params_flow.rb:28:10:28:22 | ( ... ) | semmle.label | ( ... ) |
51+
| params_flow.rb:28:11:28:16 | kwargs [element :p2] : | semmle.label | kwargs [element :p2] : |
52+
| params_flow.rb:28:11:28:21 | ...[...] : | semmle.label | ...[...] : |
53+
| params_flow.rb:29:10:29:22 | ( ... ) | semmle.label | ( ... ) |
54+
| params_flow.rb:29:11:29:16 | kwargs [element :p3] : | semmle.label | kwargs [element :p3] : |
55+
| params_flow.rb:29:11:29:21 | ...[...] : | semmle.label | ...[...] : |
56+
| params_flow.rb:33:12:33:19 | call to taint : | semmle.label | call to taint : |
57+
| params_flow.rb:33:26:33:34 | call to taint : | semmle.label | call to taint : |
58+
| params_flow.rb:33:41:33:49 | call to taint : | semmle.label | call to taint : |
59+
| params_flow.rb:34:14:34:22 | call to taint : | semmle.label | call to taint : |
60+
| params_flow.rb:35:12:35:20 | call to taint : | semmle.label | call to taint : |
61+
| params_flow.rb:35:23:35:28 | ** ... [element :p3] : | semmle.label | ** ... [element :p3] : |
62+
| params_flow.rb:35:25:35:28 | args [element :p3] : | semmle.label | args [element :p3] : |
3263
subpaths
3364
#select
3465
| params_flow.rb:10:10:10:11 | p1 | params_flow.rb:14:12:14:19 | call to taint : | params_flow.rb:10:10:10:11 | p1 | $@ | params_flow.rb:14:12:14:19 | call to taint : | call to taint : |
@@ -39,3 +70,8 @@ subpaths
3970
| params_flow.rb:18:10:18:11 | p2 | params_flow.rb:21:27:21:34 | call to taint : | params_flow.rb:18:10:18:11 | p2 | $@ | params_flow.rb:21:27:21:34 | call to taint : | call to taint : |
4071
| params_flow.rb:18:10:18:11 | p2 | params_flow.rb:22:13:22:20 | call to taint : | params_flow.rb:18:10:18:11 | p2 | $@ | params_flow.rb:22:13:22:20 | call to taint : | call to taint : |
4172
| params_flow.rb:18:10:18:11 | p2 | params_flow.rb:23:16:23:23 | call to taint : | params_flow.rb:18:10:18:11 | p2 | $@ | params_flow.rb:23:16:23:23 | call to taint : | call to taint : |
73+
| params_flow.rb:26:10:26:11 | p1 | params_flow.rb:33:12:33:19 | call to taint : | params_flow.rb:26:10:26:11 | p1 | $@ | params_flow.rb:33:12:33:19 | call to taint : | call to taint : |
74+
| params_flow.rb:26:10:26:11 | p1 | params_flow.rb:35:12:35:20 | call to taint : | params_flow.rb:26:10:26:11 | p1 | $@ | params_flow.rb:35:12:35:20 | call to taint : | call to taint : |
75+
| params_flow.rb:28:10:28:22 | ( ... ) | params_flow.rb:33:26:33:34 | call to taint : | params_flow.rb:28:10:28:22 | ( ... ) | $@ | params_flow.rb:33:26:33:34 | call to taint : | call to taint : |
76+
| params_flow.rb:29:10:29:22 | ( ... ) | params_flow.rb:33:41:33:49 | call to taint : | params_flow.rb:29:10:29:22 | ( ... ) | $@ | params_flow.rb:33:41:33:49 | call to taint : | call to taint : |
77+
| params_flow.rb:29:10:29:22 | ( ... ) | params_flow.rb:34:14:34:22 | call to taint : | params_flow.rb:29:10:29:22 | ( ... ) | $@ | params_flow.rb:34:14:34:22 | call to taint : | call to taint : |

ruby/ql/test/library-tests/dataflow/params/params_flow.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,15 @@ def keyword(p1:, p2:)
2121
keyword(p1: taint(3), p2: taint(4))
2222
keyword(p2: taint(5), p1: taint(6))
2323
keyword(:p2 => taint(7), :p1 => taint(8))
24+
25+
def kwargs(p1:, **kwargs)
26+
sink p1 # $ hasValueFlow=9 $ hasValueFlow=13
27+
sink (kwargs[:p1])
28+
sink (kwargs[:p2]) # $ hasValueFlow=10
29+
sink (kwargs[:p3]) # $ hasValueFlow=11 $ hasValueFlow=12
30+
sink (kwargs[:p4])
31+
end
32+
33+
kwargs(p1: taint(9), p2: taint(10), p3: taint(11), p4: "")
34+
args = { p3: taint(12), p4: "" }
35+
kwargs(p1: taint(13), **args)

ruby/ql/test/library-tests/dataflow/type-tracker/TypeTracker.expected

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,7 @@ track
139139
| type_tracker.rb:27:5:27:11 | call to puts | type tracker without call steps | type_tracker.rb:31:1:31:21 | call to keyword |
140140
| type_tracker.rb:27:5:27:11 | call to puts | type tracker without call steps | type_tracker.rb:32:1:32:27 | call to keyword |
141141
| type_tracker.rb:27:10:27:11 | [post] p2 | type tracker without call steps | type_tracker.rb:27:10:27:11 | [post] p2 |
142+
| type_tracker.rb:30:1:30:21 | ** | type tracker without call steps | type_tracker.rb:30:1:30:21 | ** |
142143
| type_tracker.rb:30:1:30:21 | [post] self | type tracker with call steps | type_tracker.rb:25:1:28:3 | self (keyword) |
143144
| type_tracker.rb:30:1:30:21 | [post] self | type tracker with call steps | type_tracker.rb:25:1:28:3 | self in keyword |
144145
| type_tracker.rb:30:1:30:21 | [post] self | type tracker without call steps | type_tracker.rb:30:1:30:21 | [post] self |
@@ -155,6 +156,7 @@ track
155156
| type_tracker.rb:30:20:30:20 | 4 | type tracker with call steps | type_tracker.rb:25:18:25:19 | p2 |
156157
| type_tracker.rb:30:20:30:20 | 4 | type tracker without call steps | type_tracker.rb:30:20:30:20 | 4 |
157158
| type_tracker.rb:30:20:30:20 | [post] 4 | type tracker without call steps | type_tracker.rb:30:20:30:20 | [post] 4 |
159+
| type_tracker.rb:31:1:31:21 | ** | type tracker without call steps | type_tracker.rb:31:1:31:21 | ** |
158160
| type_tracker.rb:31:1:31:21 | [post] self | type tracker with call steps | type_tracker.rb:25:1:28:3 | self (keyword) |
159161
| type_tracker.rb:31:1:31:21 | [post] self | type tracker with call steps | type_tracker.rb:25:1:28:3 | self in keyword |
160162
| type_tracker.rb:31:1:31:21 | [post] self | type tracker without call steps | type_tracker.rb:31:1:31:21 | [post] self |
@@ -171,6 +173,7 @@ track
171173
| type_tracker.rb:31:20:31:20 | 6 | type tracker with call steps | type_tracker.rb:25:13:25:14 | p1 |
172174
| type_tracker.rb:31:20:31:20 | 6 | type tracker without call steps | type_tracker.rb:31:20:31:20 | 6 |
173175
| type_tracker.rb:31:20:31:20 | [post] 6 | type tracker without call steps | type_tracker.rb:31:20:31:20 | [post] 6 |
176+
| type_tracker.rb:32:1:32:27 | ** | type tracker without call steps | type_tracker.rb:32:1:32:27 | ** |
174177
| type_tracker.rb:32:1:32:27 | [post] self | type tracker without call steps | type_tracker.rb:32:1:32:27 | [post] self |
175178
| type_tracker.rb:32:1:32:27 | call to keyword | type tracker without call steps | type_tracker.rb:32:1:32:27 | call to keyword |
176179
| type_tracker.rb:32:9:32:11 | :p2 | type tracker without call steps | type_tracker.rb:32:9:32:11 | :p2 |
@@ -393,6 +396,7 @@ trackEnd
393396
| type_tracker.rb:27:5:27:11 | call to puts | type_tracker.rb:31:1:31:21 | call to keyword |
394397
| type_tracker.rb:27:5:27:11 | call to puts | type_tracker.rb:32:1:32:27 | call to keyword |
395398
| type_tracker.rb:27:10:27:11 | [post] p2 | type_tracker.rb:27:10:27:11 | [post] p2 |
399+
| type_tracker.rb:30:1:30:21 | ** | type_tracker.rb:30:1:30:21 | ** |
396400
| type_tracker.rb:30:1:30:21 | [post] self | type_tracker.rb:25:1:28:3 | self (keyword) |
397401
| type_tracker.rb:30:1:30:21 | [post] self | type_tracker.rb:25:1:28:3 | self in keyword |
398402
| type_tracker.rb:30:1:30:21 | [post] self | type_tracker.rb:26:5:26:11 | self |
@@ -415,6 +419,7 @@ trackEnd
415419
| type_tracker.rb:30:20:30:20 | 4 | type_tracker.rb:27:10:27:11 | p2 |
416420
| type_tracker.rb:30:20:30:20 | 4 | type_tracker.rb:30:20:30:20 | 4 |
417421
| type_tracker.rb:30:20:30:20 | [post] 4 | type_tracker.rb:30:20:30:20 | [post] 4 |
422+
| type_tracker.rb:31:1:31:21 | ** | type_tracker.rb:31:1:31:21 | ** |
418423
| type_tracker.rb:31:1:31:21 | [post] self | type_tracker.rb:25:1:28:3 | self (keyword) |
419424
| type_tracker.rb:31:1:31:21 | [post] self | type_tracker.rb:25:1:28:3 | self in keyword |
420425
| type_tracker.rb:31:1:31:21 | [post] self | type_tracker.rb:26:5:26:11 | self |
@@ -436,6 +441,7 @@ trackEnd
436441
| type_tracker.rb:31:20:31:20 | 6 | type_tracker.rb:26:10:26:11 | p1 |
437442
| type_tracker.rb:31:20:31:20 | 6 | type_tracker.rb:31:20:31:20 | 6 |
438443
| type_tracker.rb:31:20:31:20 | [post] 6 | type_tracker.rb:31:20:31:20 | [post] 6 |
444+
| type_tracker.rb:32:1:32:27 | ** | type_tracker.rb:32:1:32:27 | ** |
439445
| type_tracker.rb:32:1:32:27 | [post] self | type_tracker.rb:32:1:32:27 | [post] self |
440446
| type_tracker.rb:32:1:32:27 | call to keyword | type_tracker.rb:32:1:32:27 | call to keyword |
441447
| type_tracker.rb:32:9:32:11 | :p2 | type_tracker.rb:32:9:32:11 | :p2 |

0 commit comments

Comments
 (0)