From 8fa1ab4c334c2f09474364d2f3108633a55f79aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20Trzci=C5=84ski?= Date: Fri, 19 Apr 2019 13:17:42 +0200 Subject: [PATCH] Support negative matches This adds support for != and !~ operators giving more flexibility in comparing values --- .../unreleased/support-negative-matches.yml | 5 ++ doc/ci/variables/README.md | 15 +++- .../pipeline/expression/lexeme/not_equals.rb | 28 +++++++ .../pipeline/expression/lexeme/not_matches.rb | 31 +++++++ lib/gitlab/ci/pipeline/expression/lexer.rb | 4 +- .../ci/pipeline/expression/statement.rb | 13 ++- .../expression/lexeme/not_equals_spec.rb | 39 +++++++++ .../expression/lexeme/not_matches_spec.rb | 80 +++++++++++++++++++ .../ci/pipeline/expression/statement_spec.rb | 12 +++ 9 files changed, 221 insertions(+), 6 deletions(-) create mode 100644 changelogs/unreleased/support-negative-matches.yml create mode 100644 lib/gitlab/ci/pipeline/expression/lexeme/not_equals.rb create mode 100644 lib/gitlab/ci/pipeline/expression/lexeme/not_matches.rb create mode 100644 spec/lib/gitlab/ci/pipeline/expression/lexeme/not_equals_spec.rb create mode 100644 spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb diff --git a/changelogs/unreleased/support-negative-matches.yml b/changelogs/unreleased/support-negative-matches.yml new file mode 100644 index 00000000000..8d3f2d3cbae --- /dev/null +++ b/changelogs/unreleased/support-negative-matches.yml @@ -0,0 +1,5 @@ +--- +title: Support negative matches +merge_request: +author: +type: added diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md index 830f015a108..9983b015b31 100644 --- a/doc/ci/variables/README.md +++ b/doc/ci/variables/README.md @@ -434,8 +434,9 @@ Below you can find supported syntax reference: 1. Equality matching using a string > Example: `$VARIABLE == "some value"` + > Example: `$VARIABLE != "some value"` _(added in 11.11)_ - You can use equality operator `==` to compare a variable content to a + You can use equality operator `==` or `!=` to compare a variable content to a string. We support both, double quotes and single quotes to define a string value, so both `$VARIABLE == "some value"` and `$VARIABLE == 'some value'` are supported. `"some value" == $VARIABLE` is correct too. @@ -443,22 +444,26 @@ Below you can find supported syntax reference: 1. Checking for an undefined value > Example: `$VARIABLE == null` + > Example: `$VARIABLE != null` _(added in 11.11)_ It sometimes happens that you want to check whether a variable is defined or not. To do that, you can compare a variable to `null` keyword, like `$VARIABLE == null`. This expression is going to evaluate to truth if - variable is not defined. + variable is not defined when `==` is used, or to falsey if `!=` is used. 1. Checking for an empty variable > Example: `$VARIABLE == ""` + > Example: `$VARIABLE != ""` _(added in 11.11)_ If you want to check whether a variable is defined, but is empty, you can - simply compare it against an empty string, like `$VAR == ''`. + simply compare it against an empty string, like `$VAR == ''` or non-empty + string `$VARIABLE != ""`. 1. Comparing two variables > Example: `$VARIABLE_1 == $VARIABLE_2` + > Example: `$VARIABLE_1 != $VARIABLE_2` _(added in 11.11)_ It is possible to compare two variables. This is going to compare values of these variables. @@ -477,9 +482,11 @@ Below you can find supported syntax reference: 1. Pattern matching _(added in 11.0)_ > Example: `$VARIABLE =~ /^content.*/` + > Example: `$VARIABLE_1 !~ /^content.*/` _(added in 11.11)_ It is possible perform pattern matching against a variable and regular - expression. Expression like this evaluates to truth if matches are found. + expression. Expression like this evaluates to truth if matches are found + when using `=~`. It evaluates to truth if matches are not found when `!~` is used. Pattern matching is case-sensitive by default. Use `i` flag modifier, like `/pattern/i` to make a pattern case-insensitive. diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/not_equals.rb b/lib/gitlab/ci/pipeline/expression/lexeme/not_equals.rb new file mode 100644 index 00000000000..5fcc9406cc8 --- /dev/null +++ b/lib/gitlab/ci/pipeline/expression/lexeme/not_equals.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Pipeline + module Expression + module Lexeme + class NotEquals < Lexeme::Operator + PATTERN = /!=/.freeze + + def initialize(left, right) + @left = left + @right = right + end + + def evaluate(variables = {}) + @left.evaluate(variables) != @right.evaluate(variables) + end + + def self.build(_value, behind, ahead) + new(behind, ahead) + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/not_matches.rb b/lib/gitlab/ci/pipeline/expression/lexeme/not_matches.rb new file mode 100644 index 00000000000..14544d33e25 --- /dev/null +++ b/lib/gitlab/ci/pipeline/expression/lexeme/not_matches.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module Gitlab + module Ci + module Pipeline + module Expression + module Lexeme + class NotMatches < Lexeme::Operator + PATTERN = /\!~/.freeze + + def initialize(left, right) + @left = left + @right = right + end + + def evaluate(variables = {}) + text = @left.evaluate(variables) + regexp = @right.evaluate(variables) + + regexp.scan(text.to_s).none? + end + + def self.build(_value, behind, ahead) + new(behind, ahead) + end + end + end + end + end + end +end diff --git a/lib/gitlab/ci/pipeline/expression/lexer.rb b/lib/gitlab/ci/pipeline/expression/lexer.rb index f26542361a2..e14edfae51d 100644 --- a/lib/gitlab/ci/pipeline/expression/lexer.rb +++ b/lib/gitlab/ci/pipeline/expression/lexer.rb @@ -15,7 +15,9 @@ module Gitlab Expression::Lexeme::Pattern, Expression::Lexeme::Null, Expression::Lexeme::Equals, - Expression::Lexeme::Matches + Expression::Lexeme::Matches, + Expression::Lexeme::NotEquals, + Expression::Lexeme::NotMatches ].freeze MAX_TOKENS = 100 diff --git a/lib/gitlab/ci/pipeline/expression/statement.rb b/lib/gitlab/ci/pipeline/expression/statement.rb index b03611f756e..ab5ae9caeea 100644 --- a/lib/gitlab/ci/pipeline/expression/statement.rb +++ b/lib/gitlab/ci/pipeline/expression/statement.rb @@ -8,13 +8,24 @@ module Gitlab StatementError = Class.new(Expression::ExpressionError) GRAMMAR = [ + # presence matchers %w[variable], + + # positive matchers %w[variable equals string], %w[variable equals variable], %w[variable equals null], %w[string equals variable], %w[null equals variable], - %w[variable matches pattern] + %w[variable matches pattern], + + # negative matchers + %w[variable notequals string], + %w[variable notequals variable], + %w[variable notequals null], + %w[string notequals variable], + %w[null notequals variable], + %w[variable notmatches pattern] ].freeze def initialize(statement, variables = {}) diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_equals_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_equals_spec.rb new file mode 100644 index 00000000000..9aa2f4efd67 --- /dev/null +++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_equals_spec.rb @@ -0,0 +1,39 @@ +require 'spec_helper' + +describe Gitlab::Ci::Pipeline::Expression::Lexeme::NotEquals do + let(:left) { double('left') } + let(:right) { double('right') } + + describe '.build' do + it 'creates a new instance of the token' do + expect(described_class.build('!=', left, right)) + .to be_a(described_class) + end + end + + describe '.type' do + it 'is an operator' do + expect(described_class.type).to eq :operator + end + end + + describe '#evaluate' do + it 'returns true when left and right are not equal' do + allow(left).to receive(:evaluate).and_return(1) + allow(right).to receive(:evaluate).and_return(2) + + operator = described_class.new(left, right) + + expect(operator.evaluate(VARIABLE: 3)).to eq true + end + + it 'returns false when left and right are equal' do + allow(left).to receive(:evaluate).and_return(1) + allow(right).to receive(:evaluate).and_return(1) + + operator = described_class.new(left, right) + + expect(operator.evaluate(VARIABLE: 3)).to eq false + end + end +end diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb new file mode 100644 index 00000000000..fa3b9651fb4 --- /dev/null +++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/not_matches_spec.rb @@ -0,0 +1,80 @@ +require 'fast_spec_helper' +require_dependency 're2' + +describe Gitlab::Ci::Pipeline::Expression::Lexeme::NotMatches do + let(:left) { double('left') } + let(:right) { double('right') } + + describe '.build' do + it 'creates a new instance of the token' do + expect(described_class.build('!~', left, right)) + .to be_a(described_class) + end + end + + describe '.type' do + it 'is an operator' do + expect(described_class.type).to eq :operator + end + end + + describe '#evaluate' do + it 'returns true when left and right do not match' do + allow(left).to receive(:evaluate).and_return('my-string') + allow(right).to receive(:evaluate) + .and_return(Gitlab::UntrustedRegexp.new('something')) + + operator = described_class.new(left, right) + + expect(operator.evaluate).to eq true + end + + it 'returns false when left and right match' do + allow(left).to receive(:evaluate).and_return('my-awesome-string') + allow(right).to receive(:evaluate) + .and_return(Gitlab::UntrustedRegexp.new('awesome.string$')) + + operator = described_class.new(left, right) + + expect(operator.evaluate).to eq false + end + + it 'supports matching against a nil value' do + allow(left).to receive(:evaluate).and_return(nil) + allow(right).to receive(:evaluate) + .and_return(Gitlab::UntrustedRegexp.new('pattern')) + + operator = described_class.new(left, right) + + expect(operator.evaluate).to eq true + end + + it 'supports multiline strings' do + allow(left).to receive(:evaluate).and_return <<~TEXT + My awesome contents + + My-text-string! + TEXT + + allow(right).to receive(:evaluate) + .and_return(Gitlab::UntrustedRegexp.new('text-string')) + + operator = described_class.new(left, right) + + expect(operator.evaluate).to eq false + end + + it 'supports regexp flags' do + allow(left).to receive(:evaluate).and_return <<~TEXT + My AWESOME content + TEXT + + allow(right).to receive(:evaluate) + .and_return(Gitlab::UntrustedRegexp.new('(?i)awesome')) + + operator = described_class.new(left, right) + + expect(operator.evaluate).to eq false + end + end +end diff --git a/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb index 11e73294f18..a9fd809409b 100644 --- a/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb +++ b/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb @@ -101,6 +101,18 @@ describe Gitlab::Ci::Pipeline::Expression::Statement do "$EMPTY_VARIABLE =~ /var.*/" | false "$UNDEFINED_VARIABLE =~ /var.*/" | false "$PRESENT_VARIABLE =~ /VAR.*/i" | true + '$PRESENT_VARIABLE != "my variable"' | false + '"my variable" != $PRESENT_VARIABLE' | false + '$PRESENT_VARIABLE != null' | true + '$EMPTY_VARIABLE != null' | true + '"" != $EMPTY_VARIABLE' | false + '$UNDEFINED_VARIABLE != null' | false + 'null != $UNDEFINED_VARIABLE' | false + "$PRESENT_VARIABLE !~ /var.*e$/" | false + "$PRESENT_VARIABLE !~ /^var.*/" | true + "$EMPTY_VARIABLE !~ /var.*/" | true + "$UNDEFINED_VARIABLE !~ /var.*/" | true + "$PRESENT_VARIABLE !~ /VAR.*/i" | false end with_them do