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 +4 -4
- data/CHANGELOG.md +17 -0
- data/README.md +40 -25
- data/Rakefile +1 -0
- data/lib/bundler/gem_bytes/actions.rb +53 -0
- data/lib/bundler/gem_bytes/bundler_command.rb +2 -0
- data/lib/bundler/gem_bytes/gemspec/delete_dependency.rb +216 -0
- data/lib/bundler/gem_bytes/gemspec/upsert_dependency.rb +336 -0
- data/lib/bundler/gem_bytes/gemspec.rb +11 -0
- data/lib/bundler/gem_bytes/script_executor.rb +4 -4
- data/lib/bundler/gem_bytes/version.rb +1 -1
- data/lib/bundler/gem_bytes.rb +4 -0
- metadata +92 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e081d49425f6d1bb155e06f8a535da63de74ccdc28a694131a116c93dcf5bb18
|
4
|
+
data.tar.gz: 04a13ce95b1d620ca6897ff356718fc7886b13ccc087c2661d9101f31bb3aa5b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
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
|
-
|
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
|
89
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
98
|
-
|
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
|
-
|
106
|
+
# 5. Modify code, set breakpoints, or add binding.{irb|pry} calls to the source
|
107
107
|
|
108
|
-
|
109
|
-
|
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
|
-
|
114
|
+
### Releasing
|
113
115
|
|
114
|
-
|
115
|
-
|
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
@@ -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
|
@@ -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
|
@@ -1,8 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# require 'pp'
|
4
3
|
require 'thor'
|
5
|
-
|
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
|
data/lib/bundler/gem_bytes.rb
CHANGED
@@ -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.
|
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-
|
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.
|
201
|
-
changelog_uri: https://rubydoc.info/gems/bundler-gem_bytes/0.
|
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:
|