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,12 +1,8 @@
|
|
1
|
-
# typed: strict
|
2
1
|
# frozen_string_literal: true
|
3
2
|
|
4
3
|
module FeatureMap
|
5
4
|
module Private
|
6
5
|
class AssignmentApplicator
|
7
|
-
extend T::Sig
|
8
|
-
|
9
|
-
sig { params(assignments: T::Array[T::Array[T.nilable(String)]]).void }
|
10
6
|
def self.apply_assignments!(assignments)
|
11
7
|
file_to_feature_map = map_files_to_feature
|
12
8
|
assignments.each do |(filepath, feature)|
|
@@ -17,7 +13,6 @@ module FeatureMap
|
|
17
13
|
end
|
18
14
|
end
|
19
15
|
|
20
|
-
sig { params(filepath: String, feature: String).void }
|
21
16
|
def self.apply_assignment(filepath, feature)
|
22
17
|
return apply_to_directory(filepath, feature) if File.directory?(filepath)
|
23
18
|
|
@@ -42,7 +37,6 @@ module FeatureMap
|
|
42
37
|
end
|
43
38
|
end
|
44
39
|
|
45
|
-
sig { params(file: T::Array[String], filepath: String, feature: String).void }
|
46
40
|
def self.apply_to_apex(file, filepath, feature)
|
47
41
|
File.open(filepath, 'w') do |f|
|
48
42
|
f.write("// @feature #{feature}\n\n")
|
@@ -50,14 +44,12 @@ module FeatureMap
|
|
50
44
|
end
|
51
45
|
end
|
52
46
|
|
53
|
-
sig { params(filepath: String, feature: String).void }
|
54
47
|
def self.apply_to_directory(filepath, feature)
|
55
48
|
feature_path = File.join(filepath, '.feature')
|
56
49
|
|
57
50
|
File.write(feature_path, "#{feature}\n")
|
58
51
|
end
|
59
52
|
|
60
|
-
sig { params(file: T::Array[String], filepath: String, feature: String).void }
|
61
53
|
def self.apply_to_html(file, filepath, feature)
|
62
54
|
File.open(filepath, 'w') do |f|
|
63
55
|
f.write("<!-- @feature #{feature} -->\n\n")
|
@@ -65,7 +57,6 @@ module FeatureMap
|
|
65
57
|
end
|
66
58
|
end
|
67
59
|
|
68
|
-
sig { params(file: T::Array[String], filepath: String, feature: String).void }
|
69
60
|
def self.apply_to_javascript(file, filepath, feature)
|
70
61
|
File.open(filepath, 'w') do |f|
|
71
62
|
f.write("// @feature #{feature}\n\n")
|
@@ -73,7 +64,6 @@ module FeatureMap
|
|
73
64
|
end
|
74
65
|
end
|
75
66
|
|
76
|
-
sig { params(file: T::Array[String], filepath: String, feature: String).void }
|
77
67
|
def self.apply_to_ruby(file, filepath, feature)
|
78
68
|
File.open(filepath, 'w') do |f|
|
79
69
|
# NOTE: No spacing newline; doing so would separate
|
@@ -86,7 +76,6 @@ module FeatureMap
|
|
86
76
|
end
|
87
77
|
end
|
88
78
|
|
89
|
-
sig { params(file: T::Array[String], filepath: String, feature: String).void }
|
90
79
|
def self.apply_to_xml(file, filepath, feature)
|
91
80
|
# NOTE: Installation of top-level comments in some XML files (notably, in Salesforce)
|
92
81
|
# breaks parsing. Instead, we'll insert them right after the opening XML declaration.
|
@@ -99,7 +88,6 @@ module FeatureMap
|
|
99
88
|
end
|
100
89
|
end
|
101
90
|
|
102
|
-
sig { returns(T::Hash[String, String]) }
|
103
91
|
def self.map_files_to_feature
|
104
92
|
Private.feature_file_assignments.reduce({}) do |content, (feature_name, files)|
|
105
93
|
mapped_files = files.to_h { |f| [f, feature_name] }
|
@@ -1,29 +1,19 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# typed: true
|
4
|
-
|
5
3
|
module FeatureMap
|
6
4
|
module Private
|
7
5
|
module AssignmentMappers
|
8
6
|
class DirectoryAssignment
|
9
|
-
extend T::Sig
|
10
7
|
include Mapper
|
11
8
|
|
12
9
|
FEATURE_DIRECTORY_ASSIGNMENT_FILE_NAME = '.feature'
|
13
10
|
|
14
|
-
@@directory_cache =
|
11
|
+
@@directory_cache = {} # rubocop:disable Style/ClassVars
|
15
12
|
|
16
|
-
sig do
|
17
|
-
override.params(file: String)
|
18
|
-
.returns(T.nilable(CodeFeatures::Feature))
|
19
|
-
end
|
20
13
|
def map_file_to_feature(file)
|
21
14
|
map_file_to_relevant_feature(file)
|
22
15
|
end
|
23
16
|
|
24
|
-
sig do
|
25
|
-
override.params(cache: GlobsToAssignedFeatureMap, files: T::Array[String]).returns(GlobsToAssignedFeatureMap)
|
26
|
-
end
|
27
17
|
def update_cache(cache, files)
|
28
18
|
globs_to_feature(files)
|
29
19
|
end
|
@@ -36,14 +26,8 @@ module FeatureMap
|
|
36
26
|
# but in practice this is not of consequence because in reality we never really want to generate feature assignments for only a
|
37
27
|
# subset of files, but rather we want feature assignments for all files.
|
38
28
|
#
|
39
|
-
sig do
|
40
|
-
override.params(files: T::Array[String])
|
41
|
-
.returns(T::Hash[String, CodeFeatures::Feature])
|
42
|
-
end
|
43
29
|
def globs_to_feature(files)
|
44
|
-
|
45
|
-
T
|
46
|
-
.unsafe(Pathname)
|
30
|
+
Pathname
|
47
31
|
.glob(File.join('**/', FEATURE_DIRECTORY_ASSIGNMENT_FILE_NAME))
|
48
32
|
.map(&:cleanpath)
|
49
33
|
.each_with_object({}) do |pathname, res|
|
@@ -53,19 +37,16 @@ module FeatureMap
|
|
53
37
|
end
|
54
38
|
end
|
55
39
|
|
56
|
-
sig { override.returns(String) }
|
57
40
|
def description
|
58
41
|
'Feature Assigned in .feature'
|
59
42
|
end
|
60
43
|
|
61
|
-
sig { override.void }
|
62
44
|
def bust_caches!
|
63
45
|
@@directory_cache = {} # rubocop:disable Style/ClassVars
|
64
46
|
end
|
65
47
|
|
66
48
|
private
|
67
49
|
|
68
|
-
sig { params(file: Pathname).returns(CodeFeatures::Feature) }
|
69
50
|
def feature_for_directory_assignment_file(file)
|
70
51
|
raw_feature_value = File.foreach(file).first.strip
|
71
52
|
|
@@ -80,10 +61,9 @@ module FeatureMap
|
|
80
61
|
# and `.feature` in that order, stopping at the first file to actually exist.
|
81
62
|
# If the provided file is a directory, it will look for `.feature` in that directory and then upwards.
|
82
63
|
# We do additional caching so that we don't have to check for file existence every time.
|
83
|
-
sig { params(file: String).returns(T.nilable(CodeFeatures::Feature)) }
|
84
64
|
def map_file_to_relevant_feature(file)
|
85
65
|
file_path = Pathname.new(file)
|
86
|
-
feature =
|
66
|
+
feature = nil
|
87
67
|
|
88
68
|
if File.directory?(file)
|
89
69
|
feature = get_feature_from_assignment_file_within_directory(file_path)
|
@@ -97,7 +77,7 @@ module FeatureMap
|
|
97
77
|
|
98
78
|
(path_components.length - 1).downto(0).each do |i|
|
99
79
|
feature = get_feature_from_assignment_file_within_directory(
|
100
|
-
Pathname.new(File.join(*
|
80
|
+
Pathname.new(File.join(*path_components[0...i]))
|
101
81
|
)
|
102
82
|
return feature unless feature.nil?
|
103
83
|
end
|
@@ -105,7 +85,6 @@ module FeatureMap
|
|
105
85
|
feature
|
106
86
|
end
|
107
87
|
|
108
|
-
sig { params(directory: Pathname).returns(T.nilable(CodeFeatures::Feature)) }
|
109
88
|
def get_feature_from_assignment_file_within_directory(directory)
|
110
89
|
potential_directory_assignment_file = directory.join(FEATURE_DIRECTORY_ASSIGNMENT_FILE_NAME)
|
111
90
|
|
@@ -125,7 +104,6 @@ module FeatureMap
|
|
125
104
|
feature
|
126
105
|
end
|
127
106
|
|
128
|
-
sig { params(file: Pathname).returns(String) }
|
129
107
|
def glob_for_directory_assignment_file(file)
|
130
108
|
unescaped = file.dirname.cleanpath.join('**/**').to_s
|
131
109
|
|
@@ -1,21 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# typed: true
|
4
|
-
|
5
3
|
module FeatureMap
|
6
4
|
module Private
|
7
5
|
module AssignmentMappers
|
8
6
|
class FeatureDefinitionAssignment
|
9
|
-
extend T::Sig
|
10
7
|
include Mapper
|
11
8
|
|
12
|
-
@@map_files_to_features =
|
9
|
+
@@map_files_to_features = @map_files_to_features # rubocop:disable Style/ClassVars
|
13
10
|
@@map_files_to_features = {} # rubocop:disable Style/ClassVars
|
14
11
|
|
15
|
-
sig do
|
16
|
-
params(files: T::Array[String])
|
17
|
-
.returns(T::Hash[String, CodeFeatures::Feature])
|
18
|
-
end
|
19
12
|
def map_files_to_features(files)
|
20
13
|
return @@map_files_to_features if @@map_files_to_features&.keys && @@map_files_to_features.keys.count.positive?
|
21
14
|
|
@@ -24,20 +17,12 @@ module FeatureMap
|
|
24
17
|
end
|
25
18
|
end
|
26
19
|
|
27
|
-
sig do
|
28
|
-
override.params(file: String)
|
29
|
-
.returns(T.nilable(CodeFeatures::Feature))
|
30
|
-
end
|
31
20
|
def map_file_to_feature(file)
|
32
21
|
return nil if Private.configuration.ignore_feature_definitions
|
33
22
|
|
34
23
|
map_files_to_features([file])[file]
|
35
24
|
end
|
36
25
|
|
37
|
-
sig do
|
38
|
-
override.params(files: T::Array[String])
|
39
|
-
.returns(T::Hash[String, CodeFeatures::Feature])
|
40
|
-
end
|
41
26
|
def globs_to_feature(files)
|
42
27
|
return {} if Private.configuration.ignore_feature_definitions
|
43
28
|
|
@@ -46,19 +31,14 @@ module FeatureMap
|
|
46
31
|
end
|
47
32
|
end
|
48
33
|
|
49
|
-
sig { override.void }
|
50
34
|
def bust_caches!
|
51
35
|
@@map_files_to_features = {} # rubocop:disable Style/ClassVars
|
52
36
|
end
|
53
37
|
|
54
|
-
sig do
|
55
|
-
override.params(cache: GlobsToAssignedFeatureMap, files: T::Array[String]).returns(GlobsToAssignedFeatureMap)
|
56
|
-
end
|
57
38
|
def update_cache(cache, files)
|
58
39
|
globs_to_feature(files)
|
59
40
|
end
|
60
41
|
|
61
|
-
sig { override.returns(String) }
|
62
42
|
def description
|
63
43
|
'Feature definition file assignment'
|
64
44
|
end
|
@@ -1,22 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# typed: true
|
4
|
-
|
5
3
|
module FeatureMap
|
6
4
|
module Private
|
7
5
|
module AssignmentMappers
|
8
6
|
class FeatureGlobs
|
9
|
-
extend T::Sig
|
10
7
|
include Mapper
|
11
8
|
include Validator
|
12
9
|
|
13
|
-
@@map_files_to_features =
|
10
|
+
@@map_files_to_features = @map_files_to_features # rubocop:disable Style/ClassVars
|
14
11
|
@@map_files_to_features = {} # rubocop:disable Style/ClassVars
|
15
12
|
|
16
|
-
sig do
|
17
|
-
params(files: T::Array[String])
|
18
|
-
.returns(T::Hash[String, FeatureMap::CodeFeatures::Feature])
|
19
|
-
end
|
20
13
|
def map_files_to_features(files)
|
21
14
|
return @@map_files_to_features if @@map_files_to_features&.keys && @@map_files_to_features.keys.count.positive?
|
22
15
|
|
@@ -29,17 +22,8 @@ module FeatureMap
|
|
29
22
|
end
|
30
23
|
end
|
31
24
|
|
32
|
-
|
33
|
-
|
34
|
-
const :feature, FeatureMap::CodeFeatures::Feature
|
35
|
-
end
|
36
|
-
|
37
|
-
class GlobOverlap < T::Struct
|
38
|
-
extend T::Sig
|
39
|
-
|
40
|
-
const :mapping_contexts, T::Array[MappingContext]
|
41
|
-
|
42
|
-
sig { returns(String) }
|
25
|
+
MappingContext = Struct.new(:glob, :feature, keyword_init: true)
|
26
|
+
GlobOverlap = Struct.new(:mapping_contexts, keyword_init: true) do
|
43
27
|
def description
|
44
28
|
# These are sorted only to prevent non-determinism in output between local and CI environments.
|
45
29
|
sorted_contexts = mapping_contexts.sort_by { |context| context.feature.config_yml.to_s }
|
@@ -51,21 +35,18 @@ module FeatureMap
|
|
51
35
|
end
|
52
36
|
end
|
53
37
|
|
54
|
-
sig do
|
55
|
-
returns(T::Array[GlobOverlap])
|
56
|
-
end
|
57
38
|
def find_overlapping_globs
|
58
|
-
mapped_files =
|
39
|
+
mapped_files = {}
|
59
40
|
FeatureMap::CodeFeatures.all.each_with_object({}) do |feature, _map|
|
60
41
|
FeaturePlugins::Assignment.for(feature).assigned_globs.each do |glob|
|
61
42
|
Dir.glob(glob).each do |filename|
|
62
43
|
mapped_files[filename] ||= []
|
63
|
-
|
44
|
+
mapped_files[filename] << MappingContext.new(glob: glob, feature: feature)
|
64
45
|
end
|
65
46
|
end
|
66
47
|
end
|
67
48
|
|
68
|
-
overlaps =
|
49
|
+
overlaps = []
|
69
50
|
mapped_files.each_value do |mapping_contexts|
|
70
51
|
if mapping_contexts.count > 1
|
71
52
|
overlaps << GlobOverlap.new(mapping_contexts: mapping_contexts)
|
@@ -79,25 +60,14 @@ module FeatureMap
|
|
79
60
|
end
|
80
61
|
end
|
81
62
|
|
82
|
-
sig do
|
83
|
-
override.params(file: String)
|
84
|
-
.returns(T.nilable(FeatureMap::CodeFeatures::Feature))
|
85
|
-
end
|
86
63
|
def map_file_to_feature(file)
|
87
64
|
map_files_to_features([file])[file]
|
88
65
|
end
|
89
66
|
|
90
|
-
sig do
|
91
|
-
override.params(cache: GlobsToAssignedFeatureMap, files: T::Array[String]).returns(GlobsToAssignedFeatureMap)
|
92
|
-
end
|
93
67
|
def update_cache(cache, files)
|
94
68
|
globs_to_feature(files)
|
95
69
|
end
|
96
70
|
|
97
|
-
sig do
|
98
|
-
override.params(files: T::Array[String])
|
99
|
-
.returns(T::Hash[String, FeatureMap::CodeFeatures::Feature])
|
100
|
-
end
|
101
71
|
def globs_to_feature(files)
|
102
72
|
FeatureMap::CodeFeatures.all.each_with_object({}) do |feature, map|
|
103
73
|
FeaturePlugins::Assignment.for(feature).assigned_globs.each do |assigned_glob|
|
@@ -106,21 +76,18 @@ module FeatureMap
|
|
106
76
|
end
|
107
77
|
end
|
108
78
|
|
109
|
-
sig { override.void }
|
110
79
|
def bust_caches!
|
111
80
|
@@map_files_to_features = {} # rubocop:disable Style/ClassVars
|
112
81
|
end
|
113
82
|
|
114
|
-
sig { override.returns(String) }
|
115
83
|
def description
|
116
84
|
'Feature-specific assigned globs'
|
117
85
|
end
|
118
86
|
|
119
|
-
sig { override.params(files: T::Array[String], autocorrect: T::Boolean, stage_changes: T::Boolean).returns(T::Array[String]) }
|
120
87
|
def validation_errors(files:, autocorrect: true, stage_changes: true)
|
121
88
|
overlapping_globs = AssignmentMappers::FeatureGlobs.new.find_overlapping_globs
|
122
89
|
|
123
|
-
errors =
|
90
|
+
errors = []
|
124
91
|
|
125
92
|
if overlapping_globs.any?
|
126
93
|
errors << <<~MSG
|
@@ -1,7 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# typed: strict
|
4
|
-
|
5
3
|
module FeatureMap
|
6
4
|
module Private
|
7
5
|
module AssignmentMappers
|
@@ -15,45 +13,31 @@ module FeatureMap
|
|
15
13
|
# ...
|
16
14
|
# }
|
17
15
|
class FileAnnotations
|
18
|
-
extend T::Sig
|
19
16
|
include Mapper
|
20
17
|
|
21
18
|
# NOTE: regex 'x' arg ignores whitespace within the _construction_ of the regex.
|
22
19
|
# regex 'm' arg allows the regex to _execute_ on multiline strings.
|
23
|
-
SINGLE_LINE_ANNOTATION_PATTERN =
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
(?<feature>.*?$) # A named capture grabs the rest as the feature until the line ends
|
39
|
-
/xm.freeze,
|
40
|
-
Regexp
|
41
|
-
)
|
20
|
+
SINGLE_LINE_ANNOTATION_PATTERN = /
|
21
|
+
\s* # Any amount of whitespace
|
22
|
+
(#{Constants::SINGLE_LINE_COMMENT_PATTERNS.join('|')}) # Single line comment start
|
23
|
+
\s* # Any amount of whitespace, not including newlines
|
24
|
+
@feature\s # We find the feature annotation followed by one space
|
25
|
+
(?<feature>.*?$) # A named capture grabs the rest as the feature until the line ends
|
26
|
+
/x.freeze
|
27
|
+
|
28
|
+
MULTILINE_ANNOTATION_PATTERN = /
|
29
|
+
(?:#{Constants::MULTILINE_COMMENT_START_PATTERNS.join('|')}) # Any comment start
|
30
|
+
.*? # Followed by any characters, including newlines, until...
|
31
|
+
@feature\s # We find the feature annotation followed by one space
|
32
|
+
(?<feature>.*?$) # A named capture grabs the rest as the feature until the line ends
|
33
|
+
/xm.freeze
|
34
|
+
|
42
35
|
DESCRIPTION = 'Annotations at the top of file'
|
43
36
|
|
44
|
-
sig do
|
45
|
-
override.params(file: String)
|
46
|
-
.returns(T.nilable(CodeFeatures::Feature))
|
47
|
-
end
|
48
37
|
def map_file_to_feature(file)
|
49
38
|
file_annotation_based_feature(file)
|
50
39
|
end
|
51
40
|
|
52
|
-
sig do
|
53
|
-
override
|
54
|
-
.params(files: T::Array[String])
|
55
|
-
.returns(T::Hash[String, CodeFeatures::Feature])
|
56
|
-
end
|
57
41
|
def globs_to_feature(files)
|
58
42
|
files.each_with_object({}) do |filename_relative_to_root, mapping|
|
59
43
|
feature = file_annotation_based_feature(filename_relative_to_root)
|
@@ -63,9 +47,6 @@ module FeatureMap
|
|
63
47
|
end
|
64
48
|
end
|
65
49
|
|
66
|
-
sig do
|
67
|
-
override.params(cache: GlobsToAssignedFeatureMap, files: T::Array[String]).returns(GlobsToAssignedFeatureMap)
|
68
|
-
end
|
69
50
|
def update_cache(cache, files)
|
70
51
|
# We map files to nil features so that files whose annotation have been removed will be properly
|
71
52
|
# overwritten (i.e. removed) from the cache.
|
@@ -89,23 +70,21 @@ module FeatureMap
|
|
89
70
|
cache
|
90
71
|
end
|
91
72
|
|
92
|
-
sig { params(lines: T::Array[String]).returns(T.nilable(String)) }
|
93
73
|
def identify_feature_from(lines)
|
94
74
|
matched_single_line_feature = lines.join("\n").match(SINGLE_LINE_ANNOTATION_PATTERN)
|
95
75
|
matched_multiline_feature = lines.join("\n").match(MULTILINE_ANNOTATION_PATTERN)
|
96
76
|
matched_feature = matched_single_line_feature || matched_multiline_feature
|
97
77
|
return if matched_feature.nil?
|
98
78
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
79
|
+
matched_feature
|
80
|
+
.values_at(:feature)
|
81
|
+
.first
|
82
|
+
.gsub(/#{Constants::MULTILINE_COMMENT_END_PATTERNS.join('|')}/, '')
|
83
|
+
.strip
|
104
84
|
rescue ArgumentError => e
|
105
85
|
raise unless e.message.include?('invalid byte sequence')
|
106
86
|
end
|
107
87
|
|
108
|
-
sig { params(filename: String).returns(T.nilable(CodeFeatures::Feature)) }
|
109
88
|
def file_annotation_based_feature(filename)
|
110
89
|
# Not too sure what the following comment means but it was carried over from the code_ownership repo, so
|
111
90
|
# I've opted to leave it unchanged in case it is helpful for future engineers:
|
@@ -131,7 +110,6 @@ module FeatureMap
|
|
131
110
|
)
|
132
111
|
end
|
133
112
|
|
134
|
-
sig { params(filename: String).void }
|
135
113
|
def remove_file_annotation!(filename)
|
136
114
|
if file_annotation_based_feature(filename)
|
137
115
|
filepath = Pathname.new(filename)
|
@@ -145,12 +123,10 @@ module FeatureMap
|
|
145
123
|
end
|
146
124
|
end
|
147
125
|
|
148
|
-
sig { override.returns(String) }
|
149
126
|
def description
|
150
127
|
DESCRIPTION
|
151
128
|
end
|
152
129
|
|
153
|
-
sig { override.void }
|
154
130
|
def bust_caches!; end
|
155
131
|
end
|
156
132
|
end
|
@@ -1,4 +1,3 @@
|
|
1
|
-
# typed: strict
|
2
1
|
# frozen_string_literal: true
|
3
2
|
|
4
3
|
require 'code_ownership'
|
@@ -11,8 +10,6 @@ module FeatureMap
|
|
11
10
|
# PR/release announcements, documentation generation, etc).
|
12
11
|
#
|
13
12
|
class AssignmentsFile
|
14
|
-
extend T::Sig
|
15
|
-
|
16
13
|
class FileContentError < StandardError; end
|
17
14
|
|
18
15
|
FILES_KEY = 'files'
|
@@ -21,42 +18,6 @@ module FeatureMap
|
|
21
18
|
FEATURES_KEY = 'features'
|
22
19
|
FEATURE_FILES_KEY = 'files'
|
23
20
|
|
24
|
-
FeatureName = T.type_alias { String }
|
25
|
-
FilePath = T.type_alias { String }
|
26
|
-
MapperDescription = T.type_alias { String }
|
27
|
-
|
28
|
-
FileDetails = T.type_alias do
|
29
|
-
T::Hash[
|
30
|
-
String,
|
31
|
-
T.any(FeatureName, MapperDescription)
|
32
|
-
]
|
33
|
-
end
|
34
|
-
|
35
|
-
FilesContent = T.type_alias do
|
36
|
-
T::Hash[
|
37
|
-
FilePath,
|
38
|
-
FileDetails
|
39
|
-
]
|
40
|
-
end
|
41
|
-
|
42
|
-
FileList = T.type_alias { T::Array[String] }
|
43
|
-
TeamList = T.type_alias { T::Array[String] }
|
44
|
-
|
45
|
-
FeatureDetails = T.type_alias do
|
46
|
-
T::Hash[
|
47
|
-
String,
|
48
|
-
T.any(FileList, TeamList)
|
49
|
-
]
|
50
|
-
end
|
51
|
-
|
52
|
-
FeaturesContent = T.type_alias do
|
53
|
-
T::Hash[
|
54
|
-
FeatureName,
|
55
|
-
FeatureDetails
|
56
|
-
]
|
57
|
-
end
|
58
|
-
|
59
|
-
sig { returns(T::Array[String]) }
|
60
21
|
def self.actual_contents_lines
|
61
22
|
if path.exist?
|
62
23
|
content = path.read
|
@@ -70,7 +31,6 @@ module FeatureMap
|
|
70
31
|
end
|
71
32
|
end
|
72
33
|
|
73
|
-
sig { returns(T::Array[T.nilable(String)]) }
|
74
34
|
def self.expected_contents_lines
|
75
35
|
cache = Private.glob_cache.raw_cache_contents
|
76
36
|
|
@@ -84,9 +44,9 @@ module FeatureMap
|
|
84
44
|
# set of files assigned to a feature change, which should be explicitly tracked.
|
85
45
|
HEADER
|
86
46
|
|
87
|
-
files_content =
|
88
|
-
files_by_feature =
|
89
|
-
features_content =
|
47
|
+
files_content = {}
|
48
|
+
files_by_feature = {}
|
49
|
+
features_content = {}
|
90
50
|
|
91
51
|
cache.each do |mapper_description, assignment_map_cache|
|
92
52
|
assignment_map_cache = assignment_map_cache.sort_by do |glob, _feature|
|
@@ -94,10 +54,10 @@ module FeatureMap
|
|
94
54
|
end
|
95
55
|
|
96
56
|
assignment_map_cache.to_h.each do |path, feature|
|
97
|
-
files_content[path] =
|
57
|
+
files_content[path] = { FILE_FEATURE_KEY => feature.name, FILE_MAPPER_KEY => mapper_description }
|
98
58
|
|
99
59
|
files_by_feature[feature.name] ||= []
|
100
|
-
|
60
|
+
files_by_feature[feature.name] << path
|
101
61
|
end
|
102
62
|
end
|
103
63
|
|
@@ -111,10 +71,10 @@ module FeatureMap
|
|
111
71
|
# repo/application.
|
112
72
|
next if expanded_files.empty?
|
113
73
|
|
114
|
-
features_content[feature.name] =
|
74
|
+
features_content[feature.name] = { 'files' => expanded_files.sort }
|
115
75
|
|
116
76
|
if !Private.configuration.skip_code_ownership
|
117
|
-
|
77
|
+
features_content[feature.name]['teams'] = expanded_files.map { |file| CodeOwnership.for_file(file)&.name }.compact.uniq.sort
|
118
78
|
end
|
119
79
|
end
|
120
80
|
|
@@ -126,18 +86,15 @@ module FeatureMap
|
|
126
86
|
]
|
127
87
|
end
|
128
88
|
|
129
|
-
sig { void }
|
130
89
|
def self.write!
|
131
90
|
FileUtils.mkdir_p(path.dirname) if !path.dirname.exist?
|
132
91
|
path.write(expected_contents_lines.join("\n"))
|
133
92
|
end
|
134
93
|
|
135
|
-
sig { returns(Pathname) }
|
136
94
|
def self.path
|
137
95
|
Pathname.pwd.join('.feature_map/assignments.yml')
|
138
96
|
end
|
139
97
|
|
140
|
-
sig { params(files: T::Array[String]).void }
|
141
98
|
def self.update_cache!(files)
|
142
99
|
cache = Private.glob_cache
|
143
100
|
# Each mapper returns a new copy of the cache subset related to that mapper,
|
@@ -149,14 +106,12 @@ module FeatureMap
|
|
149
106
|
end
|
150
107
|
end
|
151
108
|
|
152
|
-
sig { returns(T::Boolean) }
|
153
109
|
def self.use_features_cache?
|
154
110
|
AssignmentsFile.path.exist? && !Private.configuration.skip_features_validation
|
155
111
|
end
|
156
112
|
|
157
|
-
sig { returns(GlobCache) }
|
158
113
|
def self.to_glob_cache
|
159
|
-
raw_cache_contents =
|
114
|
+
raw_cache_contents = {}
|
160
115
|
features_by_name = CodeFeatures.all.each_with_object({}) do |feature, map|
|
161
116
|
map[feature.name] = feature
|
162
117
|
end
|
@@ -175,7 +130,6 @@ module FeatureMap
|
|
175
130
|
GlobCache.new(raw_cache_contents)
|
176
131
|
end
|
177
132
|
|
178
|
-
sig { returns(FeaturesContent) }
|
179
133
|
def self.load_features!
|
180
134
|
assignments_content = YAML.load_file(path)
|
181
135
|
|