2021-06-17 09:09:53 +00:00
# frozen_string_literal: true
require_relative '../../code_reuse_helpers'
module RuboCop
module Cop
module Gitlab
# This cop tracks the usage of feature flags among the codebase.
#
# The files set in `tmp/feature_flags/*.used` can then be used for verification purpose.
#
class MarkUsedFeatureFlags < RuboCop :: Cop :: Cop
include RuboCop :: CodeReuseHelpers
FEATURE_METHODS = % i [ enabled? disabled? ] . freeze
EXPERIMENTATION_METHODS = % i [ active? ] . freeze
EXPERIMENT_METHODS = % i [
experiment
experiment_enabled?
push_frontend_experiment
] . freeze
RUGGED_METHODS = % i [
use_rugged?
] . freeze
WORKER_METHODS = % i [
data_consistency
deduplicate
] . freeze
SELF_METHODS = % i [
push_frontend_feature_flag
2022-04-08 21:09:52 +00:00
push_force_frontend_feature_flag
2021-06-17 09:09:53 +00:00
limit_feature_flag =
2021-07-31 00:10:15 +00:00
limit_feature_flag_for_override =
2021-06-17 09:09:53 +00:00
] . freeze + EXPERIMENT_METHODS + RUGGED_METHODS + WORKER_METHODS
2022-08-24 15:12:19 +00:00
RESTRICT_ON_SEND = FEATURE_METHODS + EXPERIMENTATION_METHODS + SELF_METHODS
2021-06-17 09:09:53 +00:00
USAGE_DATA_COUNTERS_EVENTS_YAML_GLOBS = [
File . expand_path ( " ../../../config/metrics/aggregates/*.yml " , __dir__ ) ,
2022-09-01 12:11:56 +00:00
File . expand_path ( " ../../../lib/gitlab/usage_data_counters/known_events/*.yml " , __dir__ ) ,
File . expand_path ( " ../../../ee/lib/ee/gitlab/usage_data_counters/known_events/*.yml " , __dir__ )
2021-06-17 09:09:53 +00:00
] . freeze
2021-09-03 12:09:03 +00:00
class << self
# We track feature flags in `on_new_investigation` only once per
# rubocop whole run instead once per file.
attr_accessor :feature_flags_already_tracked
end
2021-06-17 09:09:53 +00:00
# Called before all on_... have been called
# When refining this method, always call `super`
def on_new_investigation
super
2021-09-03 12:09:03 +00:00
return if self . class . feature_flags_already_tracked
self . class . feature_flags_already_tracked = true
2021-06-17 09:09:53 +00:00
track_usage_data_counters_known_events!
end
def on_casgn ( node )
_ , lhs_name , rhs = * node
save_used_feature_flag ( rhs . value ) if lhs_name == :FEATURE_FLAG
end
def on_send ( node )
return if in_spec? ( node )
return unless trackable_flag? ( node )
flag_arg = flag_arg ( node )
flag_value = flag_value ( node )
return unless flag_value
2021-09-03 12:09:03 +00:00
if flag_arg_is_str_or_sym? ( flag_arg )
2021-06-17 09:09:53 +00:00
if caller_is_feature_gitaly? ( node )
save_used_feature_flag ( " gitaly_ #{ flag_value } " )
else
save_used_feature_flag ( flag_value )
end
if experiment_method? ( node ) || experimentation_method? ( node )
# Additionally, mark experiment-related feature flag as used as well
matching_feature_flags = defined_feature_flags . select { | flag | flag == " #{ flag_value } _experiment_percentage " }
matching_feature_flags . each do | matching_feature_flag |
2022-08-30 21:09:41 +00:00
puts_if_debug ( node , " The ' #{ matching_feature_flag } ' feature flag tracks the #{ flag_value } experiment, which is still in use, so we'll mark it as used. " )
2021-06-17 09:09:53 +00:00
save_used_feature_flag ( matching_feature_flag )
end
end
2021-09-03 12:09:03 +00:00
elsif flag_arg_is_send_type? ( flag_arg )
2022-08-30 21:09:41 +00:00
puts_if_debug ( node , " Feature flag is dynamic: ' #{ flag_value } . " )
2021-09-03 12:09:03 +00:00
elsif flag_arg_is_dstr_or_dsym? ( flag_arg )
2021-06-17 09:09:53 +00:00
str_prefix = flag_arg . children [ 0 ]
rest_children = flag_arg . children [ 1 .. ]
if rest_children . none? { | child | child . str_type? }
matching_feature_flags = defined_feature_flags . select { | flag | flag . start_with? ( str_prefix . value ) }
matching_feature_flags . each do | matching_feature_flag |
2022-08-30 21:09:41 +00:00
puts_if_debug ( node , " The ' #{ matching_feature_flag } ' feature flag starts with ' #{ str_prefix . value } ', so we'll optimistically mark it as used. " )
2021-06-17 09:09:53 +00:00
save_used_feature_flag ( matching_feature_flag )
end
else
2022-08-30 21:09:41 +00:00
puts_if_debug ( node , " Interpolated feature flag name has multiple static string parts, we won't track it. " )
2021-06-17 09:09:53 +00:00
end
else
2022-08-30 21:09:41 +00:00
puts_if_debug ( node , " Feature flag has an unknown type: #{ flag_arg . type } . " )
2021-06-17 09:09:53 +00:00
end
end
private
2022-08-30 21:09:41 +00:00
def puts_if_debug ( node , text )
return unless RuboCop :: ConfigLoader . debug
warn " #{ text } (call: ` #{ node . source } `, source: #{ node . location . expression . source_buffer . name } ) "
2021-06-17 09:09:53 +00:00
end
def save_used_feature_flag ( feature_flag_name )
used_feature_flag_file = File . expand_path ( " ../../../tmp/feature_flags/ #{ feature_flag_name } .used " , __dir__ )
return if File . exist? ( used_feature_flag_file )
FileUtils . touch ( used_feature_flag_file )
end
def class_caller ( node )
node . children [ 0 ] & . const_name . to_s
end
def method_name ( node )
node . children [ 1 ]
end
def flag_arg ( node )
if worker_method? ( node )
return unless node . children . size > 3
node . children [ 3 ] . each_pair . find do | pair |
pair . key . value == :feature_flag
end & . value
else
arg_index = rugged_method? ( node ) ? 3 : 2
node . children [ arg_index ]
end
end
def flag_value ( node )
flag_arg = flag_arg ( node )
return unless flag_arg
if flag_arg . respond_to? ( :value )
flag_arg . value
else
flag_arg
end . to_s . tr ( " \n / " , ' _' )
end
2021-09-03 12:09:03 +00:00
def flag_arg_is_str_or_sym? ( flag_arg )
2021-06-17 09:09:53 +00:00
flag_arg . str_type? || flag_arg . sym_type?
end
2021-09-03 12:09:03 +00:00
def flag_arg_is_send_type? ( flag_arg )
flag_arg . send_type?
2021-06-17 09:09:53 +00:00
end
2021-09-03 12:09:03 +00:00
def flag_arg_is_dstr_or_dsym? ( flag_arg )
( flag_arg . dstr_type? || flag_arg . dsym_type? ) && flag_arg . children [ 0 ] . str_type?
2021-06-17 09:09:53 +00:00
end
def caller_is_feature? ( node )
2022-08-30 09:13:15 +00:00
%w[ Feature YamlProcessor::FeatureFlags ] . include? ( class_caller ( node ) )
2021-06-17 09:09:53 +00:00
end
def caller_is_feature_gitaly? ( node )
class_caller ( node ) == " Feature::Gitaly "
end
def caller_is_experimentation? ( node )
class_caller ( node ) == " Gitlab::Experimentation "
end
def experiment_method? ( node )
EXPERIMENT_METHODS . include? ( method_name ( node ) )
end
def rugged_method? ( node )
RUGGED_METHODS . include? ( method_name ( node ) )
end
def feature_method? ( node )
FEATURE_METHODS . include? ( method_name ( node ) ) && ( caller_is_feature? ( node ) || caller_is_feature_gitaly? ( node ) )
end
def experimentation_method? ( node )
EXPERIMENTATION_METHODS . include? ( method_name ( node ) ) && caller_is_experimentation? ( node )
end
def worker_method? ( node )
WORKER_METHODS . include? ( method_name ( node ) )
end
def self_method? ( node )
SELF_METHODS . include? ( method_name ( node ) ) && class_caller ( node ) . empty?
end
def trackable_flag? ( node )
2022-08-24 15:12:19 +00:00
feature_method? ( node ) || experimentation_method? ( node ) || self_method? ( node )
2021-06-17 09:09:53 +00:00
end
# Marking all event's feature flags as used as Gitlab::UsageDataCounters::HLLRedisCounter.track_event{,context}
# is mostly used with dynamic event name.
def track_usage_data_counters_known_events!
usage_data_counters_known_event_feature_flags . each ( & method ( :save_used_feature_flag ) )
end
def usage_data_counters_known_event_feature_flags
USAGE_DATA_COUNTERS_EVENTS_YAML_GLOBS . each_with_object ( Set . new ) do | glob , memo |
Dir . glob ( glob ) . each do | path |
YAML . safe_load ( File . read ( path ) ) & . each do | hash |
memo << hash [ 'feature_flag' ] if hash [ 'feature_flag' ]
end
end
end
end
def defined_feature_flags
@defined_feature_flags || = begin
flags_paths = [
'config/feature_flags/**/*.yml'
]
# For EE additionally process `ee/` feature flags
2021-12-02 18:11:52 +00:00
if ee?
2021-06-17 09:09:53 +00:00
flags_paths << 'ee/config/feature_flags/**/*.yml'
end
2021-11-03 09:10:11 +00:00
# For JH additionally process `jh/` feature flags
2021-12-02 18:11:52 +00:00
if jh?
2021-11-03 09:10:11 +00:00
flags_paths << 'jh/config/feature_flags/**/*.yml'
end
2021-06-17 09:09:53 +00:00
flags_paths . each_with_object ( [ ] ) do | flags_path , memo |
flags_path = File . expand_path ( " ../../../ #{ flags_path } " , __dir__ )
Dir . glob ( flags_path ) . each do | path |
feature_flag_name = File . basename ( path , '.yml' )
memo << feature_flag_name
end
end
end
end
end
end
end
end