feature_map 1.2.2 → 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +0 -1
  3. data/bin/featuremap +0 -1
  4. data/lib/feature_map/cli.rb +0 -2
  5. data/lib/feature_map/code_features/plugin.rb +2 -21
  6. data/lib/feature_map/code_features/plugins/identity.rb +1 -8
  7. data/lib/feature_map/code_features.rb +1 -31
  8. data/lib/feature_map/commit.rb +0 -19
  9. data/lib/feature_map/configuration.rb +40 -17
  10. data/lib/feature_map/constants.rb +3 -5
  11. data/lib/feature_map/mapper.rb +0 -26
  12. data/lib/feature_map/output_color.rb +0 -11
  13. data/lib/feature_map/private/additional_metrics_file.rb +9 -103
  14. data/lib/feature_map/private/assignment_applicator.rb +0 -12
  15. data/lib/feature_map/private/assignment_mappers/directory_assignment.rb +4 -26
  16. data/lib/feature_map/private/assignment_mappers/feature_definition_assignment.rb +1 -21
  17. data/lib/feature_map/private/assignment_mappers/feature_globs.rb +7 -40
  18. data/lib/feature_map/private/assignment_mappers/file_annotations.rb +20 -44
  19. data/lib/feature_map/private/assignments_file.rb +8 -54
  20. data/lib/feature_map/private/code_cov.rb +2 -29
  21. data/lib/feature_map/private/cyclomatic_complexity_calculator.rb +1 -7
  22. data/lib/feature_map/private/docs/index.html +2 -2
  23. data/lib/feature_map/private/documentation_site.rb +0 -16
  24. data/lib/feature_map/private/extension_loader.rb +0 -3
  25. data/lib/feature_map/private/feature_assigner.rb +0 -4
  26. data/lib/feature_map/private/feature_metrics_calculator.rb +2 -16
  27. data/lib/feature_map/private/feature_plugins/assignment.rb +0 -6
  28. data/lib/feature_map/private/glob_cache.rb +2 -29
  29. data/lib/feature_map/private/health_calculator.rb +122 -0
  30. data/lib/feature_map/private/lines_of_code_calculator.rb +10 -21
  31. data/lib/feature_map/private/metrics_file.rb +1 -25
  32. data/lib/feature_map/private/percentile_metrics_calculator.rb +117 -0
  33. data/lib/feature_map/private/release_notification_builder.rb +1 -13
  34. data/lib/feature_map/private/test_coverage_file.rb +12 -39
  35. data/lib/feature_map/private/test_pyramid_file.rb +0 -41
  36. data/lib/feature_map/private/todo_inspector.rb +16 -30
  37. data/lib/feature_map/private/validations/features_up_to_date.rb +1 -6
  38. data/lib/feature_map/private/validations/files_have_features.rb +2 -7
  39. data/lib/feature_map/private/validations/files_have_unique_features.rb +1 -6
  40. data/lib/feature_map/private.rb +7 -44
  41. data/lib/feature_map/validator.rb +0 -13
  42. data/lib/feature_map.rb +8 -49
  43. metadata +4 -44
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: da72179134e412ae870b9e28e3642fce5968f893d43a1ccf6bdf9ebb0c4f955c
4
- data.tar.gz: '0825071345a6e6589dcd331f01f5fb1f31c20824f3dadef6fd28330e686499f6'
3
+ metadata.gz: b56fb3fcd3da34a3f15aa1a15714c37287c2dc9117ceb02a5df849ba076e9376
4
+ data.tar.gz: 644e0c180a214481117b8d612f6dc400ae8a5eda7bbe171061f03bc1ddb73a95
5
5
  SHA512:
6
- metadata.gz: 8e723bc85047a19677f535ee7f569454101618f76a677338ae2f78a9eb5114726713d46a85a0b1bd7ba87a020d629d373e7b8e9b0d5b34566d607d2594a05058
7
- data.tar.gz: 558061825589dc8a45d6a61c6a59843dbf9a657ef7b5484704f036d950aaf7219fc71cd5e4ec403735e9eaa5a9865169932c441f1e0ff29ada1ee0e210c9a3a3
6
+ metadata.gz: b900d2781d2374618d966655f0292cb0568e1b707a1c38b41a80d8396bd31b0e95663371b6471391bd7680f6782762f1dfeb0f05c7752fcd2b25f927dd0b1795
7
+ data.tar.gz: 5d47dd3e2f2250d84cfc05ccc6944c85ca390530ce3fb147569dfabe9c83cd526ee8cbb74c3ef17716a29c9f1968b9cc5d9a58fd99715b5eb7aa25df59e242cb
data/README.md CHANGED
@@ -33,7 +33,6 @@ Contributions are welcome and appreciated. Here's how to get started:
33
33
  - install dependencies: `$ bundle install`
34
34
  - run tests: `$ bundle exec rspec`
35
35
  - run Rubocop: `$ bundle exec rubocop`
36
- - run Sorbet: `$ bundle exec srb tc`
37
36
 
38
37
  That's it! Assuming you can complete all of these steps without any error or issues, you should be good to go.
39
38
 
data/bin/featuremap CHANGED
@@ -1,5 +1,4 @@
1
1
  #!/usr/bin/env ruby
2
- # typed: strict
3
2
 
4
3
  require 'feature_map'
5
4
  FeatureMap::Cli.run!(ARGV)
@@ -1,5 +1,3 @@
1
- # typed: true
2
-
3
1
  require 'optparse'
4
2
  require 'pathname'
5
3
  require 'fileutils'
@@ -1,56 +1,39 @@
1
- # typed: strict
2
-
3
1
  module FeatureMap
4
2
  module CodeFeatures
5
3
  # Plugins allow a client to add validation on custom keys in the feature YML.
6
4
  # For now, only a single plugin is allowed to manage validation on a top-level key.
7
5
  # In the future we can think of allowing plugins to be gracefully merged with each other.
8
6
  class Plugin
9
- extend T::Helpers
10
- extend T::Sig
11
-
12
- abstract!
13
-
14
- sig { params(feature: Feature).void }
15
7
  def initialize(feature)
16
8
  @feature = feature
17
9
  end
18
10
 
19
- sig { params(base: T.untyped).void }
20
11
  def self.inherited(base) # rubocop:disable Lint/MissingSuper
21
- all_plugins << T.cast(base, T.class_of(Plugin))
12
+ all_plugins << base
22
13
  end
23
14
 
24
- sig { returns(T::Array[T.class_of(Plugin)]) }
25
15
  def self.all_plugins
26
- @all_plugins ||= T.let(@all_plugins, T.nilable(T::Array[T.class_of(Plugin)]))
27
16
  @all_plugins ||= []
28
17
  @all_plugins
29
18
  end
30
19
 
31
- sig { params(features: T::Array[Feature]).returns(T::Array[String]) }
32
20
  def self.validation_errors(features)
33
21
  []
34
22
  end
35
23
 
36
- sig { params(feature: Feature).returns(T.attached_class) }
37
24
  def self.for(feature)
38
25
  register_feature(feature)
39
26
  end
40
27
 
41
- sig { params(feature: Feature, key: String).returns(String) }
42
28
  def self.missing_key_error_message(feature, key)
43
29
  "#{feature.name} is missing required key `#{key}`"
44
30
  end
45
31
 
46
- sig { returns(T::Hash[T.nilable(String), T::Hash[T.class_of(Plugin), Plugin]]) }
47
32
  def self.registry
48
- @registry ||= T.let(@registry, T.nilable(T::Hash[String, T::Hash[T.class_of(Plugin), Plugin]]))
49
33
  @registry ||= {}
50
34
  @registry
51
35
  end
52
36
 
53
- sig { params(feature: Feature).returns(T.attached_class) }
54
37
  def self.register_feature(feature)
55
38
  # We pull from the hash since `feature.name` uses the registry
56
39
  feature_name = feature.raw_hash['name']
@@ -59,15 +42,13 @@ module FeatureMap
59
42
  registry_for_feature = registry[feature_name] || {}
60
43
  registry[feature_name] ||= {}
61
44
  registry_for_feature[self] ||= new(feature)
62
- T.unsafe(registry_for_feature[self])
45
+ registry_for_feature[self]
63
46
  end
64
47
 
65
- sig { void }
66
48
  def self.bust_caches!
67
49
  all_plugins.each(&:clear_feature_registry!)
68
50
  end
69
51
 
70
- sig { void }
71
52
  def self.clear_feature_registry!
72
53
  @registry = nil
73
54
  end
@@ -1,24 +1,17 @@
1
- # typed: true
2
-
3
1
  module FeatureMap
4
2
  module CodeFeatures
5
3
  module Plugins
6
4
  class Identity < Plugin
7
- extend T::Sig
8
- extend T::Helpers
9
-
10
5
  IdentityStruct = Struct.new(:name)
11
6
 
12
- sig { returns(IdentityStruct) }
13
7
  def identity
14
8
  IdentityStruct.new(
15
9
  @feature.raw_hash['name']
16
10
  )
17
11
  end
18
12
 
19
- sig { override.params(features: T::Array[CodeFeatures::Feature]).returns(T::Array[String]) }
20
13
  def self.validation_errors(features)
21
- errors = T.let([], T::Array[String])
14
+ errors = []
22
15
 
23
16
  uniq_set = Set.new
24
17
  features.each do |feature|
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # typed: strict
4
-
5
3
  require 'yaml'
6
4
  require 'csv'
7
5
  require 'set'
@@ -11,22 +9,16 @@ require 'feature_map/code_features/plugins/identity'
11
9
 
12
10
  module FeatureMap
13
11
  module CodeFeatures
14
- extend T::Sig
15
-
16
- NON_BREAKING_SPACE = T.let(65_279.chr(Encoding::UTF_8), String)
12
+ NON_BREAKING_SPACE = 65_279.chr(Encoding::UTF_8)
17
13
 
18
14
  class IncorrectPublicApiUsageError < StandardError; end
19
15
 
20
- sig { returns(T::Array[Feature]) }
21
16
  def self.all
22
- @all = T.let(@all, T.nilable(T::Array[Feature]))
23
17
  @all ||= from_csv('.feature_map/feature_definitions.csv')
24
18
  @all ||= for_directory('.feature_map/definitions')
25
19
  end
26
20
 
27
- sig { params(name: String).returns(T.nilable(Feature)) }
28
21
  def self.find(name)
29
- @index_by_name = T.let(@index_by_name, T.nilable(T::Hash[String, CodeFeatures::Feature]))
30
22
  @index_by_name ||= begin
31
23
  result = {}
32
24
  all.each { |t| result[t.name] = t }
@@ -36,7 +28,6 @@ module FeatureMap
36
28
  @index_by_name[name]
37
29
  end
38
30
 
39
- sig { params(file_path: String).returns(T.nilable(T::Array[Feature])) }
40
31
  def self.from_csv(file_path)
41
32
  return nil if !File.exist?(file_path)
42
33
 
@@ -53,7 +44,6 @@ module FeatureMap
53
44
  end
54
45
  end
55
46
 
56
- sig { params(dir: String).returns(T::Array[Feature]) }
57
47
  def self.for_directory(dir)
58
48
  Pathname.new(dir).glob('**/*.yml').map do |path|
59
49
  Feature.from_yml(path.to_s)
@@ -62,14 +52,12 @@ module FeatureMap
62
52
  end
63
53
  end
64
54
 
65
- sig { params(features: T::Array[Feature]).returns(T::Array[String]) }
66
55
  def self.validation_errors(features)
67
56
  Plugin.all_plugins.flat_map do |plugin|
68
57
  plugin.validation_errors(features)
69
58
  end
70
59
  end
71
60
 
72
- sig { params(string: String).returns(String) }
73
61
  def self.tag_value_for(string)
74
62
  string.tr('&', ' ').gsub(/\s+/, '_').downcase
75
63
  end
@@ -77,7 +65,6 @@ module FeatureMap
77
65
  # Generally, you should not ever need to do this, because once your ruby process loads, cached content should not change.
78
66
  # Namely, the YML files that are the source of truth for features should not change, so we should not need to look at the YMLs again to verify.
79
67
  # The primary reason this is helpful is for tests where each context is testing against a different set of features
80
- sig { void }
81
68
  def self.bust_caches!
82
69
  Plugin.bust_caches!
83
70
  @all = nil
@@ -85,9 +72,6 @@ module FeatureMap
85
72
  end
86
73
 
87
74
  class Feature
88
- extend T::Sig
89
-
90
- sig { params(config_yml: String).returns(Feature) }
91
75
  def self.from_yml(config_yml)
92
76
  hash = YAML.load_file(config_yml)
93
77
 
@@ -97,7 +81,6 @@ module FeatureMap
97
81
  )
98
82
  end
99
83
 
100
- sig { params(raw_hash: T::Hash[T.untyped, T.untyped]).returns(Feature) }
101
84
  def self.from_hash(raw_hash)
102
85
  new(
103
86
  config_yml: nil,
@@ -105,34 +88,22 @@ module FeatureMap
105
88
  )
106
89
  end
107
90
 
108
- sig { returns(T::Hash[T.untyped, T.untyped]) }
109
91
  attr_reader :raw_hash
110
-
111
- sig { returns(T.nilable(String)) }
112
92
  attr_reader :config_yml
113
93
 
114
- sig do
115
- params(
116
- config_yml: T.nilable(String),
117
- raw_hash: T::Hash[T.untyped, T.untyped]
118
- ).void
119
- end
120
94
  def initialize(config_yml:, raw_hash:)
121
95
  @config_yml = config_yml
122
96
  @raw_hash = raw_hash
123
97
  end
124
98
 
125
- sig { returns(String) }
126
99
  def name
127
100
  Plugins::Identity.for(self).identity.name
128
101
  end
129
102
 
130
- sig { returns(String) }
131
103
  def to_tag
132
104
  CodeFeatures.tag_value_for(name)
133
105
  end
134
106
 
135
- sig { params(other: Object).returns(T::Boolean) }
136
107
  def ==(other)
137
108
  if other.is_a?(CodeFeatures::Feature)
138
109
  name == other.name
@@ -143,7 +114,6 @@ module FeatureMap
143
114
 
144
115
  alias eql? ==
145
116
 
146
- sig { returns(Integer) }
147
117
  def hash
148
118
  name.hash
149
119
  end
@@ -1,29 +1,10 @@
1
- # typed: strict
2
-
3
1
  module FeatureMap
4
2
  class Commit
5
- extend T::Sig
6
-
7
- sig { returns(T.nilable(String)) }
8
3
  attr_reader :sha
9
-
10
- sig { returns(T.nilable(String)) }
11
4
  attr_reader :description
12
-
13
- sig { returns(T.nilable(String)) }
14
5
  attr_reader :pull_request_number
15
-
16
- sig { returns(T::Array[String]) }
17
6
  attr_reader :files
18
7
 
19
- sig do
20
- params(
21
- sha: T.nilable(String),
22
- description: T.nilable(String),
23
- pull_request_number: T.nilable(String),
24
- files: T::Array[String]
25
- ).void
26
- end
27
8
  def initialize(sha: nil, description: nil, pull_request_number: nil, files: [])
28
9
  @sha = sha
29
10
  @description = description
@@ -1,23 +1,46 @@
1
- # typed: strict
2
-
3
1
  module FeatureMap
4
- class Configuration < T::Struct
5
- extend T::Sig
2
+ class Configuration
3
+ attr_reader :assigned_globs
4
+ attr_reader :unassigned_globs
5
+ attr_reader :unbuilt_gems_path
6
+ attr_reader :skip_features_validation
7
+ attr_reader :raw_hash
8
+ attr_reader :skip_code_ownership
9
+ attr_reader :require_assignment_for_teams
10
+ attr_reader :ignore_feature_definitions
11
+ attr_reader :code_cov
12
+ attr_reader :repository
13
+ attr_reader :documentation_site
14
+ attr_reader :documentation_site_url
6
15
 
7
- const :assigned_globs, T::Array[String]
8
- const :unassigned_globs, T::Array[String]
9
- const :unbuilt_gems_path, T.nilable(String)
10
- const :skip_features_validation, T::Boolean
11
- const :raw_hash, T::Hash[T.untyped, T.untyped]
12
- const :skip_code_ownership, T::Boolean
13
- const :require_assignment_for_teams, T.nilable(T::Array[String])
14
- const :ignore_feature_definitions, T::Boolean
15
- const :code_cov, T::Hash[String, T.nilable(String)]
16
- const :repository, T::Hash[String, T.nilable(String)]
17
- const :documentation_site, T::Hash[String, T.untyped]
18
- const :documentation_site_url, T.nilable(String)
16
+ def initialize(
17
+ assigned_globs: nil,
18
+ unassigned_globs: nil,
19
+ unbuilt_gems_path: nil,
20
+ skip_features_validation: nil,
21
+ raw_hash: nil,
22
+ skip_code_ownership: nil,
23
+ require_assignment_for_teams: nil,
24
+ ignore_feature_definitions: nil,
25
+ code_cov: nil,
26
+ repository: nil,
27
+ documentation_site: nil,
28
+ documentation_site_url: nil
29
+ )
30
+ @assigned_globs = assigned_globs
31
+ @unassigned_globs = unassigned_globs
32
+ @unbuilt_gems_path = unbuilt_gems_path
33
+ @skip_features_validation = skip_features_validation
34
+ @raw_hash = raw_hash
35
+ @skip_code_ownership = skip_code_ownership
36
+ @require_assignment_for_teams = require_assignment_for_teams
37
+ @ignore_feature_definitions = ignore_feature_definitions
38
+ @code_cov = code_cov
39
+ @repository = repository
40
+ @documentation_site = documentation_site
41
+ @documentation_site_url = documentation_site_url
42
+ end
19
43
 
20
- sig { returns(Configuration) }
21
44
  def self.fetch
22
45
  config_hash = YAML.load_file('.feature_map/config.yml')
23
46
 
@@ -1,11 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # typed: strict
4
-
5
3
  module FeatureMap
6
4
  module Constants
7
- SINGLE_LINE_COMMENT_PATTERNS = T.let(['#', '//'].map { |r| Regexp.escape(r) }.freeze, T::Array[String])
8
- MULTILINE_COMMENT_START_PATTERNS = T.let(['/*', '<!--', '"""', "'''"].map { |r| Regexp.escape(r) }.freeze, T::Array[String])
9
- MULTILINE_COMMENT_END_PATTERNS = T.let(['*/', '-->', '"""', "'''"].map { |r| Regexp.escape(r) }.freeze, T::Array[String])
5
+ SINGLE_LINE_COMMENT_PATTERNS = ['#', '//'].map { |r| Regexp.escape(r) }.freeze
6
+ MULTILINE_COMMENT_START_PATTERNS = ['/*', '<!--', '"""', "'''"].map { |r| Regexp.escape(r) }.freeze
7
+ MULTILINE_COMMENT_END_PATTERNS = ['*/', '-->', '"""', "'''"].map { |r| Regexp.escape(r) }.freeze
10
8
  end
11
9
  end
@@ -1,25 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # typed: strict
4
-
5
3
  module FeatureMap
6
4
  module Mapper
7
- extend T::Sig
8
- extend T::Helpers
9
-
10
- interface!
11
-
12
5
  class << self
13
- extend T::Sig
14
-
15
- sig { params(base: T::Class[Mapper]).void }
16
6
  def included(base)
17
- @mappers ||= T.let(@mappers, T.nilable(T::Array[T::Class[Mapper]]))
18
7
  @mappers ||= []
19
8
  @mappers << base
20
9
  end
21
10
 
22
- sig { returns(T::Array[Mapper]) }
23
11
  def all
24
12
  (@mappers || []).map(&:new)
25
13
  end
@@ -28,36 +16,22 @@ module FeatureMap
28
16
  #
29
17
  # This should be fast when run with ONE file
30
18
  #
31
- sig do
32
- abstract.params(file: String)
33
- .returns(T.nilable(CodeFeatures::Feature))
34
- end
35
19
  def map_file_to_feature(file); end
36
20
 
37
21
  #
38
22
  # This should be fast when run with MANY files
39
23
  #
40
- sig do
41
- abstract.params(files: T::Array[String])
42
- .returns(T::Hash[String, CodeFeatures::Feature])
43
- end
44
24
  def globs_to_feature(files); end
45
25
 
46
26
  #
47
27
  # This should be fast when run with MANY files
48
28
  #
49
- sig do
50
- abstract.params(cache: GlobsToAssignedFeatureMap, files: T::Array[String]).returns(GlobsToAssignedFeatureMap)
51
- end
52
29
  def update_cache(cache, files); end
53
30
 
54
- sig { abstract.returns(String) }
55
31
  def description; end
56
32
 
57
- sig { abstract.void }
58
33
  def bust_caches!; end
59
34
 
60
- sig { returns(Private::GlobCache) }
61
35
  def self.to_glob_cache
62
36
  glob_to_feature_map_by_mapper_description = {}
63
37
 
@@ -1,41 +1,30 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # typed: strict
4
-
5
3
  module OutputColor
6
- extend T::Sig
7
-
8
- sig { params(color_code: Integer, text: String).returns(String) }
9
4
  def self.colorize(color_code, text)
10
5
  "\e[#{color_code}m#{text}\e[0m"
11
6
  end
12
7
 
13
- sig { params(text: String).returns(String) }
14
8
  def self.red(text)
15
9
  colorize(31, text)
16
10
  end
17
11
 
18
- sig { params(text: String).returns(String) }
19
12
  def self.green(text)
20
13
  colorize(32, text)
21
14
  end
22
15
 
23
- sig { params(text: String).returns(String) }
24
16
  def self.yellow(text)
25
17
  colorize(33, text)
26
18
  end
27
19
 
28
- sig { params(text: String).returns(String) }
29
20
  def self.blue(text)
30
21
  colorize(34, text)
31
22
  end
32
23
 
33
- sig { params(text: String).returns(String) }
34
24
  def self.pink(text)
35
25
  colorize(35, text)
36
26
  end
37
27
 
38
- sig { params(text: String).returns(String) }
39
28
  def self.light_blue(text)
40
29
  colorize(36, text)
41
30
  end
@@ -1,44 +1,22 @@
1
- # typed: strict
2
1
  # frozen_string_literal: true
3
2
 
4
3
  module FeatureMap
5
4
  module Private
6
5
  class AdditionalMetricsFile
7
- extend T::Sig
8
-
9
6
  class FileContentError < StandardError; end
10
7
 
11
8
  FEATURES_KEY = 'features'
12
9
 
13
- FeatureName = T.type_alias { String }
14
-
15
- FeatureMetrics = T.type_alias do
16
- T::Hash[
17
- String,
18
- T.any(Integer, Float, T::Hash[String, String])
19
- ]
20
- end
21
-
22
- FeaturesContent = T.type_alias do
23
- T::Hash[
24
- FeatureName,
25
- FeatureMetrics
26
- ]
27
- end
28
-
29
- sig { params(metrics: T::Hash[String, T.untyped], test_coverage: T::Hash[String, T.untyped], health_config: T::Hash[String, T.untyped]).void }
30
10
  def self.write!(metrics, test_coverage, health_config)
31
11
  FileUtils.mkdir_p(path.dirname) if !path.dirname.exist?
32
12
 
33
13
  path.write([header_comment, "\n", generate_content(metrics, test_coverage, health_config).to_yaml].join)
34
14
  end
35
15
 
36
- sig { returns(Pathname) }
37
16
  def self.path
38
17
  Pathname.pwd.join('.feature_map/additional-metrics.yml')
39
18
  end
40
19
 
41
- sig { returns(String) }
42
20
  def self.header_comment
43
21
  <<~HEADER
44
22
  # STOP! - DO NOT EDIT THIS FILE MANUALLY
@@ -53,35 +31,26 @@ module FeatureMap
53
31
  HEADER
54
32
  end
55
33
 
56
- sig { params(feature_metrics: T::Hash[String, T.untyped], feature_test_coverage: T::Hash[String, T.untyped], health_config: T::Hash[String, T.untyped]).returns(T::Hash[String, FeaturesContent]) }
57
34
  def self.generate_content(feature_metrics, feature_test_coverage, health_config)
58
- feature_additional_metrics = T.let({}, FeaturesContent)
35
+ feature_additional_metrics = {}
59
36
 
60
- cyclomatic_complexity_ratios = feature_metrics.map { |_k, m| m[FeatureMetricsCalculator::COMPLEXITY_RATIO_METRIC] }.compact
61
- encapsulation_ratios = feature_metrics.map { |_k, m| m[FeatureMetricsCalculator::ENCAPSULATION_RATIO_METRIC] }.compact
62
- feature_sizes = feature_metrics.map { |_k, m| m[FeatureMetricsCalculator::LINES_OF_CODE_METRIC] }.compact
63
- test_coverage_ratios = feature_test_coverage.map { |_k, c| c[TestCoverageFile::COVERAGE_RATIO] }.compact
37
+ percentile_metrics = PercentileMetricsCalculator.new(metrics: feature_metrics, test_coverage: feature_test_coverage)
38
+ health_calculator = HealthCalculator.new(percentile_metrics: percentile_metrics, health_config: health_config)
64
39
 
65
40
  Private.feature_file_assignments.each_key do |feature_name|
66
- cyclomatic_complexity = calculate(cyclomatic_complexity_ratios, feature_metrics.dig(feature_name, FeatureMetricsCalculator::COMPLEXITY_RATIO_METRIC) || 0)
67
- encapsulation = calculate(encapsulation_ratios, feature_metrics.dig(feature_name, FeatureMetricsCalculator::ENCAPSULATION_RATIO_METRIC) || 0)
68
- feature_size = calculate(feature_sizes, feature_metrics.dig(feature_name, FeatureMetricsCalculator::LINES_OF_CODE_METRIC) || 0)
69
- test_coverage = calculate(test_coverage_ratios, feature_test_coverage.dig(feature_name, TestCoverageFile::COVERAGE_RATIO) || 0)
70
- health = health_score_for(cyclomatic_complexity, encapsulation, test_coverage, health_config)
71
-
72
41
  feature_additional_metrics[feature_name] = {
73
- 'cyclomatic_complexity' => cyclomatic_complexity,
74
- 'encapsulation' => encapsulation,
75
- 'feature_size' => feature_size,
76
- 'test_coverage' => test_coverage,
77
- 'health' => health
42
+ 'cyclomatic_complexity' => percentile_metrics.cyclomatic_complexity_for(feature_name),
43
+ 'encapsulation' => percentile_metrics.encapsulation_for(feature_name),
44
+ 'feature_size' => percentile_metrics.feature_size_for(feature_name),
45
+ 'test_coverage' => percentile_metrics.test_coverage_for(feature_name),
46
+ 'todo_count' => percentile_metrics.todo_count_for(feature_name),
47
+ 'health' => health_calculator.health_score_for(feature_name)
78
48
  }
79
49
  end
80
50
 
81
51
  { FEATURES_KEY => feature_additional_metrics }
82
52
  end
83
53
 
84
- sig { returns(FeaturesContent) }
85
54
  def self.load_features!
86
55
  metrics_content = YAML.load_file(path)
87
56
 
@@ -93,69 +62,6 @@ module FeatureMap
93
62
  rescue Errno::ENOENT
94
63
  raise FileContentError, "No feature metrics file found at #{path}. Use `bin/featuremap additional_metrics` to generate it and try again."
95
64
  end
96
-
97
- sig { params(collection: T::Array[T.any(Integer, Float)], score: T.any(Integer, Float)).returns({ 'percentile' => Float, 'percent_of_max' => Integer, 'score' => T.any(Integer, Float) }) }
98
- def self.calculate(collection, score)
99
- max = collection.max || 0
100
- percentile = percentile_of(collection, score)
101
- percent_of_max = max.zero? ? 0 : ((score.to_f / max) * 100).round.to_i
102
-
103
- { 'percentile' => percentile, 'percent_of_max' => percent_of_max, 'score' => score }
104
- end
105
-
106
- sig { params(arr: T::Array[T.any(Integer, Float)], val: T.any(Integer, Float)).returns(Float) }
107
- def self.percentile_of(arr, val)
108
- return 0.0 if arr.empty?
109
-
110
- ensure_array_of_floats = arr.map(&:to_f)
111
- ensure_float_value = val.to_f
112
-
113
- below_or_equal_count = ensure_array_of_floats.reduce(0) do |acc, v|
114
- if v < ensure_float_value
115
- acc + 1
116
- elsif v == ensure_float_value
117
- acc + 0.5
118
- else
119
- acc
120
- end
121
- end
122
-
123
- ((100 * below_or_equal_count) / ensure_array_of_floats.length).to_f
124
- end
125
-
126
- sig { params(awardable_points: Integer, score: T.any(Float, Integer), score_threshold: Integer, percent_of_max: T.nilable(T.any(Integer, Float)), percent_of_max_threshold: T.nilable(T.any(Integer, Float))).returns({ 'awardable_points' => Integer, 'health_score' => T.any(Float, Integer), 'close_to_maximum_score' => T::Boolean, 'exceeds_score_threshold' => T::Boolean }) }
127
- def self.health_score_component(awardable_points, score, score_threshold, percent_of_max = 0, percent_of_max_threshold = 100)
128
- close_to_maximum_score = T.must(percent_of_max) >= T.must(percent_of_max_threshold)
129
- exceeds_score_threshold = score >= score_threshold
130
-
131
- if close_to_maximum_score || exceeds_score_threshold
132
- { 'awardable_points' => awardable_points, 'health_score' => awardable_points, 'close_to_maximum_score' => close_to_maximum_score, 'exceeds_score_threshold' => exceeds_score_threshold }
133
- else
134
- { 'awardable_points' => awardable_points, 'health_score' => (score.to_f / score_threshold) * awardable_points, 'close_to_maximum_score' => close_to_maximum_score, 'exceeds_score_threshold' => exceeds_score_threshold }
135
- end
136
- end
137
-
138
- sig {
139
- params(
140
- encapsulation: T::Hash[String, T.any(Integer, Float)],
141
- cyclomatic_complexity: T::Hash[String, T.any(Integer, Float)],
142
- test_coverage: T::Hash[String, T.any(Integer, Float)],
143
- health_config: T::Hash[String, T.untyped]
144
- ).returns(T::Hash[String, T.untyped])
145
- }
146
- def self.health_score_for(encapsulation, cyclomatic_complexity, test_coverage, health_config)
147
- cyclomatic_complexity_config = health_config['components']['cyclomatic_complexity']
148
- encapsulation_config = health_config['components']['encapsulation']
149
- test_coverage_config = health_config['components']['test_coverage']
150
-
151
- test_coverage_component = health_score_component(test_coverage_config['weight'], T.must(test_coverage['score']), test_coverage_config['score_threshold'])
152
- cyclomatic_complexity_component = health_score_component(cyclomatic_complexity_config['weight'], T.must(cyclomatic_complexity['percentile']), cyclomatic_complexity_config['score_threshold'], cyclomatic_complexity['percent_of_max'], 100 - cyclomatic_complexity_config['minimum_variance'])
153
- encapsulation_component = health_score_component(encapsulation_config['weight'], T.must(encapsulation['percentile']), encapsulation_config['score_threshold'], encapsulation['percent_of_max'], 100 - encapsulation_config['minimum_variance'])
154
-
155
- overall = test_coverage_component['health_score'] + cyclomatic_complexity_component['health_score'] + encapsulation_component['health_score']
156
-
157
- { 'test_coverage_component' => test_coverage_component, 'cyclomatic_complexity_component' => cyclomatic_complexity_component, 'encapsulation_component' => encapsulation_component, 'overall' => overall }
158
- end
159
65
  end
160
66
  end
161
67
  end