rebundler 0.1.1 → 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: 202f3a3cebed796e24330e45c80ec05cfb1cff61c19b032dc6abf7b5743413e0
4
- data.tar.gz: cbe1e6738646b51a0d472ed9ee814f5826cdcf990ef95efc922532593101a363
3
+ metadata.gz: b98128fc72f25553bf3989f1cda39f09d0b38553185bbb9bd6e92a3fe4646cc8
4
+ data.tar.gz: 580694dd7a91592f95f1c32173e625f1a17c6c7dfc9bfc1c708bc565e422f4fa
5
5
  SHA512:
6
- metadata.gz: 8a5f1efa74df81a6713ab98ca15e7dd8f0a03338475558eb21ed76e5550f8d627f17023c7d54f04537b6967c571b59562115efc54d26000b1e822fa014ea2fbd
7
- data.tar.gz: d737b0d7b18c28b322cb874bb1530da1eeff03d1103eb5164cdf2370e95220e17196429b6809dc9055f06d275cb8f2b71c65a786a2bde75a54583d422781a278
6
+ metadata.gz: c83d1b995968f668a96b656036c9ba19f646bf3af2ee32611867ba820894acdfbead0b9662e60f1db3d08e1a8cef8476283cf3be855aefed921b7bf00e15d5d9
7
+ data.tar.gz: a4f681d93564b7111de675f8b576f62b3837fe0f2704f75cfa96d4a0ff7a0b106e726c09a26e7a8b8744a44f2ba24200a95be42249a8987f991583c1bf98c257
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Rebundler
2
2
 
3
- Rebundler automatically reorders and annotes your Gemfile.
3
+ Rebundler automatically reorders and annotates your Gemfile.
4
4
 
5
5
  ## Why would you want that?
6
6
 
data/exe/rebundle CHANGED
@@ -3,7 +3,9 @@
3
3
 
4
4
  require_relative "../lib/rebundler"
5
5
 
6
- file = ARGV[0] || (Dir.pwd + "/Gemfile")
6
+ # Parse command line arguments
7
+ ci_mode = ARGV.include?("--ci")
8
+ file = ARGV.reject { |arg| arg.start_with?("--") }[0] || (Dir.pwd + "/Gemfile")
7
9
 
8
10
  puts "Reordering and annotating Gemfile..."
9
11
 
@@ -12,7 +14,26 @@ begin
12
14
  # having to hit rubygems.org
13
15
  Bundler.setup
14
16
  parser = Rebundler::Parser.new(file)
15
- File.write(file, parser.write!)
17
+ new_content = parser.parse_and_write!
18
+
19
+ if ci_mode
20
+ # In CI mode, compare the new content with the existing file
21
+ current_content = File.read(file)
22
+
23
+ if new_content == current_content
24
+ puts "✓ Gemfile is already properly formatted"
25
+ exit 0
26
+ else
27
+ puts "✗ Gemfile is not properly formatted"
28
+ puts "Run 'rebundle' to fix formatting issues"
29
+ exit 1
30
+ end
31
+ else
32
+ # In normal mode, write the new content to the file
33
+ File.write(file, new_content)
34
+ puts "✓ Gemfile has been reordered and annotated"
35
+ end
16
36
  rescue StandardError => e
17
37
  puts "Error parsing Gemfile: #{e.message}"
38
+ exit 1
18
39
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "bundler"
4
+ require "tempfile"
4
5
 
5
6
  module Bundler
6
7
  class Rebundle < Bundler::Plugin::API
@@ -25,7 +26,15 @@ module Bundler
25
26
  Bundler.setup
26
27
 
27
28
  parser = ::Rebundler::Parser.new(file)
28
- File.write(file, parser.write!)
29
+ content = parser.parse_and_write!
30
+
31
+ # Use atomic write to prevent corruption if write fails
32
+ Tempfile.create("Gemfile", Bundler.root.to_s) do |tmp|
33
+ tmp.write(content)
34
+ tmp.flush
35
+ tmp.close
36
+ FileUtils.mv(tmp.path, file)
37
+ end
29
38
  rescue StandardError => e
30
39
  Bundler.ui.error "Error parsing Gemfile: #{e.message}"
31
40
  raise Bundler::PluginError, e
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rebundler
4
+ class GemFetcher
5
+ class << self
6
+ def parse_gem(node)
7
+ return unless node.type == :call_node && SORTABLE_NODES.include?(node.name)
8
+
9
+ gem_name = node.arguments.child_nodes[0].content
10
+
11
+ find_gem(gem_name).merge(node:)
12
+ end
13
+
14
+ private
15
+
16
+ def find_gem(name)
17
+ summary = find_loaded_gem_summary(name) || find_external_gem_summary(name)
18
+
19
+ { name:, summary: }
20
+ end
21
+
22
+ def find_loaded_gem_summary(name)
23
+ gem = Gem::Specification.find_by_name(name)
24
+
25
+ gem.summary
26
+ rescue Gem::MissingSpecError
27
+ nil
28
+ end
29
+
30
+ def find_external_gem_summary(name)
31
+ spec = Gem::SpecFetcher.fetcher.spec_for_dependency Gem::Dependency.new(name)
32
+
33
+ return unless spec
34
+ # SpecFetcher returns [[], []] when gem exists but has no versions available
35
+ return if spec == [[], []]
36
+
37
+ spec.first.first.first.summary
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rebundler
4
+ class GemSet
5
+ include Comparable
6
+
7
+ attr_reader :name, :node, :plugins, :gems, :default
8
+
9
+ def initialize(name: nil, node: nil, default: false)
10
+ @name = name
11
+ @node = node
12
+ @default = default
13
+
14
+ @plugins = []
15
+ @gems = []
16
+ end
17
+
18
+ def <=>(other)
19
+ return nil unless other.is_a?(GemSet)
20
+
21
+ # Default first, then sort by name
22
+ [default ? 0 : 1, name] <=> [other.default ? 0 : 1, other.name]
23
+ end
24
+
25
+ def ==(other)
26
+ other.is_a?(GemSet) && other.name == name
27
+ end
28
+ end
29
+ end
@@ -4,157 +4,41 @@ require "prism"
4
4
 
5
5
  module Rebundler
6
6
  class Parser
7
- SORTABLE_NODES = %i[plugin gem].freeze
8
- BLOCK_NODES = %i[gemspec git group path platforms ruby source].freeze
9
-
10
- attr_reader :file, :before, :sets, :frozen_string_literal
7
+ attr_reader :file, :preamble_nodes, :gem_sets, :frozen_string_literal
11
8
 
12
9
  def initialize(file)
13
10
  @file = file
14
11
  @frozen_string_literal = false
15
- @before = []
16
- @sets = [
17
- { node: nil, plugin: [], gem: [] } # all gems outside a specific block (group, source, etc) will end up here
18
- ]
12
+ @preamble_nodes = []
13
+ @gem_sets = [GemSet.new(default: true)] # all gems outside a specific block (group, source, etc) will end up here
19
14
  end
20
15
 
21
- def write!
22
- parse!
23
-
24
- buffer = []
25
-
26
- buffer << "# frozen_string_literal: true" if frozen_string_literal
27
-
28
- before.each do |node|
29
- buffer << node_to_s(node)
16
+ def build_set(name:, node: nil)
17
+ GemSet.new(name:, node:).tap do |set|
18
+ @gem_sets << set
30
19
  end
31
-
32
- sets.sort_by { node_to_s(_1[:node]) || "" }.each do |set|
33
- set_node = set[:node]
34
- set_buffer = []
35
-
36
- set_buffer << [node_to_s(set_node), set_node.block.opening].compact.join(" ") if set_node
37
-
38
- set_buffer << SORTABLE_NODES.map do |node_type|
39
- nodes = set[node_type].sort_by { _1[:name] }
40
-
41
- nodes.map do |gem|
42
- line = +""
43
- line << " " if set_node
44
- line << node_to_s(gem[:node])
45
- line << " # #{gem[:summary]}" if gem[:summary]
46
-
47
- line
48
- end.join("\n")
49
- end.reject(&:empty?).join("\n\n")
50
-
51
- set_buffer << set_node.block.closing if set_node&.block
52
-
53
- buffer << set_buffer.join("\n")
54
- end
55
-
56
- buffer.reject(&:empty?).join("\n\n") + "\n"
57
20
  end
58
21
 
59
- private
60
-
61
22
  def parse!
23
+ return if @parsed
24
+
62
25
  parsed = Prism.parse(File.read(file))
63
26
 
64
27
  @frozen_string_literal = parsed.magic_comments.map(&:key).include?("frozen_string_literal")
65
28
 
66
- parsed.value.compact_child_nodes[0].compact_child_nodes.each do |node|
67
- case node.type
68
- when :call_node
69
- case node.name
70
- when *SORTABLE_NODES
71
- @sets[0][node.name] << parse_gem(node)
72
- when *BLOCK_NODES
73
- if node.block
74
- children = node.block.body.compact_child_nodes
75
-
76
- set = { node:, plugin: [], gem: [] }
77
-
78
- children.each do |child_node|
79
- parsed_node = parse_gem(child_node)
80
-
81
- set[child_node.name] << parsed_node if parsed_node
82
- end
29
+ visitor = Visitor.new(self)
30
+ parsed.value.accept(visitor)
83
31
 
84
- @sets << set
85
- else
86
- @before << node
87
- end
88
- end
89
- end
90
- end
91
- end
92
-
93
- def find_gem(name)
94
- summary = find_loaded_gem_summary(name) || find_external_gem_summary(name)
95
-
96
- { name:, summary: }
97
- end
98
-
99
- def find_loaded_gem_summary(name)
100
- gem = Gem::Specification.find_by_name(name)
101
-
102
- gem.summary
103
- rescue Gem::MissingSpecError
104
- nil
32
+ @parsed = true
105
33
  end
106
34
 
107
- def find_external_gem_summary(name)
108
- spec = Gem::SpecFetcher.fetcher.spec_for_dependency Gem::Dependency.new(name)
109
-
110
- return unless spec
111
- return if spec == [[], []]
112
-
113
- spec.first.first.first.summary
114
- end
115
-
116
- def node_to_s(node)
117
- return if node.nil?
118
-
119
- case node.type
120
- when :call_node
121
- [node.name, node_to_s(node.arguments)].compact.join(" ")
122
- when :keyword_hash_node
123
- node.elements.map do |element|
124
- key = node_to_s(element.key)
125
- value = node_to_s(element.value)
126
-
127
- "#{key} #{value}"
128
- end
129
- when :symbol_node
130
- "#{node.opening}#{node.value}#{node.closing}"
131
- when :string_node
132
- "#{node.opening}#{node.content}#{node.closing}"
133
- when :array_node
134
- separator = node.opening == "[" ? ", " : " "
135
-
136
- node.opening + node.elements.map do |element|
137
- node_to_s(element)
138
- end.join(separator) + node.closing
139
- when :true_node
140
- "true"
141
- when :false_node
142
- "false"
143
- when :arguments_node
144
- return if node.child_nodes.empty?
145
-
146
- node.child_nodes.map { node_to_s(_1) }.join(", ")
147
- else
148
- raise NotImplementedError, "Unknown node type: #{node.type}"
149
- end
35
+ def parse_and_write!
36
+ parse!
37
+ Writer.new(self).write!
150
38
  end
151
39
 
152
- def parse_gem(node)
153
- return unless node.type == :call_node && SORTABLE_NODES.include?(node.name)
154
-
155
- gem_name = node.arguments.child_nodes[0].content
156
-
157
- find_gem(gem_name).merge(node:)
40
+ def find_or_build_set(name:, node: nil)
41
+ gem_sets.find { |set| set.name == name } || build_set(name:, node:)
158
42
  end
159
43
  end
160
44
  end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rebundler
4
+ class Serializer
5
+ def self.node_to_s(node)
6
+ return if node.nil?
7
+
8
+ case node.type
9
+ when :call_node
10
+ [node.name, node_to_s(node.arguments)].compact.join(" ")
11
+ when :keyword_hash_node
12
+ node.elements.map do |element|
13
+ key = node_to_s(element.key)
14
+ value = node_to_s(element.value)
15
+
16
+ "#{key} #{value}"
17
+ end
18
+ when :symbol_node
19
+ "#{node.opening}#{node.value}#{node.closing}"
20
+ when :string_node
21
+ "#{node.opening}#{node.content}#{node.closing}"
22
+ when :array_node
23
+ separator = node.opening == "[" ? ", " : " "
24
+
25
+ node.opening + node.elements.map do |element|
26
+ node_to_s(element)
27
+ end.join(separator) + node.closing
28
+ when :true_node
29
+ "true"
30
+ when :false_node
31
+ "false"
32
+ when :arguments_node
33
+ return if node.child_nodes.empty?
34
+
35
+ node.child_nodes.map { |child| node_to_s(child) }.join(", ")
36
+ else
37
+ raise Rebundler::Error, "Unsupported node type: #{node.type}"
38
+ end
39
+ end
40
+ end
41
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rebundler
4
- VERSION = "0.1.1"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rebundler
4
+ class Visitor < Prism::Visitor
5
+ def initialize(parser)
6
+ super()
7
+ @parser = parser
8
+ @current_set = @parser.gem_sets.find(&:default)
9
+ end
10
+
11
+ def visit_call_node(node)
12
+ case node.name
13
+ when :plugin
14
+ parsed_gem = GemFetcher.parse_gem(node)
15
+ @current_set.plugins << parsed_gem if parsed_gem
16
+ when :gem
17
+ parsed_gem = GemFetcher.parse_gem(node)
18
+ @current_set.gems << parsed_gem if parsed_gem
19
+ when *BLOCK_NODES
20
+ if node.block
21
+ name = Serializer.node_to_s(node)
22
+
23
+ previous_set = @current_set
24
+ @current_set = @parser.find_or_build_set(name:, node:)
25
+ node.block.body&.accept(self)
26
+ @current_set = previous_set
27
+ else
28
+ @parser.preamble_nodes << node
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rebundler
4
+ class Writer
5
+ def initialize(parser)
6
+ @parser = parser
7
+ end
8
+
9
+ def write!
10
+ buffer = []
11
+
12
+ buffer << "# frozen_string_literal: true" if frozen_string_literal
13
+
14
+ preamble_nodes.each do |node|
15
+ buffer << Serializer.node_to_s(node)
16
+ end
17
+
18
+ gem_sets.sort.each do |set|
19
+ set_buffer = []
20
+
21
+ set_buffer << [Serializer.node_to_s(set.node), set.node.block.opening].compact.join(" ") if set.node
22
+
23
+ set_buffer << [set.plugins, set.gems].map do |nodes|
24
+ sorted = nodes.sort_by { |node| node[:name].tr("-_", "").downcase }
25
+
26
+ sorted.map do |gem|
27
+ line = +""
28
+ line << " " if set.node
29
+ line << Serializer.node_to_s(gem[:node])
30
+ line << " # #{gem[:summary]}" if gem[:summary]
31
+
32
+ line
33
+ end.join("\n")
34
+ end.reject(&:empty?).join("\n\n")
35
+
36
+ set_buffer << set.node.block.closing if set.node&.block
37
+
38
+ buffer << set_buffer.join("\n")
39
+ end
40
+
41
+ buffer.reject(&:empty?).join("\n\n") + "\n"
42
+ end
43
+
44
+ private
45
+
46
+ def preamble_nodes
47
+ @parser.preamble_nodes
48
+ end
49
+
50
+ def gem_sets
51
+ @parser.gem_sets
52
+ end
53
+
54
+ def frozen_string_literal
55
+ @parser.frozen_string_literal
56
+ end
57
+ end
58
+ end
data/lib/rebundler.rb CHANGED
@@ -1,11 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "rebundler/version"
4
- require_relative "rebundler/parser"
5
-
6
- require_relative "bundler/rebundle"
3
+ require "zeitwerk"
4
+ loader = Zeitwerk::Loader.for_gem
5
+ loader.ignore("#{__dir__}/bundler")
6
+ loader.setup
7
7
 
8
8
  module Rebundler
9
9
  class Error < StandardError; end
10
- # Your code goes here...
10
+
11
+ SORTABLE_NODES = %i[plugin gem].freeze
12
+ BLOCK_NODES = %i[gemspec git group path platforms ruby source].freeze
11
13
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rebundler
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dennis Paagman
@@ -25,8 +25,13 @@ files:
25
25
  - lib/bundler/rebundle.rb
26
26
  - lib/rebundler.rb
27
27
  - lib/rebundler/Rakefile
28
+ - lib/rebundler/gem_fetcher.rb
29
+ - lib/rebundler/gem_set.rb
28
30
  - lib/rebundler/parser.rb
31
+ - lib/rebundler/serializer.rb
29
32
  - lib/rebundler/version.rb
33
+ - lib/rebundler/visitor.rb
34
+ - lib/rebundler/writer.rb
30
35
  - plugins.rb
31
36
  homepage: https://github.com/dennispaagman/rebundler
32
37
  licenses:
@@ -50,7 +55,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
50
55
  - !ruby/object:Gem::Version
51
56
  version: '0'
52
57
  requirements: []
53
- rubygems_version: 3.6.7
58
+ rubygems_version: 3.6.9
54
59
  specification_version: 4
55
60
  summary: Rebundler makes your Gemfile look good.
56
61
  test_files: []