rfmt 1.7.0 → 2.0.0.beta1

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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -0
  3. data/Cargo.lock +90 -1225
  4. data/Cargo.toml +6 -0
  5. data/README.md +32 -28
  6. data/ext/rfmt/Cargo.toml +9 -28
  7. data/ext/rfmt/src/ast/mod.rs +2 -0
  8. data/ext/rfmt/src/config/mod.rs +267 -30
  9. data/ext/rfmt/src/error/mod.rs +14 -3
  10. data/ext/rfmt/src/format/formatter.rs +22 -11
  11. data/ext/rfmt/src/format/registry.rs +8 -0
  12. data/ext/rfmt/src/format/rule.rs +208 -136
  13. data/ext/rfmt/src/format/rules/call.rs +14 -22
  14. data/ext/rfmt/src/format/rules/fallback.rs +4 -11
  15. data/ext/rfmt/src/format/rules/if_unless.rs +25 -3
  16. data/ext/rfmt/src/format/rules/loops.rs +3 -1
  17. data/ext/rfmt/src/format/rules/variable_write.rs +16 -9
  18. data/ext/rfmt/src/lib.rs +45 -12
  19. data/ext/rfmt/src/parser/mod.rs +2 -0
  20. data/ext/rfmt/src/parser/native_adapter.rs +2735 -0
  21. data/ext/rfmt/src/parser/prism_adapter.rs +5 -0
  22. data/ext/rfmt/src/validation.rs +69 -0
  23. data/ext/rfmt/tests/fixtures/parity/comments_mixed.rb +9 -0
  24. data/ext/rfmt/tests/fixtures/parity/constructs.rb +50 -0
  25. data/ext/rfmt/tests/fixtures/parity/embdoc.rb +12 -0
  26. data/ext/rfmt/tests/fixtures/parity/heredoc_assign.rb +15 -0
  27. data/ext/rfmt/tests/fixtures/parity/heredoc_call_args.rb +17 -0
  28. data/ext/rfmt/tests/fixtures/parity/metadata_classes.rb +30 -0
  29. data/ext/rfmt/tests/fixtures/parity/metadata_conditionals.rb +14 -0
  30. data/ext/rfmt/tests/fixtures/parity/metadata_defs.rb +33 -0
  31. data/ext/rfmt/tests/fixtures/parity/multibyte.rb +14 -0
  32. data/ext/rfmt/tests/fixtures/parity/numeric.rb +6 -0
  33. data/ext/rfmt/tests/fixtures/parity/plain.rb +31 -0
  34. data/ext/rfmt/tests/native_parity.rs +245 -0
  35. data/ext/rfmt/tests/ruby_prism_smoke.rs +66 -0
  36. data/lib/rfmt/cli.rb +37 -7
  37. data/lib/rfmt/configuration.rb +2 -20
  38. data/lib/rfmt/version.rb +1 -1
  39. data/lib/rfmt.rb +47 -19
  40. metadata +17 -18
  41. data/lib/rfmt/prism_bridge.rb +0 -529
  42. data/lib/rfmt/prism_node_extractor.rb +0 -115
data/lib/rfmt/cli.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'fileutils'
3
4
  require 'thor'
4
5
 
5
6
  # Check for verbose flag before loading rfmt to set debug mode early
@@ -44,11 +45,15 @@ module Rfmt
44
45
 
45
46
  # Command Line Interface for rfmt
46
47
  class CLI < Thor
48
+ def self.exit_on_failure?
49
+ true
50
+ end
51
+
47
52
  # Constants
48
53
  PROGRESS_THRESHOLD = 20 # Show progress for file counts >= this
49
54
  PROGRESS_INTERVAL = 10 # Update progress every N files
50
55
 
51
- class_option :config, type: :string, desc: 'Path to configuration file'
56
+ class_option :config, type: :string, desc: 'Path to configuration file (formatting options and file selection)'
52
57
  class_option :verbose, type: :boolean, desc: 'Verbose output'
53
58
 
54
59
  default_command :format
@@ -120,11 +125,11 @@ module Rfmt
120
125
  say "Rust extension: #{Rfmt.rust_version}"
121
126
  end
122
127
 
123
- desc 'config', 'Show current configuration'
128
+ desc 'config', 'Show the effective configuration the formatter will use'
124
129
  def config_cmd
125
- config = load_config
126
- require 'json'
127
- say JSON.pretty_generate(config.config)
130
+ say Rfmt.resolved_config(config_path: options[:config])
131
+ rescue Rfmt::Error => e
132
+ raise Thor::Error, e.message
128
133
  end
129
134
 
130
135
  desc 'cache SUBCOMMAND', 'Manage cache'
@@ -183,12 +188,24 @@ module Rfmt
183
188
 
184
189
  def load_config
185
190
  if options[:config]
191
+ validate_explicit_config!(options[:config])
186
192
  Configuration.new(file: options[:config])
187
193
  else
188
194
  Configuration.discover
189
195
  end
190
196
  end
191
197
 
198
+ # Fail fast once instead of repeating the same load error per file
199
+ def validate_explicit_config!(path)
200
+ raise Thor::Error, "Configuration file not found: #{path}" unless File.exist?(path)
201
+
202
+ begin
203
+ Rfmt.resolved_config(config_path: path)
204
+ rescue Rfmt::Error => e
205
+ raise Thor::Error, e.message
206
+ end
207
+ end
208
+
192
209
  def initialize_cache_if_enabled
193
210
  return nil unless options[:cache]
194
211
 
@@ -255,7 +272,7 @@ module Rfmt
255
272
  start_time = Time.now
256
273
  source = File.read(file)
257
274
 
258
- formatted = Rfmt.format(source)
275
+ formatted = Rfmt.format(source, config_path: options[:config])
259
276
  changed = source != formatted
260
277
 
261
278
  {
@@ -325,11 +342,24 @@ module Rfmt
325
342
  end
326
343
 
327
344
  def write_formatted_file(result, cache)
328
- File.write(result[:file], result[:formatted])
345
+ atomic_write(result[:file], result[:formatted])
329
346
  say "✓ Formatted #{result[:file]}", :green unless options[:quiet]
330
347
  cache&.mark_formatted(result[:file])
331
348
  end
332
349
 
350
+ # Temp file must live in the same directory: rename across filesystems is not atomic.
351
+ def atomic_write(path, content)
352
+ # rename would replace a symlink itself; write to its target instead
353
+ path = File.realpath(path) if File.symlink?(path)
354
+ tmp_path = "#{path}.rfmt-#{Process.pid}.tmp"
355
+ mode = File.stat(path).mode
356
+ File.write(tmp_path, content)
357
+ File.chmod(mode, tmp_path)
358
+ File.rename(tmp_path, path)
359
+ ensure
360
+ FileUtils.rm_f(tmp_path)
361
+ end
362
+
333
363
  def display_summary(stats, total_files)
334
364
  @last_stats = stats # Store for verbose details
335
365
  unchanged_count = total_files - stats[:changed] - stats[:errors]
@@ -3,17 +3,13 @@
3
3
  require 'yaml'
4
4
 
5
5
  module Rfmt
6
- # Configuration management for rfmt
6
+ # File selection and cache concerns only: formatter settings (indent_width
7
+ # etc.) live in the Rust Config, reached via Rfmt.format(config_path:).
7
8
  class Configuration
8
9
  class ConfigError < StandardError; end
9
10
 
10
11
  DEFAULT_CONFIG = {
11
12
  'version' => '1.0',
12
- 'formatting' => {
13
- 'line_length' => 100,
14
- 'indent_width' => 2,
15
- 'indent_style' => 'spaces'
16
- },
17
13
  'include' => ['**/*.rb'],
18
14
  'exclude' => ['vendor/**/*', 'tmp/**/*', 'node_modules/**/*']
19
15
  }.freeze
@@ -43,11 +39,6 @@ module Rfmt
43
39
  (included_files - excluded_files).select { |f| File.file?(f) }
44
40
  end
45
41
 
46
- # Get formatting configuration
47
- def formatting_config
48
- @config['formatting']
49
- end
50
-
51
42
  private
52
43
 
53
44
  def load_configuration(options)
@@ -64,18 +55,9 @@ module Rfmt
64
55
  options.delete('file')
65
56
  config = deep_merge(config, options) unless options.empty?
66
57
 
67
- validate_config!(config)
68
58
  config
69
59
  end
70
60
 
71
- def validate_config!(config)
72
- line_length = config.dig('formatting', 'line_length')
73
- raise ConfigError, 'line_length must be positive' if line_length && line_length <= 0
74
-
75
- indent_width = config.dig('formatting', 'indent_width')
76
- raise ConfigError, 'indent_width must be positive' if indent_width && indent_width <= 0
77
- end
78
-
79
61
  def deep_merge(hash1, hash2)
80
62
  hash1.merge(hash2) do |_key, old_val, new_val|
81
63
  if old_val.is_a?(Hash) && new_val.is_a?(Hash)
data/lib/rfmt/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Rfmt
4
- VERSION = '1.7.0'
4
+ VERSION = '2.0.0.beta1'
5
5
  end
data/lib/rfmt.rb CHANGED
@@ -2,7 +2,6 @@
2
2
 
3
3
  require_relative 'rfmt/version'
4
4
  require_relative 'rfmt/native_extension_loader'
5
- require_relative 'rfmt/prism_bridge'
6
5
 
7
6
  # Load native extension with version-aware loader
8
7
  Rfmt::NativeExtensionLoader.load_extension
@@ -14,25 +13,43 @@ module Rfmt
14
13
  # AST validation errors
15
14
  class ValidationError < RfmtError; end
16
15
 
16
+ # Rust reports errors as plain StandardError with a [Rfmt::<kind>] prefix;
17
+ # these two kinds map onto the public exception classes.
18
+ NATIVE_PARSE_ERROR_PREFIX = '[Rfmt::ParseError] '
19
+ NATIVE_VALIDATION_ERROR_PREFIX = '[Rfmt::ValidationError] '
20
+ NATIVE_CONFIG_ERROR_PREFIX = '[Rfmt::ConfigError] '
21
+ private_constant :NATIVE_PARSE_ERROR_PREFIX, :NATIVE_VALIDATION_ERROR_PREFIX,
22
+ :NATIVE_CONFIG_ERROR_PREFIX
23
+
17
24
  # Format Ruby source code
25
+ # Parsing, config resolution, and output validation all happen natively in Rust
18
26
  # @param source [String] Ruby source code to format
27
+ # @param config_path [String, nil] Explicit config file path; nil discovers
28
+ # rfmt.yml/.rfmt.yml from the current directory upward (cached per process)
19
29
  # @return [String] Formatted Ruby code
20
- def self.format(source)
21
- # Step 1: Parse with Prism (Ruby side)
22
- prism_json = PrismBridge.parse(source)
23
-
24
- # Step 2: Format in Rust
25
- # Pass both source and AST to enable source extraction fallback
26
- format_code(source, prism_json)
27
- rescue PrismBridge::ParseError => e
28
- # Re-raise with more context
29
- raise Error, "Failed to parse Ruby code: #{e.message}"
30
- rescue RfmtError
31
- # Rust side errors are re-raised as-is to preserve error details
32
- raise
30
+ def self.format(source, config_path: nil)
31
+ if config_path
32
+ format_code_with_config(source, config_path.to_s)
33
+ else
34
+ format_code(source)
35
+ end
33
36
  rescue StandardError => e
34
- raise Error, "Unexpected error during formatting: #{e.class}: #{e.message}"
37
+ raise wrap_native_error(e)
38
+ end
39
+
40
+ def self.wrap_native_error(error)
41
+ message = error.message
42
+ if message.start_with?(NATIVE_PARSE_ERROR_PREFIX)
43
+ Error.new("Failed to parse Ruby code: #{message.delete_prefix(NATIVE_PARSE_ERROR_PREFIX)}")
44
+ elsif message.start_with?(NATIVE_VALIDATION_ERROR_PREFIX)
45
+ ValidationError.new(message.delete_prefix(NATIVE_VALIDATION_ERROR_PREFIX))
46
+ elsif message.start_with?(NATIVE_CONFIG_ERROR_PREFIX)
47
+ Error.new("Configuration error: #{message.delete_prefix(NATIVE_CONFIG_ERROR_PREFIX)}")
48
+ else
49
+ Error.new("Unexpected error during formatting: #{error.class}: #{message}")
50
+ end
35
51
  end
52
+ private_class_method :wrap_native_error
36
53
 
37
54
  # Format a Ruby file
38
55
  # @param path [String] Path to Ruby file
@@ -44,6 +61,15 @@ module Rfmt
44
61
  raise Error, "File not found: #{path}"
45
62
  end
46
63
 
64
+ # Effective configuration as the Rust formatter resolves it
65
+ # @param config_path [String, nil] Explicit config file path; nil discovers
66
+ # @return [String] YAML dump of the resolved configuration
67
+ def self.resolved_config(config_path: nil)
68
+ resolved_config_yaml(config_path&.to_s)
69
+ rescue StandardError => e
70
+ raise wrap_native_error(e)
71
+ end
72
+
47
73
  # Get version information
48
74
  # @return [String] Version string including Ruby and Rust versions
49
75
  def self.version_info
@@ -54,8 +80,9 @@ module Rfmt
54
80
  # @param source [String] Ruby source code
55
81
  # @return [String] AST representation
56
82
  def self.parse(source)
57
- prism_json = PrismBridge.parse(source)
58
- parse_to_json(prism_json)
83
+ parse_to_json(source)
84
+ rescue StandardError => e
85
+ raise wrap_native_error(e)
59
86
  end
60
87
 
61
88
  # Configuration management
@@ -114,7 +141,8 @@ module Rfmt
114
141
  current_dir = Dir.pwd
115
142
 
116
143
  loop do
117
- ['.rfmt.yml', '.rfmt.yaml', 'rfmt.yml', 'rfmt.yaml'].each do |filename|
144
+ # Same search order as the Rust side (config/mod.rs CONFIG_FILE_NAMES)
145
+ ['rfmt.yml', 'rfmt.yaml', '.rfmt.yml', '.rfmt.yaml'].each do |filename|
118
146
  config_path = File.join(current_dir, filename)
119
147
  return config_path if File.exist?(config_path)
120
148
  end
@@ -132,7 +160,7 @@ module Rfmt
132
160
  nil
133
161
  end
134
162
  if home_dir
135
- ['.rfmt.yml', '.rfmt.yaml', 'rfmt.yml', 'rfmt.yaml'].each do |filename|
163
+ ['rfmt.yml', 'rfmt.yaml', '.rfmt.yml', '.rfmt.yaml'].each do |filename|
136
164
  config_path = File.join(home_dir, filename)
137
165
  return config_path if File.exist?(config_path)
138
166
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rfmt
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.7.0
4
+ version: 2.0.0.beta1
5
5
  platform: ruby
6
6
  authors:
7
7
  - fujitani sora
@@ -65,20 +65,6 @@ dependencies:
65
65
  - - "~>"
66
66
  - !ruby/object:Gem::Version
67
67
  version: '1.24'
68
- - !ruby/object:Gem::Dependency
69
- name: prism
70
- requirement: !ruby/object:Gem::Requirement
71
- requirements:
72
- - - "~>"
73
- - !ruby/object:Gem::Version
74
- version: '1.6'
75
- type: :runtime
76
- prerelease: false
77
- version_requirements: !ruby/object:Gem::Requirement
78
- requirements:
79
- - - "~>"
80
- - !ruby/object:Gem::Version
81
- version: '1.6'
82
68
  - !ruby/object:Gem::Dependency
83
69
  name: rb_sys
84
70
  requirement: !ruby/object:Gem::Requirement
@@ -156,9 +142,24 @@ files:
156
142
  - ext/rfmt/src/logging/logger.rs
157
143
  - ext/rfmt/src/logging/mod.rs
158
144
  - ext/rfmt/src/parser/mod.rs
145
+ - ext/rfmt/src/parser/native_adapter.rs
159
146
  - ext/rfmt/src/parser/prism_adapter.rs
160
147
  - ext/rfmt/src/policy/mod.rs
161
148
  - ext/rfmt/src/policy/validation.rs
149
+ - ext/rfmt/src/validation.rs
150
+ - ext/rfmt/tests/fixtures/parity/comments_mixed.rb
151
+ - ext/rfmt/tests/fixtures/parity/constructs.rb
152
+ - ext/rfmt/tests/fixtures/parity/embdoc.rb
153
+ - ext/rfmt/tests/fixtures/parity/heredoc_assign.rb
154
+ - ext/rfmt/tests/fixtures/parity/heredoc_call_args.rb
155
+ - ext/rfmt/tests/fixtures/parity/metadata_classes.rb
156
+ - ext/rfmt/tests/fixtures/parity/metadata_conditionals.rb
157
+ - ext/rfmt/tests/fixtures/parity/metadata_defs.rb
158
+ - ext/rfmt/tests/fixtures/parity/multibyte.rb
159
+ - ext/rfmt/tests/fixtures/parity/numeric.rb
160
+ - ext/rfmt/tests/fixtures/parity/plain.rb
161
+ - ext/rfmt/tests/native_parity.rs
162
+ - ext/rfmt/tests/ruby_prism_smoke.rs
162
163
  - lib/rfmt.rb
163
164
  - lib/rfmt/cache.rb
164
165
  - lib/rfmt/cli.rb
@@ -171,8 +172,6 @@ files:
171
172
  - lib/rfmt/lsp/uri.rb
172
173
  - lib/rfmt/lsp/workspace.rb
173
174
  - lib/rfmt/native_extension_loader.rb
174
- - lib/rfmt/prism_bridge.rb
175
- - lib/rfmt/prism_node_extractor.rb
176
175
  - lib/rfmt/version.rb
177
176
  - lib/ruby_lsp/rfmt/addon.rb
178
177
  - lib/ruby_lsp/rfmt/formatter_runner.rb
@@ -192,7 +191,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
192
191
  requirements:
193
192
  - - ">="
194
193
  - !ruby/object:Gem::Version
195
- version: 3.1.0
194
+ version: 3.3.0
196
195
  required_rubygems_version: !ruby/object:Gem::Requirement
197
196
  requirements:
198
197
  - - ">="