Skip to content

Commit bcfe4ec

Browse files
authored
Merge pull request github#10918 from asgerf/rb/constant-compound-assignment
Ruby: handle compound constant-assignment
2 parents cac2e2e + b3855b0 commit bcfe4ec

File tree

16 files changed

+568
-11
lines changed

16 files changed

+568
-11
lines changed

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

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,20 @@ private class ConstantReadAccessSynth extends ConstantAccess, TConstantReadAcces
252252
final override predicate hasGlobalScope() { value.matches("::%") }
253253
}
254254

255+
private class ConstantWriteAccessSynth extends ConstantAccess, TConstantWriteAccessSynth {
256+
private string value;
257+
258+
ConstantWriteAccessSynth() { this = TConstantWriteAccessSynth(_, _, value) }
259+
260+
final override string getName() {
261+
if this.hasGlobalScope() then result = value.suffix(2) else result = value
262+
}
263+
264+
final override Expr getScopeExpr() { synthChild(this, 0, result) }
265+
266+
final override predicate hasGlobalScope() { value.matches("::%") }
267+
}
268+
255269
/**
256270
* A use (read) of a constant.
257271
*
@@ -323,7 +337,9 @@ class ConstantReadAccess extends ConstantAccess {
323337
*/
324338
class ConstantWriteAccess extends ConstantAccess {
325339
ConstantWriteAccess() {
326-
explicitAssignmentNode(toGenerated(this), _) or this instanceof TNamespace
340+
explicitAssignmentNode(toGenerated(this), _) or
341+
this instanceof TNamespace or
342+
this instanceof TConstantWriteAccessSynth
327343
}
328344

329345
override string getAPrimaryQlClass() { result = "ConstantWriteAccess" }

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ class ArgumentList extends Expr, TArgumentList {
6161

6262
private class LhsExpr_ =
6363
TVariableAccess or TTokenConstantAccess or TScopeResolutionConstantAccess or TMethodCall or
64-
TDestructuredLhsExpr;
64+
TDestructuredLhsExpr or TConstantWriteAccessSynth;
6565

6666
/**
6767
* A "left-hand-side" (LHS) expression. An `LhsExpr` can occur on the left-hand side of

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

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ private module Cached {
116116
TConstantReadAccessSynth(Ast::AstNode parent, int i, string value) {
117117
mkSynthChild(ConstantReadAccessKind(value), parent, i)
118118
} or
119+
TConstantWriteAccessSynth(Ast::AstNode parent, int i, string value) {
120+
mkSynthChild(ConstantWriteAccessKind(value), parent, i)
121+
} or
119122
TDefinedExpr(Ruby::Unary g) { g instanceof @ruby_unary_definedquestion } or
120123
TDelimitedSymbolLiteral(Ruby::DelimitedSymbol g) or
121124
TDestructuredLeftAssignment(Ruby::DestructuredLeftAssignment g) {
@@ -373,12 +376,13 @@ private module Cached {
373376
class TAstNodeSynth =
374377
TAddExprSynth or TAssignExprSynth or TBitwiseAndExprSynth or TBitwiseOrExprSynth or
375378
TBitwiseXorExprSynth or TBraceBlockSynth or TClassVariableAccessSynth or
376-
TConstantReadAccessSynth or TDivExprSynth or TExponentExprSynth or
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;
379+
TConstantReadAccessSynth or TConstantWriteAccessSynth or TDivExprSynth or
380+
TExponentExprSynth or TGlobalVariableAccessSynth or TIfSynth or
381+
TInstanceVariableAccessSynth or TIntegerLiteralSynth or TLShiftExprSynth or
382+
TLocalVariableAccessSynth or TLogicalAndExprSynth or TLogicalOrExprSynth or
383+
TMethodCallSynth or TModuloExprSynth or TMulExprSynth or TNilLiteralSynth or
384+
TRShiftExprSynth or TRangeLiteralSynth or TSelfSynth or TSimpleParameterSynth or
385+
TSplatExprSynth or TStmtSequenceSynth or TSubExprSynth;
382386

383387
/**
384388
* Gets the underlying TreeSitter entity for a given AST node. This does not
@@ -565,6 +569,8 @@ private module Cached {
565569
or
566570
result = TConstantReadAccessSynth(parent, i, _)
567571
or
572+
result = TConstantWriteAccessSynth(parent, i, _)
573+
or
568574
result = TDivExprSynth(parent, i)
569575
or
570576
result = TExponentExprSynth(parent, i)
@@ -672,7 +678,8 @@ class TMethodCall =
672678
class TSuperCall = TTokenSuperCall or TRegularSuperCall;
673679

674680
class TConstantAccess =
675-
TTokenConstantAccess or TScopeResolutionConstantAccess or TNamespace or TConstantReadAccessSynth;
681+
TTokenConstantAccess or TScopeResolutionConstantAccess or TNamespace or
682+
TConstantReadAccessSynth or TConstantWriteAccessSynth;
676683

677684
class TControlExpr = TConditionalExpr or TCaseExpr or TCaseMatch or TLoop;
678685

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

Lines changed: 232 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,8 @@ newtype SynthKind =
4242
StmtSequenceKind() or
4343
SelfKind(SelfVariable v) or
4444
SubExprKind() or
45-
ConstantReadAccessKind(string value) { any(Synthesis s).constantReadAccess(value) }
45+
ConstantReadAccessKind(string value) { any(Synthesis s).constantReadAccess(value) } or
46+
ConstantWriteAccessKind(string value) { any(Synthesis s).constantWriteAccess(value) }
4647

4748
/**
4849
* An AST child.
@@ -107,6 +108,11 @@ class Synthesis extends TSynthesis {
107108
*/
108109
predicate constantReadAccess(string name) { none() }
109110

111+
/**
112+
* Holds if a constant write access of `name` is needed.
113+
*/
114+
predicate constantWriteAccess(string name) { none() }
115+
110116
/**
111117
* Holds if `n` should be excluded from `ControlFlowTree` in the CFG construction.
112118
*/
@@ -493,6 +499,231 @@ private module AssignOperationDesugar {
493499
}
494500
}
495501

502+
/**
503+
* An assignment operation where the left-hand side is a constant
504+
* without scope expression, such as`FOO` or `::Foo`.
505+
*/
506+
private class ConstantAssignOperation extends AssignOperation {
507+
string name;
508+
509+
pragma[nomagic]
510+
ConstantAssignOperation() {
511+
name =
512+
any(Ruby::Constant constant | TTokenConstantAccess(constant) = this.getLeftOperand())
513+
.getValue()
514+
or
515+
name =
516+
"::" +
517+
any(Ruby::Constant constant |
518+
TScopeResolutionConstantAccess(any(Ruby::ScopeResolution g | not exists(g.getScope())),
519+
constant) = this.getLeftOperand()
520+
).getValue()
521+
}
522+
523+
final string getName() { result = name }
524+
}
525+
526+
pragma[nomagic]
527+
private predicate constantAssignOperationSynthesis(AstNode parent, int i, Child child) {
528+
exists(ConstantAssignOperation cao |
529+
parent = cao and
530+
i = -1 and
531+
child = SynthChild(AssignExprKind())
532+
or
533+
exists(AstNode assign | assign = TAssignExprSynth(cao, -1) |
534+
parent = assign and
535+
i = 0 and
536+
child = childRef(cao.getLeftOperand())
537+
or
538+
parent = assign and
539+
i = 1 and
540+
child = SynthChild(getKind(cao))
541+
or
542+
parent = getSynthChild(assign, 1) and
543+
(
544+
i = 0 and
545+
child = SynthChild(ConstantReadAccessKind(cao.getName()))
546+
or
547+
i = 1 and
548+
child = childRef(cao.getRightOperand())
549+
)
550+
)
551+
)
552+
}
553+
554+
/**
555+
* ```rb
556+
* FOO += y
557+
* ```
558+
*
559+
* desugars to
560+
*
561+
* ```rb
562+
* FOO = FOO + y
563+
* ```
564+
*/
565+
private class ConstantAssignOperationSynthesis extends Synthesis {
566+
final override predicate child(AstNode parent, int i, Child child) {
567+
constantAssignOperationSynthesis(parent, i, child)
568+
}
569+
570+
final override predicate constantReadAccess(string name) {
571+
name = any(ConstantAssignOperation o).getName()
572+
}
573+
574+
final override predicate location(AstNode n, Location l) {
575+
exists(ConstantAssignOperation cao, BinaryOperation bo |
576+
bo = cao.getDesugared().(AssignExpr).getRightOperand()
577+
|
578+
n = bo and
579+
l = getAssignOperationLocation(cao)
580+
or
581+
n = bo.getLeftOperand() and
582+
hasLocation(cao.getLeftOperand(), l)
583+
)
584+
}
585+
}
586+
587+
/**
588+
* An assignment operation where the left-hand side is a constant
589+
* with scope expression, such as `expr::FOO`.
590+
*/
591+
private class ScopeResolutionAssignOperation extends AssignOperation {
592+
string name;
593+
Expr scope;
594+
595+
pragma[nomagic]
596+
ScopeResolutionAssignOperation() {
597+
exists(Ruby::Constant constant, Ruby::ScopeResolution g |
598+
TScopeResolutionConstantAccess(g, constant) = this.getLeftOperand() and
599+
name = constant.getValue() and
600+
toGenerated(scope) = g.getScope()
601+
)
602+
}
603+
604+
final string getName() { result = name }
605+
606+
final Expr getScopeExpr() { result = scope }
607+
}
608+
609+
pragma[nomagic]
610+
private predicate scopeResolutionAssignOperationSynthesis(AstNode parent, int i, Child child) {
611+
exists(ScopeResolutionAssignOperation cao |
612+
parent = cao and
613+
i = -1 and
614+
child = SynthChild(StmtSequenceKind())
615+
or
616+
exists(AstNode stmts | stmts = TStmtSequenceSynth(cao, -1) |
617+
parent = stmts and
618+
i = 0 and
619+
child = SynthChild(AssignExprKind())
620+
or
621+
exists(AstNode assign | assign = TAssignExprSynth(stmts, 0) |
622+
parent = assign and
623+
i = 0 and
624+
child = SynthChild(LocalVariableAccessSynthKind(TLocalVariableSynth(cao, 0)))
625+
or
626+
parent = assign and
627+
i = 1 and
628+
child = childRef(cao.getScopeExpr())
629+
)
630+
or
631+
parent = stmts and
632+
i = 1 and
633+
child = SynthChild(AssignExprKind())
634+
or
635+
exists(AstNode assign | assign = TAssignExprSynth(stmts, 1) |
636+
parent = assign and
637+
i = 0 and
638+
child = SynthChild(ConstantWriteAccessKind(cao.getName()))
639+
or
640+
exists(AstNode cwa | cwa = TConstantWriteAccessSynth(assign, 0, cao.getName()) |
641+
parent = cwa and
642+
i = 0 and
643+
child = SynthChild(LocalVariableAccessSynthKind(TLocalVariableSynth(cao, 0)))
644+
)
645+
or
646+
parent = assign and
647+
i = 1 and
648+
child = SynthChild(getKind(cao))
649+
or
650+
exists(AstNode op | op = getSynthChild(assign, 1) |
651+
parent = op and
652+
i = 0 and
653+
child = SynthChild(ConstantReadAccessKind(cao.getName()))
654+
or
655+
exists(AstNode cra | cra = TConstantReadAccessSynth(op, 0, cao.getName()) |
656+
parent = cra and
657+
i = 0 and
658+
child = SynthChild(LocalVariableAccessSynthKind(TLocalVariableSynth(cao, 0)))
659+
)
660+
or
661+
parent = op and
662+
i = 1 and
663+
child = childRef(cao.getRightOperand())
664+
)
665+
)
666+
)
667+
)
668+
}
669+
670+
/**
671+
* ```rb
672+
* expr::FOO += y
673+
* ```
674+
*
675+
* desugars to
676+
*
677+
* ```rb
678+
* __synth__0 = expr
679+
* __synth__0::FOO = _synth__0::FOO + y
680+
* ```
681+
*/
682+
private class ScopeResolutionAssignOperationSynthesis extends Synthesis {
683+
final override predicate child(AstNode parent, int i, Child child) {
684+
scopeResolutionAssignOperationSynthesis(parent, i, child)
685+
}
686+
687+
final override predicate constantReadAccess(string name) {
688+
name = any(ScopeResolutionAssignOperation o).getName()
689+
}
690+
691+
final override predicate localVariable(AstNode n, int i) {
692+
n instanceof ScopeResolutionAssignOperation and
693+
i = 0
694+
}
695+
696+
final override predicate constantWriteAccess(string name) { this.constantReadAccess(name) }
697+
698+
final override predicate location(AstNode n, Location l) {
699+
exists(ScopeResolutionAssignOperation cao, StmtSequence stmts | stmts = cao.getDesugared() |
700+
n = stmts.getStmt(0) and
701+
hasLocation(cao.getScopeExpr(), l)
702+
or
703+
exists(AssignExpr assign | assign = stmts.getStmt(1) |
704+
n = assign and hasLocation(cao, l)
705+
or
706+
n = assign.getLeftOperand() and
707+
hasLocation(cao.getLeftOperand(), l)
708+
or
709+
n = assign.getLeftOperand().(ConstantAccess).getScopeExpr() and
710+
hasLocation(cao.getScopeExpr(), l)
711+
or
712+
exists(BinaryOperation bo | bo = assign.getRightOperand() |
713+
n = bo and
714+
l = getAssignOperationLocation(cao)
715+
or
716+
n = bo.getLeftOperand() and
717+
hasLocation(cao.getLeftOperand(), l)
718+
or
719+
n = bo.getLeftOperand().(ConstantAccess).getScopeExpr() and
720+
hasLocation(cao.getScopeExpr(), l)
721+
)
722+
)
723+
)
724+
}
725+
}
726+
496727
/** An assignment operation where the left-hand side is a method call. */
497728
private class SetterAssignOperation extends AssignOperation {
498729
private MethodCall mc;

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -890,7 +890,12 @@ module Trees {
890890
private class ConstantAccessTree extends PostOrderTree, ConstantAccess {
891891
ConstantAccessTree() {
892892
not this instanceof ClassDeclaration and
893-
not this instanceof ModuleDeclaration
893+
not this instanceof ModuleDeclaration and
894+
// constant accesses with scope expression in compound assignments are desugared
895+
not (
896+
this = any(AssignOperation op).getLeftOperand() and
897+
exists(this.getScopeExpr())
898+
)
894899
}
895900

896901
final override predicate propagatesAbnormal(AstNode child) { child = this.getScopeExpr() }

ruby/ql/test/library-tests/ast/Ast.expected

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2833,6 +2833,29 @@ operations/operations.rb:
28332833
# 96| getStmt: [AssignMulExpr] ... *= ...
28342834
# 96| getAnOperand/getLeftOperand: [GlobalVariableAccess] $global_var
28352835
# 96| getAnOperand/getRightOperand: [IntegerLiteral] 6
2836+
# 98| getStmt: [AssignExpr] ... = ...
2837+
# 98| getAnOperand/getLeftOperand: [ConstantAssignment] CONSTANT1
2838+
# 98| getAnOperand/getRightOperand: [IntegerLiteral] 5
2839+
# 99| getStmt: [AssignAddExpr] ... += ...
2840+
# 99| getAnOperand/getLeftOperand: [ConstantAssignment, ConstantReadAccess] CONSTANT2
2841+
# 99| getAnOperand/getRightOperand: [IntegerLiteral] 6
2842+
# 100| getStmt: [AssignLogicalOrExpr] ... ||= ...
2843+
# 100| getAnOperand/getLeftOperand: [ConstantAssignment, ConstantReadAccess] CONSTANT3
2844+
# 100| getAnOperand/getRightOperand: [IntegerLiteral] 7
2845+
# 101| getStmt: [AssignLogicalOrExpr] ... ||= ...
2846+
# 101| getAnOperand/getLeftOperand: [ConstantAssignment, ConstantReadAccess] MemberConstant
2847+
# 101| getScopeExpr: [ConstantReadAccess] Foo
2848+
# 101| getAnOperand/getRightOperand: [IntegerLiteral] 8
2849+
# 102| getStmt: [AssignLogicalOrExpr] ... ||= ...
2850+
# 102| getAnOperand/getLeftOperand: [ConstantAssignment, ConstantReadAccess] OtherConstant
2851+
# 102| getScopeExpr: [MethodCall] call to bar
2852+
# 102| getReceiver: [MethodCall] call to foo
2853+
# 102| getReceiver: [SelfVariableAccess] self
2854+
# 102| getArgument: [IntegerLiteral] 1
2855+
# 102| getAnOperand/getRightOperand: [IntegerLiteral] 7
2856+
# 103| getStmt: [AssignLogicalOrExpr] ... ||= ...
2857+
# 103| getAnOperand/getLeftOperand: [ConstantAssignment, ConstantReadAccess] CONSTANT4
2858+
# 103| getAnOperand/getRightOperand: [IntegerLiteral] 7
28362859
params/params.rb:
28372860
# 1| [Toplevel] params.rb
28382861
# 4| getStmt: [Method] identifier_method_params

0 commit comments

Comments
 (0)