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.
- checksums.yaml +4 -4
- data/README.md +0 -1
- data/bin/featuremap +0 -1
- data/lib/feature_map/cli.rb +0 -2
- data/lib/feature_map/code_features/plugin.rb +2 -21
- data/lib/feature_map/code_features/plugins/identity.rb +1 -8
- data/lib/feature_map/code_features.rb +1 -31
- data/lib/feature_map/commit.rb +0 -19
- data/lib/feature_map/configuration.rb +40 -17
- data/lib/feature_map/constants.rb +3 -5
- data/lib/feature_map/mapper.rb +0 -26
- data/lib/feature_map/output_color.rb +0 -11
- data/lib/feature_map/private/additional_metrics_file.rb +9 -103
- data/lib/feature_map/private/assignment_applicator.rb +0 -12
- data/lib/feature_map/private/assignment_mappers/directory_assignment.rb +4 -26
- data/lib/feature_map/private/assignment_mappers/feature_definition_assignment.rb +1 -21
- data/lib/feature_map/private/assignment_mappers/feature_globs.rb +7 -40
- data/lib/feature_map/private/assignment_mappers/file_annotations.rb +20 -44
- data/lib/feature_map/private/assignments_file.rb +8 -54
- data/lib/feature_map/private/code_cov.rb +2 -29
- data/lib/feature_map/private/cyclomatic_complexity_calculator.rb +1 -7
- data/lib/feature_map/private/docs/index.html +2 -2
- data/lib/feature_map/private/documentation_site.rb +0 -16
- data/lib/feature_map/private/extension_loader.rb +0 -3
- data/lib/feature_map/private/feature_assigner.rb +0 -4
- data/lib/feature_map/private/feature_metrics_calculator.rb +2 -16
- data/lib/feature_map/private/feature_plugins/assignment.rb +0 -6
- data/lib/feature_map/private/glob_cache.rb +2 -29
- data/lib/feature_map/private/health_calculator.rb +122 -0
- data/lib/feature_map/private/lines_of_code_calculator.rb +10 -21
- data/lib/feature_map/private/metrics_file.rb +1 -25
- data/lib/feature_map/private/percentile_metrics_calculator.rb +117 -0
- data/lib/feature_map/private/release_notification_builder.rb +1 -13
- data/lib/feature_map/private/test_coverage_file.rb +12 -39
- data/lib/feature_map/private/test_pyramid_file.rb +0 -41
- data/lib/feature_map/private/todo_inspector.rb +16 -30
- data/lib/feature_map/private/validations/features_up_to_date.rb +1 -6
- data/lib/feature_map/private/validations/files_have_features.rb +2 -7
- data/lib/feature_map/private/validations/files_have_unique_features.rb +1 -6
- data/lib/feature_map/private.rb +7 -44
- data/lib/feature_map/validator.rb +0 -13
- data/lib/feature_map.rb +8 -49
- metadata +4 -44
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b56fb3fcd3da34a3f15aa1a15714c37287c2dc9117ceb02a5df849ba076e9376
|
4
|
+
data.tar.gz: 644e0c180a214481117b8d612f6dc400ae8a5eda7bbe171061f03bc1ddb73a95
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
data/lib/feature_map/cli.rb
CHANGED
@@ -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 <<
|
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
|
-
|
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 =
|
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
|
-
|
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
|
data/lib/feature_map/commit.rb
CHANGED
@@ -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
|
5
|
-
|
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
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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 =
|
8
|
-
MULTILINE_COMMENT_START_PATTERNS =
|
9
|
-
MULTILINE_COMMENT_END_PATTERNS =
|
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
|
data/lib/feature_map/mapper.rb
CHANGED
@@ -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 =
|
35
|
+
feature_additional_metrics = {}
|
59
36
|
|
60
|
-
|
61
|
-
|
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' =>
|
74
|
-
'encapsulation' =>
|
75
|
-
'feature_size' =>
|
76
|
-
'test_coverage' =>
|
77
|
-
'
|
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
|