bundler-gem_bytes 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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: