rfmt 1.6.3-aarch64-linux → 2.0.0.beta1-aarch64-linux

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: be2a07afb756e17769afe0d1036e8bea93935fa58438ee277f5d49358d4b067f
4
- data.tar.gz: 761e3b33656a52fa953a22bd74cc485726002d06ad507275145e1564171ece17
3
+ metadata.gz: f0c6681074bc422a604388e60cc5dee756651e2a645363f8226b81f5dd96992b
4
+ data.tar.gz: 0bddc889fcf62ba16e2868e4ea47254430954aea4755267e83e38e515d321c93
5
5
  SHA512:
6
- metadata.gz: 7076830aa866c79630799d381ec4c25de31d614c18dc590530a40abb6bcd222134c7f7f060d6951acbbb82550e548dd05afb218eb7c0fff674bf3366b007fa12
7
- data.tar.gz: fd6cb75b7f92f5add6ab88bbfc4a20c53c51b78db138c2ebf4ddb6408a536156dfae007cf7a9669be623626936be44c886021edfe5b1da12c2842d001318cfb3
6
+ metadata.gz: 87fe10e37abd3c2b7f9c63ec96899201aa7cb3c320baf427a7e9501b0dbe8541761e43438a7e5cc4b9c97e1137d6b052ceb3017602dad3891cd4ce560ba36d61
7
+ data.tar.gz: fdf482ae1350e6aa4ad44c70e68c9246eaf8a22e35a8f39bfb821a4f5ebc11756b8245c4af69b86b54a950ee88dcb97b4fadb89282f19e817624eac4803c53f8
data/CHANGELOG.md CHANGED
@@ -1,5 +1,43 @@
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
+
28
+ ## [1.7.0] - 2026-06-04
29
+
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.
31
+
32
+ ### Added
33
+
34
+ - Standalone LSP server `rfmt-lsp` (#108). Provides format-on-save in any LSP-capable editor without requiring Ruby LSP or a Gemfile:
35
+ - `textDocument/formatting` with full document sync; unsaved buffer contents are formatted via `didOpen`/`didChange` tracking
36
+ - `.rfmt.yml` is resolved relative to the workspace root (`rootUri`/`workspaceFolders`)
37
+ - Graceful handling of syntax errors (returns no edits instead of crashing), empty files, and unsupported LSP methods
38
+ - New `exe/rfmt-lsp` executable shipped with the gem
39
+ - Editor setup guides for the standalone server (Neovim, Helix, Emacs eglot) in `docs/editors.md`
40
+
3
41
  ## [1.6.3] - 2026-04-24
4
42
 
5
43
  Minor stability refinements on top of the 1.6.x architecture release. See the 1.6.1 notes below for the feature set this series delivers.
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**: Core formatter implemented in Rust
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
- rfmt delivers consistent, fast formatting across projects of any size:
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
- | Project Size | Files | Execution Time | Throughput |
55
- |-------------|-------|----------------|------------|
56
- | Small | 9 files | ~105ms | 85 files/sec |
57
- | Medium | 35 files | ~110ms | 315 files/sec |
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
- **Key Performance Characteristics:**
59
+ Measured with `scripts/bench_format.rb` over rfmt's own `lib/` corpus on arm64 macOS, Ruby 3.4. Reproduce with:
61
60
 
62
- - **Constant Time**: Execution time stays around 100ms regardless of project size
63
- - **Parallel Processing**: Automatic scaling with available CPU cores
64
- - **High Throughput**: Up to 1,500+ files per second on large projects
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
- *Built with Rust for optimal performance and memory efficiency.*
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 detailed performance comparisons and benchmarks, see [Performance Benchmarks](docs/benchmark.md).
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.0 or higher
73
+ - Ruby 3.3 or higher
81
74
  - Rust 1.70 or higher (for building from source)
82
75
 
83
76
  ### From RubyGems
@@ -361,7 +354,46 @@ end
361
354
 
362
355
  ## Editor Integration
363
356
 
364
- rfmt integrates with editors through [Ruby LSP](https://shopify.github.io/ruby-lsp/). For detailed setup instructions, see [Editor Integration Guide](docs/editors.md).
357
+ rfmt can integrate with editors in two ways:
358
+
359
+ - Standalone LSP: run `rfmt-lsp` directly from your editor. This works well for single
360
+ Ruby scripts or projects without a Gemfile.
361
+ - Ruby LSP add-on: use rfmt as the formatter inside
362
+ [Ruby LSP](https://shopify.github.io/ruby-lsp/).
363
+
364
+ For detailed setup instructions, see [Editor Integration Guide](docs/editors.md).
365
+
366
+ ### Standalone LSP
367
+
368
+ After installing rfmt, configure your editor's Ruby language server command to `rfmt-lsp`.
369
+
370
+ ```bash
371
+ gem install rfmt
372
+ rfmt-lsp
373
+ ```
374
+
375
+ Example Neovim configuration (with `nvim-lspconfig`):
376
+
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
391
+
392
+ lspconfig.rfmt.setup({})
393
+ ```
394
+
395
+ Helix, Emacs, and Zed configurations are covered in the
396
+ [Editor Integration Guide](docs/editors.md).
365
397
 
366
398
  ### VSCode (Quick Start)
367
399
 
data/exe/rfmt-lsp ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ $VERBOSE = nil unless ENV['RFMT_VERBOSE']
5
+
6
+ require 'rfmt/lsp/server'
7
+
8
+ exit Rfmt::LSP::Server.new.run
data/exe/rfmt_fast ADDED
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Fast launcher for rfmt without gem warnings
5
+
6
+ # Suppress all warnings
7
+ $VERBOSE = nil
8
+
9
+ # Direct load without rubygems overhead
10
+ require_relative '../lib/rfmt/cli'
11
+
12
+ Rfmt::CLI.start(ARGV)
@@ -0,0 +1,9 @@
1
+ # leading comment
2
+ x = 1 # trailing comment
3
+
4
+ =begin
5
+ embdoc line one
6
+ line two
7
+ =end
8
+ y = 2
9
+ # final comment
@@ -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,12 @@
1
+ =begin
2
+ Block documentation spanning
3
+ multiple lines.
4
+ =end
5
+ value = 42
6
+
7
+ =begin
8
+ Another embedded document right before a method.
9
+ =end
10
+ def answer
11
+ value
12
+ end
@@ -0,0 +1,15 @@
1
+ sql = <<~SQL
2
+ SELECT *
3
+ FROM users
4
+ WHERE active = true
5
+ SQL
6
+
7
+ body = <<-TEXT
8
+ indented terminator keeps its column
9
+ TEXT
10
+
11
+ plain = <<HTML
12
+ <p>no squiggly</p>
13
+ HTML
14
+
15
+ after = sql.length + body.length
@@ -0,0 +1,17 @@
1
+ result = execute(<<~SQL, timeout: 5)
2
+ SELECT id
3
+ FROM events
4
+ SQL
5
+
6
+ logger.info(prefix, <<~MSG)
7
+ first line
8
+ second line
9
+ MSG
10
+
11
+ wrap(inner(<<~ONE), <<~TWO)
12
+ one body
13
+ ONE
14
+ two body
15
+ TWO
16
+
17
+ puts result
@@ -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,14 @@
1
+ result = flag ? 1 : 2
2
+
3
+ if flag
4
+ a = 1
5
+ else
6
+ a = 2
7
+ end
8
+
9
+ unless flag
10
+ b = 3
11
+ end
12
+
13
+ c = 4 if flag
14
+ d = 5 unless flag
@@ -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,14 @@
1
+ greeting = "こんにちは、世界"
2
+ farewell = 'さようなら'; short = "あ"
3
+
4
+ def label(polite: false)
5
+ polite ? "です" : "だ"
6
+ end
7
+
8
+ mixed = "emoji 🎉 と 日本語"
9
+ message = <<~TEXT
10
+ 一行目の本文
11
+ 二行目の本文
12
+ TEXT
13
+
14
+ combined = "#{greeting} #{mixed}".freeze
@@ -0,0 +1,6 @@
1
+ a = 3r
2
+ b = 3.14r
3
+ c = 2i
4
+ d = 2ri
5
+ e = 6.02e2i
6
+ f = a + b * c - d / e
@@ -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.so CHANGED
Binary file
data/lib/rfmt/3.4/rfmt.so 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 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)
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rfmt
4
+ module LSP
5
+ class DocumentStore
6
+ def initialize
7
+ @documents = {}
8
+ end
9
+
10
+ def open(uri, text)
11
+ @documents[uri] = text
12
+ end
13
+
14
+ def change(uri, text)
15
+ @documents[uri] = text
16
+ end
17
+
18
+ def close(uri)
19
+ @documents.delete(uri)
20
+ end
21
+
22
+ def source_for(uri)
23
+ @documents[uri]
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rfmt'
4
+
5
+ module Rfmt
6
+ module LSP
7
+ class Formatter
8
+ def self.format_edits(source)
9
+ formatted = source.empty? ? "\n" : Rfmt.format(source)
10
+ return [] if formatted == source
11
+
12
+ [
13
+ {
14
+ range: full_document_range(source),
15
+ newText: formatted
16
+ }
17
+ ]
18
+ rescue Rfmt::Error
19
+ []
20
+ end
21
+
22
+ def self.full_document_range(source)
23
+ return { start: { line: 0, character: 0 }, end: { line: 0, character: 0 } } if source.empty?
24
+
25
+ lines = source.split("\n", -1)
26
+
27
+ {
28
+ start: { line: 0, character: 0 },
29
+ end: {
30
+ line: lines.length - 1,
31
+ character: lines.last.length
32
+ }
33
+ }
34
+ end
35
+ end
36
+ end
37
+ end