gem_bench 2.0.2 → 2.0.4

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.
@@ -1,14 +1,51 @@
1
1
  module GemBench
2
+ # Line by line parser of a Gemfile
3
+ # Uses regular expressions, which, I know, GROSS, but also meh
4
+ # You aren't using this as a runtime gem in your app are you?
5
+ # Let me know how you use it!
2
6
  class GemfileLineTokenizer
3
7
  GEM_REGEX = /\A\s*gem\s+([^#]*).*\Z/.freeze # run against gem lines like: "gem 'aftership', # Ruby SDK of AfterShip API."
4
- GEM_NAME_REGEX = /\A\s*gem\s+['"]{1}(?<name>[^'"]*)['"].*\Z/.freeze # run against gem lines like: "gem 'aftership', # Ruby SDK of AfterShip API."
5
- VERSION_CONSTRAINT = /['"]{1}([^'"]*)['"]/.freeze
8
+ # HEREDOC support? (?<op_heredoc><<[~-]?[A-Z0-9_]+\.?[a-z0-9_]+)
9
+ OP_QUO_REG_PROC = lambda { |idx = nil| /((?<op_quo#{idx}>['"]{1})|(?<op_pct_q#{idx}>%[Qq]?[\(\[\{]{1})|(?<op_heredoc><<[~-]?[A-Z0-9_]+\.?[a-z0-9_]+))/x }
10
+ # No close for the heredoc, as it will be on a different line...
11
+ CL_QUO_REG_PROC = lambda { |idx = nil| /((?<cl_quo#{idx}>['"])|(?<cl_pct_q#{idx}>[\)\]\}]{1}))/x }
12
+ GEM_NAME_REGEX = /\A\s*gem\s+#{OP_QUO_REG_PROC.call.source}(?<name>[^'")]*)#{CL_QUO_REG_PROC.call.source}?.*\Z/.freeze # run against gem lines like: "gem 'aftership', # Ruby SDK of AfterShip API."
13
+ VERSION_CONSTRAINT = /#{OP_QUO_REG_PROC.call.source}(?<version>[^'")]*)#{CL_QUO_REG_PROC.call.source}/.freeze
6
14
  GEMFILE_HASH_CONFIG_KEY_REGEX_PROC = lambda { |key|
7
- /\A\s*[^#]*(?<key1>#{key}: *)['"]{1}(?<value1>[^'"]*)['"]|(?<key2>['"]#{key}['"] *=> *)['"]{1}(?<value2>[^'"]*)['"]|(?<key3>:#{key} *=> *)['"]{1}(?<value3>[^'"]*)['"]/
15
+ /
16
+ \A\s*[^#]*
17
+ (
18
+ # when key is "branch" will find: `branch: "main"`
19
+ (?<key1>#{key}:\s*)
20
+ #{OP_QUO_REG_PROC.call("k1").source}(?<value1>[^'")]*)?#{CL_QUO_REG_PROC.call("k1").source}
21
+ )
22
+ |
23
+ (
24
+ # when key is "branch" will find: `"branch" => "main"`
25
+ (?<key2>#{OP_QUO_REG_PROC.call("k2a").source}#{key}#{CL_QUO_REG_PROC.call("k2a").source}\s*=>\s*)
26
+ #{OP_QUO_REG_PROC.call("k2b").source}(?<value2>[^'")]*)?#{CL_QUO_REG_PROC.call("k2b").source}
27
+ )
28
+ |
29
+ (
30
+ # when key is "branch" will find: `:branch => "main"`
31
+ (?<key3>:#{key}\s*=>\s*)
32
+ #{OP_QUO_REG_PROC.call("k3").source}(?<value3>[^'")]*)?#{CL_QUO_REG_PROC.call("k3").source}
33
+ )
34
+ |
35
+ (
36
+ # when key is "branch" will find: `"branch": "main"`
37
+ (?<key4>#{OP_QUO_REG_PROC.call("k4a").source}#{key}#{CL_QUO_REG_PROC.call("k4a").source}:\s*)
38
+ #{OP_QUO_REG_PROC.call("k4b").source}(?<value4>[^'")]*)?#{CL_QUO_REG_PROC.call("k4b").source}
39
+ )
40
+ /x
8
41
  }
9
42
  VERSION_PATH = GEMFILE_HASH_CONFIG_KEY_REGEX_PROC.call("path").freeze
10
43
  VERSION_GIT = GEMFILE_HASH_CONFIG_KEY_REGEX_PROC.call("git").freeze
11
44
  VERSION_GITHUB = GEMFILE_HASH_CONFIG_KEY_REGEX_PROC.call("github").freeze
45
+ VERSION_GITLAB = GEMFILE_HASH_CONFIG_KEY_REGEX_PROC.call("gitlab").freeze
46
+ VERSION_BITBUCKET = GEMFILE_HASH_CONFIG_KEY_REGEX_PROC.call("bitbucket").freeze
47
+ VERSION_CODEBERG = GEMFILE_HASH_CONFIG_KEY_REGEX_PROC.call("codeberg").freeze
48
+ VERSION_SRCHUT = GEMFILE_HASH_CONFIG_KEY_REGEX_PROC.call("srchut").freeze
12
49
  VERSION_GIT_REF = GEMFILE_HASH_CONFIG_KEY_REGEX_PROC.call("ref").freeze
13
50
  VERSION_GIT_TAG = GEMFILE_HASH_CONFIG_KEY_REGEX_PROC.call("tag").freeze
14
51
  VERSION_GIT_BRANCH = GEMFILE_HASH_CONFIG_KEY_REGEX_PROC.call("branch").freeze
@@ -17,13 +54,19 @@ module GemBench
17
54
  git_ref
18
55
  git_tag
19
56
  ]
20
- # branch is only valid if the branch is not master
57
+ STR_SYNTAX_TYPES = {
58
+ quoted: true,
59
+ pct_q: true,
60
+ # We could try to support HEREDOC via parsing the lines following in all_lines, but... ugh.
61
+ heredoc: false,
62
+ unknown: false,
63
+ }.freeze
21
64
  attr_reader :line
22
65
  attr_reader :relevant_lines, :is_gem, :all_lines, :index, :tokens, :version_type, :name, :parse_success, :valid
23
66
  # version will be a string if it is a normal constraint like '~> 1.2.3'
24
67
  # version will be a hash if it is an alternative constraint like:
25
68
  # git: "blah/blah", ref: "shasha"
26
- attr_reader :version
69
+ attr_reader :version, :str_syntax_type
27
70
 
28
71
  def initialize(all_lines, line, index)
29
72
  @line = line.strip
@@ -31,9 +74,9 @@ module GemBench
31
74
  if is_gem
32
75
  @all_lines = all_lines
33
76
  @index = index
34
- @tokens = self.line.split(",")
77
+ @tokens = self.line.split(",").map(&:strip)
35
78
  determine_name
36
- if name
79
+ if name && STR_SYNTAX_TYPES[str_syntax_type]
37
80
  determine_relevant_lines
38
81
  determine_version
39
82
  @parse_success = true
@@ -49,35 +92,61 @@ module GemBench
49
92
  private
50
93
 
51
94
  # not a gem line. noop.
95
+ #
96
+ # @return void
52
97
  def noop
53
98
  @parse_success = false
54
99
  @valid = false
100
+
101
+ nil
55
102
  end
56
103
 
104
+ # @return void
57
105
  def determine_name
58
106
  # uses @tokens[0] because the gem name must be before the first comma
59
107
  match_data = @tokens[0].match(GEM_NAME_REGEX)
108
+ @str_syntax_type =
109
+ if match_data[:op_quo] && match_data[:cl_quo]
110
+ :quoted
111
+ elsif match_data[:op_pct_q] && match_data[:cl_pct_q]
112
+ :pct_q
113
+ # Not handling heredoc, aside from not exploding, as it isn't a reasonable use case.
114
+ # elsif match_data[:op_heredoc]
115
+ # :heredoc
116
+ else
117
+ :unknown
118
+ end
60
119
  @name = match_data[:name]
120
+
121
+ nil
61
122
  end
62
123
 
124
+ # @return void
63
125
  def determine_relevant_lines
64
126
  @relevant_lines = [line, *following_non_gem_lines].compact
127
+
128
+ nil
65
129
  end
66
130
 
131
+ # @return void
67
132
  def determine_version
68
- version_path ||
69
- (
70
- (version_git || version_github) && (
71
- check_for_version_of_type_git_ref ||
72
- check_for_version_of_type_git_tag ||
73
- check_for_version_of_type_git_branch
74
- )
75
- ) ||
133
+ @version = {}
134
+ return if version_path
135
+
136
+ (
137
+ (version_git || version_provider) && (
138
+ check_for_version_of_type_git_tag ||
139
+ check_for_version_of_type_git_branch
140
+ )
141
+ ) ||
76
142
  # Needs to be the last check because it can only check for a quoted string,
77
143
  # and quoted strings are part of the other types, so they have to be checked first with higher specificity
78
144
  check_for_version_of_type_constraint
145
+
146
+ nil
79
147
  end
80
148
 
149
+ # @return [true, false]
81
150
  def check_for_version_of_type_constraint
82
151
  # index 1 of the comma-split tokens will usually be the version constraint, if there is one
83
152
  possible_constraint = @tokens[1]
@@ -85,7 +154,7 @@ module GemBench
85
154
 
86
155
  match_data = possible_constraint.strip.match(VERSION_CONSTRAINT)
87
156
  # the version constraint is in a regex capture group
88
- if match_data && (@version = match_data[1].strip)
157
+ if match_data && (@version = match_data[:version].strip)
89
158
  @version_type = :constraint
90
159
  true
91
160
  else
@@ -93,8 +162,8 @@ module GemBench
93
162
  end
94
163
  end
95
164
 
165
+ # @return [true, false]
96
166
  def version_path
97
- @version = {}
98
167
  line = relevant_lines.detect { |next_line| next_line.match(VERSION_PATH) }
99
168
  return false unless line
100
169
 
@@ -105,8 +174,8 @@ module GemBench
105
174
  )
106
175
  end
107
176
 
177
+ # @return [true, false]
108
178
  def version_git
109
- @version = {}
110
179
  line = relevant_lines.detect { |next_line| next_line.match(VERSION_GIT) }
111
180
  return false unless line
112
181
 
@@ -117,21 +186,40 @@ module GemBench
117
186
  )
118
187
  end
119
188
 
120
- def version_github
121
- @version = {}
122
- line = relevant_lines.detect { |next_line| next_line.match(VERSION_GITHUB) }
123
- return false unless line
189
+ # @return [true, false]
190
+ def version_provider
191
+ matcher = nil
192
+ line = relevant_lines.detect do |next_line|
193
+ matcher =
194
+ case next_line
195
+ when VERSION_GITHUB
196
+ VERSION_GITHUB
197
+ when VERSION_GITLAB
198
+ VERSION_GITLAB
199
+ when VERSION_BITBUCKET
200
+ VERSION_BITBUCKET
201
+ when VERSION_CODEBERG
202
+ VERSION_CODEBERG
203
+ when VERSION_SRCHUT
204
+ VERSION_SRCHUT
205
+ end
206
+ end
207
+ return false unless line && matcher
124
208
 
125
209
  enhance_version(
126
- line.match(VERSION_GITHUB),
210
+ line.match(matcher),
127
211
  :github,
128
212
  :github,
129
213
  )
130
214
  end
131
215
 
132
- def check_for_version_of_type_git_ref
216
+ # @return [true, false]
217
+ def check_for_version_of_type_git_branch
218
+ return false unless _check_for_version_of_type_git_branch
219
+
133
220
  line = relevant_lines.detect { |next_line| next_line.match(VERSION_GIT_REF) }
134
- return false unless line
221
+ # At this point we at least have a branch, though perhaps not a ref.
222
+ return true unless line
135
223
 
136
224
  enhance_version(
137
225
  line.match(VERSION_GIT_REF),
@@ -140,6 +228,7 @@ module GemBench
140
228
  )
141
229
  end
142
230
 
231
+ # @return [true, false]
143
232
  def check_for_version_of_type_git_tag
144
233
  line = relevant_lines.detect { |next_line| next_line.match(VERSION_GIT_TAG) }
145
234
  return false unless line
@@ -151,7 +240,8 @@ module GemBench
151
240
  )
152
241
  end
153
242
 
154
- def check_for_version_of_type_git_branch
243
+ # @return [true, false]
244
+ def _check_for_version_of_type_git_branch
155
245
  line = relevant_lines.detect { |next_line| next_line.match(VERSION_GIT_BRANCH) }
156
246
  return false unless line
157
247
 
@@ -162,7 +252,7 @@ module GemBench
162
252
  )
163
253
  end
164
254
 
165
- # returns an array with each line following the current line, which is not a gem line
255
+ # @return [Array[String]] - each line following the current line, which is not a gem line
166
256
  def following_non_gem_lines
167
257
  all_lines[(index + 1)..-1]
168
258
  .reject { |x| x.strip.empty? || x.match(GemBench::TRASH_REGEX) }
@@ -174,23 +264,25 @@ module GemBench
174
264
  end
175
265
  end
176
266
 
177
- # returns a hash like:
178
- # {"key" => ":git => ", "value" => "https://github.com/cte/aftership-sdk-ruby.git"}
179
- def normalize_match_data_captures(match_data)
180
- match_data.names.each_with_object({}) do |capture, mem|
181
- mem[capture.gsub(/\d/, "")] = match_data[capture]
182
- break mem if mem.keys.length >= 2
183
- end
267
+ # @return [String] the name of the named capture which has a value (one of: value1, value2, value3, etc.)
268
+ def determine_named_capture(match_data)
269
+ match_data.names
270
+ .select { |name| name.start_with?("value") }
271
+ .detect { |capture| !match_data[capture]&.empty? }
184
272
  end
185
273
 
274
+ # @return [true]
186
275
  def enhance_version(match_data, version_key, type)
187
- return false unless match_data
188
-
189
- normalized_capture = normalize_match_data_captures(match_data) if match_data
190
- return false unless normalized_capture
276
+ named_capture = determine_named_capture(match_data)
277
+ value = match_data[named_capture]
278
+ if value
279
+ @version[version_key] = value
280
+ @version_type = type
281
+ else
282
+ @version[version_key] = ""
283
+ @version_type = :invalid
284
+ end
191
285
 
192
- @version.merge!({version_key => normalized_capture["value"]})
193
- @version_type = type
194
286
  true
195
287
  end
196
288
  end
@@ -1,29 +1,32 @@
1
1
  # Std Libs Dependencies
2
2
  require "tmpdir"
3
3
 
4
- # Re-write a gem to a temp directory, re-namespace the primary namespace of that gem module, and load it.
5
- # If the original gem defines multiple top-level namespaces, they can all be renamed by providing more key value pairs.
6
- # If the original gem monkey patches other libraries, that behavior can't be isolated, so YMMV.
7
- #
8
- # NOTE: Non-top-level namespaces do not need to be renamed, as they are isolated within their parent namespace.
9
- #
10
- # Usage
11
- #
12
- # jersey = GemBench::Jersey.new(
13
- # gem_name: "alt_memery"
14
- # trades: {
15
- # "Memery" => "AltMemery"
16
- # },
17
- # metadata: {
18
- # something: "a value here",
19
- # something_else: :obviously,
20
- # },
21
- # )
22
- # jersey.doff_and_don
23
- # # The re-namespaced constant is now available!
24
- # AltMemery # => AltMemery
25
- #
26
4
  module GemBench
5
+ # Re-write a gem to a temp directory, re-namespace the primary namespace of that gem module, and load it.
6
+ # If the original gem defines multiple top-level namespaces, they can all be renamed by providing more key value pairs.
7
+ # If the original gem monkey patches other libraries, that behavior can't be isolated, so YMMV.
8
+ #
9
+ # NOTE: Non-top-level namespaces do not need to be renamed, as they are isolated within their parent namespace.
10
+ #
11
+ # Usage
12
+ #
13
+ # jersey = GemBench::Jersey.new(
14
+ # gem_name: "alt_memery"
15
+ # trades: {
16
+ # "Memery" => "AltMemery"
17
+ # },
18
+ # metadata: {
19
+ # something: "a value here",
20
+ # something_else: :obviously,
21
+ # },
22
+ # )
23
+ # jersey.doff_and_don
24
+ # # The re-namespaced constant is now available!
25
+ # AltMemery # => AltMemery
26
+ #
27
+ # Benchmarking Example
28
+ #
29
+ # See: https://github.com/panorama-ed/memo_wise/blob/main/benchmarks/benchmarks.rb
27
30
  class Jersey
28
31
  attr_reader :gem_name
29
32
  attr_reader :gem_path
@@ -41,110 +44,141 @@ module GemBench
41
44
  @verbose = verbose
42
45
  end
43
46
 
47
+ # return [true, false] proxy for whether the copied, re-namespaced gem has been successfully loaded
44
48
  def required?
45
49
  gem_path && trades.values.all? { |new_namespace| Object.const_defined?(new_namespace) }
46
50
  end
47
51
 
48
- # Generates tempfiles and requires them, resulting
52
+ # Generates a temp directory, and creates a copy of a gem within it.
53
+ # Re-namespaces the copy according to the `trades` configuration.
54
+ # Then requires each file of the "copied gem", resulting
49
55
  # in a loaded gem that will not have namespace
50
- # collisions when alongside the original-namespaced gem.
56
+ # collisions when loaded alongside the original-namespaced gem.
57
+ # Note that "copied gem" in the previous sentence is ambiguous without the supporting context.
58
+ # The "copied gem" can mean either the original, or the "copy", which is why this gem refers to
59
+ # a "doffed gem" (the original) and a "donned gem" (the copy).
51
60
  # If a block is provided the contents of each file will be yielded to the block,
52
- # after all namespace substitutions are complete, but before the contents
61
+ # after all namespace substitutions from `trades` are complete, but before the contents
53
62
  # are written to the re-namespaced gem. The return value of the block will be
54
63
  # written to the file in this scenario.
55
64
  #
56
65
  # @return void
57
66
  def doff_and_don(&block)
58
- return puts "Skipping #{gem_name} (not loaded on #{RUBY_VERSION})" unless gem_path
67
+ return puts "[#{gem_name}] Skipped (not loaded on #{RUBY_VERSION})" unless gem_path
59
68
 
60
- puts "Doffing #{gem_path}" if verbose
61
- Dir.mktmpdir do |directory|
69
+ puts "[#{gem_name}] Doffing #{gem_path}" if verbose
70
+ Dir.mktmpdir do |tmp_dir|
62
71
  files = []
63
72
  Dir[File.join(gem_path, "lib", "**", "*.rb")].map do |file|
64
73
  if verbose
65
- puts file
66
- puts File.basename(file)
67
- puts "--------------------------------"
74
+ puts "[#{gem_name}] --------------------------------"
75
+ puts "[#{gem_name}] Doffing file #{file}"
76
+ puts "[#{gem_name}] --------------------------------"
68
77
  end
78
+ basename = File.basename(file)
69
79
  dirname = File.dirname(file)
70
- puts "dirname: #{dirname}" if verbose
80
+ puts "[#{gem_name}][#{basename}] dirname: #{dirname}" if verbose
71
81
  is_at_gem_root = dirname[(-4)..(-1)] == "/lib"
72
- puts "is_at_gem_root: #{is_at_gem_root}" if verbose
82
+ puts "[#{gem_name}][#{basename}] is_at_gem_root: #{is_at_gem_root}" if verbose
73
83
  lib_split = dirname.split("/lib/")[-1]
74
- puts "lib_split: #{lib_split}" if verbose
84
+ puts "[#{gem_name}][#{basename}] lib_split: #{lib_split}" if verbose
75
85
  # lib_split could be like:
76
86
  # - "ruby/gems/3.2.0/gems/method_source-1.1.0/lib"
77
87
  # - "method_source"
78
88
  # Se we check to make sure it is actually a subdir of the gem's lib directory
79
89
  full_path = File.join(gem_path, "lib", lib_split)
80
90
  relative_path = !is_at_gem_root && Dir.exist?(full_path) && lib_split
81
- puts "relative_path: #{relative_path}" if verbose
82
- filename = File.basename(file)[0..-4]
83
- puts "filename: #{filename}" if verbose
91
+ puts "[#{gem_name}][#{basename}] relative_path: #{relative_path}" if verbose
84
92
 
85
93
  if relative_path
86
- dir_path = File.join(directory, relative_path)
94
+ dir_path = File.join(tmp_dir, relative_path)
87
95
  Dir.mkdir(dir_path) unless Dir.exist?(dir_path)
88
- puts "creating #{filename} in #{dir_path}" if verbose
89
- files << create_tempfile_copy(file, filename, dir_path, :dd1, &block)
96
+ puts "[#{gem_name}][#{basename}] copying file to #{dir_path}" if verbose
97
+ files << create_tempfile_copy(file, dir_path, basename, :dd1, &block)
90
98
  else
91
- puts "directory not relative (#{directory}) for file #{filename}" if verbose
92
- files << create_tempfile_copy(file, filename, directory, :dd2, &block)
99
+ puts "[#{gem_name}][#{basename}] directory not relative (#{tmp_dir})" if verbose
100
+ files << create_tempfile_copy(file, tmp_dir, basename, :dd2, &block)
93
101
  end
94
102
  end
95
- load_gem_copy(files)
103
+ load_gem_copy(tmp_dir, files)
96
104
  end
97
105
 
98
106
  nil
99
107
  end
100
108
 
101
- def primary_namespace
109
+ # @return [String] Namespace of the doffed (original) gem
110
+ def doffed_primary_namespace
111
+ trades.keys.first
112
+ end
113
+
114
+ # @return [String] Namespace of the donned gem
115
+ def donned_primary_namespace
102
116
  trades.values.first
103
117
  end
104
118
 
105
119
  # Will raise NameError if called before #doff_and_don
120
+ # @return [Class, nil]
106
121
  def as_klass
107
- Object.const_get(primary_namespace) if gem_path
122
+ Object.const_get(donned_primary_namespace) if gem_path
108
123
  end
109
124
 
110
125
  private
111
126
 
112
- def load_gem_copy(files)
127
+ # @param tmp_dir [String] absolute file path of the tmp directory
128
+ # @param files [Array[String]] absolute file path of each file in the donned gem
129
+ # @return void
130
+ def load_gem_copy(tmp_dir, files)
131
+ if verbose
132
+ puts "[#{gem_name}] Doffed gem located at #{gem_path}"
133
+ puts "[#{gem_name}] Donned gem located at #{tmp_dir}"
134
+ puts "[#{gem_name}] Primary namespace updated: #{doffed_primary_namespace} => #{donned_primary_namespace}"
135
+ puts "[#{gem_name}] Donned files:\n\t#{files.join("\n\t")}"
136
+ end
113
137
  files.each do |filepath|
114
- # begin
138
+ # But files required here may not load their own internal files properly if they are still using `require`.
139
+ # Since Ruby 2.2, best practice for ruby libraries is to use require_relative for internal files,
140
+ # and require for external files and dependencies.
141
+ # Ref: https://github.com/panorama-ed/memo_wise/issues/349
142
+ # We *can* use `require` *here*, because filepath here is an absolute paths
115
143
  require filepath
116
- # rescue LoadError => e
117
- # puts file.to_s
118
- # puts tempfile.path
119
- # puts e.class
120
- # puts e.message
121
- # end
122
144
  end
145
+
146
+ nil
123
147
  end
124
148
 
149
+ # @param orig_file_path [String] absolute file path of the original file
150
+ # @param tmp_dir [String] absolute file path of the tmp directory
151
+ # @param basename [String] the basename of the file being copied
152
+ # @param debug_info [Symbol] for debugging purposes
125
153
  # @return [String] the file path of the new copy of the original file
126
- def create_tempfile_copy(file, filename, directory, from, &block)
127
- # Value of block is returned from File.open
128
- File.open(File.join(directory, "#{filename}.rb"), "w") do |file_copy|
129
- new_jersey(file, file_copy, from, &block)
154
+ def create_tempfile_copy(orig_file_path, tmp_dir, basename, debug_info, &block)
155
+ File.open(File.join(tmp_dir, basename), "w") do |donned_file|
156
+ # Value of block is returned from File.open, and thus from this method
157
+ new_jersey(orig_file_path, donned_file, basename, debug_info, &block)
130
158
  end
131
159
  end
132
160
 
133
- # @return [String] the file path of the new copy of the original file
134
- def new_jersey(file, file_copy, from)
135
- nj = File.read(file)
161
+ # New Jersey is not Ohio. Writes donned files to disk.
162
+ #
163
+ # @param doffed_file_path [String] absolute file path of the original file
164
+ # @param donned_file [File] the file which needs to be written to disk
165
+ # @param basename [String] the basename of the file for verbose logging
166
+ # @param debug_info [Symbol] for debugging purposes
167
+ # @return [String] the file path of the donned file
168
+ def new_jersey(doffed_file_path, donned_file, basename, debug_info = nil)
169
+ nj = File.read(doffed_file_path)
136
170
  trades.each do |old_namespace, new_namespace|
137
171
  nj.gsub!(old_namespace, new_namespace)
138
172
  end
139
173
  if verbose
140
- puts "new_jersey has from: #{from}"
141
- puts "new_jersey has file: #{file}"
142
- puts "new_jersey file_copy path: #{file_copy.path}"
174
+ puts "[#{gem_name}][#{basename}] new_jersey doffed_file_path: #{doffed_file_path}"
175
+ puts "[#{gem_name}][#{basename}] new_jersey donned_file path: #{donned_file.path}"
176
+ puts "[#{gem_name}][#{basename}] new_jersey debug_info: #{debug_info}"
143
177
  end
144
178
  nj = yield nj if block_given?
145
- file_copy.write(nj)
146
- file_copy.close
147
- file_copy.path
179
+ donned_file.write(nj)
180
+ donned_file.close
181
+ donned_file.path
148
182
  end
149
183
  end
150
184
  end
@@ -1,4 +1,6 @@
1
1
  module GemBench
2
+ # Each gem either needs to be required at boot time or not.
3
+ # Player helps determine which gems can use `require: false` in the Gemfile to cut down load times.
2
4
  class Player
3
5
  # MAJOR.MINOR split on point length == 2
4
6
  # MAJOR.MINOR.PATCH split on point length == 3
@@ -1,12 +1,13 @@
1
1
  # Scout's job is to figure out where gems are hiding
2
2
  #
3
3
  module GemBench
4
+ # Looks through loaded gems' (RubyGems & Bundler) source code searching for stuff
4
5
  class Scout
5
6
  attr_reader :gem_paths, :gemfile_path, :gemfile_lines, :gemfile_trash, :loaded_gems
6
7
 
7
- def initialize(check_gemfile: nil)
8
+ def initialize(check_gemfile: nil, **options)
8
9
  @check_gemfile = check_gemfile.nil? ? true : check_gemfile
9
- @gemfile_path = "#{Dir.pwd}/Gemfile"
10
+ @gemfile_path = options.fetch(:gemfile_path, "#{Dir.pwd}/Gemfile")
10
11
  gem_lookup_paths_from_bundler
11
12
  gem_lines_from_gemfile
12
13
  # Gem.loaded_specs are the gems that have been loaded / required.
@@ -27,10 +28,10 @@ module GemBench
27
28
  .map { |x| x.to_s }
28
29
  .reject { |p| p.empty? }
29
30
  .map { |x| "#{x}/gems" }
30
- @gem_paths << "#{Bundler.install_path}"
31
+ @gem_paths << Bundler.install_path.to_s # Pathname => String
31
32
  @gem_paths << "#{Bundler.bundle_path}/gems"
32
33
  @gem_paths.uniq!
33
- rescue Bundler::GemfileNotFound => e
34
+ rescue Bundler::GemfileNotFound => _e
34
35
  # Don't fail here, but also don't check the Gemfile.
35
36
  @check_gemfile = false
36
37
  ensure
@@ -42,6 +43,7 @@ module GemBench
42
43
  file = File.open(gemfile_path)
43
44
  # Get all lines as an array
44
45
  all_lines = file.readlines
46
+ file.close
45
47
  # Remove all the commented || blank lines
46
48
  @gemfile_trash, @gemfile_lines = all_lines.partition { |x| x =~ GemBench::TRASH_REGEX }
47
49
  @gemfile_trash.reject! { |x| x == "\n" } # remove blank lines
@@ -1,11 +1,13 @@
1
1
  module GemBench
2
+ # Helps determine if a gem dependency is "valid" according to the strict rules of:
3
+ # - every gem must have a version requirement of some sort
2
4
  class StrictVersionGem
3
5
  attr_reader :name, :version, :version_type, :valid, :relevant_lines, :index, :tokenized_line
4
6
 
5
7
  class << self
6
8
  def from_line(all_lines, line, index, opts = {})
7
9
  tokenized_line = GemfileLineTokenizer.new(all_lines, line, index)
8
- return unless tokenized_line.is_gem
10
+ return unless tokenized_line.parse_success
9
11
 
10
12
  new(
11
13
  tokenized_line.name,
@@ -1,9 +1,10 @@
1
1
  module GemBench
2
+ # Helps enforce a version requirement for every dependency in a Gemfile
2
3
  class StrictVersionRequirement
3
4
  attr_reader :gemfile_path, :gems, :starters, :benchers, :verbose
4
5
 
5
6
  def initialize(options = {})
6
- @gemfile_path = "#{Dir.pwd}/Gemfile"
7
+ @gemfile_path = options.fetch(:gemfile_path, "#{Dir.pwd}/Gemfile")
7
8
  file = File.open(gemfile_path)
8
9
  # Get all lines as an array
9
10
  all_lines = file.readlines
@@ -1,6 +1,11 @@
1
1
  require "forwardable"
2
2
 
3
3
  module GemBench
4
+ # It doesn't make sense to use Team unless the Gemfile you want to evaluate is currently loaded.
5
+ # For example:
6
+ # - if you are in a rails console, and want to evaluate the Gemfile of the Rails app, that's great!
7
+ # - if you are in a context with no Gemfile loaded, or a different Gemfile loaded than the one you want to evaluate,
8
+ # this class may not give sensible results. This is because it checks loaded gems via RubyGems and Bundler.
4
9
  class Team
5
10
  EXCLUDE = %w[
6
11
  bundler
@@ -19,6 +24,8 @@ module GemBench
19
24
  cancan
20
25
  friendly_id
21
26
  faker
27
+ capistrano3-puma
28
+ wkhtmltopdf-binary
22
29
  ]
23
30
  # A comment preceding the require: false anywhere on the line should not be considered an active require: false
24
31
  extend Forwardable
@@ -34,13 +41,16 @@ module GemBench
34
41
  :current_gemfile_suggestions,
35
42
  :bad_ideas
36
43
 
37
- def initialize(options = {})
44
+ def initialize(**options)
38
45
  @look_for_regex = options[:look_for_regex]
39
46
  # find: Find gems containing specific strings in code
40
47
  # bench: Find gems that can probably be benched (require: false) in the Gemfile
41
48
  @check_type = @look_for_regex ? :find : :bench
42
49
  @benching = @check_type == :bench
43
- @scout = GemBench::Scout.new(check_gemfile: options[:check_gemfile] || benching?)
50
+ @scout = GemBench::Scout.new(
51
+ check_gemfile: options.fetch(:check_gemfile, benching?),
52
+ gemfile_path: options.fetch(:gemfile_path, "#{Dir.pwd}/Gemfile"),
53
+ )
44
54
  @exclude_file_pattern_regex_proc = options[:exclude_file_pattern_regex_proc].respond_to?(:call) ? options[:exclude_file_pattern_regex_proc] : GemBench::EXCLUDE_FILE_PATTERN_REGEX_PROC
45
55
  # Among the loaded gems there may be some that did not need to be.
46
56
  @excluded, @all = @scout.loaded_gems.partition { |x| EXCLUDE.include?(x[0]) }
@@ -192,13 +202,13 @@ module GemBench
192
202
  end
193
203
 
194
204
  def check(player)
195
- gem_paths.each do |path|
205
+ gem_paths.detect do |path|
196
206
  glob_path = "#{path}/#{player.file_path_glob}"
197
- file_paths = Dir.glob("#{glob_path}")
207
+ file_paths = Dir.glob(glob_path)
198
208
  puts "[GemBench] checking #{player} at #{glob_path} (#{file_paths.length} files)" if extra_verbose?
199
- file_paths.each do |file_path|
209
+ file_paths.detect do |file_path|
200
210
  player.set_starter(file_path, line_match: look_for_regex)
201
- return if player.starter?
211
+ player.starter?
202
212
  end
203
213
  end
204
214
  end