Skip to content

Commit e41cd81

Browse files
authored
Merge pull request #7154 from aibaars/ruby-pattern-matching
Ruby: pattern matching
2 parents cde853c + 830908b commit e41cd81

33 files changed

+4783
-151
lines changed

.github/workflows/ruby-dataset-measure.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ jobs:
2424
strategy:
2525
fail-fast: false
2626
matrix:
27-
repo: [rails/rails, discourse/discourse, spree/spree]
27+
repo: [rails/rails, discourse/discourse, spree/spree, ruby/ruby]
2828
runs-on: ubuntu-latest
2929
steps:
3030
- uses: actions/checkout@v2

ruby/Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ruby/extractor/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ flate2 = "1.0"
1111
node-types = { path = "../node-types" }
1212
tree-sitter = "0.19"
1313
tree-sitter-embedded-template = "0.19"
14-
tree-sitter-ruby = { git = "https://github.com/tree-sitter/tree-sitter-ruby.git", rev = "bb6a42e42b048627a74a127d3e0184c1eef01de9" }
14+
tree-sitter-ruby = { git = "https://github.com/tree-sitter/tree-sitter-ruby.git", rev = "951799c6780deaabb0666b846cb7ad4eab627bbb" }
1515
clap = "2.33"
1616
tracing = "0.1"
1717
tracing-subscriber = { version = "0.2", features = ["env-filter"] }

ruby/generator/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ node-types = { path = "../node-types" }
1212
tracing = "0.1"
1313
tracing-subscriber = { version = "0.2", features = ["env-filter"] }
1414
tree-sitter-embedded-template = "0.19"
15-
tree-sitter-ruby = { git = "https://github.com/tree-sitter/tree-sitter-ruby.git", rev = "bb6a42e42b048627a74a127d3e0184c1eef01de9" }
15+
tree-sitter-ruby = { git = "https://github.com/tree-sitter/tree-sitter-ruby.git", rev = "951799c6780deaabb0666b846cb7ad4eab627bbb" }

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

Lines changed: 121 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
private import codeql.ruby.AST
22
private import internal.AST
3+
private import internal.Control
34
private import internal.TreeSitter
45

56
/**
@@ -308,13 +309,36 @@ class TernaryIfExpr extends ConditionalExpr, TTernaryIfExpr {
308309
}
309310
}
310311

311-
class CaseExpr extends ControlExpr, TCaseExpr {
312-
private Ruby::Case g;
313-
314-
CaseExpr() { this = TCaseExpr(g) }
315-
316-
final override string getAPrimaryQlClass() { result = "CaseExpr" }
317-
312+
/**
313+
* A `case` statement. There are three forms of `case` statements:
314+
* ```rb
315+
* # a value-less case expression acting like an if-elsif expression:
316+
* case
317+
* when x == 0 then puts "zero"
318+
* when x > 0 then puts "positive"
319+
* else puts "negative"
320+
* end
321+
*
322+
* # a case expression that matches a value using `when` clauses:
323+
* case value
324+
* when 1, 2 then puts "a is one or two"
325+
* when 3 then puts "a is three"
326+
* else puts "I don't know what a is"
327+
* end
328+
*
329+
* # a case expression that matches a value against patterns using `in` clauses:
330+
* config = {db: {user: 'admin', password: 'abc123'}}
331+
* case config
332+
* in db: {user:} # matches subhash and puts matched value in variable user
333+
* puts "Connect with user '#{user}'"
334+
* in connection: {username: } unless username == 'admin'
335+
* puts "Connect with user '#{username}'"
336+
* else
337+
* puts "Unrecognized structure of config"
338+
* end
339+
* ```
340+
*/
341+
class CaseExpr extends ControlExpr instanceof CaseExprImpl {
318342
/**
319343
* Gets the expression being compared, if any. For example, `foo` in the following example.
320344
* ```rb
@@ -334,22 +358,25 @@ class CaseExpr extends ControlExpr, TCaseExpr {
334358
* end
335359
* ```
336360
*/
337-
final Expr getValue() { toGenerated(result) = g.getValue() }
361+
final Expr getValue() { result = super.getValue() }
338362

339363
/**
340-
* Gets the `n`th branch of this case expression, either a `WhenExpr` or a
341-
* `StmtSequence`.
364+
* Gets the `n`th branch of this case expression, either a `WhenExpr`, an
365+
* `InClause`, or a `StmtSequence`.
342366
*/
343-
final Expr getBranch(int n) { toGenerated(result) = g.getChild(n) }
367+
final Expr getBranch(int n) { result = super.getBranch(n) }
344368

345369
/**
346-
* Gets a branch of this case expression, either a `WhenExpr` or an
347-
* `ElseExpr`.
370+
* Gets a branch of this case expression, either a `WhenExpr`, an
371+
* `InClause`, or a `StmtSequence`.
348372
*/
349373
final Expr getABranch() { result = this.getBranch(_) }
350374

375+
/** Gets the `n`th `when` branch of this case expression. */
376+
deprecated final WhenExpr getWhenBranch(int n) { result = this.getBranch(n) }
377+
351378
/** Gets a `when` branch of this case expression. */
352-
final WhenExpr getAWhenBranch() { result = this.getABranch() }
379+
deprecated final WhenExpr getAWhenBranch() { result = this.getABranch() }
353380

354381
/** Gets the `else` branch of this case expression, if any. */
355382
final StmtSequence getElseBranch() { result = this.getABranch() }
@@ -359,14 +386,18 @@ class CaseExpr extends ControlExpr, TCaseExpr {
359386
*/
360387
final int getNumberOfBranches() { result = count(this.getBranch(_)) }
361388

389+
final override string getAPrimaryQlClass() { result = "CaseExpr" }
390+
362391
final override string toString() { result = "case ..." }
363392

364393
override AstNode getAChild(string pred) {
365-
result = super.getAChild(pred)
394+
result = ControlExpr.super.getAChild(pred)
366395
or
367396
pred = "getValue" and result = this.getValue()
368397
or
369398
pred = "getBranch" and result = this.getBranch(_)
399+
or
400+
pred = "getElseBranch" and result = this.getElseBranch()
370401
}
371402
}
372403

@@ -422,6 +453,81 @@ class WhenExpr extends Expr, TWhenExpr {
422453
}
423454
}
424455

456+
/**
457+
* An `in` clause of a `case` expression.
458+
* ```rb
459+
* case foo
460+
* in [ a ] then a
461+
* end
462+
* ```
463+
*/
464+
class InClause extends Expr, TInClause {
465+
private Ruby::InClause g;
466+
467+
InClause() { this = TInClause(g) }
468+
469+
final override string getAPrimaryQlClass() { result = "InClause" }
470+
471+
/** Gets the body of this case-in expression. */
472+
final Stmt getBody() { toGenerated(result) = g.getBody() }
473+
474+
/**
475+
* Gets the pattern in this case-in expression. In the
476+
* following example, the pattern is `Point{ x:, y: }`.
477+
* ```rb
478+
* case foo
479+
* in Point{ x:, y: }
480+
* x + y
481+
* end
482+
* ```
483+
*/
484+
final CasePattern getPattern() { toGenerated(result) = g.getPattern() }
485+
486+
/**
487+
* Gets the pattern guard condition in this case-in expression. In the
488+
* following example, there are two pattern guard conditions `x > 10` and `x < 0`.
489+
* ```rb
490+
* case foo
491+
* in [ x ] if x > 10 then ...
492+
* in [ x ] unless x < 0 then ...
493+
* end
494+
* ```
495+
*/
496+
final Expr getCondition() { toGenerated(result) = g.getGuard().getAFieldOrChild() }
497+
498+
/**
499+
* Holds if the pattern guard in this case-in expression is an `if` condition. For example:
500+
* ```rb
501+
* case foo
502+
* in [ x ] if x > 10 then ...
503+
* end
504+
* ```
505+
*/
506+
predicate hasIfCondition() { g.getGuard() instanceof Ruby::IfGuard }
507+
508+
/**
509+
* Holds if the pattern guard in this case-in expression is an `unless` condition. For example:
510+
* ```rb
511+
* case foo
512+
* in [ x ] unless x < 10 then ...
513+
* end
514+
* ```
515+
*/
516+
predicate hasUnlessCondition() { g.getGuard() instanceof Ruby::UnlessGuard }
517+
518+
final override string toString() { result = "in ... then ..." }
519+
520+
override AstNode getAChild(string pred) {
521+
result = super.getAChild(pred)
522+
or
523+
pred = "getBody" and result = this.getBody()
524+
or
525+
pred = "getPattern" and result = this.getPattern()
526+
or
527+
pred = "getCondition" and result = this.getCondition()
528+
}
529+
}
530+
425531
/**
426532
* A loop. That is, a `for` loop, a `while` or `until` loop, or their
427533
* expression-modifier variants.

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,40 @@ private class FalseLiteral extends BooleanLiteral, TFalseLiteral {
223223
final override predicate isFalse() { any() }
224224
}
225225

226+
/**
227+
* An `__ENCODING__` literal.
228+
*/
229+
class EncodingLiteral extends Literal, TEncoding {
230+
final override string getAPrimaryQlClass() { result = "EncodingLiteral" }
231+
232+
final override string toString() { result = "__ENCODING__" }
233+
234+
// TODO: return the encoding defined by a magic encoding: comment, if any.
235+
override string getValueText() { result = "UTF-8" }
236+
}
237+
238+
/**
239+
* A `__LINE__` literal.
240+
*/
241+
class LineLiteral extends Literal, TLine {
242+
final override string getAPrimaryQlClass() { result = "LineLiteral" }
243+
244+
final override string toString() { result = "__LINE__" }
245+
246+
override string getValueText() { result = this.getLocation().getStartLine().toString() }
247+
}
248+
249+
/**
250+
* A `__FILE__` literal.
251+
*/
252+
class FileLiteral extends Literal, TFile {
253+
final override string getAPrimaryQlClass() { result = "FileLiteral" }
254+
255+
final override string toString() { result = "__FILE__" }
256+
257+
override string getValueText() { result = this.getLocation().getFile().getAbsolutePath() }
258+
}
259+
226260
/**
227261
* The base class for a component of a string: `StringTextComponent`,
228262
* `StringEscapeSequenceComponent`, or `StringInterpolationComponent`.

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

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,23 @@ class HashSplatParameter extends NamedParameter, THashSplatParameter {
131131
final override string getName() { result = g.getName().getValue() }
132132
}
133133

134+
/**
135+
* A `nil` hash splat (`**nil`) indicating that there are no keyword parameters or keyword patterns.
136+
* For example:
137+
* ```rb
138+
* def foo(bar, **nil)
139+
* case bar
140+
* in { x:, **nil } then puts x
141+
* end
142+
* end
143+
* ```
144+
*/
145+
class HashSplatNilParameter extends Parameter, THashSplatNilParameter {
146+
final override string getAPrimaryQlClass() { result = "HashSplatNilParameter" }
147+
148+
final override string toString() { result = "**nil" }
149+
}
150+
134151
/**
135152
* A keyword parameter, including a default value if the parameter is optional.
136153
* For example, in the following example, `foo` is a keyword parameter with a

0 commit comments

Comments
 (0)