rfmt 1.7.0-arm64-darwin → 2.0.0.beta1-arm64-darwin
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 +25 -0
- data/README.md +32 -28
- data/ext/rfmt/tests/fixtures/parity/comments_mixed.rb +9 -0
- data/ext/rfmt/tests/fixtures/parity/constructs.rb +50 -0
- data/ext/rfmt/tests/fixtures/parity/embdoc.rb +12 -0
- data/ext/rfmt/tests/fixtures/parity/heredoc_assign.rb +15 -0
- data/ext/rfmt/tests/fixtures/parity/heredoc_call_args.rb +17 -0
- data/ext/rfmt/tests/fixtures/parity/metadata_classes.rb +30 -0
- data/ext/rfmt/tests/fixtures/parity/metadata_conditionals.rb +14 -0
- data/ext/rfmt/tests/fixtures/parity/metadata_defs.rb +33 -0
- data/ext/rfmt/tests/fixtures/parity/multibyte.rb +14 -0
- data/ext/rfmt/tests/fixtures/parity/numeric.rb +6 -0
- data/ext/rfmt/tests/fixtures/parity/plain.rb +31 -0
- data/lib/rfmt/3.3/rfmt.bundle +0 -0
- data/lib/rfmt/3.4/rfmt.bundle +0 -0
- data/lib/rfmt/cli.rb +37 -7
- data/lib/rfmt/configuration.rb +2 -20
- data/lib/rfmt/version.rb +1 -1
- data/lib/rfmt.rb +47 -19
- metadata +13 -18
- data/lib/rfmt/prism_bridge.rb +0 -529
- data/lib/rfmt/prism_node_extractor.rb +0 -115
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2e48a20dec3297e9b379a630a804d753f3d64c0ad799008cc82afe8901939180
|
|
4
|
+
data.tar.gz: 271e9efdd6b8147ced7f01edd322b04979ebee53a1697e0d942ddc370e82ff70
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e9e6eb7d91b7af5b66a61a62653d65b51675d9c097812fdd22cd5a5970cf1e04d626e9a1ac14775f3d37f7962a9ff07c561e33526de752bc476bf7cbf492b90a
|
|
7
|
+
data.tar.gz: 612c79ff387d04cb287d300084c8a5abd96dfb6a6fc090d3165438bde58049d406cf5b71bc74403bb9bd1cef4b53eb256bc6fd2a40ef178f1172722cc460dd41
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [2.0.0.beta1] - 2026-07-22
|
|
4
|
+
|
|
5
|
+
Parsing now happens natively in Rust. The Ruby-side Prism parse and JSON handoff have been replaced by the ruby-prism crate with prism statically linked into the extension; Ruby remains the CLI/LSP shell.
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Native parsing via the ruby-prism crate: in-process formatting is about 22x faster: 0.19 ms/file measured with `scripts/bench_format.rb`, against a historical pre-migration baseline of 4.28 ms/file
|
|
10
|
+
- `--config` is now honored when formatting
|
|
11
|
+
- File writes are atomic (write to a temp file, then rename)
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- Output-validation guard: formatted output is re-parsed and rejected if it fails to parse
|
|
16
|
+
- Corpus check (`scripts/corpus_check.rb`): reformats every Ruby file in the repository and verifies AST equivalence with the prism gem; runs in CI alongside the parity fixtures
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- The `__END__` data section is no longer dropped from formatted output; everything from `__END__` to EOF is preserved byte-for-byte
|
|
21
|
+
- Method chain continuation indentation is now computed from the statement's output position instead of its input column, so misindented input converges in a single pass; heredoc bodies inside a reformatted chain keep their content byte-identical
|
|
22
|
+
- A trailing comment on an `else` line stays on that line instead of moving below it
|
|
23
|
+
|
|
24
|
+
### Removed
|
|
25
|
+
|
|
26
|
+
- prism gem runtime dependency; it remains a development-only dependency for the corpus check and parity fixtures
|
|
27
|
+
|
|
3
28
|
## [1.7.0] - 2026-06-04
|
|
4
29
|
|
|
5
30
|
rfmt now ships a standalone LSP server. Point any LSP-capable editor at `rfmt-lsp` and you get format-on-save — no Ruby LSP, no Gemfile, and no editor-specific plugin required. This makes rfmt usable from Helix, Neovim, Emacs, and any other LSP client, including in projects that don't bundle rfmt.
|
data/README.md
CHANGED
|
@@ -30,7 +30,7 @@ A Ruby code formatter written in Rust
|
|
|
30
30
|
- **Opinionated**: Minimal configuration with consistent output
|
|
31
31
|
- **Idempotent**: Running multiple times produces identical results
|
|
32
32
|
- **Comment preservation**: Maintains existing comment placement
|
|
33
|
-
- **Rust implementation**:
|
|
33
|
+
- **Rust implementation**: Parsing and formatting both run natively in Rust (via the [ruby-prism](https://crates.io/crates/ruby-prism) crate); Ruby provides the CLI and LSP shell
|
|
34
34
|
|
|
35
35
|
## Features
|
|
36
36
|
|
|
@@ -49,35 +49,28 @@ Enforces code style rules:
|
|
|
49
49
|
|
|
50
50
|
## Performance
|
|
51
51
|
|
|
52
|
-
|
|
52
|
+
Parsing and formatting both run natively in Rust (the [ruby-prism](https://crates.io/crates/ruby-prism) crate, with prism statically linked), so the per-file cost is well under a millisecond:
|
|
53
53
|
|
|
54
|
-
|
|
|
55
|
-
|
|
56
|
-
|
|
|
57
|
-
|
|
|
58
|
-
| Large | 151 files | ~100ms | 1,560 files/sec |
|
|
54
|
+
| Pipeline | In-process format time |
|
|
55
|
+
|----------|------------------------|
|
|
56
|
+
| Before native parsing (Ruby Prism parse + JSON handoff to Rust; historical, not reproducible from this checkout) | 4.28 ms/file |
|
|
57
|
+
| Now (parsing and formatting in Rust) | 0.19 ms/file |
|
|
59
58
|
|
|
60
|
-
|
|
59
|
+
Measured with `scripts/bench_format.rb` over rfmt's own `lib/` corpus on arm64 macOS, Ruby 3.4. Reproduce with:
|
|
61
60
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
- **Low Overhead**: Minimal startup time and memory usage
|
|
66
|
-
|
|
67
|
-
**Test Environment:**
|
|
68
|
-
- CPU: Apple Silicon (arm64)
|
|
69
|
-
- Ruby: 3.4.8
|
|
70
|
-
- Average of 5 runs per test
|
|
61
|
+
```bash
|
|
62
|
+
bundle exec ruby scripts/bench_format.rb
|
|
63
|
+
```
|
|
71
64
|
|
|
72
|
-
|
|
65
|
+
A cold CLI invocation (`rfmt --check FILE`) takes roughly 0.1-0.25 s of wall-clock time; that is Ruby VM startup, not formatting.
|
|
73
66
|
|
|
74
|
-
For
|
|
67
|
+
For more detail and a historical comparison against RuboCop, see [Performance Benchmarks](docs/benchmark.md).
|
|
75
68
|
|
|
76
69
|
## Installation
|
|
77
70
|
|
|
78
71
|
### Requirements
|
|
79
72
|
|
|
80
|
-
- Ruby 3.
|
|
73
|
+
- Ruby 3.3 or higher
|
|
81
74
|
- Rust 1.70 or higher (for building from source)
|
|
82
75
|
|
|
83
76
|
### From RubyGems
|
|
@@ -379,18 +372,29 @@ gem install rfmt
|
|
|
379
372
|
rfmt-lsp
|
|
380
373
|
```
|
|
381
374
|
|
|
382
|
-
Example
|
|
375
|
+
Example Neovim configuration (with `nvim-lspconfig`):
|
|
383
376
|
|
|
384
|
-
```
|
|
385
|
-
|
|
386
|
-
|
|
377
|
+
```lua
|
|
378
|
+
local configs = require("lspconfig.configs")
|
|
379
|
+
local lspconfig = require("lspconfig")
|
|
380
|
+
|
|
381
|
+
if not configs.rfmt then
|
|
382
|
+
configs.rfmt = {
|
|
383
|
+
default_config = {
|
|
384
|
+
cmd = { "rfmt-lsp" },
|
|
385
|
+
filetypes = { "ruby" },
|
|
386
|
+
root_dir = lspconfig.util.root_pattern(".rfmt.yml", ".git"),
|
|
387
|
+
single_file_support = true,
|
|
388
|
+
},
|
|
389
|
+
}
|
|
390
|
+
end
|
|
387
391
|
|
|
388
|
-
|
|
389
|
-
name = "ruby"
|
|
390
|
-
language-servers = ["rfmt"]
|
|
391
|
-
auto-format = true
|
|
392
|
+
lspconfig.rfmt.setup({})
|
|
392
393
|
```
|
|
393
394
|
|
|
395
|
+
Helix, Emacs, and Zed configurations are covered in the
|
|
396
|
+
[Editor Integration Guide](docs/editors.md).
|
|
397
|
+
|
|
394
398
|
### VSCode (Quick Start)
|
|
395
399
|
|
|
396
400
|
1. Install [Ruby LSP extension](https://marketplace.visualstudio.com/items?itemName=Shopify.ruby-lsp)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
expected = :ready
|
|
2
|
+
|
|
3
|
+
case payload
|
|
4
|
+
in [Integer => first, *rest]
|
|
5
|
+
first + rest.sum
|
|
6
|
+
in { status: String => status, code: 200 | 201 }
|
|
7
|
+
status
|
|
8
|
+
in ^expected
|
|
9
|
+
:pinned
|
|
10
|
+
else
|
|
11
|
+
:unmatched
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
begin
|
|
15
|
+
risky!
|
|
16
|
+
rescue KeyError => e
|
|
17
|
+
recover(e)
|
|
18
|
+
rescue StandardError
|
|
19
|
+
retry_count += 1
|
|
20
|
+
retry if retry_count < 3
|
|
21
|
+
else
|
|
22
|
+
celebrate
|
|
23
|
+
ensure
|
|
24
|
+
cleanup
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
counters = Hash.new(0)
|
|
28
|
+
counters[:hits] ||= 0
|
|
29
|
+
counters[:hits] += 1
|
|
30
|
+
@cache &&= @cache.compact
|
|
31
|
+
$verbose ||= false
|
|
32
|
+
CONFIG = { a: 3r, b: 3.14r, c: 2i, d: 2ri }
|
|
33
|
+
|
|
34
|
+
class << self
|
|
35
|
+
def singleton_helper = :ok
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
alias aka singleton_helper
|
|
39
|
+
alias $err $stderr
|
|
40
|
+
undef :aka
|
|
41
|
+
|
|
42
|
+
flip = items.select { |l| (l == first)..(l == last) }
|
|
43
|
+
|
|
44
|
+
lines = ->(input) { input.each_line.to_a }
|
|
45
|
+
x, *middle, z = lines.call(<<~TXT)
|
|
46
|
+
alpha
|
|
47
|
+
beta
|
|
48
|
+
gamma
|
|
49
|
+
TXT
|
|
50
|
+
puts x, middle, z
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
class Foo::Bar < Baz::Qux
|
|
2
|
+
def change
|
|
3
|
+
x = compute(1)
|
|
4
|
+
@count = x
|
|
5
|
+
end
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
class AddUsers < ActiveRecord::Migration[8.1]
|
|
9
|
+
def change
|
|
10
|
+
create_table :users
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
class Simple < Base
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class Plain
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
module Alpha::Beta
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
module Gamma
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
class ::TopScoped < ::Deep::Nested::Base
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
module Alpha::Beta::Gamma
|
|
30
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
def !@
|
|
2
|
+
true
|
|
3
|
+
end
|
|
4
|
+
|
|
5
|
+
def +@
|
|
6
|
+
self
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def []=(key, value)
|
|
10
|
+
store(key, value)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.build(name)
|
|
14
|
+
new(name)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
obj = Object.new
|
|
18
|
+
|
|
19
|
+
def obj.run
|
|
20
|
+
1
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def bare arg
|
|
24
|
+
arg
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def none
|
|
28
|
+
0
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def empty_parens()
|
|
32
|
+
nil
|
|
33
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
class Greeter
|
|
2
|
+
attr_reader :name
|
|
3
|
+
|
|
4
|
+
def initialize(name)
|
|
5
|
+
@name = name
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
def greet(loud: false)
|
|
9
|
+
message = "hello, #{name}"
|
|
10
|
+
if loud
|
|
11
|
+
message.upcase
|
|
12
|
+
else
|
|
13
|
+
message
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
module Registry
|
|
19
|
+
DEFAULT = Greeter.new("world")
|
|
20
|
+
|
|
21
|
+
def self.lookup(key)
|
|
22
|
+
entries.fetch(key) { DEFAULT }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
items = [1, 2.5, :three, nil, true]
|
|
27
|
+
totals = { "sum" => items.compact.size, count: items.length }
|
|
28
|
+
items.each_with_index do |item, index|
|
|
29
|
+
puts "#{index}: #{item}" unless item.nil?
|
|
30
|
+
end
|
|
31
|
+
result = Registry.lookup(:default)&.greet(loud: totals[:count] > 3)
|
data/lib/rfmt/3.3/rfmt.bundle
CHANGED
|
Binary file
|
data/lib/rfmt/3.4/rfmt.bundle
CHANGED
|
Binary file
|
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
|
|
128
|
+
desc 'config', 'Show the effective configuration the formatter will use'
|
|
124
129
|
def config_cmd
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
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]
|
data/lib/rfmt/configuration.rb
CHANGED
|
@@ -3,17 +3,13 @@
|
|
|
3
3
|
require 'yaml'
|
|
4
4
|
|
|
5
5
|
module Rfmt
|
|
6
|
-
#
|
|
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
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
|
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
|
-
|
|
58
|
-
|
|
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
|
-
|
|
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
|
-
['
|
|
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
|