gem_bench 2.0.2 → 2.0.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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