From 17772f74558e14ea4ea439e2de73257c0057ce4e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?R=C3=A9my=20Coutable?= <remy@rymai.me>
Date: Fri, 3 May 2024 11:53:34 +0200
Subject: [PATCH] Improve architecture of danger/feature_flag/
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Rémy Coutable <remy@rymai.me>
---
 danger/feature_flag/Dangerfile           | 101 ++++++++++++-----------
 lib/feature/shared.rb                    |   4 +
 spec/lib/feature/shared_spec.rb          |  17 ++++
 spec/tooling/danger/feature_flag_spec.rb |   6 +-
 tooling/danger/feature_flag.rb           |   4 +-
 5 files changed, 80 insertions(+), 52 deletions(-)
 create mode 100644 spec/lib/feature/shared_spec.rb

diff --git a/danger/feature_flag/Dangerfile b/danger/feature_flag/Dangerfile
index 816b0ad201fd6..307c7d23b68c1 100644
--- a/danger/feature_flag/Dangerfile
+++ b/danger/feature_flag/Dangerfile
@@ -27,32 +27,23 @@ module Tooling
     For guidance on when to use a feature flag, please see the [documentation](https://about.gitlab.com/handbook/product-development-flow/feature-flag-lifecycle/#when-to-use-feature-flags).
     WARNING_MESSAGE
 
-    def initialize(context:, feature_flag_helper:, helper:)
+    def initialize(context:, added_files:, modified_files:, helper:)
       @context = context
-      @feature_flag_helper = feature_flag_helper
+      @added_files = added_files
+      @modified_files = modified_files
       @helper = helper
     end
 
-    def check_feature_flag_yaml(feature_flag)
-      unless feature_flag.valid?
-        context.failure("#{helper.html_link(feature_flag.path)} isn't valid YAML! #{SEE_DOC}")
-        return
-      end
-
-      check_group(feature_flag)
-      check_feature_issue_url(feature_flag)
-      # Note: we don't check introduced_by_url as it's already done by danger/config_files/Dangerfile
-      check_rollout_issue_url(feature_flag)
-      check_milestone(feature_flag)
-      check_default_enabled(feature_flag)
-    end
-
     def check_touched_feature_flag_files
       touched_feature_flag_files.each do |feature_flag|
         check_feature_flag_yaml(feature_flag)
       end
     end
 
+    def feature_flag_file_touched?
+      touched_feature_flag_files.any?
+    end
+
     def mr_has_backend_or_frontend_changes?
       changes = helper.changes_by_category
       changes.has_key?(:backend) || changes.has_key?(:frontend)
@@ -68,22 +59,24 @@ module Tooling
 
     private
 
-    attr_reader :context, :feature_flag_helper, :helper
+    attr_reader :context, :added_files, :modified_files, :helper
 
-    def touched_feature_flag_files
-      added_feature_flag_files + modified_feature_flag_files
-    end
-
-    def added_feature_flag_files
-      feature_flag_helper.feature_flag_files(change_type: :added)
-    end
+    def check_feature_flag_yaml(feature_flag)
+      unless feature_flag.valid?
+        context.failure("#{helper.html_link(feature_flag.path)} isn't valid YAML! #{SEE_DOC}")
+        return
+      end
 
-    def modified_feature_flag_files
-      feature_flag_helper.feature_flag_files(change_type: :modified)
+      check_group(feature_flag)
+      check_feature_issue_url(feature_flag)
+      # Note: we don't check introduced_by_url as it's already done by danger/config_files/Dangerfile
+      check_rollout_issue_url(feature_flag)
+      check_milestone(feature_flag)
+      check_default_enabled(feature_flag)
     end
 
-    def feature_flag_file_touched?
-      added_feature_flag_files.any? || modified_feature_flag_files.any?
+    def touched_feature_flag_files
+      added_files + modified_files
     end
 
     def check_group(feature_flag)
@@ -102,13 +95,13 @@ module Tooling
         return
       end
 
-      mr_line = feature_flag.find_line_index("group:")
-
-      if mr_line
-        context.failure(format(SUGGEST_MR_COMMENT, group: mr_group_label), file: feature_flag.path, line: mr_line.succ)
-      else
-        context.failure(%(Please add `group: "#{mr_group_label}"` in #{helper.html_link(feature_flag.path)}. #{SEE_DOC}))
-      end
+      add_message_on_line(
+        feature_flag: feature_flag,
+        needle: "group:",
+        note: format(SUGGEST_MR_COMMENT, group: mr_group_label),
+        fallback_note: %(Please add `group: "#{mr_group_label}"` in #{helper.html_link(feature_flag.path)}. #{SEE_DOC}),
+        message_method: :failure
+      )
     end
 
     def message_for_feature_flag_with_group!(feature_flag:, mr_group_label:)
@@ -121,27 +114,36 @@ module Tooling
         `group` is set to ~"#{feature_flag.group}" in #{helper.html_link(feature_flag.path)},
         which does not match ~"#{mr_group_label}" set on the MR!
         FAILURE_MESSAGE
-        mr_line = feature_flag.find_line_index("group:")
 
-        if mr_line
-          context.failure(note, file: feature_flag.path, line: mr_line.succ)
-        else
-          context.failure(note)
-        end
+        add_message_on_line(
+          feature_flag: feature_flag,
+          needle: "group:",
+          note: note,
+          message_method: :failure
+        )
       end
     end
 
     def check_feature_issue_url(feature_flag)
       return unless feature_flag.missing_feature_issue_url?
 
-      note = "Consider filling `feature_issue_url:`"
-      mr_line = feature_flag.find_line_index("feature_issue_url:")
+      add_message_on_line(
+        feature_flag: feature_flag,
+        needle: "feature_issue_url:",
+        note: "Consider filling `feature_issue_url:`"
+      )
+    end
+
+    def add_message_on_line(feature_flag:, needle:, note:, fallback_note: note, message_method: :message)
+      mr_line = feature_flag.find_line_index(needle)
 
+      # rubocop:disable GitlabSecurity/PublicSend -- we allow calling context.message, context.warning & context.failure
       if mr_line
-        context.message(note, file: feature_flag.path, line: mr_line.succ)
+        context.public_send(message_method, note, file: feature_flag.path, line: mr_line.succ)
       else
-        context.message(note)
+        context.public_send(message_method, fallback_note)
       end
+      # rubocop:enable GitlabSecurity/PublicSend
     end
 
     def check_rollout_issue_url(feature_flag)
@@ -160,7 +162,7 @@ module Tooling
     def check_default_enabled(feature_flag)
       return unless feature_flag.default_enabled?
 
-      if ::Feature::Shared::TYPES.dig(feature_flag.type.to_sym, :can_be_default_enabled)
+      if ::Feature::Shared.can_be_default_enabled?(feature_flag.type)
         note = <<~SUGGEST_COMMENT
           You're about to [release the feature with the feature flag](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/issue_templates/Feature%20Flag%20Roll%20Out.md#optional-release-the-feature-with-the-feature-flag).
           This process can only be done **after** the [global rollout on production](https://gitlab.com/gitlab-org/gitlab/-/blob/master/.gitlab/issue_templates/Feature%20Flag%20Roll%20Out.md#global-rollout-on-production).
@@ -192,7 +194,12 @@ module Tooling
   end
 end
 
-feature_flag_dangerfile = Tooling::FeatureFlagDangerfile.new(context: self, feature_flag_helper: feature_flag, helper: helper)
+feature_flag_dangerfile = Tooling::FeatureFlagDangerfile.new(
+  context: self,
+  added_files: feature_flag.feature_flag_files(danger_helper: helper, change_type: :added),
+  modified_files: feature_flag.feature_flag_files(danger_helper: helper, change_type: :modified),
+  helper: helper
+)
 
 feature_flag_dangerfile.check_touched_feature_flag_files
 
diff --git a/lib/feature/shared.rb b/lib/feature/shared.rb
index 87cb4de9b0e60..cc711874b3f72 100644
--- a/lib/feature/shared.rb
+++ b/lib/feature/shared.rb
@@ -99,5 +99,9 @@ module Shared
       group
       default_enabled
     ].freeze
+
+    def self.can_be_default_enabled?(feature_flag_type)
+      TYPES.dig(feature_flag_type.to_sym, :can_be_default_enabled)
+    end
   end
 end
diff --git a/spec/lib/feature/shared_spec.rb b/spec/lib/feature/shared_spec.rb
new file mode 100644
index 0000000000000..cd9616c67c4ca
--- /dev/null
+++ b/spec/lib/feature/shared_spec.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Feature::Shared, feature_category: :tooling do
+  describe '.can_be_default_enabled?' do
+    subject { described_class.can_be_default_enabled?(type) }
+
+    described_class::TYPES.each do |type, data|
+      context "when type is #{type}" do
+        let(:type) { type }
+
+        it { is_expected.to eq(data[:can_be_default_enabled]) }
+      end
+    end
+  end
+end
diff --git a/spec/tooling/danger/feature_flag_spec.rb b/spec/tooling/danger/feature_flag_spec.rb
index 0124414cd9520..7f3466a750652 100644
--- a/spec/tooling/danger/feature_flag_spec.rb
+++ b/spec/tooling/danger/feature_flag_spec.rb
@@ -33,14 +33,14 @@
 
     shared_examples 'an array of Found objects' do |change_type|
       it 'returns an array of Found objects' do
-        expect(feature_flag.feature_flag_files(change_type: change_type)).to contain_exactly(an_instance_of(described_class::Found), an_instance_of(described_class::Found))
-        expect(feature_flag.feature_flag_files(change_type: change_type).map(&:path)).to eq(feature_flag_files)
+        expect(feature_flag.feature_flag_files(danger_helper: fake_helper, change_type: change_type)).to contain_exactly(an_instance_of(described_class::Found), an_instance_of(described_class::Found))
+        expect(feature_flag.feature_flag_files(danger_helper: fake_helper, change_type: change_type).map(&:path)).to eq(feature_flag_files)
       end
     end
 
     shared_examples 'an empty array' do |change_type|
       it 'returns an array of Found objects' do
-        expect(feature_flag.feature_flag_files(change_type: change_type)).to be_empty
+        expect(feature_flag.feature_flag_files(danger_helper: fake_helper, change_type: change_type)).to be_empty
       end
     end
 
diff --git a/tooling/danger/feature_flag.rb b/tooling/danger/feature_flag.rb
index af249ff237dc0..2540cfc154022 100644
--- a/tooling/danger/feature_flag.rb
+++ b/tooling/danger/feature_flag.rb
@@ -10,8 +10,8 @@ module FeatureFlag
       #   - :added
       #   - :modified
       #   - :deleted
-      def feature_flag_files(change_type:)
-        files = helper.public_send("#{change_type}_files") # rubocop:disable GitlabSecurity/PublicSend
+      def feature_flag_files(danger_helper:, change_type:)
+        files = danger_helper.public_send("#{change_type}_files") # rubocop:disable GitlabSecurity/PublicSend -- we allow calling danger_helper.added_files & danger_helper.modified_files
 
         files.select { |path| path =~ %r{\A(ee/)?config/feature_flags/} }.map { |path| Found.build(path) }
       end
-- 
GitLab