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 +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:
|