feature_map 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (37) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +269 -0
  3. data/bin/featuremap +5 -0
  4. data/lib/feature_map/cli.rb +243 -0
  5. data/lib/feature_map/code_features/plugin.rb +79 -0
  6. data/lib/feature_map/code_features/plugins/identity.rb +39 -0
  7. data/lib/feature_map/code_features.rb +152 -0
  8. data/lib/feature_map/configuration.rb +43 -0
  9. data/lib/feature_map/constants.rb +11 -0
  10. data/lib/feature_map/mapper.rb +78 -0
  11. data/lib/feature_map/output_color.rb +42 -0
  12. data/lib/feature_map/private/assignment_mappers/directory_assignment.rb +150 -0
  13. data/lib/feature_map/private/assignment_mappers/feature_definition_assignment.rb +68 -0
  14. data/lib/feature_map/private/assignment_mappers/feature_globs.rb +138 -0
  15. data/lib/feature_map/private/assignment_mappers/file_annotations.rb +158 -0
  16. data/lib/feature_map/private/assignments_file.rb +190 -0
  17. data/lib/feature_map/private/code_cov.rb +96 -0
  18. data/lib/feature_map/private/cyclomatic_complexity_calculator.rb +46 -0
  19. data/lib/feature_map/private/docs/index.html +247 -0
  20. data/lib/feature_map/private/documentation_site.rb +128 -0
  21. data/lib/feature_map/private/extension_loader.rb +24 -0
  22. data/lib/feature_map/private/feature_assigner.rb +22 -0
  23. data/lib/feature_map/private/feature_metrics_calculator.rb +76 -0
  24. data/lib/feature_map/private/feature_plugins/assignment.rb +17 -0
  25. data/lib/feature_map/private/glob_cache.rb +80 -0
  26. data/lib/feature_map/private/lines_of_code_calculator.rb +49 -0
  27. data/lib/feature_map/private/metrics_file.rb +86 -0
  28. data/lib/feature_map/private/test_coverage_file.rb +97 -0
  29. data/lib/feature_map/private/test_pyramid_file.rb +151 -0
  30. data/lib/feature_map/private/todo_inspector.rb +57 -0
  31. data/lib/feature_map/private/validations/features_up_to_date.rb +78 -0
  32. data/lib/feature_map/private/validations/files_have_features.rb +45 -0
  33. data/lib/feature_map/private/validations/files_have_unique_features.rb +34 -0
  34. data/lib/feature_map/private.rb +204 -0
  35. data/lib/feature_map/validator.rb +29 -0
  36. data/lib/feature_map.rb +212 -0
  37. metadata +253 -0
@@ -0,0 +1,212 @@
1
+ # frozen_string_literal: true
2
+
3
+ # typed: strict
4
+
5
+ require 'set'
6
+ require 'sorbet-runtime'
7
+ require 'json'
8
+ require 'yaml'
9
+ require 'feature_map/code_features'
10
+ require 'feature_map/mapper'
11
+ require 'feature_map/validator'
12
+ require 'feature_map/private'
13
+ require 'feature_map/cli'
14
+ require 'feature_map/configuration'
15
+
16
+ module FeatureMap
17
+ module_function
18
+
19
+ extend T::Sig
20
+ extend T::Helpers
21
+
22
+ requires_ancestor { Kernel }
23
+ GlobsToAssignedFeatureMap = T.type_alias { T::Hash[String, CodeFeatures::Feature] }
24
+
25
+ sig { params(file: String).returns(T.nilable(CodeFeatures::Feature)) }
26
+ def for_file(file)
27
+ @for_file ||= T.let(@for_file, T.nilable(T::Hash[String, T.nilable(CodeFeatures::Feature)]))
28
+ @for_file ||= {}
29
+
30
+ return nil if file.start_with?('./')
31
+ return @for_file[file] if @for_file.key?(file)
32
+
33
+ Private.load_configuration!
34
+
35
+ feature = T.let(nil, T.nilable(CodeFeatures::Feature))
36
+
37
+ Mapper.all.each do |mapper|
38
+ feature = mapper.map_file_to_feature(file)
39
+ break if feature # TODO: what if there are multiple features? Should we respond with an error instead of the first match?
40
+ end
41
+
42
+ @for_file[file] = feature
43
+ end
44
+
45
+ sig { params(feature: T.any(CodeFeatures::Feature, String)).returns(String) }
46
+ def for_feature(feature)
47
+ feature = T.must(CodeFeatures.find(feature)) if feature.is_a?(String)
48
+ feature_report = T.let([], T::Array[String])
49
+
50
+ feature_report << "# Report for `#{feature.name}` Feature"
51
+
52
+ Private.glob_cache.raw_cache_contents.each do |mapper_description, glob_to_assigned_feature_map|
53
+ feature_report << "## #{mapper_description}"
54
+ file_assignments_for_mapper = []
55
+ glob_to_assigned_feature_map.each do |glob, assigned_feature|
56
+ next if assigned_feature != feature
57
+
58
+ file_assignments_for_mapper << "- #{glob}"
59
+ end
60
+
61
+ if file_assignments_for_mapper.empty?
62
+ feature_report << 'This feature does not have any files in this category.'
63
+ else
64
+ feature_report += file_assignments_for_mapper.sort
65
+ end
66
+
67
+ feature_report << ''
68
+ end
69
+
70
+ feature_report.join("\n")
71
+ end
72
+
73
+ class InvalidFeatureMapConfigurationError < StandardError
74
+ end
75
+
76
+ sig { params(filename: String).void }
77
+ def self.remove_file_annotation!(filename)
78
+ Private::AssignmentMappers::FileAnnotations.new.remove_file_annotation!(filename)
79
+ end
80
+
81
+ sig do
82
+ params(
83
+ autocorrect: T::Boolean,
84
+ stage_changes: T::Boolean,
85
+ files: T.nilable(T::Array[String])
86
+ ).void
87
+ end
88
+ def validate!(
89
+ autocorrect: true,
90
+ stage_changes: true,
91
+ files: nil
92
+ )
93
+ Private.load_configuration!
94
+
95
+ tracked_file_subset = if files
96
+ files.select { |f| Private.file_tracked?(f) }
97
+ else
98
+ Private.tracked_files
99
+ end
100
+
101
+ Private.validate!(files: tracked_file_subset, autocorrect: autocorrect, stage_changes: stage_changes)
102
+ end
103
+
104
+ sig { params(git_ref: T.nilable(String)).void }
105
+ def generate_docs!(git_ref)
106
+ Private.generate_docs!(git_ref)
107
+ end
108
+
109
+ sig do
110
+ params(
111
+ unit_path: String,
112
+ integration_path: String,
113
+ regression_path: String,
114
+ regression_assignments_path: String
115
+ ).void
116
+ end
117
+ def generate_test_pyramid!(unit_path, integration_path, regression_path, regression_assignments_path)
118
+ Private.generate_test_pyramid!(unit_path, integration_path, regression_path, regression_assignments_path)
119
+ end
120
+
121
+ sig { params(commit_sha: String, code_cov_token: String).void }
122
+ def gather_test_coverage!(commit_sha, code_cov_token)
123
+ Private.gather_test_coverage!(commit_sha, code_cov_token)
124
+ end
125
+
126
+ # Given a backtrace from either `Exception#backtrace` or `caller`, find the
127
+ # first line that corresponds to a file with an assigned feature
128
+ sig { params(backtrace: T.nilable(T::Array[String]), excluded_features: T::Array[CodeFeatures::Feature]).returns(T.nilable(CodeFeatures::Feature)) }
129
+ def for_backtrace(backtrace, excluded_features: [])
130
+ first_assigned_file_for_backtrace(backtrace, excluded_features: excluded_features)&.first
131
+ end
132
+
133
+ # Given a backtrace from either `Exception#backtrace` or `caller`, find the
134
+ # first assigned file in it, useful for figuring out which file is being blamed.
135
+ sig { params(backtrace: T.nilable(T::Array[String]), excluded_features: T::Array[CodeFeatures::Feature]).returns(T.nilable([CodeFeatures::Feature, String])) }
136
+ def first_assigned_file_for_backtrace(backtrace, excluded_features: [])
137
+ backtrace_with_feature_assignments(backtrace).each do |(feature, file)|
138
+ if feature && !excluded_features.include?(feature)
139
+ return [feature, file]
140
+ end
141
+ end
142
+
143
+ nil
144
+ end
145
+
146
+ sig { params(backtrace: T.nilable(T::Array[String])).returns(T::Enumerable[[T.nilable(CodeFeatures::Feature), String]]) }
147
+ def backtrace_with_feature_assignments(backtrace)
148
+ return [] unless backtrace
149
+
150
+ # The pattern for a backtrace hasn't changed in forever and is considered
151
+ # stable: https://github.com/ruby/ruby/blob/trunk/vm_backtrace.c#L303-L317
152
+ #
153
+ # This pattern matches a line like the following:
154
+ #
155
+ # ./app/controllers/some_controller.rb:43:in `block (3 levels) in create'
156
+ #
157
+ backtrace_line = %r{\A(#{Pathname.pwd}/|\./)?
158
+ (?<file>.+) # Matches 'app/controllers/some_controller.rb'
159
+ :
160
+ (?<line>\d+) # Matches '43'
161
+ :in\s
162
+ `(?<function>.*)' # Matches "`block (3 levels) in create'"
163
+ \z}x
164
+
165
+ backtrace.lazy.filter_map do |line|
166
+ match = line.match(backtrace_line)
167
+ next unless match
168
+
169
+ file = T.must(match[:file])
170
+
171
+ [
172
+ FeatureMap.for_file(file),
173
+ file
174
+ ]
175
+ end
176
+ end
177
+ private_class_method(:backtrace_with_feature_assignments)
178
+
179
+ sig { params(klass: T.nilable(T.any(T::Class[T.anything], Module))).returns(T.nilable(CodeFeatures::Feature)) }
180
+ def for_class(klass)
181
+ @memoized_values ||= T.let(@memoized_values, T.nilable(T::Hash[String, T.nilable(CodeFeatures::Feature)]))
182
+ @memoized_values ||= {}
183
+ # We use key because the memoized value could be `nil`
184
+ if @memoized_values.key?(klass.to_s)
185
+ @memoized_values[klass.to_s]
186
+ else
187
+ path = Private.path_from_klass(klass)
188
+ return nil if path.nil?
189
+
190
+ value_to_memoize = for_file(path)
191
+ @memoized_values[klass.to_s] = value_to_memoize
192
+ value_to_memoize
193
+ end
194
+ end
195
+
196
+ # Generally, you should not ever need to do this, because once your ruby process loads, cached content should not change.
197
+ # Namely, the set of files, and directories which are tracked for feature assignment should not change.
198
+ # The primary reason this is helpful is for clients of FeatureMap who want to test their code, and each test context
199
+ # has different feature assignments and tracked files.
200
+ sig { void }
201
+ def self.bust_caches!
202
+ @for_file = nil
203
+ @memoized_values = nil
204
+ Private.bust_caches!
205
+ Mapper.all.each(&:bust_caches!)
206
+ end
207
+
208
+ sig { returns(Configuration) }
209
+ def self.configuration
210
+ Private.configuration
211
+ end
212
+ end
metadata ADDED
@@ -0,0 +1,253 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: feature_map
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Beyond Finance
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-01-30 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: code_ownership
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.34'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.34'
27
+ - !ruby/object:Gem::Dependency
28
+ name: faraday
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.7'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.7'
41
+ - !ruby/object:Gem::Dependency
42
+ name: packs-specification
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: sorbet-runtime
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '0.5'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '0.5'
83
+ - !ruby/object:Gem::Dependency
84
+ name: debug
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '1.9'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '1.9'
97
+ - !ruby/object:Gem::Dependency
98
+ name: railties
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '7.2'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '7.2'
111
+ - !ruby/object:Gem::Dependency
112
+ name: rake
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '13.2'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '13.2'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rspec
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '3.0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '3.0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: sorbet
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '0.5'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '0.5'
153
+ - !ruby/object:Gem::Dependency
154
+ name: tapioca
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: '0.16'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: '0.16'
167
+ - !ruby/object:Gem::Dependency
168
+ name: webmock
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: '3.24'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - "~>"
179
+ - !ruby/object:Gem::Version
180
+ version: '3.24'
181
+ description: FeatureMap helps identify and manage features within large Ruby and Rails
182
+ applications. This gem works best in large, usually monolithic code bases for applications
183
+ that incorporate a wide range of features with various dependencies.
184
+ email:
185
+ - engineering@beyondfinance.com
186
+ executables:
187
+ - featuremap
188
+ extensions: []
189
+ extra_rdoc_files: []
190
+ files:
191
+ - README.md
192
+ - bin/featuremap
193
+ - lib/feature_map.rb
194
+ - lib/feature_map/cli.rb
195
+ - lib/feature_map/code_features.rb
196
+ - lib/feature_map/code_features/plugin.rb
197
+ - lib/feature_map/code_features/plugins/identity.rb
198
+ - lib/feature_map/configuration.rb
199
+ - lib/feature_map/constants.rb
200
+ - lib/feature_map/mapper.rb
201
+ - lib/feature_map/output_color.rb
202
+ - lib/feature_map/private.rb
203
+ - lib/feature_map/private/assignment_mappers/directory_assignment.rb
204
+ - lib/feature_map/private/assignment_mappers/feature_definition_assignment.rb
205
+ - lib/feature_map/private/assignment_mappers/feature_globs.rb
206
+ - lib/feature_map/private/assignment_mappers/file_annotations.rb
207
+ - lib/feature_map/private/assignments_file.rb
208
+ - lib/feature_map/private/code_cov.rb
209
+ - lib/feature_map/private/cyclomatic_complexity_calculator.rb
210
+ - lib/feature_map/private/docs/index.html
211
+ - lib/feature_map/private/documentation_site.rb
212
+ - lib/feature_map/private/extension_loader.rb
213
+ - lib/feature_map/private/feature_assigner.rb
214
+ - lib/feature_map/private/feature_metrics_calculator.rb
215
+ - lib/feature_map/private/feature_plugins/assignment.rb
216
+ - lib/feature_map/private/glob_cache.rb
217
+ - lib/feature_map/private/lines_of_code_calculator.rb
218
+ - lib/feature_map/private/metrics_file.rb
219
+ - lib/feature_map/private/test_coverage_file.rb
220
+ - lib/feature_map/private/test_pyramid_file.rb
221
+ - lib/feature_map/private/todo_inspector.rb
222
+ - lib/feature_map/private/validations/features_up_to_date.rb
223
+ - lib/feature_map/private/validations/files_have_features.rb
224
+ - lib/feature_map/private/validations/files_have_unique_features.rb
225
+ - lib/feature_map/validator.rb
226
+ homepage: https://github.com/Beyond-Finance/feature_map
227
+ licenses:
228
+ - MIT
229
+ metadata:
230
+ homepage_uri: https://github.com/Beyond-Finance/feature_map
231
+ source_code_uri: https://github.com/Beyond-Finance/feature_map
232
+ changelog_uri: https://github.com/Beyond-Finance/feature_map/releases
233
+ allowed_push_host: https://rubygems.org
234
+ post_install_message:
235
+ rdoc_options: []
236
+ require_paths:
237
+ - lib
238
+ required_ruby_version: !ruby/object:Gem::Requirement
239
+ requirements:
240
+ - - ">="
241
+ - !ruby/object:Gem::Version
242
+ version: '2.6'
243
+ required_rubygems_version: !ruby/object:Gem::Requirement
244
+ requirements:
245
+ - - ">="
246
+ - !ruby/object:Gem::Version
247
+ version: '0'
248
+ requirements: []
249
+ rubygems_version: 3.4.10
250
+ signing_key:
251
+ specification_version: 4
252
+ summary: A gem to help identify and manage features within large Ruby and Rails applications
253
+ test_files: []