Class: Necropsy::Configuration

Inherits:
Object
  • Object
show all
Defined in:
lib/necropsy/configuration.rb

Constant Summary collapse

DEFAULT_FAIL_ON =
:high
DEFAULT_BASELINE =
'.necropsy_baseline.yml'
RTA_PRUNING_MODES =
%w[rank_only legacy].freeze
STATIC_ANALYZER_ORDER =
%w[name_resolution cha rta].freeze
QUARANTINE_EXPIRY_POLICIES =
%w[warn fail ignore].freeze
WORLD_MODES =
%w[application library].freeze
LOAD_ROOT_POLICIES =
%w[known all].freeze
FRAMEWORK_GEMS =
{
  'rails' => 'rails',
  'rubocop' => 'rubocop',
  'sidekiq' => 'sidekiq',
  'graphql' => 'graphql',
  'view_component' => 'view_component',
  'active_model_serializers' => 'active_model_serializers',
  'blueprinter' => 'blueprinter'
}.freeze
TOP_LEVEL_KEYS =
%w[
  analysis analyzers frameworks entry_points ci quarantine bench cache rta resolution paths report logging
  implicit_callers
].freeze
NESTED_KEYS =
{
  'analysis' => %w[world load_roots],
  'analyzers' => %w[static dynamic custom],
  'analyzers.dynamic' => %w[coverage coverband trace_point],
  'entry_points' => %w[extra],
  'ci' => %w[baseline fail_on],
  'quarantine' => %w[days expiry],
  'bench' => %w[precision_threshold recall_threshold],
  'cache' => %w[enabled path],
  'rta' => %w[factory_methods pruning],
  'resolution' => %w[ambiguity_limit],
  'paths' => %w[analyze reference test include exclude],
  'report' => %w[include exclude],
  'logging' => %w[verbose]
}.freeze
DYNAMIC_KEYS =
%w[source min_observation_days expected_source_revision keys key pattern connect_timeout read_timeout].freeze
IMPLICIT_CALLER_KEYS =
%w[name_pattern owner_ancestors reason].freeze
CUSTOM_ANALYZER_KEYS =
%w[class require trusted].freeze
REMOTE_DYNAMIC_KEYS =
%w[
  total_timeout max_response_bytes max_bulk_bytes max_array_elements max_resp_depth max_keys max_payload_depth
].freeze
DYNAMIC_TIMEOUT_KEYS =
%w[connect_timeout read_timeout total_timeout].freeze
DYNAMIC_LIMIT_KEYS =
(REMOTE_DYNAMIC_KEYS - %w[total_timeout]).freeze

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(root:, path:, data:) ⇒ Configuration

Returns a new instance of Configuration.



58
59
60
61
62
63
# File 'lib/necropsy/configuration.rb', line 58

def initialize(root:, path:, data:)
  @root = File.expand_path(root)
  @path = path
  @data = stringify_keys(data)
  validate!
end

Instance Attribute Details

#dataObject (readonly)

Returns the value of attribute data.



48
49
50
# File 'lib/necropsy/configuration.rb', line 48

def data
  @data
end

#pathObject (readonly)

Returns the value of attribute path.



48
49
50
# File 'lib/necropsy/configuration.rb', line 48

def path
  @path
end

#rootObject (readonly)

Returns the value of attribute root.



48
49
50
# File 'lib/necropsy/configuration.rb', line 48

def root
  @root
end

Class Method Details

.load(root:, path: nil) ⇒ Object



50
51
52
53
54
55
56
# File 'lib/necropsy/configuration.rb', line 50

def self.load(root:, path: nil)
  config_path = path ? File.expand_path(path, root) : File.join(root, '.necropsy.yml')
  data = File.exist?(config_path) ? YAML.safe_load_file(config_path, permitted_classes: [Symbol], aliases: true) : {}
  new(root: root, path: config_path, data: data || {})
rescue Psych::Exception => e
  raise Error, "Could not parse configuration #{config_path}: #{e.message}"
end

Instance Method Details

#ambiguity_limitObject



183
184
185
186
187
188
189
190
191
192
193
194
# File 'lib/necropsy/configuration.rb', line 183

def ambiguity_limit
  value = fetch('resolution', 'ambiguity_limit')
  return 4 if value.nil?
  return Float::INFINITY if value.to_s == 'unlimited'

  limit = Integer(value)
  return limit if limit.positive?

  raise Error, 'resolution.ambiguity_limit must be a positive integer or unlimited'
rescue ArgumentError, TypeError
  raise Error, 'resolution.ambiguity_limit must be a positive integer or unlimited'
end

#analyze_pathsObject



196
197
198
199
200
# File 'lib/necropsy/configuration.rb', line 196

def analyze_paths
  configured = fetch('paths', 'analyze')
  configured = fetch('paths', 'include') if configured.nil?
  Array(configured).map(&:to_s)
end

#baseline_pathObject



114
115
116
# File 'lib/necropsy/configuration.rb', line 114

def baseline_path
  fetch('ci', 'baseline') || DEFAULT_BASELINE
end

#bench_precision_thresholdObject



149
150
151
# File 'lib/necropsy/configuration.rb', line 149

def bench_precision_threshold
  Float(fetch('bench', 'precision_threshold') || 0.85)
end

#bench_recall_thresholdObject



153
154
155
156
# File 'lib/necropsy/configuration.rb', line 153

def bench_recall_threshold
  value = fetch('bench', 'recall_threshold')
  value && Float(value)
end

#cache_enabled?Boolean

Returns:

  • (Boolean)


158
159
160
161
# File 'lib/necropsy/configuration.rb', line 158

def cache_enabled?
  value = fetch('cache', 'enabled')
  value.nil? || value != false
end

#cache_pathObject



163
164
165
# File 'lib/necropsy/configuration.rb', line 163

def cache_path
  fetch('cache', 'path') || '.necropsy_cache/scan.json'
end

#custom_analyzersObject



93
94
95
# File 'lib/necropsy/configuration.rb', line 93

def custom_analyzers
  Array(fetch('analyzers', 'custom')).compact
end

#dynamic_config(name) ⇒ Object



89
90
91
# File 'lib/necropsy/configuration.rb', line 89

def dynamic_config(name)
  fetch('analyzers', 'dynamic', name.to_s) || {}
end

#entry_point_patternsObject



97
98
99
# File 'lib/necropsy/configuration.rb', line 97

def entry_point_patterns
  Array(fetch('entry_points', 'extra')).compact.map(&:to_s)
end

#exclude_pathsObject



220
221
222
# File 'lib/necropsy/configuration.rb', line 220

def exclude_paths
  Array(fetch('paths', 'exclude')).map(&:to_s)
end

#factory_methodsObject



171
172
173
# File 'lib/necropsy/configuration.rb', line 171

def factory_methods
  Array(fetch('rta', 'factory_methods') || %w[build create build_stubbed]).map(&:to_s)
end

#fail_onObject

Raises:



118
119
120
121
122
123
# File 'lib/necropsy/configuration.rb', line 118

def fail_on
  level = (fetch('ci', 'fail_on') || DEFAULT_FAIL_ON).to_sym
  return level if CONFIDENCE_LEVELS.key?(level)

  raise Error, "Invalid ci.fail_on confidence level: #{level}"
end

#frameworks(reference_files: nil) ⇒ Object



101
102
103
104
105
106
107
# File 'lib/necropsy/configuration.rb', line 101

def frameworks(reference_files: nil)
  return @frameworks if defined?(@frameworks)

  reference_files ||= Project.new(root: root, config: self).reference_files
  configured = Array(data['frameworks']).map(&:to_s)
  @frameworks = (configured + detected_frameworks(reference_files)).uniq.sort.freeze
end

#implicit_callersObject



232
233
234
235
236
237
238
239
240
# File 'lib/necropsy/configuration.rb', line 232

def implicit_callers
  @implicit_callers ||= Array(data['implicit_callers']).map do |entry|
    {
      name_pattern: Regexp.new(entry.fetch('name_pattern')),
      owner_ancestors: Array(entry['owner_ancestors']).map(&:to_s),
      reason: entry['reason']&.to_s
    }
  end
end

#include_pathsObject



212
213
214
# File 'lib/necropsy/configuration.rb', line 212

def include_paths
  analyze_paths
end

#legacy_include_paths?Boolean

Returns:

  • (Boolean)


216
217
218
# File 'lib/necropsy/configuration.rb', line 216

def legacy_include_paths?
  !fetch('paths', 'include').nil? && fetch('paths', 'analyze').nil?
end

#library_world?Boolean

Returns:

  • (Boolean)


77
78
79
# File 'lib/necropsy/configuration.rb', line 77

def library_world?
  world == :library
end

#load_rootsObject

Raises:



81
82
83
84
85
86
87
# File 'lib/necropsy/configuration.rb', line 81

def load_roots
  configured = fetch('analysis', 'load_roots')
  policy = configured.nil? ? 'known' : configured.to_s
  return policy.to_sym if LOAD_ROOT_POLICIES.include?(policy)

  raise Error, "analysis.load_roots must be one of: #{LOAD_ROOT_POLICIES.join(', ')}"
end

#min_observation_daysObject



125
126
127
128
129
# File 'lib/necropsy/configuration.rb', line 125

def min_observation_days
  coverage_days = fetch('analyzers', 'dynamic', 'coverage', 'min_observation_days')
  coverband_days = fetch('analyzers', 'dynamic', 'coverband', 'min_observation_days')
  Integer(coverage_days || coverband_days || 30)
end

#quarantine_daysObject



137
138
139
# File 'lib/necropsy/configuration.rb', line 137

def quarantine_days
  Integer(fetch('quarantine', 'days') || 30)
end

#quarantine_expiryObject

Raises:



141
142
143
144
145
146
147
# File 'lib/necropsy/configuration.rb', line 141

def quarantine_expiry
  configured = fetch('quarantine', 'expiry')
  policy = configured.nil? ? 'warn' : configured.to_s
  return policy.to_sym if QUARANTINE_EXPIRY_POLICIES.include?(policy)

  raise Error, "quarantine.expiry must be one of: #{QUARANTINE_EXPIRY_POLICIES.join(', ')}"
end

#rails_enabled?(reference_files: nil) ⇒ Boolean

Returns:

  • (Boolean)


109
110
111
112
# File 'lib/necropsy/configuration.rb', line 109

def rails_enabled?(reference_files: nil)
  reference_files ||= Project.new(root: root, config: self).reference_files
  frameworks(reference_files: reference_files).include?('rails')
end

#reference_pathsObject



202
203
204
205
# File 'lib/necropsy/configuration.rb', line 202

def reference_paths
  configured = fetch('paths', 'reference')
  Array(configured.nil? ? ['**/*'] : configured).map(&:to_s)
end

#report_exclude_pathsObject



228
229
230
# File 'lib/necropsy/configuration.rb', line 228

def report_exclude_paths
  Array(fetch('report', 'exclude')).map(&:to_s)
end

#report_include_pathsObject



224
225
226
# File 'lib/necropsy/configuration.rb', line 224

def report_include_paths
  Array(fetch('report', 'include')).map(&:to_s)
end

#rta_pruningObject

Raises:



175
176
177
178
179
180
181
# File 'lib/necropsy/configuration.rb', line 175

def rta_pruning
  configured = fetch('rta', 'pruning')
  mode = configured.nil? ? 'rank_only' : configured.to_s
  return mode.to_sym if RTA_PRUNING_MODES.include?(mode)

  raise Error, "rta.pruning must be one of: #{RTA_PRUNING_MODES.join(', ')}"
end

#scan_cache_keyObject



167
168
169
# File 'lib/necropsy/configuration.rb', line 167

def scan_cache_key
  data.except('cache', 'report')
end

#static_analyzersObject



65
66
67
# File 'lib/necropsy/configuration.rb', line 65

def static_analyzers
  Array(fetch('analyzers', 'static') || %w[name_resolution cha rta]).map(&:to_s)
end

#test_pathsObject



207
208
209
210
# File 'lib/necropsy/configuration.rb', line 207

def test_paths
  configured = fetch('paths', 'test')
  Array(configured.nil? ? %w[spec/** test/**] : configured).map(&:to_s)
end

#verbose?Boolean

Returns:

  • (Boolean)


242
243
244
# File 'lib/necropsy/configuration.rb', line 242

def verbose?
  fetch('logging', 'verbose') == true
end

#worldObject

Raises:



69
70
71
72
73
74
75
# File 'lib/necropsy/configuration.rb', line 69

def world
  configured = fetch('analysis', 'world')
  mode = configured.nil? ? 'application' : configured.to_s
  return mode.to_sym if WORLD_MODES.include?(mode)

  raise Error, "analysis.world must be one of: #{WORLD_MODES.join(', ')}"
end