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
@@ -1,4 +1,3 @@
|
|
1
|
-
# typed: strict
|
2
1
|
# frozen_string_literal: true
|
3
2
|
|
4
3
|
module FeatureMap
|
@@ -9,44 +8,21 @@ module FeatureMap
|
|
9
8
|
# a variety of engineering team utilities (e.g. PR/release announcements, documentation generation, etc).
|
10
9
|
#
|
11
10
|
class TestCoverageFile
|
12
|
-
extend T::Sig
|
13
|
-
|
14
|
-
COVERAGE_RATIO = 'coverage_ratio'
|
15
|
-
|
16
11
|
class FileContentError < StandardError; end
|
17
12
|
|
13
|
+
COVERAGE_RATIO = 'coverage_ratio'
|
18
14
|
FEATURES_KEY = 'features'
|
19
15
|
|
20
|
-
FeatureName = T.type_alias { String }
|
21
|
-
CoverageStat = T.type_alias { String }
|
22
|
-
|
23
|
-
FeatureCoverage = T.type_alias do
|
24
|
-
T::Hash[
|
25
|
-
CoverageStat,
|
26
|
-
Integer
|
27
|
-
]
|
28
|
-
end
|
29
|
-
|
30
|
-
FeaturesContent = T.type_alias do
|
31
|
-
T::Hash[
|
32
|
-
FeatureName,
|
33
|
-
FeatureCoverage
|
34
|
-
]
|
35
|
-
end
|
36
|
-
|
37
|
-
sig { params(coverage_stats: CodeCov::TestCoverageStats).void }
|
38
16
|
def self.write!(coverage_stats)
|
39
17
|
FileUtils.mkdir_p(path.dirname) if !path.dirname.exist?
|
40
18
|
|
41
19
|
path.write([header_comment, "\n", generate_content(coverage_stats).to_yaml].join)
|
42
20
|
end
|
43
21
|
|
44
|
-
sig { returns(Pathname) }
|
45
22
|
def self.path
|
46
23
|
Pathname.pwd.join('.feature_map/test-coverage.yml')
|
47
24
|
end
|
48
25
|
|
49
|
-
sig { returns(String) }
|
50
26
|
def self.header_comment
|
51
27
|
<<~HEADER
|
52
28
|
# STOP! - DO NOT EDIT THIS FILE MANUALLY
|
@@ -61,34 +37,31 @@ module FeatureMap
|
|
61
37
|
HEADER
|
62
38
|
end
|
63
39
|
|
64
|
-
sig { params(coverage_stats: CodeCov::TestCoverageStats).returns(T::Hash[String, FeaturesContent]) }
|
65
40
|
def self.generate_content(coverage_stats)
|
66
|
-
feature_test_coverage =
|
41
|
+
feature_test_coverage = {}
|
67
42
|
|
68
43
|
Private.feature_file_assignments.each do |feature_name, files|
|
69
|
-
feature_test_coverage[feature_name] =
|
44
|
+
feature_test_coverage[feature_name] = { 'lines' => 0, 'hits' => 0, 'misses' => 0, 'coverage_ratio' => 0 }
|
70
45
|
|
71
|
-
files.each_with_object(
|
46
|
+
files.each_with_object(feature_test_coverage[feature_name]) do |file_path, coverage|
|
72
47
|
next unless coverage_stats[file_path]
|
73
48
|
|
74
|
-
coverage['lines'] =
|
75
|
-
coverage['hits'] =
|
76
|
-
coverage['misses'] =
|
49
|
+
coverage['lines'] = coverage['lines'] + (coverage_stats[file_path]['lines'] || 0)
|
50
|
+
coverage['hits'] = coverage['hits'] + (coverage_stats[file_path]['hits'] || 0)
|
51
|
+
coverage['misses'] = coverage['misses'] + (coverage_stats[file_path]['misses'] || 0)
|
77
52
|
|
78
53
|
coverage
|
79
54
|
end
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
end
|
55
|
+
feature_test_coverage[feature_name][COVERAGE_RATIO] = if feature_test_coverage[feature_name]['lines'].zero?
|
56
|
+
0
|
57
|
+
else
|
58
|
+
((feature_test_coverage[feature_name]['hits'].to_f / feature_test_coverage[feature_name]['lines']) * 100).round
|
59
|
+
end
|
86
60
|
end
|
87
61
|
|
88
62
|
{ FEATURES_KEY => feature_test_coverage }
|
89
63
|
end
|
90
64
|
|
91
|
-
sig { returns(FeaturesContent) }
|
92
65
|
def self.load_features!
|
93
66
|
test_coverage_content = YAML.load_file(path)
|
94
67
|
|
@@ -1,4 +1,3 @@
|
|
1
|
-
# typed: strict
|
2
1
|
# frozen_string_literal: true
|
3
2
|
|
4
3
|
module FeatureMap
|
@@ -9,37 +8,10 @@ module FeatureMap
|
|
9
8
|
# (e.g. PR/release announcements, documentation generation, etc).
|
10
9
|
#
|
11
10
|
class TestPyramidFile
|
12
|
-
extend T::Sig
|
13
|
-
|
14
11
|
class FileContentError < StandardError; end
|
15
12
|
|
16
13
|
FEATURES_KEY = 'features'
|
17
14
|
|
18
|
-
FeatureName = T.type_alias { String }
|
19
|
-
PyramidStat = T.type_alias { String }
|
20
|
-
|
21
|
-
FeaturePyramid = T.type_alias do
|
22
|
-
T::Hash[
|
23
|
-
PyramidStat,
|
24
|
-
Integer
|
25
|
-
]
|
26
|
-
end
|
27
|
-
|
28
|
-
FeaturesContent = T.type_alias do
|
29
|
-
T::Hash[
|
30
|
-
FeatureName,
|
31
|
-
FeaturePyramid
|
32
|
-
]
|
33
|
-
end
|
34
|
-
|
35
|
-
sig do
|
36
|
-
params(
|
37
|
-
unit_examples: T::Array[T::Hash[T.untyped, T.untyped]],
|
38
|
-
integration_examples: T::Array[T::Hash[T.untyped, T.untyped]],
|
39
|
-
regression_examples: T::Array[T::Hash[T.untyped, T.untyped]],
|
40
|
-
regression_assignments: T::Hash[T.untyped, T.untyped]
|
41
|
-
).void
|
42
|
-
end
|
43
15
|
def self.write!(unit_examples, integration_examples, regression_examples, regression_assignments)
|
44
16
|
FileUtils.mkdir_p(path.dirname) if !path.dirname.exist?
|
45
17
|
|
@@ -56,12 +28,10 @@ module FeatureMap
|
|
56
28
|
path.write([header_comment, "\n", { FEATURES_KEY => content }.to_yaml].join)
|
57
29
|
end
|
58
30
|
|
59
|
-
sig { returns(Pathname) }
|
60
31
|
def self.path
|
61
32
|
Pathname.pwd.join('.feature_map/test-pyramid.yml')
|
62
33
|
end
|
63
34
|
|
64
|
-
sig { returns(String) }
|
65
35
|
def self.header_comment
|
66
36
|
<<~HEADER
|
67
37
|
# STOP! - DO NOT EDIT THIS FILE MANUALLY
|
@@ -76,14 +46,6 @@ module FeatureMap
|
|
76
46
|
HEADER
|
77
47
|
end
|
78
48
|
|
79
|
-
sig do
|
80
|
-
params(
|
81
|
-
unit_examples: T::Hash[T.untyped, T.untyped],
|
82
|
-
integration_examples: T::Hash[T.untyped, T.untyped],
|
83
|
-
regression_examples: T::Hash[T.untyped, T.untyped],
|
84
|
-
regression_file_assignments: T::Hash[T.untyped, T.untyped]
|
85
|
-
).returns(FeaturesContent)
|
86
|
-
end
|
87
49
|
def self.generate_content(unit_examples, integration_examples, regression_examples, regression_file_assignments)
|
88
50
|
Private.feature_file_assignments.reduce({}) do |content, (feature_name, files)|
|
89
51
|
regression_files = regression_file_assignments[feature_name] || []
|
@@ -118,7 +80,6 @@ module FeatureMap
|
|
118
80
|
end
|
119
81
|
end
|
120
82
|
|
121
|
-
sig { returns(FeaturesContent) }
|
122
83
|
def self.load_features!
|
123
84
|
test_coverage_content = YAML.load_file(path)
|
124
85
|
|
@@ -131,14 +92,12 @@ module FeatureMap
|
|
131
92
|
raise FileContentError, "No feature test coverage file found at #{path}. Use `bin/featuremap test_coverage` to generate it and try again."
|
132
93
|
end
|
133
94
|
|
134
|
-
sig { params(examples: T.nilable(T::Array[T::Hash[T.untyped, T.untyped]])).returns(T::Array[Integer]) }
|
135
95
|
def self.split(examples)
|
136
96
|
return [0, 0] if examples.nil?
|
137
97
|
|
138
98
|
examples.partition { |ex| ex['status'] == 'passed' }.map(&:count)
|
139
99
|
end
|
140
100
|
|
141
|
-
sig { params(pathlike: String).returns(String) }
|
142
101
|
def self.filepath(pathlike)
|
143
102
|
File
|
144
103
|
.join(File.dirname(pathlike), File.basename(pathlike, '.*'))
|
@@ -1,50 +1,36 @@
|
|
1
|
-
# typed: strict
|
2
1
|
# frozen_string_literal: true
|
3
2
|
|
4
3
|
module FeatureMap
|
5
4
|
module Private
|
6
5
|
class TodoInspector
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
)
|
22
|
-
|
23
|
-
TODO_PATTERN = T.let(
|
24
|
-
/
|
25
|
-
TODO:?\s* # TODO with optional colon with whitespace
|
26
|
-
(?<content>.*?) # The actual TODO content
|
27
|
-
(#{Constants::MULTILINE_COMMENT_END_PATTERNS.join('|')})?$ # ignores comment end patterns
|
28
|
-
/xi.freeze,
|
29
|
-
Regexp
|
30
|
-
)
|
31
|
-
|
32
|
-
sig { params(file_path: String).void }
|
6
|
+
ENTERING_COMMENT = /
|
7
|
+
(#{(Constants::SINGLE_LINE_COMMENT_PATTERNS + Constants::MULTILINE_COMMENT_START_PATTERNS).join('|')})
|
8
|
+
/x.freeze
|
9
|
+
|
10
|
+
EXITING_COMMENT = /
|
11
|
+
(#{(Constants::SINGLE_LINE_COMMENT_PATTERNS + Constants::MULTILINE_COMMENT_END_PATTERNS).join('|')})
|
12
|
+
/x.freeze
|
13
|
+
|
14
|
+
TODO_PATTERN = /
|
15
|
+
TODO:?\s* # TODO with optional colon with whitespace
|
16
|
+
(?<content>.*?) # The actual TODO content
|
17
|
+
(#{Constants::MULTILINE_COMMENT_END_PATTERNS.join('|')})?$ # ignores comment end patterns
|
18
|
+
/xi.freeze
|
19
|
+
|
33
20
|
def initialize(file_path)
|
34
21
|
@file_path = file_path
|
35
22
|
end
|
36
23
|
|
37
|
-
sig { returns(T::Hash[String, String]) }
|
38
24
|
def calculate
|
39
25
|
todos = {}
|
40
26
|
content = File.read(@file_path)
|
41
|
-
in_comment =
|
27
|
+
in_comment = false
|
42
28
|
|
43
29
|
content.each_line.with_index do |line, index|
|
44
30
|
in_comment ||= line.match?(ENTERING_COMMENT)
|
45
31
|
|
46
32
|
if in_comment && (match = line.match(TODO_PATTERN))
|
47
|
-
todos["#{@file_path}:#{index + 1}"] =
|
33
|
+
todos["#{@file_path}:#{index + 1}"] = match[:content].strip
|
48
34
|
end
|
49
35
|
|
50
36
|
in_comment &&= !line.match?(EXITING_COMMENT)
|
@@ -1,14 +1,9 @@
|
|
1
|
-
# typed: strict
|
2
|
-
|
3
1
|
module FeatureMap
|
4
2
|
module Private
|
5
3
|
module Validations
|
6
4
|
class FeaturesUpToDate
|
7
|
-
extend T::Sig
|
8
|
-
extend T::Helpers
|
9
5
|
include Validator
|
10
6
|
|
11
|
-
sig { override.params(files: T::Array[String], autocorrect: T::Boolean, stage_changes: T::Boolean).returns(T::Array[String]) }
|
12
7
|
def validation_errors(files:, autocorrect: true, stage_changes: true)
|
13
8
|
return [] if Private.configuration.skip_features_validation
|
14
9
|
|
@@ -16,7 +11,7 @@ module FeatureMap
|
|
16
11
|
expected_content_lines = AssignmentsFile.expected_contents_lines
|
17
12
|
|
18
13
|
features_file_up_to_date = actual_content_lines == expected_content_lines
|
19
|
-
errors =
|
14
|
+
errors = []
|
20
15
|
|
21
16
|
if !features_file_up_to_date
|
22
17
|
if autocorrect
|
@@ -1,16 +1,11 @@
|
|
1
|
-
# typed: strict
|
2
|
-
|
3
1
|
require 'code_ownership'
|
4
2
|
|
5
3
|
module FeatureMap
|
6
4
|
module Private
|
7
5
|
module Validations
|
8
6
|
class FilesHaveFeatures
|
9
|
-
extend T::Sig
|
10
|
-
extend T::Helpers
|
11
7
|
include Validator
|
12
8
|
|
13
|
-
sig { override.params(files: T::Array[String], autocorrect: T::Boolean, stage_changes: T::Boolean).returns(T::Array[String]) }
|
14
9
|
def validation_errors(files:, autocorrect: true, stage_changes: true)
|
15
10
|
cache = Private.glob_cache
|
16
11
|
file_mappings = cache.mapper_descriptions_that_map_files(files)
|
@@ -18,14 +13,14 @@ module FeatureMap
|
|
18
13
|
mapper_descriptions.count.zero?
|
19
14
|
end
|
20
15
|
|
21
|
-
errors =
|
16
|
+
errors = []
|
22
17
|
|
23
18
|
# When a set of teams are configured that require assignments, ignore any files NOT
|
24
19
|
# assigned to one of these teams.
|
25
20
|
unless Private.configuration.require_assignment_for_teams.nil?
|
26
21
|
files_not_mapped_at_all.filter! do |file, _mappers|
|
27
22
|
file_team = CodeOwnership.for_file(file)
|
28
|
-
file_team &&
|
23
|
+
file_team && Private.configuration.require_assignment_for_teams.include?(file_team.name)
|
29
24
|
end
|
30
25
|
end
|
31
26
|
|
@@ -1,14 +1,9 @@
|
|
1
|
-
# typed: strict
|
2
|
-
|
3
1
|
module FeatureMap
|
4
2
|
module Private
|
5
3
|
module Validations
|
6
4
|
class FilesHaveUniqueFeatures
|
7
|
-
extend T::Sig
|
8
|
-
extend T::Helpers
|
9
5
|
include Validator
|
10
6
|
|
11
|
-
sig { override.params(files: T::Array[String], autocorrect: T::Boolean, stage_changes: T::Boolean).returns(T::Array[String]) }
|
12
7
|
def validation_errors(files:, autocorrect: true, stage_changes: true)
|
13
8
|
cache = Private.glob_cache
|
14
9
|
file_mappings = cache.mapper_descriptions_that_map_files(files)
|
@@ -16,7 +11,7 @@ module FeatureMap
|
|
16
11
|
mapper_descriptions.count > 1
|
17
12
|
end
|
18
13
|
|
19
|
-
errors =
|
14
|
+
errors = []
|
20
15
|
|
21
16
|
if files_mapped_by_multiple_mappers.any?
|
22
17
|
errors << <<~MSG
|
data/lib/feature_map/private.rb
CHANGED
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# typed: strict
|
4
|
-
|
5
3
|
require 'code_ownership'
|
6
4
|
require 'csv'
|
7
5
|
|
@@ -11,6 +9,8 @@ require 'feature_map/private/cyclomatic_complexity_calculator'
|
|
11
9
|
require 'feature_map/private/lines_of_code_calculator'
|
12
10
|
require 'feature_map/private/todo_inspector'
|
13
11
|
require 'feature_map/private/feature_metrics_calculator'
|
12
|
+
require 'feature_map/private/health_calculator'
|
13
|
+
require 'feature_map/private/percentile_metrics_calculator'
|
14
14
|
require 'feature_map/private/assignments_file'
|
15
15
|
require 'feature_map/private/assignment_applicator'
|
16
16
|
require 'feature_map/private/metrics_file'
|
@@ -33,45 +33,29 @@ require 'feature_map/private/release_notification_builder'
|
|
33
33
|
|
34
34
|
module FeatureMap
|
35
35
|
module Private
|
36
|
-
extend T::Sig
|
37
|
-
|
38
|
-
FeatureName = T.type_alias { String }
|
39
|
-
FileList = T.type_alias { T::Array[String] }
|
40
|
-
FeatureFiles = T.type_alias do
|
41
|
-
T::Hash[
|
42
|
-
FeatureName,
|
43
|
-
FileList
|
44
|
-
]
|
45
|
-
end
|
46
|
-
|
47
|
-
sig { params(assignments_file_path: String).void }
|
48
36
|
def self.apply_assignments!(assignments_file_path)
|
49
37
|
assignments = CSV.read(assignments_file_path)
|
50
38
|
AssignmentApplicator.apply_assignments!(assignments)
|
51
39
|
end
|
52
40
|
|
53
|
-
sig { returns(Configuration) }
|
54
41
|
def self.configuration
|
55
|
-
@configuration ||= T.let(@configuration, T.nilable(Configuration))
|
56
42
|
@configuration ||= Configuration.fetch
|
57
43
|
end
|
58
44
|
|
59
45
|
# This is just an alias for `configuration` that makes it more explicit what we're doing instead of just calling `configuration`.
|
60
46
|
# This is necessary because configuration may contain extensions of feature map, so those extensions should be loaded prior to
|
61
47
|
# calling APIs that provide feature assignment information.
|
62
|
-
|
48
|
+
|
63
49
|
def self.load_configuration!
|
64
50
|
configuration
|
65
51
|
end
|
66
52
|
|
67
|
-
sig { void }
|
68
53
|
def self.bust_caches!
|
69
54
|
@configuration = nil
|
70
55
|
@tracked_files = nil
|
71
56
|
@glob_cache = nil
|
72
57
|
end
|
73
58
|
|
74
|
-
sig { params(files: T::Array[String], autocorrect: T::Boolean, stage_changes: T::Boolean).void }
|
75
59
|
def self.validate!(files:, autocorrect: true, stage_changes: true)
|
76
60
|
AssignmentsFile.update_cache!(files) if AssignmentsFile.use_features_cache?
|
77
61
|
|
@@ -91,7 +75,6 @@ module FeatureMap
|
|
91
75
|
MetricsFile.write!
|
92
76
|
end
|
93
77
|
|
94
|
-
sig { params(git_ref: T.nilable(String)).void }
|
95
78
|
def self.generate_docs!(git_ref)
|
96
79
|
feature_assignments = AssignmentsFile.load_features!
|
97
80
|
feature_metrics = MetricsFile.load_features!
|
@@ -115,18 +98,10 @@ module FeatureMap
|
|
115
98
|
feature_test_pyramid,
|
116
99
|
feature_additional_metrics,
|
117
100
|
configuration.raw_hash,
|
118
|
-
|
101
|
+
git_ref || configuration.repository['main_branch']
|
119
102
|
)
|
120
103
|
end
|
121
104
|
|
122
|
-
sig do
|
123
|
-
params(
|
124
|
-
unit_path: String,
|
125
|
-
integration_path: String,
|
126
|
-
regression_path: T.nilable(String),
|
127
|
-
regression_assignments_path: T.nilable(String)
|
128
|
-
).void
|
129
|
-
end
|
130
105
|
def self.generate_test_pyramid!(unit_path, integration_path, regression_path, regression_assignments_path)
|
131
106
|
unit_examples = JSON.parse(File.read(unit_path))&.fetch('examples')
|
132
107
|
integration_examples = JSON.parse(File.read(integration_path))&.fetch('examples')
|
@@ -135,14 +110,12 @@ module FeatureMap
|
|
135
110
|
TestPyramidFile.write!(unit_examples, integration_examples, regression_examples, regression_assignments)
|
136
111
|
end
|
137
112
|
|
138
|
-
sig { params(commit_sha: String, code_cov_token: String).void }
|
139
113
|
def self.gather_test_coverage!(commit_sha, code_cov_token)
|
140
114
|
coverage_stats = CodeCov.fetch_coverage_stats(commit_sha, code_cov_token)
|
141
115
|
|
142
116
|
TestCoverageFile.write!(coverage_stats)
|
143
117
|
end
|
144
118
|
|
145
|
-
sig { void }
|
146
119
|
def self.generate_additional_metrics!
|
147
120
|
feature_metrics = MetricsFile.load_features!
|
148
121
|
feature_test_coverage = TestCoverageFile.path.exist? ? TestCoverageFile.load_features! : {}
|
@@ -152,7 +125,6 @@ module FeatureMap
|
|
152
125
|
|
153
126
|
# Returns a string version of the relative path to a Rails constant,
|
154
127
|
# or nil if it can't find something
|
155
|
-
sig { params(klass: T.nilable(T.any(T::Class[T.anything], Module))).returns(T.nilable(String)) }
|
156
128
|
def self.path_from_klass(klass)
|
157
129
|
if klass
|
158
130
|
path = Object.const_source_location(klass.to_s)&.first
|
@@ -167,13 +139,10 @@ module FeatureMap
|
|
167
139
|
#
|
168
140
|
# The output of this function is string pathnames relative to the root.
|
169
141
|
#
|
170
|
-
sig { returns(T::Array[String]) }
|
171
142
|
def self.tracked_files
|
172
|
-
@tracked_files ||= T.let(@tracked_files, T.nilable(T::Array[String]))
|
173
143
|
@tracked_files ||= Dir.glob(configuration.assigned_globs) - Dir.glob(configuration.unassigned_globs)
|
174
144
|
end
|
175
145
|
|
176
|
-
sig { params(file: String).returns(T::Boolean) }
|
177
146
|
def self.file_tracked?(file)
|
178
147
|
# Another way to accomplish this is
|
179
148
|
# (Dir.glob(configuration.assigned_globs) - Dir.glob(configuration.unassigned_globs)).include?(file)
|
@@ -190,7 +159,6 @@ module FeatureMap
|
|
190
159
|
in_assigned_globs && !in_unassigned_globs && File.exist?(file)
|
191
160
|
end
|
192
161
|
|
193
|
-
sig { params(feature_name: String, location_of_reference: String).returns(CodeFeatures::Feature) }
|
194
162
|
def self.find_feature!(feature_name, location_of_reference)
|
195
163
|
found_feature = CodeFeatures.find(feature_name)
|
196
164
|
if found_feature.nil?
|
@@ -200,9 +168,7 @@ module FeatureMap
|
|
200
168
|
end
|
201
169
|
end
|
202
170
|
|
203
|
-
sig { returns(GlobCache) }
|
204
171
|
def self.glob_cache
|
205
|
-
@glob_cache ||= T.let(@glob_cache, T.nilable(GlobCache))
|
206
172
|
@glob_cache ||= if AssignmentsFile.use_features_cache?
|
207
173
|
AssignmentsFile.to_glob_cache
|
208
174
|
else
|
@@ -210,20 +176,18 @@ module FeatureMap
|
|
210
176
|
end
|
211
177
|
end
|
212
178
|
|
213
|
-
sig { returns(FeatureFiles) }
|
214
179
|
def self.feature_file_assignments
|
215
|
-
glob_cache.raw_cache_contents.values.each_with_object(
|
180
|
+
glob_cache.raw_cache_contents.values.each_with_object({}) do |assignment_map_cache, feature_files|
|
216
181
|
assignment_map_cache.to_h.each do |path, feature|
|
217
|
-
feature_files[feature.name] ||=
|
182
|
+
feature_files[feature.name] ||= []
|
218
183
|
files = Dir.glob(path).reject { |glob_entry| File.directory?(glob_entry) }
|
219
|
-
files.each { |file|
|
184
|
+
files.each { |file| feature_files[feature.name] << file }
|
220
185
|
end
|
221
186
|
|
222
187
|
feature_files
|
223
188
|
end
|
224
189
|
end
|
225
190
|
|
226
|
-
sig { params(feature: CodeFeatures::Feature).returns(T::Array[CodeTeams::Team]) }
|
227
191
|
def self.all_teams_for_feature(feature)
|
228
192
|
return [] if configuration.skip_code_ownership
|
229
193
|
|
@@ -235,7 +199,6 @@ module FeatureMap
|
|
235
199
|
feature_files.map { |file| CodeOwnership.for_file(file) }.compact.uniq
|
236
200
|
end
|
237
201
|
|
238
|
-
sig { params(commits_by_feature: CommitsByFeature).returns(ReleaseNotificationBuilder::BlockKitPayload) }
|
239
202
|
def self.generate_release_notification(commits_by_feature)
|
240
203
|
ReleaseNotificationBuilder.build(commits_by_feature)
|
241
204
|
end
|
@@ -1,26 +1,13 @@
|
|
1
|
-
# typed: strict
|
2
|
-
|
3
1
|
module FeatureMap
|
4
2
|
module Validator
|
5
|
-
extend T::Sig
|
6
|
-
extend T::Helpers
|
7
|
-
|
8
|
-
interface!
|
9
|
-
|
10
|
-
sig { abstract.params(files: T::Array[String], autocorrect: T::Boolean, stage_changes: T::Boolean).returns(T::Array[String]) }
|
11
3
|
def validation_errors(files:, autocorrect: true, stage_changes: true); end
|
12
4
|
|
13
5
|
class << self
|
14
|
-
extend T::Sig
|
15
|
-
|
16
|
-
sig { params(base: T::Class[Validator]).void }
|
17
6
|
def included(base)
|
18
|
-
@validators ||= T.let(@validators, T.nilable(T::Array[T::Class[Validator]]))
|
19
7
|
@validators ||= []
|
20
8
|
@validators << base
|
21
9
|
end
|
22
10
|
|
23
|
-
sig { returns(T::Array[Validator]) }
|
24
11
|
def all
|
25
12
|
(@validators || []).map(&:new)
|
26
13
|
end
|