bundler-gem_bytes 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cd1c19bddd6ef1887fcae3ed68a346395ba811b781e2c1321140015a1403e5d1
4
- data.tar.gz: d48849f9cf568c0dc3bd2f8b29cd8122b15993650d9bde3fbc41703931970fb7
3
+ metadata.gz: e081d49425f6d1bb155e06f8a535da63de74ccdc28a694131a116c93dcf5bb18
4
+ data.tar.gz: 04a13ce95b1d620ca6897ff356718fc7886b13ccc087c2661d9101f31bb3aa5b
5
5
  SHA512:
6
- metadata.gz: 9a42c3cee7bd326a9564144e2d6029a273be1676eece61bcb9fce50fac5b33e2939e328bef28a5121acdef69352f351c0c251ff16e36b918804b5c1d4935cdf4
7
- data.tar.gz: 95be78f3a9535dd2385425e231dae7de2e3bd4c186a9db24db53506eaae3843f293e89b6cf8c9642e14562f849060a1223f005e66286eeeabedc856df83afa83
6
+ metadata.gz: 861709bf72507d147e63ff1c3cf8b34159ec3ecc8ec12a1a70172afcc1e23d569bc1995db97db14e2eb1c3c5a87a9859a29ea3b9fc366c3780766b64b148c276
7
+ data.tar.gz: ba3a92619c32912a7d4abb5aee9ab0c0f089a2d6d82e19b819641c7c0ed331b42e5b67f6b79b4e7c8b8374270174d8b2e6fd53ee9948751385cd0825c86fd33a
data/CHANGELOG.md CHANGED
@@ -5,6 +5,23 @@ All notable changes to the process_executer gem will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## v0.2.0 (2024-10-30)
9
+
10
+ [Full Changelog](https://github.com/main-branch/bundler-gem_bytes/compare/v0.1.0..v0.2.0)
11
+
12
+ Changes since v0.1.0:
13
+
14
+ * 96829fd docs: update the releasing guidelines
15
+ * 25c57f9 feat: make #remove_dependency available for gembytes scripts
16
+ * b285af5 docs: add development debugging instructions in the README.md
17
+ * ea05a44 fix: output an informative error when the gemspec parsed is not valid Ruby
18
+ * 2147776 test: add fully integrated test that installs and runs gembytes via bundler
19
+ * ab907b4 test: exclude lines from test coverage due to JRuby false positives
20
+ * 03692ad feat: make #add_dependency available for gembytes scripts
21
+ * 5d41e08 chore: add Bundler::GemBytes::Actions module to extend the API for gembytes scripts
22
+ * 6f7be90 chore: add class to upsert a gem dependency into a gemspec
23
+ * cf1ab4f chore: rake clobber should remove the .bundle directory
24
+
8
25
  ## v0.1.0 (2024-10-17)
9
26
 
10
27
  [Full Changelog](https://github.com/main-branch/bundler-gem_bytes/compare/ce13f25..v0.1.0)
data/README.md CHANGED
@@ -39,6 +39,8 @@ own script**
39
39
  * [Example](#example)
40
40
  * [Handling Errors](#handling-errors)
41
41
  * [Development](#development)
42
+ * [Debugging](#debugging)
43
+ * [Releasing](#releasing)
42
44
  * [Contributing](#contributing)
43
45
  * [Commit message guidelines](#commit-message-guidelines)
44
46
  * [Pull request guidelines](#pull-request-guidelines)
@@ -47,7 +49,7 @@ own script**
47
49
 
48
50
  ## Installation
49
51
 
50
- Install this bundler plugin as follows:
52
+ Install the `bundler gem-bytes` command as follows:
51
53
 
52
54
  ```shell
53
55
  bundle plugin install bunder-gem_bytes
@@ -79,41 +81,54 @@ After checking out the repo, run `bin/setup` to install dependencies. Then, run
79
81
  workflow. You can also run `bin/console` for an interactive prompt that will allow
80
82
  you to experiment.
81
83
 
82
- To install this gem onto your local machine, run `bundle exec rake install`. To
83
- release a new version, update the version number in `version.rb`, and then run
84
- `bundle exec rake release`, which will create a git tag for the version, push git
85
- commits and the created tag, and push the `.gem` file to
86
- [rubygems.org](https://rubygems.org).
84
+ ### Debugging
87
85
 
88
- To install this bundler plugin from the source code, run the following command from
89
- the project's root directory:
86
+ To debug this gem it is recommended that you create a test project and install
87
+ this plugin with bundler from source code as follows:
90
88
 
91
89
  ```shell
92
- bundler plugin install --path . bundler-gem_bytes
93
- ```
90
+ # 1. Create a temp directory for testing (from the root directory of the project)
91
+ mkdir temp
92
+ cd temp
94
93
 
95
- and then run `bundler plugin list` to make sure it was installed correctly:
94
+ # 2. Create an new, empty RubyGem project to test
95
+ BUNDLE_IGNORE_CONFIG=TRUE bundle gem foo --no-test --no-ci --no-mit --no-coc --no-linter --no-changelog
96
+ cd foo
96
97
 
97
- ```shell
98
- $ bundler plugin list
99
- bundler-gem_bytes
100
- -----
101
- gem-bytes
98
+ # 3. Install the plugin from source
99
+ BUNDLE_IGNORE_CONFIG=TRUE bundle plugin install --path ../.. bundler-gem_bytes
102
100
 
103
- $
104
- ```
101
+ # 4. Create a gembytes script to add a development dependency on rubocop
102
+ cat <<SCRIPT > gem_bytes_script.rb
103
+ add_dependency :development, "rubocop", "~> 1.6"
104
+ SCRIPT
105
105
 
106
- Once installed, the bundler plugin can be run with the following command:
106
+ # 5. Modify code, set breakpoints, or add binding.{irb|pry} calls to the source
107
107
 
108
- ```shell
109
- bundler gem-bytes
108
+ # 6. Run the plugin
109
+ BUNDLE_IGNORE_CONFIG=TRUE bundle gem-bytes gem_bytes_script.rb
110
+
111
+ # Repeat 4 - 6 until satisified :)
110
112
  ```
111
113
 
112
- To uninstall the plugin, run:
114
+ ### Releasing
113
115
 
114
- ```shell
115
- bundler uninstall bundler-gem_bytes
116
- ```
116
+ To release a new version of this gem, run `create-github-release [TYPE]` where
117
+ TYPE is MAJOR, MINOR, or PATCH according to SemVer based on the changes that
118
+ have been made since the last release:
119
+
120
+ * MAJOR: changes that break compatibility with previous versions, such as removing a
121
+ public method, changing a method signature, or modifying the expected behavior of a
122
+ method.
123
+ * MINOR: changes that add new features, enhance existing features, or deprecate
124
+ features in a backward-compatible way, such as adding a new method or improving
125
+ performance without breaking existing functionality.
126
+ * PATCH: changes that fix bugs or make other small modifications that do not affect
127
+ the API or alter existing functionality, such as fixing user-facing typos or
128
+ updating user documentation.
129
+
130
+ This command must be run from the project root directory with a clean worktree on the
131
+ default branch.
117
132
 
118
133
  ## Contributing
119
134
 
data/Rakefile CHANGED
@@ -85,3 +85,4 @@ end
85
85
 
86
86
  CLOBBER << 'package-lock.json'
87
87
  CLOBBER << 'node_modules'
88
+ CLOBBER << '.bundle'
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_bytes'
4
+
5
+ module Bundler
6
+ module GemBytes
7
+ # The API for GemBytes templates
8
+ # @api public
9
+ module Actions
10
+ # Adds (or updates) a dependency in the project's gemspec file
11
+ #
12
+ # @example
13
+ # add_dependency(:development, 'rspec', '~> 3.13')
14
+ # add_dependency(:runtime, 'activesupport', '>= 6.0.0')
15
+ #
16
+ # @param dependency_type [Symbol] the type of dependency to add (either :development or :runtime)
17
+ # @param gem_name [String] the name of the gem to add
18
+ # @param version_constraint [String] the version constraint for the gem
19
+ # @param force [Boolean] whether to overwrite the existing dependency
20
+ # @param gemspec [String] the path to the gemspec file
21
+ #
22
+ # @return [void]
23
+ #
24
+ # @api public
25
+ #
26
+ def add_dependency(dependency_type, gem_name, version_constraint, force: false, gemspec: Dir['*.gemspec'].first)
27
+ source = File.read(gemspec)
28
+ updated_source = Bundler::GemBytes::Gemspec::UpsertDependency.new(
29
+ dependency_type, gem_name, version_constraint, force: force
30
+ ).call(source, path: gemspec)
31
+ File.write(gemspec, updated_source)
32
+ end
33
+
34
+ # Removes a dependency from the project's gemspec file
35
+ #
36
+ # @example
37
+ # remove_dependency('rspec')
38
+ #
39
+ # @param gem_name [String] the name of the gem to add
40
+ # @param gemspec [String] the path to the gemspec file
41
+ #
42
+ # @return [void]
43
+ #
44
+ # @api public
45
+ #
46
+ def remove_dependency(gem_name, gemspec: Dir['*.gemspec'].first)
47
+ source = File.read(gemspec)
48
+ updated_source = Bundler::GemBytes::Gemspec::DeleteDependency.new(gem_name).call(source, path: gemspec)
49
+ File.write(gemspec, updated_source)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'bundler'
4
+
3
5
  module Bundler
4
6
  module GemBytes
5
7
  # A bundler command that adds features to your existing Ruby Gems project
@@ -0,0 +1,216 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'parser/current'
4
+ require 'rubocop-ast'
5
+ require 'active_support/core_ext/object'
6
+
7
+ module Bundler
8
+ module GemBytes
9
+ module Gemspec
10
+ # Delete a dependency in a gemspec file
11
+ #
12
+ # This class works by parsing the gemspec file into an AST and then walking the
13
+ # AST to find the Gem::Specification block (via #on_block). Once the block is
14
+ # found, AST within that block is walked to locate the dependency declarations
15
+ # (via #on_send). Any dependency declaration that matches the given gem name is
16
+ # collected into the found_dependencies array.
17
+ #
18
+ # Once the Gem::Specification block is fully processed, any dependencies on the
19
+ # given gem are deleted from the gemspec source.
20
+ #
21
+ # If the dependency is not found, the gemspec source is returned unmodified.
22
+ #
23
+ # @example
24
+ # require 'bundler/gem_bytes'
25
+ #
26
+ # delete_dependency = Bundler::GemBytes::Gemspec::DeleteDependency.new('test_tool')
27
+ #
28
+ # gemspec_file = 'foo.gemspec'
29
+ # gemspec = File.read(gemspec_file)
30
+ # updated_gemspec = delete_dependency.call(gemspec, path: gemspec_file)
31
+ # File.write(gemspec_file, updated_gemspec)
32
+ #
33
+ # @!attribute [r] gem_name
34
+ # The name of the gem to add a dependency on (i.e. 'rubocop')
35
+ # @return [String]
36
+ # @api private
37
+ #
38
+ # @!attribute [r] receiver_name
39
+ # The name of the receiver for the Gem::Specification block
40
+ #
41
+ # i.e. 'spec' in `spec.add_dependency 'rubocop', '~> 1.0'`
42
+ #
43
+ # @return [Symbol]
44
+ # @api private
45
+ #
46
+ # @!attribute [r] found_gemspec_block
47
+ # Whether the Gem::Specification block was found in the gemspec file
48
+ #
49
+ # Only valid after calling `#call`.
50
+ # @return [Boolean]
51
+ # @api private
52
+ #
53
+ # @!attribute [r] found_dependencies
54
+ # The dependencies found in the gemspec file
55
+ #
56
+ # Only valid after calling `#call`.
57
+ # @return [Array<Hash>]
58
+ # @api private
59
+ #
60
+ # @api public
61
+ class DeleteDependency < Parser::TreeRewriter
62
+ # Create a new instance of a dependency upserter
63
+ # @example
64
+ # command = Bundler::GemBytes::Gemspec::DeleteDependency.new('my_gem')
65
+ # @param gem_name [String] The name of the gem to add a dependency on
66
+ def initialize(gem_name)
67
+ super()
68
+
69
+ @gem_name = gem_name
70
+
71
+ @found_dependencies = []
72
+ end
73
+
74
+ # Returns the content of the gemspec file with the dependency deleted
75
+ #
76
+ # @param code [String] The content of the gemspec file
77
+ # @param path [String] This should be the path to the gemspspec file
78
+ #
79
+ # path is used to generate error messages only
80
+ #
81
+ # @return [String] The updated gemspec content with the dependency deleted
82
+ #
83
+ # @raise [ArgumentError] if the Gem Specification block is not found in the given gemspec
84
+ #
85
+ # @example
86
+ # code = File.read('project.gemspec')
87
+ # command = Bundler::GemBytes::DeleteDependency.new('my_gem')
88
+ # updated_code = command.call(code)
89
+ # puts updated_code
90
+ #
91
+ def call(code, path: '(string)')
92
+ @found_gemspec_block = false
93
+ rewrite(*parse(code, path)).tap do |_result|
94
+ raise ArgumentError, 'Gem::Specification block not found' unless found_gemspec_block
95
+ end
96
+ end
97
+
98
+ attr_reader :gem_name, :receiver_name, :found_gemspec_block, :found_dependencies
99
+
100
+ # Handles block nodes within the AST to locate the Gem Specification block
101
+ #
102
+ # @param node [Parser::AST::Node] The block node within the AST
103
+ # @return [void]
104
+ # @api private
105
+ def on_block(node)
106
+ return if receiver_name # already processing the Gem Specification block
107
+
108
+ @found_gemspec_block = true
109
+ @receiver_name = gem_specification_pattern.match(node)
110
+
111
+ return unless receiver_name
112
+
113
+ super # process the children of this node to find the existing dependencies
114
+
115
+ delete_dependencies
116
+
117
+ @receiver_name = nil
118
+ end
119
+
120
+ # Handles `send` nodes within the AST to locate dependency calls
121
+ #
122
+ # If receiver_name is not present then we are not in a Gem Specification block.
123
+ #
124
+ # @param node [Parser::AST::Node] The `send` node to check for dependency patterns
125
+ # @return [void]
126
+ # @api private
127
+ def on_send(node)
128
+ return unless receiver_name.present?
129
+ return unless (match = dependency_pattern.match(node))
130
+
131
+ found_dependencies << { node:, match: }
132
+ end
133
+
134
+ private
135
+
136
+ # Parses the given code into an AST
137
+ # @param code [String] The code to parse
138
+ # @param path [String] The path to the file being parsed (used for error messages only)
139
+ # @return [Array<Parser::AST::Node, Parser::Source::Buffer>] The AST and buffer
140
+ # @api private
141
+ def parse(code, path)
142
+ buffer = Parser::Source::Buffer.new(path, source: code)
143
+ processed_source = RuboCop::AST::ProcessedSource.new(code, ruby_version, path)
144
+ unless processed_source.valid_syntax?
145
+ raise "Invalid syntax in #{path}\n#{processed_source.diagnostics.map(&:render).join("\n")}"
146
+ end
147
+
148
+ ast = processed_source.ast
149
+ [buffer, ast]
150
+ end
151
+
152
+ # Deletes any dependency on the given gem within the Gem::Specification block
153
+ # @param node [Parser::AST::Node] The block node within the AST
154
+ # @return [void]
155
+ # @api private
156
+ def delete_dependencies
157
+ found_dependencies.each do |found_dependency|
158
+ dependency_node = found_dependency[:node]
159
+ remove(range_including_leading_spaces(dependency_node))
160
+ end
161
+ end
162
+
163
+ # Returns the range of the dependency node including any leading spaces & newline
164
+ # @param node [Parser::AST::Node] The node
165
+ # @return [Parser::Source::Range] The range of the dependency node
166
+ # @api private
167
+ def range_including_leading_spaces(node)
168
+ leading_spaces = leading_whitespace_count(node)
169
+ range = node.loc.expression
170
+ range.with(begin_pos: range.begin_pos - leading_spaces - 1, end_pos: range.end_pos)
171
+ end
172
+
173
+ # Returns the # of leading whitespace chars in the source line before the node
174
+ # @param node [Parser::AST::Node] The node
175
+ # @return [Integer] The number of leading whitespace characters
176
+ # @api private
177
+ def leading_whitespace_count(node)
178
+ match_data = node.loc.expression.source_line.match(/^\s*/)
179
+ match_data ? match_data[0].size : 0
180
+ end
181
+
182
+ # Returns the Ruby version in use as a float (MAJOR.MINOR only)
183
+ # @return [Float] The Ruby version number, e.g., 3.0
184
+ # @api private
185
+ def ruby_version = RUBY_VERSION.match(/^(?<version>\d+\.\d+)/)['version'].to_f
186
+
187
+ # The pattern to match a dependency declaration in the AST
188
+ # @return [RuboCop::AST::NodePattern] The dependency pattern
189
+ # @api private
190
+ def dependency_pattern
191
+ # :nocov: JRuby give false positive for this line being uncovered by tests
192
+ @dependency_pattern ||=
193
+ RuboCop::AST::NodePattern.new(<<~PATTERN)
194
+ (send
195
+ { (send _ :#{receiver_name}) | (lvar :#{receiver_name}) }
196
+ ${ :add_dependency :add_runtime_dependency :add_development_dependency }
197
+ (str #{gem_name ? "$\"#{gem_name}\"" : '$_gem_name'})
198
+ <(str $_version_constraint) ...>
199
+ )
200
+ PATTERN
201
+ # :nocov:
202
+ end
203
+
204
+ # The pattern to match the Gem::Specification block in the AST
205
+ # @return [RuboCop::AST::NodePattern] The Gem::Specification pattern
206
+ # @api private
207
+ def gem_specification_pattern
208
+ @gem_specification_pattern ||=
209
+ RuboCop::AST::NodePattern.new(<<~PATTERN)
210
+ (block (send (const (const nil? :Gem) :Specification) :new)(args (arg $_)) ...)
211
+ PATTERN
212
+ end
213
+ end
214
+ end
215
+ end
216
+ end
@@ -0,0 +1,336 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'parser/current'
4
+ require 'rubocop-ast'
5
+ require 'active_support/core_ext/object'
6
+
7
+ module Bundler
8
+ module GemBytes
9
+ module Gemspec
10
+ # Add or update a dependency in a gemspec file
11
+ #
12
+ # This class allows the addition of a new dependency or the updating of an
13
+ # existing dependency in a gemspec file.
14
+ #
15
+ # This class works by parsing the gemspec file into an AST and then walking the
16
+ # AST to find the Gem::Specification block (via #on_block). Once the block is
17
+ # found, AST within that block is walked to locate the dependency declarations
18
+ # (via #on_send). Any dependency declaration that matches the given gem name is
19
+ # collected into the found_dependencies array.
20
+ #
21
+ # Once the Gem::Specification block is fully processed, if a dependency on the
22
+ # given gem is not found, a new dependency is added to the end of the
23
+ # Gem::Specification block.
24
+ #
25
+ # If one or more dependencies are found, the version constraint is updated to
26
+ # the given version constraint. If the dependency type is different from the
27
+ # existing dependency, an error is raised unless the `force` option is set to
28
+ # true.
29
+ #
30
+ # @example
31
+ # require 'bundler/gem_bytes'
32
+ #
33
+ # add_dependency = Bundler::GemBytes::Gemspec::UpsertDependency.new(:dependency, 'test_tool', '~> 2.1')
34
+ #
35
+ # gemspec = File.read('bundler-gem_bytes.gemspec')
36
+ # updated_gemspec = add_dependency.call(gemspec)
37
+ # File.write('project.gemspec', updated_gemspec)
38
+ #
39
+ # @!attribute [r] dependency_type
40
+ # The type of dependency to add
41
+ # @return [:runtime, :depenncy]
42
+ # @api private
43
+ #
44
+ # @!attribute [r] gem_name
45
+ # The name of the gem to add a dependency on (i.e. 'rubocop')
46
+ # @return [String]
47
+ # @api private
48
+ #
49
+ # @!attribute [r] version_constraint
50
+ # The version constraint for the gem (i.e. '~> 2.1')
51
+ # @return [String]
52
+ # @api private
53
+ #
54
+ # @!attribute [r] force
55
+ # Whether to update the dependency even if the type is different
56
+ # @return [Boolean]
57
+ # @api private
58
+ #
59
+ # @!attribute [r] receiver_name
60
+ # The name of the receiver for the Gem::Specification block
61
+ #
62
+ # i.e. 'spec' in `spec.add_dependency 'rubocop', '~> 1.0'`
63
+ #
64
+ # @return [Symbol]
65
+ # @api private
66
+ #
67
+ # @!attribute [r] found_gemspec_block
68
+ # Whether the Gem::Specification block was found in the gemspec file
69
+ #
70
+ # Only valid after calling `#call`.
71
+ # @return [Boolean]
72
+ # @api private
73
+ #
74
+ # @!attribute [r] found_dependencies
75
+ # The dependencies found in the gemspec file
76
+ #
77
+ # Only valid after calling `#call`.
78
+ # @return [Array<Hash>]
79
+ # @api private
80
+ #
81
+ # @api public
82
+ class UpsertDependency < Parser::TreeRewriter # rubocop:disable Metrics/ClassLength
83
+ # Create a new instance of a dependency upserter
84
+ # @example
85
+ # add_dependency = Bundler::GemBytes::Gemspec::UpsertDependency.new(:runtime, 'my_gem', '~> 1.0')
86
+ # @param dependency_type [Symbol] The type of dependency to add
87
+ # @param gem_name [String] The name of the gem to add a dependency on
88
+ # @param version_constraint [String] The version constraint for the gem
89
+ # @param force [Boolean] Whether to update the dependency even if the type is different
90
+ def initialize(dependency_type, gem_name, version_constraint, force: false)
91
+ super()
92
+
93
+ self.dependency_type = dependency_type
94
+ @gem_name = gem_name
95
+ self.version_constraint = version_constraint
96
+ @force = force
97
+
98
+ @found_dependencies = []
99
+ end
100
+
101
+ # Returns the content of the gemspec file with the new/updated dependency
102
+ #
103
+ # @param code [String] The content of the gemspec file
104
+ # @param path [String] This should be the path to the gemspspec file
105
+ #
106
+ # path is used to generate error messages only
107
+ #
108
+ # @return [String] The updated gemspec content with the new/added dependency
109
+ #
110
+ # @raise [ArgumentError] if the Gem Specification block is not found in the given gemspec
111
+ #
112
+ # @example
113
+ # code = File.read('project.gemspec')
114
+ # add_dependency = Bundler::GemBytes::AddDependency.new(:runtime, 'my_gem', '~> 1.0')
115
+ # updated_code = add_dependency.call(code)
116
+ # puts updated_code
117
+ #
118
+ def call(code, path: '(string)')
119
+ @found_gemspec_block = false
120
+ rewrite(*parse(code, path)).tap do |_result|
121
+ raise ArgumentError, 'Gem::Specification block not found' unless found_gemspec_block
122
+ end
123
+ end
124
+
125
+ attr_reader :dependency_type, :gem_name, :version_constraint, :force,
126
+ :receiver_name, :found_gemspec_block, :found_dependencies
127
+
128
+ # Handles block nodes within the AST to locate the Gem Specification block
129
+ #
130
+ # @param node [Parser::AST::Node] The block node within the AST
131
+ # @return [void]
132
+ # @api private
133
+ def on_block(node)
134
+ return if receiver_name # already processing the Gem Specification block
135
+
136
+ @found_gemspec_block = true
137
+ @receiver_name = gem_specification_pattern.match(node)
138
+
139
+ return unless receiver_name
140
+
141
+ super # process the children of this node to find the existing dependencies
142
+
143
+ upsert_dependency(node)
144
+
145
+ @receiver_name = nil
146
+ end
147
+
148
+ # Handles `send` nodes within the AST to locate dependency calls
149
+ #
150
+ # If receiver_name is not present then we are not in a Gem Specification block.
151
+ #
152
+ # @param node [Parser::AST::Node] The `send` node to check for dependency patterns
153
+ # @return [void]
154
+ # @api private
155
+ def on_send(node)
156
+ return unless receiver_name.present?
157
+ return unless (match = dependency_pattern.match(node))
158
+
159
+ found_dependencies << { node:, match: }
160
+ end
161
+
162
+ private
163
+
164
+ # Parses the given code into an AST
165
+ # @param code [String] The code to parse
166
+ # @param path [String] The path to the file being parsed (used for error messages only)
167
+ # @return [Array<Parser::AST::Node, Parser::Source::Buffer>] The AST and buffer
168
+ # @api private
169
+ def parse(code, path)
170
+ buffer = Parser::Source::Buffer.new(path, source: code)
171
+ processed_source = RuboCop::AST::ProcessedSource.new(code, ruby_version, path)
172
+ unless processed_source.valid_syntax?
173
+ raise "Invalid syntax in #{path}\n#{processed_source.diagnostics.map(&:render).join("\n")}"
174
+ end
175
+
176
+ ast = processed_source.ast
177
+ [buffer, ast]
178
+ end
179
+
180
+ # Adds or updates the given dependency in the Gem::Specification block
181
+ # @param node [Parser::AST::Node] The block node within the AST
182
+ # @return [void]
183
+ # @api private
184
+ def upsert_dependency(node)
185
+ if found_dependencies.empty?
186
+ add_dependency(node)
187
+ else
188
+ update_dependency
189
+ end
190
+ end
191
+
192
+ # Adds a new dependency to the Gem::Specification block
193
+ # @param node [Parser::AST::Node] The Gem::Specification block node within the AST
194
+ # @return [void]
195
+ # @api private
196
+ def add_dependency(node)
197
+ insert_after(node.children[2].children.last.loc.expression, "\n #{dependency_source_code}")
198
+ end
199
+
200
+ # The dependency type (:runtime or :development) based on a given method name
201
+ # @param method [Symbol] The method name to convert to a dependency type
202
+ # @return [Symbol] The dependency type
203
+ # @api private
204
+ def dependency_method_to_type(method)
205
+ method == :add_development_dependency ? :development : :runtime
206
+ end
207
+
208
+ # Error message for a dependency type conflict
209
+ # @param node [Parser::AST::Node] The existing dependency node
210
+ # @return [String] The error message
211
+ # @api private
212
+ def dependency_type_conflict_error(node)
213
+ # :nocov: JRuby give false positive for this line being uncovered by tests
214
+ <<~MESSAGE.chomp.gsub("\n", ' ')
215
+ Trying to add a
216
+ #{dependency_method_to_type(dependency_type_method).upcase}
217
+ dependency on "#{gem_name}" which conflicts with the existing
218
+ #{dependency_method_to_type(node.children[1]).upcase}
219
+ dependency.
220
+ Pass force: true to update dependencies where the
221
+ dependency type is different.
222
+ MESSAGE
223
+ # :nocov:
224
+ end
225
+
226
+ # Checks if the given dependency type conflicts with the existing dependency type
227
+ #
228
+ # Returns false if {#force} is true.
229
+ #
230
+ # @param dependency_node [Parser::AST::Node] The existing dependency node
231
+ # @return [Boolean] Whether the dependency type conflicts
232
+ # @api private
233
+ def dependency_type_conflict?(dependency_node)
234
+ dependency_node.children[1] != dependency_type_method && !force
235
+ end
236
+
237
+ # The source code for the updated dependency declaration
238
+ # @param existing_dependency_node [Parser::AST::Node] The existing dependency node
239
+ # @return [String] The source code for the dependency declaration
240
+ # @api private
241
+ def dependency_source_code(existing_dependency_node = nil)
242
+ # Use existing quote character for string literals
243
+ q = existing_dependency_node ? existing_dependency_node.children[3].loc.expression.source[0] : "'"
244
+ "#{receiver_name}.#{dependency_type_method} #{q}#{gem_name}#{q}, #{q}#{version_constraint}#{q}"
245
+ end
246
+
247
+ # Replaces the existing dependency node with the updated dependency declaration
248
+ # @param dependency_node [Parser::AST::Node] The existing dependency node
249
+ # @return [void]
250
+ # @api private
251
+ def replace_dependency_node(dependency_node)
252
+ replace(dependency_node.loc.expression, dependency_source_code(dependency_node))
253
+ end
254
+
255
+ # Updates the found_dependencies from the Gem::Specification block
256
+ # @return [void]
257
+ # @api private
258
+ def update_dependency
259
+ found_dependencies.each do |found_dependency|
260
+ dependency_node = found_dependency[:node]
261
+ raise(dependency_type_conflict_error(dependency_node)) if dependency_type_conflict?(dependency_node)
262
+
263
+ replace_dependency_node(dependency_node)
264
+ end
265
+ end
266
+
267
+ # Validates and sets the dependency type
268
+ # @param dependency_type [Symbol] The type of dependency to add (must be :runtime or :development)
269
+ # @raise [ArgumentError] if the dependency type is not :runtime or :development
270
+ # @return [Symbol] The dependency type
271
+ # @api private
272
+ def dependency_type=(dependency_type)
273
+ unless %i[runtime development].include?(dependency_type)
274
+ message = "Invalid dependency type: #{dependency_type.inspect}"
275
+ raise(ArgumentError, message)
276
+ end
277
+ @dependency_type = dependency_type
278
+ end
279
+
280
+ # Validates and sets the version constraint
281
+ # @param version_constraint [String] The version constraint to set
282
+ # @raise [ArgumentError] if the version constraint is invalid
283
+ # @return [String] The version constraint
284
+ # @api private
285
+ def version_constraint=(version_constraint)
286
+ begin
287
+ Gem::Requirement.new(version_constraint)
288
+ true
289
+ rescue Gem::Requirement::BadRequirementError
290
+ raise ArgumentError, "Invalid version constraint: #{version_constraint.inspect}"
291
+ end
292
+ @version_constraint = version_constraint
293
+ end
294
+
295
+ # Returns the Ruby version in use as a float (MAJOR.MINOR only)
296
+ # @return [Float] The Ruby version number, e.g., 3.0
297
+ # @api private
298
+ def ruby_version = RUBY_VERSION.match(/^(?<version>\d+\.\d+)/)['version'].to_f
299
+
300
+ # Determines the dependency method based on the dependency type
301
+ # @return [Symbol] Either :add_development_dependency or :add_dependency
302
+ # @api private
303
+ def dependency_type_method
304
+ dependency_type == :development ? :add_development_dependency : :add_dependency
305
+ end
306
+
307
+ # The pattern to match a dependency declaration in the AST
308
+ # @return [RuboCop::AST::NodePattern] The dependency pattern
309
+ # @api private
310
+ def dependency_pattern
311
+ # :nocov: JRuby give false positive for this line being uncovered by tests
312
+ @dependency_pattern ||=
313
+ RuboCop::AST::NodePattern.new(<<~PATTERN)
314
+ (send
315
+ { (send _ :#{receiver_name}) | (lvar :#{receiver_name}) }
316
+ ${ :add_dependency :add_runtime_dependency :add_development_dependency }
317
+ (str #{gem_name ? "$\"#{gem_name}\"" : '$_gem_name'})
318
+ <(str $_version_constraint) ...>
319
+ )
320
+ PATTERN
321
+ # :nocov:
322
+ end
323
+
324
+ # The pattern to match the Gem::Specification block in the AST
325
+ # @return [RuboCop::AST::NodePattern] The Gem::Specification pattern
326
+ # @api private
327
+ def gem_specification_pattern
328
+ @gem_specification_pattern ||=
329
+ RuboCop::AST::NodePattern.new(<<~PATTERN)
330
+ (block (send (const (const nil? :Gem) :Specification) :new)(args (arg $_)) ...)
331
+ PATTERN
332
+ end
333
+ end
334
+ end
335
+ end
336
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bundler
4
+ module GemBytes
5
+ # The namespec for classes that modify the gemspec file
6
+ module Gemspec; end
7
+ end
8
+ end
9
+
10
+ require_relative 'gemspec/delete_dependency'
11
+ require_relative 'gemspec/upsert_dependency'
@@ -1,8 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # require 'pp'
4
3
  require 'thor'
5
- require 'uri'
4
+ require_relative 'actions'
6
5
 
7
6
  module Bundler
8
7
  module GemBytes
@@ -13,8 +12,9 @@ module Bundler
13
12
  # @example Executing a script from a file or URI
14
13
  # executor = Bundler::GemBytes::ScriptExecutor.new
15
14
  # executor.execute('path_or_uri_to_script')
16
- class ScriptExecutor < Thor::Group
17
- include Thor::Actions
15
+ class ScriptExecutor < ::Thor::Group
16
+ include ::Thor::Actions
17
+ include Bundler::GemBytes::Actions
18
18
 
19
19
  # Set the source paths for Thor to use
20
20
  # @return [Array<String>] the source paths
@@ -3,6 +3,6 @@
3
3
  module Bundler
4
4
  module GemBytes
5
5
  # The version of this gem
6
- VERSION = '0.1.0'
6
+ VERSION = '0.2.0'
7
7
  end
8
8
  end
@@ -13,6 +13,10 @@ module Bundler
13
13
  end
14
14
  end
15
15
 
16
+ require 'active_support'
17
+ require 'thor'
18
+
16
19
  require_relative 'gem_bytes/bundler_command'
20
+ require_relative 'gem_bytes/gemspec'
17
21
  require_relative 'gem_bytes/script_executor'
18
22
  require_relative 'gem_bytes/version'
metadata CHANGED
@@ -1,15 +1,57 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bundler-gem_bytes
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - James Couball
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-10-17 00:00:00.000000000 Z
11
+ date: 2024-10-31 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: parser
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.3'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rubocop-ast
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.32'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.32'
13
55
  - !ruby/object:Gem::Dependency
14
56
  name: thor
15
57
  requirement: !ruby/object:Gem::Requirement
@@ -52,6 +94,20 @@ dependencies:
52
94
  - - ">="
53
95
  - !ruby/object:Gem::Version
54
96
  version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: process_executer
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '1.2'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '1.2'
55
111
  - !ruby/object:Gem::Dependency
56
112
  name: rake
57
113
  requirement: !ruby/object:Gem::Requirement
@@ -108,6 +164,20 @@ dependencies:
108
164
  - - "~>"
109
165
  - !ruby/object:Gem::Version
110
166
  version: '0.22'
167
+ - !ruby/object:Gem::Dependency
168
+ name: simplecov-lcov
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: '0.8'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - "~>"
179
+ - !ruby/object:Gem::Version
180
+ version: '0.8'
111
181
  - !ruby/object:Gem::Dependency
112
182
  name: simplecov-rspec
113
183
  requirement: !ruby/object:Gem::Requirement
@@ -122,6 +192,20 @@ dependencies:
122
192
  - - "~>"
123
193
  - !ruby/object:Gem::Version
124
194
  version: '0.4'
195
+ - !ruby/object:Gem::Dependency
196
+ name: turnip
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - "~>"
200
+ - !ruby/object:Gem::Version
201
+ version: '4.4'
202
+ type: :development
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - "~>"
207
+ - !ruby/object:Gem::Version
208
+ version: '4.4'
125
209
  - !ruby/object:Gem::Dependency
126
210
  name: redcarpet
127
211
  requirement: !ruby/object:Gem::Requirement
@@ -185,7 +269,11 @@ files:
185
269
  - README.md
186
270
  - Rakefile
187
271
  - lib/bundler/gem_bytes.rb
272
+ - lib/bundler/gem_bytes/actions.rb
188
273
  - lib/bundler/gem_bytes/bundler_command.rb
274
+ - lib/bundler/gem_bytes/gemspec.rb
275
+ - lib/bundler/gem_bytes/gemspec/delete_dependency.rb
276
+ - lib/bundler/gem_bytes/gemspec/upsert_dependency.rb
189
277
  - lib/bundler/gem_bytes/script_executor.rb
190
278
  - lib/bundler/gem_bytes/version.rb
191
279
  - package.json
@@ -197,8 +285,8 @@ metadata:
197
285
  allowed_push_host: https://rubygems.org
198
286
  homepage_uri: https://github.com/main-branch/bundler-gem_bytes
199
287
  source_code_uri: https://github.com/main-branch/bundler-gem_bytes
200
- documentation_uri: https://rubydoc.info/gems/bundler-gem_bytes/0.1.0
201
- changelog_uri: https://rubydoc.info/gems/bundler-gem_bytes/0.1.0/file/CHANGELOG.md
288
+ documentation_uri: https://rubydoc.info/gems/bundler-gem_bytes/0.2.0
289
+ changelog_uri: https://rubydoc.info/gems/bundler-gem_bytes/0.2.0/file/CHANGELOG.md
202
290
  bug_tracker_uri: https://github.com/main-branch/bundler-gem_bytes/issues
203
291
  rubygems_mfa_required: 'true'
204
292
  post_install_message: