Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
1
Merge Requests
1
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
nexedi
gitlab-ce
Commits
7f33b584
Commit
7f33b584
authored
Jul 29, 2020
by
Kamil Trzciński
Committed by
Fabio Pitino
Jul 29, 2020
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add parenthesis support for if: conditions
This extends RPN to support `()` in expressions
parent
156970ca
Changes
30
Show whitespace changes
Inline
Side-by-side
Showing
30 changed files
with
365 additions
and
67 deletions
+365
-67
changelogs/unreleased/support-parentheses-in-conditions.yml
changelogs/unreleased/support-parentheses-in-conditions.yml
+5
-0
doc/ci/variables/README.md
doc/ci/variables/README.md
+34
-0
lib/gitlab/ci/features.rb
lib/gitlab/ci/features.rb
+4
-0
lib/gitlab/ci/pipeline/expression/lexeme/and.rb
lib/gitlab/ci/pipeline/expression/lexeme/and.rb
+1
-1
lib/gitlab/ci/pipeline/expression/lexeme/base.rb
lib/gitlab/ci/pipeline/expression/lexeme/base.rb
+8
-0
lib/gitlab/ci/pipeline/expression/lexeme/equals.rb
lib/gitlab/ci/pipeline/expression/lexeme/equals.rb
+1
-1
lib/gitlab/ci/pipeline/expression/lexeme/logical_operator.rb
lib/gitlab/ci/pipeline/expression/lexeme/logical_operator.rb
+35
-0
lib/gitlab/ci/pipeline/expression/lexeme/matches.rb
lib/gitlab/ci/pipeline/expression/lexeme/matches.rb
+1
-1
lib/gitlab/ci/pipeline/expression/lexeme/not_equals.rb
lib/gitlab/ci/pipeline/expression/lexeme/not_equals.rb
+1
-1
lib/gitlab/ci/pipeline/expression/lexeme/not_matches.rb
lib/gitlab/ci/pipeline/expression/lexeme/not_matches.rb
+1
-1
lib/gitlab/ci/pipeline/expression/lexeme/null.rb
lib/gitlab/ci/pipeline/expression/lexeme/null.rb
+5
-1
lib/gitlab/ci/pipeline/expression/lexeme/operator.rb
lib/gitlab/ci/pipeline/expression/lexeme/operator.rb
+1
-15
lib/gitlab/ci/pipeline/expression/lexeme/or.rb
lib/gitlab/ci/pipeline/expression/lexeme/or.rb
+1
-1
lib/gitlab/ci/pipeline/expression/lexeme/parenthesis_close.rb
...gitlab/ci/pipeline/expression/lexeme/parenthesis_close.rb
+23
-0
lib/gitlab/ci/pipeline/expression/lexeme/parenthesis_open.rb
lib/gitlab/ci/pipeline/expression/lexeme/parenthesis_open.rb
+24
-0
lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb
lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb
+5
-1
lib/gitlab/ci/pipeline/expression/lexeme/string.rb
lib/gitlab/ci/pipeline/expression/lexeme/string.rb
+5
-1
lib/gitlab/ci/pipeline/expression/lexeme/value.rb
lib/gitlab/ci/pipeline/expression/lexeme/value.rb
+4
-0
lib/gitlab/ci/pipeline/expression/lexeme/variable.rb
lib/gitlab/ci/pipeline/expression/lexeme/variable.rb
+4
-4
lib/gitlab/ci/pipeline/expression/lexer.rb
lib/gitlab/ci/pipeline/expression/lexer.rb
+25
-1
lib/gitlab/ci/pipeline/expression/parser.rb
lib/gitlab/ci/pipeline/expression/parser.rb
+39
-4
spec/lib/gitlab/ci/pipeline/expression/lexeme/and_spec.rb
spec/lib/gitlab/ci/pipeline/expression/lexeme/and_spec.rb
+1
-1
spec/lib/gitlab/ci/pipeline/expression/lexeme/equals_spec.rb
spec/lib/gitlab/ci/pipeline/expression/lexeme/equals_spec.rb
+1
-1
spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb
.../lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb
+1
-1
spec/lib/gitlab/ci/pipeline/expression/lexeme/not_equals_spec.rb
...b/gitlab/ci/pipeline/expression/lexeme/not_equals_spec.rb
+1
-1
spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb
.../gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb
+1
-1
spec/lib/gitlab/ci/pipeline/expression/lexeme/or_spec.rb
spec/lib/gitlab/ci/pipeline/expression/lexeme/or_spec.rb
+1
-1
spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb
spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb
+29
-0
spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb
spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb
+91
-26
spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb
spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb
+12
-2
No files found.
changelogs/unreleased/support-parentheses-in-conditions.yml
0 → 100644
View file @
7f33b584
---
title
:
'
Add
parenthesis
support
for
if:
conditions'
merge_request
:
37574
author
:
type
:
added
doc/ci/variables/README.md
View file @
7f33b584
...
@@ -742,6 +742,40 @@ Precedence of operators follows the
...
@@ -742,6 +742,40 @@ Precedence of operators follows the
[
Ruby 2.5 standard
](
https://ruby-doc.org/core-2.5.0/doc/syntax/precedence_rdoc.html
)
,
[
Ruby 2.5 standard
](
https://ruby-doc.org/core-2.5.0/doc/syntax/precedence_rdoc.html
)
,
so
`&&`
is evaluated before
`||`
.
so
`&&`
is evaluated before
`||`
.
#### Parentheses
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/230938) in GitLab 13.3
It is possible to use parentheses to group conditions. Parentheses have the highest
precedence of all operators. Expressions enclosed in parentheses are evaluated first,
and the result is used for the rest of the expression.
Many nested parentheses can be used to create complex conditions, and the inner-most
expressions in parentheses are evaluated first. For an expression to be valid an equal
number of
`(`
and
`)`
need to be used.
Examples:
-
`($VARIABLE1 =~ /^content.*/ || $VARIABLE2) && ($VARIABLE3 =~ /thing$/ || $VARIABLE4)`
-
`($VARIABLE1 =~ /^content.*/ || $VARIABLE2 =~ /thing$/) && $VARIABLE3`
-
`$CI_COMMIT_BRANCH == "my-branch" || (($VARIABLE1 == "thing" || $VARIABLE2 == "thing") && $VARIABLE3)`
The feature is currently deployed behind a feature flag that is
**disabled by default**
.
[
GitLab administrators with access to the GitLab Rails console
](
../../administration/feature_flags.md
)
can opt to enable it for your instance.
To enable it:
```
ruby
Feature
.
enable
(
:ci_if_parenthesis_enabled
)
```
To disable it:
```
ruby
Feature
.
disable
(
:ci_if_parenthesis_enabled
)
```
### Storing regular expressions in variables
### Storing regular expressions in variables
It is possible to store a regular expression in a variable, to be used for pattern matching:
It is possible to store a regular expression in a variable, to be used for pattern matching:
...
...
lib/gitlab/ci/features.rb
View file @
7f33b584
...
@@ -70,6 +70,10 @@ module Gitlab
...
@@ -70,6 +70,10 @@ module Gitlab
::
Feature
.
enabled?
(
:ci_bulk_insert_on_create
,
project
,
default_enabled:
true
)
::
Feature
.
enabled?
(
:ci_bulk_insert_on_create
,
project
,
default_enabled:
true
)
end
end
def
self
.
ci_if_parenthesis_enabled?
::
Feature
.
enabled?
(
:ci_if_parenthesis_enabled
)
end
def
self
.
allow_to_create_merge_request_pipelines_in_target_project?
(
target_project
)
def
self
.
allow_to_create_merge_request_pipelines_in_target_project?
(
target_project
)
::
Feature
.
enabled?
(
:ci_allow_to_create_merge_request_pipelines_in_target_project
,
target_project
,
default_enabled:
true
)
::
Feature
.
enabled?
(
:ci_allow_to_create_merge_request_pipelines_in_target_project
,
target_project
,
default_enabled:
true
)
end
end
...
...
lib/gitlab/ci/pipeline/expression/lexeme/and.rb
View file @
7f33b584
...
@@ -5,7 +5,7 @@ module Gitlab
...
@@ -5,7 +5,7 @@ module Gitlab
module
Pipeline
module
Pipeline
module
Expression
module
Expression
module
Lexeme
module
Lexeme
class
And
<
Lexeme
::
Operator
class
And
<
Lexeme
::
Logical
Operator
PATTERN
=
/&&/
.
freeze
PATTERN
=
/&&/
.
freeze
def
evaluate
(
variables
=
{})
def
evaluate
(
variables
=
{})
...
...
lib/gitlab/ci/pipeline/expression/lexeme/base.rb
View file @
7f33b584
...
@@ -10,6 +10,10 @@ module Gitlab
...
@@ -10,6 +10,10 @@ module Gitlab
raise
NotImplementedError
raise
NotImplementedError
end
end
def
name
self
.
class
.
name
.
demodulize
.
underscore
end
def
self
.
build
(
token
)
def
self
.
build
(
token
)
raise
NotImplementedError
raise
NotImplementedError
end
end
...
@@ -23,6 +27,10 @@ module Gitlab
...
@@ -23,6 +27,10 @@ module Gitlab
def
self
.
pattern
def
self
.
pattern
self
::
PATTERN
self
::
PATTERN
end
end
def
self
.
consume?
(
lexeme
)
lexeme
&&
precedence
>=
lexeme
.
precedence
end
end
end
end
end
end
end
...
...
lib/gitlab/ci/pipeline/expression/lexeme/equals.rb
View file @
7f33b584
...
@@ -5,7 +5,7 @@ module Gitlab
...
@@ -5,7 +5,7 @@ module Gitlab
module
Pipeline
module
Pipeline
module
Expression
module
Expression
module
Lexeme
module
Lexeme
class
Equals
<
Lexeme
::
Operator
class
Equals
<
Lexeme
::
Logical
Operator
PATTERN
=
/==/
.
freeze
PATTERN
=
/==/
.
freeze
def
evaluate
(
variables
=
{})
def
evaluate
(
variables
=
{})
...
...
lib/gitlab/ci/pipeline/expression/lexeme/logical_operator.rb
0 → 100644
View file @
7f33b584
# frozen_string_literal: true
module
Gitlab
module
Ci
module
Pipeline
module
Expression
module
Lexeme
class
LogicalOperator
<
Lexeme
::
Operator
# This operator class is design to handle single operators that take two
# arguments. Expression::Parser was originally designed to read infix operators,
# and so the two operands are called "left" and "right" here. If we wish to
# implement an Operator that takes a greater or lesser number of arguments, a
# structural change or additional Operator superclass will likely be needed.
def
initialize
(
left
,
right
)
raise
OperatorError
,
'Invalid left operand'
unless
left
.
respond_to?
:evaluate
raise
OperatorError
,
'Invalid right operand'
unless
right
.
respond_to?
:evaluate
@left
=
left
@right
=
right
end
def
inspect
"
#{
name
}
(
#{
@left
.
inspect
}
,
#{
@right
.
inspect
}
)"
end
def
self
.
type
:logical_operator
end
end
end
end
end
end
end
lib/gitlab/ci/pipeline/expression/lexeme/matches.rb
View file @
7f33b584
...
@@ -5,7 +5,7 @@ module Gitlab
...
@@ -5,7 +5,7 @@ module Gitlab
module
Pipeline
module
Pipeline
module
Expression
module
Expression
module
Lexeme
module
Lexeme
class
Matches
<
Lexeme
::
Operator
class
Matches
<
Lexeme
::
Logical
Operator
PATTERN
=
/=~/
.
freeze
PATTERN
=
/=~/
.
freeze
def
evaluate
(
variables
=
{})
def
evaluate
(
variables
=
{})
...
...
lib/gitlab/ci/pipeline/expression/lexeme/not_equals.rb
View file @
7f33b584
...
@@ -5,7 +5,7 @@ module Gitlab
...
@@ -5,7 +5,7 @@ module Gitlab
module
Pipeline
module
Pipeline
module
Expression
module
Expression
module
Lexeme
module
Lexeme
class
NotEquals
<
Lexeme
::
Operator
class
NotEquals
<
Lexeme
::
Logical
Operator
PATTERN
=
/!=/
.
freeze
PATTERN
=
/!=/
.
freeze
def
evaluate
(
variables
=
{})
def
evaluate
(
variables
=
{})
...
...
lib/gitlab/ci/pipeline/expression/lexeme/not_matches.rb
View file @
7f33b584
...
@@ -5,7 +5,7 @@ module Gitlab
...
@@ -5,7 +5,7 @@ module Gitlab
module
Pipeline
module
Pipeline
module
Expression
module
Expression
module
Lexeme
module
Lexeme
class
NotMatches
<
Lexeme
::
Operator
class
NotMatches
<
Lexeme
::
Logical
Operator
PATTERN
=
/\!~/
.
freeze
PATTERN
=
/\!~/
.
freeze
def
evaluate
(
variables
=
{})
def
evaluate
(
variables
=
{})
...
...
lib/gitlab/ci/pipeline/expression/lexeme/null.rb
View file @
7f33b584
...
@@ -9,13 +9,17 @@ module Gitlab
...
@@ -9,13 +9,17 @@ module Gitlab
PATTERN
=
/null/
.
freeze
PATTERN
=
/null/
.
freeze
def
initialize
(
value
=
nil
)
def
initialize
(
value
=
nil
)
@value
=
nil
super
end
end
def
evaluate
(
variables
=
{})
def
evaluate
(
variables
=
{})
nil
nil
end
end
def
inspect
'null'
end
def
self
.
build
(
_value
)
def
self
.
build
(
_value
)
self
.
new
self
.
new
end
end
...
...
lib/gitlab/ci/pipeline/expression/lexeme/operator.rb
View file @
7f33b584
...
@@ -6,24 +6,10 @@ module Gitlab
...
@@ -6,24 +6,10 @@ module Gitlab
module
Expression
module
Expression
module
Lexeme
module
Lexeme
class
Operator
<
Lexeme
::
Base
class
Operator
<
Lexeme
::
Base
# This operator class is design to handle single operators that take two
# arguments. Expression::Parser was originally designed to read infix operators,
# and so the two operands are called "left" and "right" here. If we wish to
# implement an Operator that takes a greater or lesser number of arguments, a
# structural change or additional Operator superclass will likely be needed.
OperatorError
=
Class
.
new
(
Expression
::
ExpressionError
)
OperatorError
=
Class
.
new
(
Expression
::
ExpressionError
)
def
initialize
(
left
,
right
)
raise
OperatorError
,
'Invalid left operand'
unless
left
.
respond_to?
:evaluate
raise
OperatorError
,
'Invalid right operand'
unless
right
.
respond_to?
:evaluate
@left
=
left
@right
=
right
end
def
self
.
type
def
self
.
type
:operat
or
raise
NotImplementedErr
or
end
end
def
self
.
precedence
def
self
.
precedence
...
...
lib/gitlab/ci/pipeline/expression/lexeme/or.rb
View file @
7f33b584
...
@@ -5,7 +5,7 @@ module Gitlab
...
@@ -5,7 +5,7 @@ module Gitlab
module
Pipeline
module
Pipeline
module
Expression
module
Expression
module
Lexeme
module
Lexeme
class
Or
<
Lexeme
::
Operator
class
Or
<
Lexeme
::
Logical
Operator
PATTERN
=
/\|\|/
.
freeze
PATTERN
=
/\|\|/
.
freeze
def
evaluate
(
variables
=
{})
def
evaluate
(
variables
=
{})
...
...
lib/gitlab/ci/pipeline/expression/lexeme/parenthesis_close.rb
0 → 100644
View file @
7f33b584
# frozen_string_literal: true
module
Gitlab
module
Ci
module
Pipeline
module
Expression
module
Lexeme
class
ParenthesisClose
<
Lexeme
::
Operator
PATTERN
=
/\)/
.
freeze
def
self
.
type
:parenthesis_close
end
def
self
.
precedence
900
end
end
end
end
end
end
end
lib/gitlab/ci/pipeline/expression/lexeme/parenthesis_open.rb
0 → 100644
View file @
7f33b584
# frozen_string_literal: true
module
Gitlab
module
Ci
module
Pipeline
module
Expression
module
Lexeme
class
ParenthesisOpen
<
Lexeme
::
Operator
PATTERN
=
/\(/
.
freeze
def
self
.
type
:parenthesis_open
end
def
self
.
precedence
# Needs to be higher than `ParenthesisClose` and all other Lexemes
901
end
end
end
end
end
end
end
lib/gitlab/ci/pipeline/expression/lexeme/pattern.rb
View file @
7f33b584
...
@@ -11,7 +11,7 @@ module Gitlab
...
@@ -11,7 +11,7 @@ module Gitlab
PATTERN
=
%r{^
\/
([^
\/
]|
\\
/)+[^
\\
]
\/
[ismU]*}
.
freeze
PATTERN
=
%r{^
\/
([^
\/
]|
\\
/)+[^
\\
]
\/
[ismU]*}
.
freeze
def
initialize
(
regexp
)
def
initialize
(
regexp
)
@value
=
regexp
.
gsub
(
/\\\//
,
'/'
)
super
(
regexp
.
gsub
(
/\\\//
,
'/'
)
)
unless
Gitlab
::
UntrustedRegexp
::
RubySyntax
.
valid?
(
@value
)
unless
Gitlab
::
UntrustedRegexp
::
RubySyntax
.
valid?
(
@value
)
raise
Lexer
::
SyntaxError
,
'Invalid regular expression!'
raise
Lexer
::
SyntaxError
,
'Invalid regular expression!'
...
@@ -24,6 +24,10 @@ module Gitlab
...
@@ -24,6 +24,10 @@ module Gitlab
raise
Expression
::
RuntimeError
,
'Invalid regular expression!'
raise
Expression
::
RuntimeError
,
'Invalid regular expression!'
end
end
def
inspect
"/
#{
value
}
/"
end
def
self
.
pattern
def
self
.
pattern
PATTERN
PATTERN
end
end
...
...
lib/gitlab/ci/pipeline/expression/lexeme/string.rb
View file @
7f33b584
...
@@ -9,13 +9,17 @@ module Gitlab
...
@@ -9,13 +9,17 @@ module Gitlab
PATTERN
=
/("(?<string>.*?)")|('(?<string>.*?)')/
.
freeze
PATTERN
=
/("(?<string>.*?)")|('(?<string>.*?)')/
.
freeze
def
initialize
(
value
)
def
initialize
(
value
)
@value
=
value
super
(
value
)
end
end
def
evaluate
(
variables
=
{})
def
evaluate
(
variables
=
{})
@value
.
to_s
@value
.
to_s
end
end
def
inspect
@value
.
inspect
end
def
self
.
build
(
string
)
def
self
.
build
(
string
)
new
(
string
.
match
(
PATTERN
)[
:string
])
new
(
string
.
match
(
PATTERN
)[
:string
])
end
end
...
...
lib/gitlab/ci/pipeline/expression/lexeme/value.rb
View file @
7f33b584
...
@@ -9,6 +9,10 @@ module Gitlab
...
@@ -9,6 +9,10 @@ module Gitlab
def
self
.
type
def
self
.
type
:value
:value
end
end
def
initialize
(
value
)
@value
=
value
end
end
end
end
end
end
end
...
...
lib/gitlab/ci/pipeline/expression/lexeme/variable.rb
View file @
7f33b584
...
@@ -8,12 +8,12 @@ module Gitlab
...
@@ -8,12 +8,12 @@ module Gitlab
class
Variable
<
Lexeme
::
Value
class
Variable
<
Lexeme
::
Value
PATTERN
=
/\$(?<name>\w+)/
.
freeze
PATTERN
=
/\$(?<name>\w+)/
.
freeze
def
initialize
(
name
)
def
evaluate
(
variables
=
{}
)
@name
=
name
variables
.
with_indifferent_access
.
fetch
(
@value
,
nil
)
end
end
def
evaluate
(
variables
=
{})
def
inspect
variables
.
with_indifferent_access
.
fetch
(
@name
,
nil
)
"$
#{
@value
}
"
end
end
def
self
.
build
(
string
)
def
self
.
build
(
string
)
...
...
lib/gitlab/ci/pipeline/expression/lexer.rb
View file @
7f33b584
...
@@ -10,6 +10,8 @@ module Gitlab
...
@@ -10,6 +10,8 @@ module Gitlab
SyntaxError
=
Class
.
new
(
Expression
::
ExpressionError
)
SyntaxError
=
Class
.
new
(
Expression
::
ExpressionError
)
LEXEMES
=
[
LEXEMES
=
[
Expression
::
Lexeme
::
ParenthesisOpen
,
Expression
::
Lexeme
::
ParenthesisClose
,
Expression
::
Lexeme
::
Variable
,
Expression
::
Lexeme
::
Variable
,
Expression
::
Lexeme
::
String
,
Expression
::
Lexeme
::
String
,
Expression
::
Lexeme
::
Pattern
,
Expression
::
Lexeme
::
Pattern
,
...
@@ -22,6 +24,28 @@ module Gitlab
...
@@ -22,6 +24,28 @@ module Gitlab
Expression
::
Lexeme
::
Or
Expression
::
Lexeme
::
Or
].
freeze
].
freeze
# To be removed with `ci_if_parenthesis_enabled`
LEGACY_LEXEMES
=
[
Expression
::
Lexeme
::
Variable
,
Expression
::
Lexeme
::
String
,
Expression
::
Lexeme
::
Pattern
,
Expression
::
Lexeme
::
Null
,
Expression
::
Lexeme
::
Equals
,
Expression
::
Lexeme
::
Matches
,
Expression
::
Lexeme
::
NotEquals
,
Expression
::
Lexeme
::
NotMatches
,
Expression
::
Lexeme
::
And
,
Expression
::
Lexeme
::
Or
].
freeze
def
self
.
lexemes
if
::
Gitlab
::
Ci
::
Features
.
ci_if_parenthesis_enabled?
LEXEMES
else
LEGACY_LEXEMES
end
end
MAX_TOKENS
=
100
MAX_TOKENS
=
100
def
initialize
(
statement
,
max_tokens:
MAX_TOKENS
)
def
initialize
(
statement
,
max_tokens:
MAX_TOKENS
)
...
@@ -47,7 +71,7 @@ module Gitlab
...
@@ -47,7 +71,7 @@ module Gitlab
return
tokens
if
@scanner
.
eos?
return
tokens
if
@scanner
.
eos?
lexeme
=
LEXEMES
.
find
do
|
type
|
lexeme
=
self
.
class
.
lexemes
.
find
do
|
type
|
type
.
scan
(
@scanner
).
tap
do
|
token
|
type
.
scan
(
@scanner
).
tap
do
|
token
|
tokens
.
push
(
token
)
if
token
.
present?
tokens
.
push
(
token
)
if
token
.
present?
end
end
...
...
lib/gitlab/ci/pipeline/expression/parser.rb
View file @
7f33b584
...
@@ -15,11 +15,18 @@ module Gitlab
...
@@ -15,11 +15,18 @@ module Gitlab
def
tree
def
tree
results
=
[]
results
=
[]
tokens_rpn
.
each
do
|
token
|
tokens
=
if
::
Gitlab
::
Ci
::
Features
.
ci_if_parenthesis_enabled?
tokens_rpn
else
legacy_tokens_rpn
end
tokens
.
each
do
|
token
|
case
token
.
type
case
token
.
type
when
:value
when
:value
results
.
push
(
token
.
build
)
results
.
push
(
token
.
build
)
when
:operator
when
:
logical_
operator
right_operand
=
results
.
pop
right_operand
=
results
.
pop
left_operand
=
results
.
pop
left_operand
=
results
.
pop
...
@@ -27,7 +34,7 @@ module Gitlab
...
@@ -27,7 +34,7 @@ module Gitlab
results
.
push
(
res
)
results
.
push
(
res
)
end
end
else
else
raise
ParseError
,
'Unprocessable token found in parse tree'
raise
ParseError
,
"Unprocessable token found in parse tree:
#{
token
.
type
}
"
end
end
end
end
...
@@ -45,6 +52,7 @@ module Gitlab
...
@@ -45,6 +52,7 @@ module Gitlab
# Parse the expression into Reverse Polish Notation
# Parse the expression into Reverse Polish Notation
# (See: Shunting-yard algorithm)
# (See: Shunting-yard algorithm)
# Taken from: https://en.wikipedia.org/wiki/Shunting-yard_algorithm#The_algorithm_in_detail
def
tokens_rpn
def
tokens_rpn
output
=
[]
output
=
[]
operators
=
[]
operators
=
[]
...
@@ -53,7 +61,34 @@ module Gitlab
...
@@ -53,7 +61,34 @@ module Gitlab
case
token
.
type
case
token
.
type
when
:value
when
:value
output
.
push
(
token
)
output
.
push
(
token
)
when
:operator
when
:logical_operator
output
.
push
(
operators
.
pop
)
while
token
.
lexeme
.
consume?
(
operators
.
last
&
.
lexeme
)
operators
.
push
(
token
)
when
:parenthesis_open
operators
.
push
(
token
)
when
:parenthesis_close
output
.
push
(
operators
.
pop
)
while
token
.
lexeme
.
consume?
(
operators
.
last
&
.
lexeme
)
raise
ParseError
,
'Unmatched parenthesis'
unless
operators
.
last
operators
.
pop
if
operators
.
last
.
lexeme
.
type
==
:parenthesis_open
end
end
output
.
concat
(
operators
.
reverse
)
end
# To be removed with `ci_if_parenthesis_enabled`
def
legacy_tokens_rpn
output
=
[]
operators
=
[]
@tokens
.
each
do
|
token
|
case
token
.
type
when
:value
output
.
push
(
token
)
when
:logical_operator
if
operators
.
any?
&&
token
.
lexeme
.
precedence
>=
operators
.
last
.
lexeme
.
precedence
if
operators
.
any?
&&
token
.
lexeme
.
precedence
>=
operators
.
last
.
lexeme
.
precedence
output
.
push
(
operators
.
pop
)
output
.
push
(
operators
.
pop
)
end
end
...
...
spec/lib/gitlab/ci/pipeline/expression/lexeme/and_spec.rb
View file @
7f33b584
...
@@ -24,7 +24,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::And do
...
@@ -24,7 +24,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::And do
describe
'.type'
do
describe
'.type'
do
it
'is an operator'
do
it
'is an operator'
do
expect
(
described_class
.
type
).
to
eq
:operator
expect
(
described_class
.
type
).
to
eq
:
logical_
operator
end
end
end
end
...
...
spec/lib/gitlab/ci/pipeline/expression/lexeme/equals_spec.rb
View file @
7f33b584
...
@@ -27,7 +27,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::Equals do
...
@@ -27,7 +27,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::Equals do
describe
'.type'
do
describe
'.type'
do
it
'is an operator'
do
it
'is an operator'
do
expect
(
described_class
.
type
).
to
eq
:operator
expect
(
described_class
.
type
).
to
eq
:
logical_
operator
end
end
end
end
...
...
spec/lib/gitlab/ci/pipeline/expression/lexeme/matches_spec.rb
View file @
7f33b584
...
@@ -28,7 +28,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::Matches do
...
@@ -28,7 +28,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::Matches do
describe
'.type'
do
describe
'.type'
do
it
'is an operator'
do
it
'is an operator'
do
expect
(
described_class
.
type
).
to
eq
:operator
expect
(
described_class
.
type
).
to
eq
:
logical_
operator
end
end
end
end
...
...
spec/lib/gitlab/ci/pipeline/expression/lexeme/not_equals_spec.rb
View file @
7f33b584
...
@@ -27,7 +27,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::NotEquals do
...
@@ -27,7 +27,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::NotEquals do
describe
'.type'
do
describe
'.type'
do
it
'is an operator'
do
it
'is an operator'
do
expect
(
described_class
.
type
).
to
eq
:operator
expect
(
described_class
.
type
).
to
eq
:
logical_
operator
end
end
end
end
...
...
spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb
View file @
7f33b584
...
@@ -28,7 +28,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::NotMatches do
...
@@ -28,7 +28,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::NotMatches do
describe
'.type'
do
describe
'.type'
do
it
'is an operator'
do
it
'is an operator'
do
expect
(
described_class
.
type
).
to
eq
:operator
expect
(
described_class
.
type
).
to
eq
:
logical_
operator
end
end
end
end
...
...
spec/lib/gitlab/ci/pipeline/expression/lexeme/or_spec.rb
View file @
7f33b584
...
@@ -24,7 +24,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::Or do
...
@@ -24,7 +24,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexeme::Or do
describe
'.type'
do
describe
'.type'
do
it
'is an operator'
do
it
'is an operator'
do
expect
(
described_class
.
type
).
to
eq
:operator
expect
(
described_class
.
type
).
to
eq
:
logical_
operator
end
end
end
end
...
...
spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb
View file @
7f33b584
...
@@ -81,6 +81,35 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexer do
...
@@ -81,6 +81,35 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Lexer do
with_them
do
with_them
do
it
{
is_expected
.
to
eq
(
tokens
)
}
it
{
is_expected
.
to
eq
(
tokens
)
}
end
end
context
'with parentheses are used'
do
where
(
:expression
,
:tokens
)
do
'($PRESENT_VARIABLE =~ /my var/) && $EMPTY_VARIABLE =~ /nope/'
|
[
'('
,
'$PRESENT_VARIABLE'
,
'=~'
,
'/my var/'
,
')'
,
'&&'
,
'$EMPTY_VARIABLE'
,
'=~'
,
'/nope/'
]
'$PRESENT_VARIABLE =~ /my var/ || ($EMPTY_VARIABLE =~ /nope/)'
|
[
'$PRESENT_VARIABLE'
,
'=~'
,
'/my var/'
,
'||'
,
'('
,
'$EMPTY_VARIABLE'
,
'=~'
,
'/nope/'
,
')'
]
'($PRESENT_VARIABLE && (null || $EMPTY_VARIABLE == ""))'
|
[
'('
,
'$PRESENT_VARIABLE'
,
'&&'
,
'('
,
'null'
,
'||'
,
'$EMPTY_VARIABLE'
,
'=='
,
'""'
,
')'
,
')'
]
end
with_them
do
context
'when ci_if_parenthesis_enabled is enabled'
do
before
do
stub_feature_flags
(
ci_if_parenthesis_enabled:
true
)
end
it
{
is_expected
.
to
eq
(
tokens
)
}
end
context
'when ci_if_parenthesis_enabled is disabled'
do
before
do
stub_feature_flags
(
ci_if_parenthesis_enabled:
false
)
end
it
do
expect
{
subject
}
.
to
raise_error
described_class
::
SyntaxError
end
end
end
end
end
end
end
end
...
...
spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb
View file @
7f33b584
# frozen_string_literal: true
# frozen_string_literal: true
require
'
fast_
spec_helper'
require
'spec_helper'
RSpec
.
describe
Gitlab
::
Ci
::
Pipeline
::
Expression
::
Parser
do
RSpec
.
describe
Gitlab
::
Ci
::
Pipeline
::
Expression
::
Parser
do
before
do
stub_feature_flags
(
ci_if_parenthesis_enabled:
true
)
end
describe
'#tree'
do
describe
'#tree'
do
context
'when using two operators'
do
context
'validates simple operators'
do
it
'returns a reverse descent parse tree'
do
using
RSpec
::
Parameterized
::
TableSyntax
expect
(
described_class
.
seed
(
'$VAR1 == "123"'
).
tree
)
.
to
be_a
Gitlab
::
Ci
::
Pipeline
::
Expression
::
Lexeme
::
Equals
where
(
:expression
,
:result_tree
)
do
'$VAR1 == "123"'
|
'equals($VAR1, "123")'
'$VAR1 == "123" == $VAR2'
|
'equals(equals($VAR1, "123"), $VAR2)'
'$VAR'
|
'$VAR'
'"some value"'
|
'"some value"'
'null'
|
'null'
'$VAR1 || $VAR2 && $VAR3'
|
'or($VAR1, and($VAR2, $VAR3))'
'$VAR1 && $VAR2 || $VAR3'
|
'or(and($VAR1, $VAR2), $VAR3)'
'$VAR1 && $VAR2 || $VAR3 && $VAR4'
|
'or(and($VAR1, $VAR2), and($VAR3, $VAR4))'
'$VAR1 && ($VAR2 || $VAR3) && $VAR4'
|
'and(and($VAR1, or($VAR2, $VAR3)), $VAR4)'
end
with_them
do
it
{
expect
(
described_class
.
seed
(
expression
).
tree
.
inspect
).
to
eq
(
result_tree
)
}
end
end
end
end
context
'when using three operators'
do
context
'when combining && and OR operators'
do
it
'returns a reverse descent parse tree'
do
subject
{
described_class
.
seed
(
'$VAR1 == "a" || $VAR2 == "b" && $VAR3 == "c" || $VAR4 == "d" && $VAR5 == "e"'
).
tree
}
expect
(
described_class
.
seed
(
'$VAR1 == "123" == $VAR2'
).
tree
)
.
to
be_a
Gitlab
::
Ci
::
Pipeline
::
Expression
::
Lexeme
::
Equals
context
'when parenthesis engine is enabled'
do
before
do
stub_feature_flags
(
ci_if_parenthesis_enabled:
true
)
end
it
'returns operations in a correct order'
do
expect
(
subject
.
inspect
)
.
to
eq
(
'or(or(equals($VAR1, "a"), and(equals($VAR2, "b"), equals($VAR3, "c"))), and(equals($VAR4, "d"), equals($VAR5, "e")))'
)
end
end
end
end
context
'when using a single variable token'
do
context
'when parenthesis engine is disabled (legacy)'
do
it
'returns a single token instance'
do
before
do
expect
(
described_class
.
seed
(
'$VAR'
).
tree
)
stub_feature_flags
(
ci_if_parenthesis_enabled:
false
)
.
to
be_a
Gitlab
::
Ci
::
Pipeline
::
Expression
::
Lexeme
::
Variable
end
end
it
'returns operations in a invalid order'
do
expect
(
subject
.
inspect
)
.
to
eq
(
'or(equals($VAR1, "a"), and(equals($VAR2, "b"), or(equals($VAR3, "c"), and(equals($VAR4, "d"), equals($VAR5, "e")))))'
)
end
end
end
context
'when using parenthesis'
do
subject
{
described_class
.
seed
(
'(($VAR1 == "a" || $VAR2 == "b") && $VAR3 == "c" || $VAR4 == "d") && $VAR5 == "e"'
).
tree
}
before
do
stub_feature_flags
(
ci_if_parenthesis_enabled:
true
)
end
end
context
'when using a single string token'
do
it
'returns operations in a correct order'
do
it
'returns a single token instance'
do
expect
(
subject
.
inspect
)
expect
(
described_class
.
seed
(
'"some value"'
).
tree
)
.
to
eq
(
'and(or(and(or(equals($VAR1, "a"), equals($VAR2, "b")), equals($VAR3, "c")), equals($VAR4, "d")), equals($VAR5, "e"))'
)
.
to
be_a
Gitlab
::
Ci
::
Pipeline
::
Expression
::
Lexeme
::
String
end
end
end
end
context
'when expression is empty'
do
context
'when expression is empty'
do
it
'r
eturns a null token
'
do
it
'r
aises a parsing error
'
do
expect
{
described_class
.
seed
(
''
).
tree
}
expect
{
described_class
.
seed
(
''
).
tree
}
.
to
raise_error
Gitlab
::
Ci
::
Pipeline
::
Expression
::
Parser
::
ParseError
.
to
raise_error
Gitlab
::
Ci
::
Pipeline
::
Expression
::
Parser
::
ParseError
end
end
end
end
context
'when expression is null'
do
it
'returns a null token'
do
expect
(
described_class
.
seed
(
'null'
).
tree
)
.
to
be_a
Gitlab
::
Ci
::
Pipeline
::
Expression
::
Lexeme
::
Null
end
end
context
'when two value tokens have no operator'
do
context
'when two value tokens have no operator'
do
it
'raises a parsing error'
do
it
'raises a parsing error'
do
expect
{
described_class
.
seed
(
'$VAR "text"'
).
tree
}
expect
{
described_class
.
seed
(
'$VAR "text"'
).
tree
}
...
@@ -66,5 +94,42 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Parser do
...
@@ -66,5 +94,42 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Parser do
.
to
raise_error
Gitlab
::
Ci
::
Pipeline
::
Expression
::
Lexeme
::
Operator
::
OperatorError
.
to
raise_error
Gitlab
::
Ci
::
Pipeline
::
Expression
::
Lexeme
::
Operator
::
OperatorError
end
end
end
end
context
'when parenthesis are unmatched'
do
context
'when parenthesis engine is enabled'
do
before
do
stub_feature_flags
(
ci_if_parenthesis_enabled:
true
)
end
where
(
:expression
)
do
[
'$VAR == ('
,
'$VAR2 == ("aa"'
,
'$VAR2 == ("aa"))'
,
'$VAR2 == "aa")'
,
'(($VAR2 == "aa")'
,
'($VAR2 == "aa"))'
]
end
with_them
do
it
'raises a ParseError'
do
expect
{
described_class
.
seed
(
expression
).
tree
}
.
to
raise_error
Gitlab
::
Ci
::
Pipeline
::
Expression
::
Parser
::
ParseError
end
end
end
context
'when parenthesis engine is disabled'
do
before
do
stub_feature_flags
(
ci_if_parenthesis_enabled:
false
)
end
it
'raises an SyntaxError'
do
expect
{
described_class
.
seed
(
'$VAR == ('
).
tree
}
.
to
raise_error
Gitlab
::
Ci
::
Pipeline
::
Expression
::
Lexer
::
SyntaxError
end
end
end
end
end
end
end
spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb
View file @
7f33b584
# frozen_string_literal: true
# frozen_string_literal: true
require
'fast_spec_helper'
require
'spec_helper'
require
'rspec-parameterized'
RSpec
.
describe
Gitlab
::
Ci
::
Pipeline
::
Expression
::
Statement
do
RSpec
.
describe
Gitlab
::
Ci
::
Pipeline
::
Expression
::
Statement
do
subject
do
subject
do
...
@@ -109,6 +108,17 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Statement do
...
@@ -109,6 +108,17 @@ RSpec.describe Gitlab::Ci::Pipeline::Expression::Statement do
'$UNDEFINED_VARIABLE || $PRESENT_VARIABLE'
|
'my variable'
'$UNDEFINED_VARIABLE || $PRESENT_VARIABLE'
|
'my variable'
'$UNDEFINED_VARIABLE == null || $PRESENT_VARIABLE'
|
true
'$UNDEFINED_VARIABLE == null || $PRESENT_VARIABLE'
|
true
'$PRESENT_VARIABLE || $UNDEFINED_VARIABLE == null'
|
'my variable'
'$PRESENT_VARIABLE || $UNDEFINED_VARIABLE == null'
|
'my variable'
'($PRESENT_VARIABLE)'
|
'my variable'
'(($PRESENT_VARIABLE))'
|
'my variable'
'(($PRESENT_VARIABLE && null) || $EMPTY_VARIABLE == "")'
|
true
'($PRESENT_VARIABLE) && (null || $EMPTY_VARIABLE == "")'
|
true
'("string" || "test") == "string"'
|
true
'(null || ("test" == "string"))'
|
false
'("string" == ("test" && "string"))'
|
true
'("string" == ("test" || "string"))'
|
false
'("string" == "test" || "string")'
|
"string"
'("string" == ("string" || (("1" == "1") && ("2" == "3"))))'
|
true
end
end
with_them
do
with_them
do
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment