From a60da632971d05dadf12a14da5adfef50aed1f3e Mon Sep 17 00:00:00 2001 From: Kazuki Nishikawa Date: Sat, 1 Mar 2025 11:59:40 +0900 Subject: [PATCH 01/30] add PrismFormatter --- lib/rufo.rb | 9 +- lib/rufo/prism_formatter.rb | 36 ++++++++ spec/lib/rufo/prism_formatter_spec.rb | 123 ++++++++++++++++++++++++++ 3 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 lib/rufo/prism_formatter.rb create mode 100644 spec/lib/rufo/prism_formatter_spec.rb diff --git a/lib/rufo.rb b/lib/rufo.rb index 24d94b06..b7044f49 100644 --- a/lib/rufo.rb +++ b/lib/rufo.rb @@ -14,7 +14,13 @@ def initialize(message, lineno) end def self.format(code, **options) - Formatter.format(code, **options) + engine = options.delete(:engine) + case engine + when :prism + PrismFormatter.format(code, **options) + else + Formatter.format(code, **options) + end end end @@ -25,6 +31,7 @@ def self.format(code, **options) require_relative "rufo/parser" require_relative "rufo/formatter" require_relative "rufo/erb_formatter" +require_relative "rufo/prism_formatter" require_relative "rufo/version" require_relative "rufo/file_list" require_relative "rufo/file_finder" diff --git a/lib/rufo/prism_formatter.rb b/lib/rufo/prism_formatter.rb new file mode 100644 index 00000000..30f3612f --- /dev/null +++ b/lib/rufo/prism_formatter.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'prism' + +class Rufo::PrismFormatter + include Rufo::Settings + + def self.format(code, **options) + formatter = new(code, **options) + formatter.format + formatter.result + end + + def initialize(code, **options) + @code = code + @parse_result = Prism.parse(code) + unless @parse_result.errors.empty? + error = @parse_result.errors.first + raise Rufo::SyntaxError.new(error.message, error.location.start_line) + end + + init_settings(options) + end + + def format + @output = @code.dup + + @output.chomp! if @output.end_with?("\n\n") + @output.lstrip! + @output = "\n" if @output.empty? + end + + def result + @output + end +end diff --git a/spec/lib/rufo/prism_formatter_spec.rb b/spec/lib/rufo/prism_formatter_spec.rb new file mode 100644 index 00000000..58a7e829 --- /dev/null +++ b/spec/lib/rufo/prism_formatter_spec.rb @@ -0,0 +1,123 @@ +require "fileutils" + +VERSION = Gem::Version.new(RUBY_VERSION) +FILE_PATH = Pathname.new(File.dirname(__FILE__)) + +def assert_source_specs(source_specs) + relative_path = Pathname.new(source_specs).relative_path_from(FILE_PATH).to_s + + describe relative_path do + tests = [] + current_test = nil + + File.foreach(source_specs).with_index do |line, index| + case + when line =~ /^#~# ORIGINAL ?([\w\s()]+)$/ + # save old test + tests.push current_test if current_test + + # start a new test + + name = $~[1].strip + name = "unnamed test" if name.empty? + + current_test = { name: name, line: index + 1, options: {}, original: "" } + when line =~ /^#~# EXPECTED$/ + current_test[:expected] = "" + when line =~ /^#~# PENDING$/ + # :nocov: + current_test[:pending] = true + # :nocov: + when line =~ /^#~# (.+)$/ + current_test[:options] = eval("{ #{$~[1]} }") + when current_test[:expected] + current_test[:expected] += line + when current_test[:original] + current_test[:original] += line + end + end + + tests.concat([current_test]).each do |test| + it "formats #{test[:name]} (line: #{test[:line]})" do + pending if test[:pending] + formatted = described_class.format(test[:original], **test[:options]) + expected = test[:expected].rstrip + "\n" + expect(formatted).to eq(expected) + idempotency_check = described_class.format(formatted, **test[:options]) + expect(idempotency_check).to eq(formatted) + end + end + end +end + +def assert_format(code, expected = code, **options) + expected = expected.rstrip + "\n" + + line = caller_locations[0].lineno + + opts = options.merge(engine: :prism) + ex = it "formats #{code.inspect} (line: #{line})" do + actual = Rufo.format(code, **opts) + if actual != expected + fail "Expected\n\n~~~\n#{code}\n~~~\nto format to:\n\n~~~\n#{expected}\n~~~\n\nbut got:\n\n~~~\n#{actual}\n~~~\n\n diff = #{expected.inspect}\n #{actual.inspect}" + end + + second = Rufo.format(actual, **opts) + if second != actual + fail "Idempotency check failed. Expected\n\n~~~\n#{actual}\n~~~\nto format to:\n\n~~~\n#{actual}\n~~~\n\nbut got:\n\n~~~\n#{second}\n~~~\n\n diff = #{second.inspect}\n #{actual.inspect}" + end + end + + # This is so we can do `rspec spec/rufo_spec.rb:26` and + # refer to line numbers for assert_format + ex.metadata[:line_number] = line +end + +RSpec.describe Rufo::PrismFormatter do + # Dir[File.join(FILE_PATH, "/prism_formatter_source_specs/*")].each do |source_specs| + # assert_source_specs(source_specs) if File.file?(source_specs) + # end + + # if VERSION >= Gem::Version.new("3.0") + # Dir[File.join(FILE_PATH, "/prism_formatter_source_specs/3.0/*")].each do |source_specs| + # assert_source_specs(source_specs) if File.file?(source_specs) + # end + # end + + # if VERSION >= Gem::Version.new("3.1") + # Dir[File.join(FILE_PATH, "/prism_formatter_source_specs/3.1/*")].each do |source_specs| + # assert_source_specs(source_specs) if File.file?(source_specs) + # end + # end + + # if VERSION >= Gem::Version.new("3.2") + # Dir[File.join(FILE_PATH, "/prism_formatter_source_specs/3.2/*")].each do |source_specs| + # assert_source_specs(source_specs) if File.file?(source_specs) + # end + # end + + # Empty + describe "empty" do + assert_format "", "" + assert_format " ", " " + assert_format "\n", "" + assert_format "\n\n", "" + assert_format "\n\n\n", "" + end + + describe "Syntax errors not handled by Prism", pending: 'no test-case for prism' do + it "raises an unknown syntax error" do + expect { + Rufo.format("def foo; FOO = 1; end", engine: :prism) + }.to raise_error(Rufo::UnknownSyntaxError) + end + end + + describe "Syntax errors handled by Prism" do + it "raises an syntax error" do + expect { + Rufo.format("def foo; FOO = 1; end", engine: :prism) + }.to raise_error(Rufo::SyntaxError) + end + end +end From dd6364320d23220d721767459e1e23e159459404 Mon Sep 17 00:00:00 2001 From: Kazuki Nishikawa Date: Sat, 1 Mar 2025 12:04:26 +0900 Subject: [PATCH 02/30] add prism as a development dependency --- Gemfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Gemfile b/Gemfile index 61159ab3..a0eb27dc 100644 --- a/Gemfile +++ b/Gemfile @@ -7,6 +7,7 @@ gemspec gem "bundler", ">= 1.15" gem "debug", ">= 1.0.0" gem "guard-rspec", "~> 4.0" +gem "prism", "~> 1.2" gem "rake", "~> 13.0" gem "rspec", "~> 3.0" gem "rspec_junit_formatter", "~> 0.6.0" From 3ab962090531e744dcbd37d84a327940f495498c Mon Sep 17 00:00:00 2001 From: Kazuki Nishikawa Date: Sat, 1 Mar 2025 12:06:58 +0900 Subject: [PATCH 03/30] add nil.rb.spec --- spec/lib/rufo/prism_formatter_source_specs/nil.rb.spec | 6 ++++++ spec/lib/rufo/prism_formatter_spec.rb | 6 +++--- 2 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 spec/lib/rufo/prism_formatter_source_specs/nil.rb.spec diff --git a/spec/lib/rufo/prism_formatter_source_specs/nil.rb.spec b/spec/lib/rufo/prism_formatter_source_specs/nil.rb.spec new file mode 100644 index 00000000..fcd14b71 --- /dev/null +++ b/spec/lib/rufo/prism_formatter_source_specs/nil.rb.spec @@ -0,0 +1,6 @@ +#~# ORIGINAL nil + +nil + +#~# EXPECTED +nil diff --git a/spec/lib/rufo/prism_formatter_spec.rb b/spec/lib/rufo/prism_formatter_spec.rb index 58a7e829..3ce082b9 100644 --- a/spec/lib/rufo/prism_formatter_spec.rb +++ b/spec/lib/rufo/prism_formatter_spec.rb @@ -74,9 +74,9 @@ def assert_format(code, expected = code, **options) end RSpec.describe Rufo::PrismFormatter do - # Dir[File.join(FILE_PATH, "/prism_formatter_source_specs/*")].each do |source_specs| - # assert_source_specs(source_specs) if File.file?(source_specs) - # end + Dir[File.join(FILE_PATH, "/prism_formatter_source_specs/*")].each do |source_specs| + assert_source_specs(source_specs) if File.file?(source_specs) + end # if VERSION >= Gem::Version.new("3.0") # Dir[File.join(FILE_PATH, "/prism_formatter_source_specs/3.0/*")].each do |source_specs| From c29bfc879b72430cc5458b71ed69cd2bc1007658 Mon Sep 17 00:00:00 2001 From: Kazuki Nishikawa Date: Sat, 1 Mar 2025 12:09:36 +0900 Subject: [PATCH 04/30] add chars.rb.spec --- spec/lib/rufo/prism_formatter_source_specs/chars.rb.spec | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 spec/lib/rufo/prism_formatter_source_specs/chars.rb.spec diff --git a/spec/lib/rufo/prism_formatter_source_specs/chars.rb.spec b/spec/lib/rufo/prism_formatter_source_specs/chars.rb.spec new file mode 100644 index 00000000..839f899d --- /dev/null +++ b/spec/lib/rufo/prism_formatter_source_specs/chars.rb.spec @@ -0,0 +1,6 @@ +#~# ORIGINAL char + +?a + +#~# EXPECTED +?a From 619d92b00c15a0d1dd62d114f1b1585b130bedc7 Mon Sep 17 00:00:00 2001 From: Kazuki Nishikawa Date: Sat, 1 Mar 2025 12:10:48 +0900 Subject: [PATCH 05/30] add integers.rb.spec --- spec/lib/rufo/prism_formatter_source_specs/integers.rb.spec | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 spec/lib/rufo/prism_formatter_source_specs/integers.rb.spec diff --git a/spec/lib/rufo/prism_formatter_source_specs/integers.rb.spec b/spec/lib/rufo/prism_formatter_source_specs/integers.rb.spec new file mode 100644 index 00000000..9816a0bc --- /dev/null +++ b/spec/lib/rufo/prism_formatter_source_specs/integers.rb.spec @@ -0,0 +1,6 @@ +#~# ORIGINAL 123 + +123 + +#~# EXPECTED +123 From 34cc82d9af316213dcddcd488cda87e2d3f3501c Mon Sep 17 00:00:00 2001 From: Kazuki Nishikawa Date: Sat, 1 Mar 2025 12:11:22 +0900 Subject: [PATCH 06/30] add class_variables.rb.spec --- .../prism_formatter_source_specs/class_variables.rb.spec | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 spec/lib/rufo/prism_formatter_source_specs/class_variables.rb.spec diff --git a/spec/lib/rufo/prism_formatter_source_specs/class_variables.rb.spec b/spec/lib/rufo/prism_formatter_source_specs/class_variables.rb.spec new file mode 100644 index 00000000..90feed28 --- /dev/null +++ b/spec/lib/rufo/prism_formatter_source_specs/class_variables.rb.spec @@ -0,0 +1,6 @@ +#~# ORIGINAL + +@@foo + +#~# EXPECTED +@@foo From 3869d5138dd5b8695549633c114d77cb258d46ab Mon Sep 17 00:00:00 2001 From: Kazuki Nishikawa Date: Sat, 1 Mar 2025 12:12:48 +0900 Subject: [PATCH 07/30] add leading_newlines.rb.spec --- .../leading_newlines.rb.spec | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 spec/lib/rufo/prism_formatter_source_specs/leading_newlines.rb.spec diff --git a/spec/lib/rufo/prism_formatter_source_specs/leading_newlines.rb.spec b/spec/lib/rufo/prism_formatter_source_specs/leading_newlines.rb.spec new file mode 100644 index 00000000..238e9846 --- /dev/null +++ b/spec/lib/rufo/prism_formatter_source_specs/leading_newlines.rb.spec @@ -0,0 +1,9 @@ +#~# ORIGINAL + + + + +a = 1 + +#~# EXPECTED +a = 1 From e65f2a9846f9926cf016cda08d921bf5779327cc Mon Sep 17 00:00:00 2001 From: Kazuki Nishikawa Date: Sat, 1 Mar 2025 12:13:40 +0900 Subject: [PATCH 08/30] add rationals.rb.spec --- .../lib/rufo/prism_formatter_source_specs/rationals.rb.spec | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 spec/lib/rufo/prism_formatter_source_specs/rationals.rb.spec diff --git a/spec/lib/rufo/prism_formatter_source_specs/rationals.rb.spec b/spec/lib/rufo/prism_formatter_source_specs/rationals.rb.spec new file mode 100644 index 00000000..224c49d4 --- /dev/null +++ b/spec/lib/rufo/prism_formatter_source_specs/rationals.rb.spec @@ -0,0 +1,6 @@ +#~# ORIGINAL + +3.141592r + +#~# EXPECTED +3.141592r From 8e0a37bc04c7c2a2d920e118b9abb5dd3bacf28a Mon Sep 17 00:00:00 2001 From: Kazuki Nishikawa Date: Sat, 1 Mar 2025 12:14:00 +0900 Subject: [PATCH 09/30] add imaginaries.rb.spec --- .../rufo/prism_formatter_source_specs/imaginaries.rb.spec | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 spec/lib/rufo/prism_formatter_source_specs/imaginaries.rb.spec diff --git a/spec/lib/rufo/prism_formatter_source_specs/imaginaries.rb.spec b/spec/lib/rufo/prism_formatter_source_specs/imaginaries.rb.spec new file mode 100644 index 00000000..b65b8262 --- /dev/null +++ b/spec/lib/rufo/prism_formatter_source_specs/imaginaries.rb.spec @@ -0,0 +1,6 @@ +#~# ORIGINAL + +3.141592i + +#~# EXPECTED +3.141592i From 990722eb6709fb79bf2c39f000eebd0d41b2c5a0 Mon Sep 17 00:00:00 2001 From: Kazuki Nishikawa Date: Sat, 1 Mar 2025 12:14:41 +0900 Subject: [PATCH 10/30] add spaces_inside_hash_brace.rb.spec --- .../spaces_inside_hash_brace.rb.spec | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 spec/lib/rufo/prism_formatter_source_specs/spaces_inside_hash_brace.rb.spec diff --git a/spec/lib/rufo/prism_formatter_source_specs/spaces_inside_hash_brace.rb.spec b/spec/lib/rufo/prism_formatter_source_specs/spaces_inside_hash_brace.rb.spec new file mode 100644 index 00000000..7e1c5df2 --- /dev/null +++ b/spec/lib/rufo/prism_formatter_source_specs/spaces_inside_hash_brace.rb.spec @@ -0,0 +1,7 @@ +#~# ORIGINAL + +{ 1 => 2 } + +#~# EXPECTED +{ 1 => 2 } + From 367288cf107c25232270f9fca23b43f8b6aaefdb Mon Sep 17 00:00:00 2001 From: Kazuki Nishikawa Date: Sat, 1 Mar 2025 12:15:28 +0900 Subject: [PATCH 11/30] add floats.rb.spec --- .../prism_formatter_source_specs/floats.rb.spec | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 spec/lib/rufo/prism_formatter_source_specs/floats.rb.spec diff --git a/spec/lib/rufo/prism_formatter_source_specs/floats.rb.spec b/spec/lib/rufo/prism_formatter_source_specs/floats.rb.spec new file mode 100644 index 00000000..c837f0ea --- /dev/null +++ b/spec/lib/rufo/prism_formatter_source_specs/floats.rb.spec @@ -0,0 +1,13 @@ +#~# ORIGINAL + +12.34 + +#~# EXPECTED +12.34 + +#~# ORIGINAL + +12.34e-10 + +#~# EXPECTED +12.34e-10 From 264c0faacd58b0cbb4efb88778e346b084943c35 Mon Sep 17 00:00:00 2001 From: Kazuki Nishikawa Date: Sat, 1 Mar 2025 12:15:54 +0900 Subject: [PATCH 12/30] add booleans.rb.spec --- .../prism_formatter_source_specs/booleans.rb.spec | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 spec/lib/rufo/prism_formatter_source_specs/booleans.rb.spec diff --git a/spec/lib/rufo/prism_formatter_source_specs/booleans.rb.spec b/spec/lib/rufo/prism_formatter_source_specs/booleans.rb.spec new file mode 100644 index 00000000..218d87dc --- /dev/null +++ b/spec/lib/rufo/prism_formatter_source_specs/booleans.rb.spec @@ -0,0 +1,13 @@ +#~# ORIGINAL false + +false + +#~# EXPECTED +false + +#~# ORIGINAL true + +true + +#~# EXPECTED +true From 0627c5f670f4f9c7d683631d0fd9ba37d2989ff0 Mon Sep 17 00:00:00 2001 From: Kazuki Nishikawa Date: Sat, 1 Mar 2025 12:17:10 +0900 Subject: [PATCH 13/30] add special_global_variables.rb.spec --- .../special_global_variables.rb.spec | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 spec/lib/rufo/prism_formatter_source_specs/special_global_variables.rb.spec diff --git a/spec/lib/rufo/prism_formatter_source_specs/special_global_variables.rb.spec b/spec/lib/rufo/prism_formatter_source_specs/special_global_variables.rb.spec new file mode 100644 index 00000000..6d280ac3 --- /dev/null +++ b/spec/lib/rufo/prism_formatter_source_specs/special_global_variables.rb.spec @@ -0,0 +1,27 @@ +#~# ORIGINAL + +$~ + +#~# EXPECTED +$~ + +#~# ORIGINAL + +$1 + +#~# EXPECTED +$1 + +#~# ORIGINAL + +$! + +#~# EXPECTED +$! + +#~# ORIGINAL + +$@ + +#~# EXPECTED +$@ From 2273743bcffda0c324bde4537d7843784bcf5908 Mon Sep 17 00:00:00 2001 From: Kazuki Nishikawa Date: Sat, 1 Mar 2025 12:17:42 +0900 Subject: [PATCH 14/30] add symbol_literals.rb.spec --- .../symbol_literals.rb.spec | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 spec/lib/rufo/prism_formatter_source_specs/symbol_literals.rb.spec diff --git a/spec/lib/rufo/prism_formatter_source_specs/symbol_literals.rb.spec b/spec/lib/rufo/prism_formatter_source_specs/symbol_literals.rb.spec new file mode 100644 index 00000000..91549979 --- /dev/null +++ b/spec/lib/rufo/prism_formatter_source_specs/symbol_literals.rb.spec @@ -0,0 +1,27 @@ +#~# ORIGINAL + +:foo + +#~# EXPECTED +:foo + +#~# ORIGINAL + +:"foo" + +#~# EXPECTED +:"foo" + +#~# ORIGINAL + +:"foo#{1}" + +#~# EXPECTED +:"foo#{1}" + +#~# ORIGINAL + +:* + +#~# EXPECTED +:* From 7d4839e08486b8bf0fa2aef12fc61fa9c40e1ddc Mon Sep 17 00:00:00 2001 From: Kazuki Nishikawa Date: Sat, 1 Mar 2025 12:19:49 +0900 Subject: [PATCH 15/30] format --- lib/rufo/prism_formatter.rb | 2 +- spec/lib/rufo/prism_formatter_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/rufo/prism_formatter.rb b/lib/rufo/prism_formatter.rb index 30f3612f..b5856679 100644 --- a/lib/rufo/prism_formatter.rb +++ b/lib/rufo/prism_formatter.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -require 'prism' +require "prism" class Rufo::PrismFormatter include Rufo::Settings diff --git a/spec/lib/rufo/prism_formatter_spec.rb b/spec/lib/rufo/prism_formatter_spec.rb index 3ce082b9..53b93d9e 100644 --- a/spec/lib/rufo/prism_formatter_spec.rb +++ b/spec/lib/rufo/prism_formatter_spec.rb @@ -105,7 +105,7 @@ def assert_format(code, expected = code, **options) assert_format "\n\n\n", "" end - describe "Syntax errors not handled by Prism", pending: 'no test-case for prism' do + describe "Syntax errors not handled by Prism", pending: "no test-case for prism" do it "raises an unknown syntax error" do expect { Rufo.format("def foo; FOO = 1; end", engine: :prism) From fc62638bcf22251efdb589812e4cf87bfbe07f59 Mon Sep 17 00:00:00 2001 From: Kazuki Nishikawa Date: Sat, 1 Mar 2025 13:53:52 +0900 Subject: [PATCH 16/30] traverse ast output by prism --- lib/rufo/prism_formatter.rb | 92 ++++++++++++++++++++++++++++++++++++- 1 file changed, 90 insertions(+), 2 deletions(-) diff --git a/lib/rufo/prism_formatter.rb b/lib/rufo/prism_formatter.rb index b5856679..caa30940 100644 --- a/lib/rufo/prism_formatter.rb +++ b/lib/rufo/prism_formatter.rb @@ -23,14 +23,102 @@ def initialize(code, **options) end def format - @output = @code.dup + visitor = FormatVisitor.new(@code) + @parse_result.value.accept(visitor) + @output = visitor.output @output.chomp! if @output.end_with?("\n\n") @output.lstrip! - @output = "\n" if @output.empty? + @output << "\n" unless @output.end_with?("\n") end def result @output end + + class FormatVisitor < Prism::Visitor + attr_reader :output + + def initialize(code) + super() + @code = code + @output = +"" + end + + def visit_nil_node(_node) + write("nil") + end + + def visit_true_node(_node) + write("true") + end + + def visit_false_node(_node) + write("false") + end + + def visit_integer_node(node) + write_code_at(node.location) + end + + def visit_float_node(node) + write_code_at(node.location) + end + + def visit_rational_node(node) + write_code_at(node.location) + end + + def visit_imaginary_node(node) + write_code_at(node.location) + end + + def visit_symbol_node(node) + write_code_at(node.location) + end + + def visit_interpolated_symbol_node(node) + write_code_at(node.location) + end + + def visit_string_node(node) + write_code_at(node.location) + end + + def visit_class_variable_read_node(node) + write_code_at(node.location) + end + + def visit_global_variable_read_node(node) + write_code_at(node.location) + end + + def visit_numbered_reference_read_node(node) + write_code_at(node.location) + end + + def visit_local_variable_write_node(node) + write(node.name.to_s) + write(" = ") + node.value.accept(self) + end + + def visit_hash_node(node) + write_code_at(node.location) + end + + private + + def write(value) + @output << value + end + + def write_code_at(location) + write(code_at(location)) + end + + def code_at(location) + @code[location.start_offset...location.end_offset] + end + end end From 6f2a1a8da50a7b84e0b42329be3d80ad026b579f Mon Sep 17 00:00:00 2001 From: Kazuki Nishikawa Date: Fri, 20 Jun 2025 20:48:59 +0900 Subject: [PATCH 17/30] variables --- lib/rufo/prism_formatter.rb | 32 +++++++++++++++++++ .../variables.rb.spec | 15 +++++++++ 2 files changed, 47 insertions(+) create mode 100644 spec/lib/rufo/prism_formatter_source_specs/variables.rb.spec diff --git a/lib/rufo/prism_formatter.rb b/lib/rufo/prism_formatter.rb index caa30940..3b2355d3 100644 --- a/lib/rufo/prism_formatter.rb +++ b/lib/rufo/prism_formatter.rb @@ -5,6 +5,8 @@ class Rufo::PrismFormatter include Rufo::Settings + DEBUG = true + def self.format(code, **options) formatter = new(code, **options) formatter.format @@ -23,6 +25,7 @@ def initialize(code, **options) end def format + debug_log @parse_result visitor = FormatVisitor.new(@code) @parse_result.value.accept(visitor) @output = visitor.output @@ -36,6 +39,12 @@ def result @output end + def debug_log(object) + if DEBUG + p [:debug, object] + end + end + class FormatVisitor < Prism::Visitor attr_reader :output @@ -97,6 +106,10 @@ def visit_numbered_reference_read_node(node) write_code_at(node.location) end + def visit_local_variable_read_node(node) + write_code_at(node.location) + end + def visit_local_variable_write_node(node) write(node.name.to_s) write(" = ") @@ -107,6 +120,19 @@ def visit_hash_node(node) write_code_at(node.location) end + def visit_instance_variable_read_node(node) + write(node.name.to_s) + end + + def visit_statements_node(node) + node.body.each do |child| + child.accept(self) + if child.newline? + write "\n" + end + end + end + private def write(value) @@ -120,5 +146,11 @@ def write_code_at(location) def code_at(location) @code[location.start_offset...location.end_offset] end + + def debug_log(object) + if Rufo::PrismFormatter::DEBUG + p [:debug, object] + end + end end end diff --git a/spec/lib/rufo/prism_formatter_source_specs/variables.rb.spec b/spec/lib/rufo/prism_formatter_source_specs/variables.rb.spec new file mode 100644 index 00000000..b9609f1c --- /dev/null +++ b/spec/lib/rufo/prism_formatter_source_specs/variables.rb.spec @@ -0,0 +1,15 @@ +#~# ORIGINAL + +a = 1 + a + +#~# EXPECTED +a = 1 +a + +#~# ORIGINAL + +@foo + +#~# EXPECTED +@foo From 1c6579366fcc6626e52eb215fff012d3ab59b0c2 Mon Sep 17 00:00:00 2001 From: Kazuki Nishikawa Date: Fri, 20 Jun 2025 20:57:11 +0900 Subject: [PATCH 18/30] undef --- lib/rufo/prism_formatter.rb | 10 ++++++++++ .../rufo/prism_formatter_source_specs/undef.rb.spec | 13 +++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 spec/lib/rufo/prism_formatter_source_specs/undef.rb.spec diff --git a/lib/rufo/prism_formatter.rb b/lib/rufo/prism_formatter.rb index 3b2355d3..ffe7ad14 100644 --- a/lib/rufo/prism_formatter.rb +++ b/lib/rufo/prism_formatter.rb @@ -124,6 +124,16 @@ def visit_instance_variable_read_node(node) write(node.name.to_s) end + def visit_undef_node(node) + write("undef ") + node.names.each_with_index do |name, i| + if i > 0 + write ", " + end + name.accept(self) + end + end + def visit_statements_node(node) node.body.each do |child| child.accept(self) diff --git a/spec/lib/rufo/prism_formatter_source_specs/undef.rb.spec b/spec/lib/rufo/prism_formatter_source_specs/undef.rb.spec new file mode 100644 index 00000000..70888a86 --- /dev/null +++ b/spec/lib/rufo/prism_formatter_source_specs/undef.rb.spec @@ -0,0 +1,13 @@ +#~# ORIGINAL + +undef foo + +#~# EXPECTED +undef foo + +#~# ORIGINAL + +undef foo , bar + +#~# EXPECTED +undef foo, bar From 1b2c0060e886e776d8adb71daa3db2241c88ee2c Mon Sep 17 00:00:00 2001 From: Kazuki Nishikawa Date: Fri, 20 Jun 2025 22:11:03 +0900 Subject: [PATCH 19/30] unary operator --- lib/rufo/prism_formatter.rb | 20 +++++++++-- .../unary_operators.rb.spec | 34 +++++++++++++++++++ 2 files changed, 52 insertions(+), 2 deletions(-) create mode 100644 spec/lib/rufo/prism_formatter_source_specs/unary_operators.rb.spec diff --git a/lib/rufo/prism_formatter.rb b/lib/rufo/prism_formatter.rb index ffe7ad14..4f917fa8 100644 --- a/lib/rufo/prism_formatter.rb +++ b/lib/rufo/prism_formatter.rb @@ -134,12 +134,28 @@ def visit_undef_node(node) end end + def visit_parentheses_node(node) + write_code_at(node.opening_loc) + node.body.accept(self) + write_code_at(node.closing_loc) + end + + def visit_call_node(node) + write(node.message) + if node.receiver + node.receiver.accept(self) + end + end + def visit_statements_node(node) + previous = nil node.body.each do |child| - child.accept(self) - if child.newline? + if previous&.newline? write "\n" end + + child.accept(self) + previous = child end end diff --git a/spec/lib/rufo/prism_formatter_source_specs/unary_operators.rb.spec b/spec/lib/rufo/prism_formatter_source_specs/unary_operators.rb.spec new file mode 100644 index 00000000..0bed1d69 --- /dev/null +++ b/spec/lib/rufo/prism_formatter_source_specs/unary_operators.rb.spec @@ -0,0 +1,34 @@ +#~# ORIGINAL + +- x + +#~# EXPECTED +-x + +#~# ORIGINAL + ++ x + +#~# EXPECTED ++x + +#~# ORIGINAL + ++x + +#~# EXPECTED ++x + +#~# ORIGINAL + ++(x) + +#~# EXPECTED ++(x) + +#~# ORIGINAL + ++ (x) + +#~# EXPECTED ++(x) From a002388b92a1d37edd397aad5cbc872d19e9065a Mon Sep 17 00:00:00 2001 From: Kazuki Nishikawa Date: Thu, 28 May 2026 21:29:37 +0900 Subject: [PATCH 20/30] fix visit_call_node receiver/message order The previous implementation wrote the message before the receiver, which produced "fooa" for "a.foo". Branch on call_operator_loc to distinguish method calls (receiver, op, message) from unary prefix operators (message, receiver). Add a regression spec covering the simple dot, chained dots, and safe navigation cases. Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/rufo/prism_formatter.rb | 11 ++++++++-- .../calls_with_receiver.rb.spec | 20 +++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 spec/lib/rufo/prism_formatter_source_specs/calls_with_receiver.rb.spec diff --git a/lib/rufo/prism_formatter.rb b/lib/rufo/prism_formatter.rb index 4f917fa8..a1860482 100644 --- a/lib/rufo/prism_formatter.rb +++ b/lib/rufo/prism_formatter.rb @@ -141,9 +141,16 @@ def visit_parentheses_node(node) end def visit_call_node(node) - write(node.message) - if node.receiver + if node.receiver && node.call_operator_loc node.receiver.accept(self) + write_code_at(node.call_operator_loc) + write(node.message) + elsif node.receiver + # Unary prefix operator (e.g. -x, +x): message before receiver. + write(node.message) + node.receiver.accept(self) + else + write(node.message) end end diff --git a/spec/lib/rufo/prism_formatter_source_specs/calls_with_receiver.rb.spec b/spec/lib/rufo/prism_formatter_source_specs/calls_with_receiver.rb.spec new file mode 100644 index 00000000..5c4b2f11 --- /dev/null +++ b/spec/lib/rufo/prism_formatter_source_specs/calls_with_receiver.rb.spec @@ -0,0 +1,20 @@ +#~# ORIGINAL simple dot + +a.foo + +#~# EXPECTED +a.foo + +#~# ORIGINAL chained dots collapse spaces + +foo . bar . baz + +#~# EXPECTED +foo.bar.baz + +#~# ORIGINAL safe navigation + +a&.foo + +#~# EXPECTED +a&.foo From 8478da4af706ca75dadfba1df8b8f5920b577e18 Mon Sep 17 00:00:00 2001 From: Kazuki Nishikawa Date: Thu, 28 May 2026 21:32:02 +0900 Subject: [PATCH 21/30] gate prism formatter DEBUG output behind env var DEBUG was hard-coded to true and printed `p [:debug, ...]` on every parse, polluting test output. Read RUFO_PRISM_DEBUG from the env so local debugging stays opt-in. Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/rufo/prism_formatter.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/rufo/prism_formatter.rb b/lib/rufo/prism_formatter.rb index a1860482..68d4054a 100644 --- a/lib/rufo/prism_formatter.rb +++ b/lib/rufo/prism_formatter.rb @@ -5,7 +5,7 @@ class Rufo::PrismFormatter include Rufo::Settings - DEBUG = true + DEBUG = !ENV["RUFO_PRISM_DEBUG"].to_s.empty? def self.format(code, **options) formatter = new(code, **options) From 95a32ddf67f4a44e1ac2b87814a8747eb44f3af2 Mon Sep 17 00:00:00 2001 From: Kazuki Nishikawa Date: Thu, 28 May 2026 21:38:36 +0900 Subject: [PATCH 22/30] interleave comments via source-offset cursor Prism exposes comments outside the AST (parse_result.comments), so the visitor needs to drain them at the right points to preserve order against AST nodes. Track @source_offset (position past the last source bytes already accounted for). write_code_at now drains comments before the location, writes the source slice, and advances the cursor. visit_statements_node drains before each child as well, so standalone comments above a statement land on their own line. PrismFormatter#format calls visitor.finish at the end to drain remaining comments past the last statement. emit_comment classifies each comment by what precedes it on its source line: whitespace-only -> standalone (own line at current position), otherwise trailing (preserve the gap from the previous emitted source position). visit_nil/true/false/instance_variable_read switch to write_code_at(node.location) for consistent cursor tracking; visit_call_node now uses write_code_at(message_loc) for the same reason. visit_local_variable_write_node and visit_undef_node manually consume up to their start so leading standalone comments are not skipped. Adds a comments.rb.spec covering the cases that do not require blank-line preservation: single standalone, two consecutive standalones, trailing-after-code, standalone-before-code, and trailing-then-standalone. Blank-line preservation between comments needs layout state (next step) and is left for then. Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/rufo/prism_formatter.rb | 75 +++++++++++++++---- .../comments.rb.spec | 40 ++++++++++ 2 files changed, 101 insertions(+), 14 deletions(-) create mode 100644 spec/lib/rufo/prism_formatter_source_specs/comments.rb.spec diff --git a/lib/rufo/prism_formatter.rb b/lib/rufo/prism_formatter.rb index 68d4054a..7a29e48d 100644 --- a/lib/rufo/prism_formatter.rb +++ b/lib/rufo/prism_formatter.rb @@ -26,8 +26,9 @@ def initialize(code, **options) def format debug_log @parse_result - visitor = FormatVisitor.new(@code) + visitor = FormatVisitor.new(@code, @parse_result.comments) @parse_result.value.accept(visitor) + visitor.finish @output = visitor.output @output.chomp! if @output.end_with?("\n\n") @@ -48,22 +49,29 @@ def debug_log(object) class FormatVisitor < Prism::Visitor attr_reader :output - def initialize(code) + def initialize(code, comments) super() @code = code @output = +"" + @comments = comments + @comment_index = 0 + @source_offset = 0 end - def visit_nil_node(_node) - write("nil") + def finish + consume_source_up_to(@code.length) end - def visit_true_node(_node) - write("true") + def visit_nil_node(node) + write_code_at(node.location) + end + + def visit_true_node(node) + write_code_at(node.location) end - def visit_false_node(_node) - write("false") + def visit_false_node(node) + write_code_at(node.location) end def visit_integer_node(node) @@ -111,6 +119,7 @@ def visit_local_variable_read_node(node) end def visit_local_variable_write_node(node) + consume_source_up_to(node.location.start_offset) write(node.name.to_s) write(" = ") node.value.accept(self) @@ -121,10 +130,11 @@ def visit_hash_node(node) end def visit_instance_variable_read_node(node) - write(node.name.to_s) + write_code_at(node.location) end def visit_undef_node(node) + consume_source_up_to(node.location.start_offset) write("undef ") node.names.each_with_index do |name, i| if i > 0 @@ -144,20 +154,21 @@ def visit_call_node(node) if node.receiver && node.call_operator_loc node.receiver.accept(self) write_code_at(node.call_operator_loc) - write(node.message) + write_code_at(node.message_loc) elsif node.receiver # Unary prefix operator (e.g. -x, +x): message before receiver. - write(node.message) + write_code_at(node.message_loc) node.receiver.accept(self) else - write(node.message) + write_code_at(node.message_loc) end end def visit_statements_node(node) previous = nil node.body.each do |child| - if previous&.newline? + consume_source_up_to(child.location.start_offset) + if previous && !@output.end_with?("\n") write "\n" end @@ -173,13 +184,49 @@ def write(value) end def write_code_at(location) - write(code_at(location)) + consume_source_up_to(location.start_offset) + @output << @code[location.start_offset...location.end_offset] + @source_offset = location.end_offset end def code_at(location) @code[location.start_offset...location.end_offset] end + # Drain comments that occur before `offset` and advance the source cursor. + # `@source_offset` is the position past the last source bytes already + # accounted for in `@output` (either copied verbatim, or skipped as + # discardable whitespace between AST nodes). + def consume_source_up_to(offset) + return if offset <= @source_offset + while @comment_index < @comments.size && @comments[@comment_index].location.start_offset < offset + emit_comment(@comments[@comment_index]) + @comment_index += 1 + end + @source_offset = offset if offset > @source_offset + end + + def emit_comment(comment) + line_start = @code.rindex("\n", comment.location.start_offset - 1) + line_start = line_start ? line_start + 1 : 0 + before_on_line = @code[line_start...comment.location.start_offset] + + if before_on_line.match?(/\A\s*\z/) + # Standalone comment — emit on its own line. + @output << "\n" unless @output.empty? || @output.end_with?("\n") + @output << comment.slice + @output << "\n" + else + # Trailing comment — preserve the spacing between the preceding code + # and the comment as it appears in the source. + gap_start = [@source_offset, line_start].max + @output << @code[gap_start...comment.location.start_offset] + @output << comment.slice + @output << "\n" + end + @source_offset = comment.location.end_offset + end + def debug_log(object) if Rufo::PrismFormatter::DEBUG p [:debug, object] diff --git a/spec/lib/rufo/prism_formatter_source_specs/comments.rb.spec b/spec/lib/rufo/prism_formatter_source_specs/comments.rb.spec new file mode 100644 index 00000000..a40e18c3 --- /dev/null +++ b/spec/lib/rufo/prism_formatter_source_specs/comments.rb.spec @@ -0,0 +1,40 @@ +#~# ORIGINAL standalone single + +# foo + +#~# EXPECTED +# foo + +#~# ORIGINAL two consecutive standalones + +# foo +# bar + +#~# EXPECTED +# foo +# bar + +#~# ORIGINAL trailing after integer + +1 # foo + +#~# EXPECTED +1 # foo + +#~# ORIGINAL standalone before code + +# a +1 + +#~# EXPECTED +# a +1 + +#~# ORIGINAL trailing then standalone + +1 # a +# b + +#~# EXPECTED +1 # a +# b From bd3474ebb4d274879c64b677940e4615682ed27a Mon Sep 17 00:00:00 2001 From: Kazuki Nishikawa Date: Thu, 28 May 2026 21:43:54 +0900 Subject: [PATCH 23/30] introduce layout state (indent / column / pending newline) Replace bare @output << x with a small write API: - write: appends content and emits the pending indent if we are at the start of a line. - write_newline / write_newline_unless_pending: ends a line, marks indent as pending for the next write. - indent_by(amount) { ... }: scoped indent push. write_code_at flows through write, so cursor tracking and indent emission stay consistent for the existing literal visitors. emit_comment uses write/write_newline too, so standalone comments land at the current indent. Add visit_if_node as the first non-literal node to validate the state machine: predicate -> newline -> indent_by(INDENT_SIZE) -> body -> newline -> end. Spec covers indent introduction on a flat body, preservation of an already-indented body, double indent for nested if, and an empty body. Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/rufo/prism_formatter.rb | 70 +++++++++++++++---- .../prism_formatter_source_specs/if.rb.spec | 45 ++++++++++++ 2 files changed, 100 insertions(+), 15 deletions(-) create mode 100644 spec/lib/rufo/prism_formatter_source_specs/if.rb.spec diff --git a/lib/rufo/prism_formatter.rb b/lib/rufo/prism_formatter.rb index 7a29e48d..426e360c 100644 --- a/lib/rufo/prism_formatter.rb +++ b/lib/rufo/prism_formatter.rb @@ -6,6 +6,7 @@ class Rufo::PrismFormatter include Rufo::Settings DEBUG = !ENV["RUFO_PRISM_DEBUG"].to_s.empty? + INDENT_SIZE = 2 def self.format(code, **options) formatter = new(code, **options) @@ -56,6 +57,9 @@ def initialize(code, comments) @comments = comments @comment_index = 0 @source_offset = 0 + @indent = 0 + @column = 0 + @indent_pending = true end def finish @@ -138,7 +142,7 @@ def visit_undef_node(node) write("undef ") node.names.each_with_index do |name, i| if i > 0 - write ", " + write(", ") end name.accept(self) end @@ -164,31 +168,67 @@ def visit_call_node(node) end end + def visit_if_node(node) + consume_source_up_to(node.location.start_offset) + write_code_at(node.if_keyword_loc) + write(" ") + node.predicate.accept(self) + write_newline + indent_by(Rufo::PrismFormatter::INDENT_SIZE) do + node.statements&.accept(self) + end + write_newline_unless_pending + write_code_at(node.end_keyword_loc) + end + def visit_statements_node(node) - previous = nil - node.body.each do |child| + node.body.each_with_index do |child, i| consume_source_up_to(child.location.start_offset) - if previous && !@output.end_with?("\n") - write "\n" - end - + write_newline if i > 0 && !@indent_pending child.accept(self) - previous = child end end private + # Append `value` to the output. Emits the pending indent first if we are + # at the start of a line. `value` is assumed not to contain "\n" — use + # `write_newline` to end a line. def write(value) + return if value.empty? + if @indent_pending + pad = " " * @indent + @output << pad + @column += pad.length + @indent_pending = false + end @output << value + @column += value.length + end + + def write_newline + @output << "\n" + @column = 0 + @indent_pending = true + end + + def write_newline_unless_pending + write_newline unless @indent_pending end def write_code_at(location) consume_source_up_to(location.start_offset) - @output << @code[location.start_offset...location.end_offset] + write(@code[location.start_offset...location.end_offset]) @source_offset = location.end_offset end + def indent_by(amount) + @indent += amount + yield + ensure + @indent -= amount + end + def code_at(location) @code[location.start_offset...location.end_offset] end @@ -213,16 +253,16 @@ def emit_comment(comment) if before_on_line.match?(/\A\s*\z/) # Standalone comment — emit on its own line. - @output << "\n" unless @output.empty? || @output.end_with?("\n") - @output << comment.slice - @output << "\n" + write_newline_unless_pending + write(comment.slice) + write_newline else # Trailing comment — preserve the spacing between the preceding code # and the comment as it appears in the source. gap_start = [@source_offset, line_start].max - @output << @code[gap_start...comment.location.start_offset] - @output << comment.slice - @output << "\n" + write(@code[gap_start...comment.location.start_offset]) + write(comment.slice) + write_newline end @source_offset = comment.location.end_offset end diff --git a/spec/lib/rufo/prism_formatter_source_specs/if.rb.spec b/spec/lib/rufo/prism_formatter_source_specs/if.rb.spec new file mode 100644 index 00000000..54baa898 --- /dev/null +++ b/spec/lib/rufo/prism_formatter_source_specs/if.rb.spec @@ -0,0 +1,45 @@ +#~# ORIGINAL indents body + +if x +1 +end + +#~# EXPECTED +if x + 1 +end + +#~# ORIGINAL preserves already indented body + +if x + 1 +end + +#~# EXPECTED +if x + 1 +end + +#~# ORIGINAL nested indents twice + +if x +if y +1 +end +end + +#~# EXPECTED +if x + if y + 1 + end +end + +#~# ORIGINAL empty body + +if x +end + +#~# EXPECTED +if x +end From 5bc54e20f37021275da28547b6c2a6a673f6cb9e Mon Sep 17 00:00:00 2001 From: Kazuki Nishikawa Date: Thu, 28 May 2026 21:47:51 +0900 Subject: [PATCH 24/30] queue heredoc body / closing until end of opening line MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prism gives heredocs three locations: opening (< --- lib/rufo/prism_formatter.rb | 32 ++++++++- .../heredoc.rb.spec | 71 +++++++++++++++++++ 2 files changed, 102 insertions(+), 1 deletion(-) create mode 100644 spec/lib/rufo/prism_formatter_source_specs/heredoc.rb.spec diff --git a/lib/rufo/prism_formatter.rb b/lib/rufo/prism_formatter.rb index 426e360c..5ccf7508 100644 --- a/lib/rufo/prism_formatter.rb +++ b/lib/rufo/prism_formatter.rb @@ -60,10 +60,12 @@ def initialize(code, comments) @indent = 0 @column = 0 @indent_pending = true + @pending_heredocs = [] end def finish consume_source_up_to(@code.length) + flush_pending_heredocs end def visit_nil_node(node) @@ -103,7 +105,13 @@ def visit_interpolated_symbol_node(node) end def visit_string_node(node) - write_code_at(node.location) + if heredoc?(node) + write_code_at(node.opening_loc) + @pending_heredocs << node + @source_offset = node.closing_loc.end_offset + else + write_code_at(node.location) + end end def visit_class_variable_read_node(node) @@ -210,6 +218,7 @@ def write_newline @output << "\n" @column = 0 @indent_pending = true + flush_pending_heredocs end def write_newline_unless_pending @@ -246,6 +255,27 @@ def consume_source_up_to(offset) @source_offset = offset if offset > @source_offset end + def heredoc?(node) + node.opening_loc&.slice&.start_with?("<<") + end + + # Append the body and closing of pending heredocs after the current + # output line. Prism keeps the opening, body, and closing in separate + # source locations because they are interleaved with whatever follows the + # opening on the same source line. + def flush_pending_heredocs + return if @pending_heredocs.empty? + @output << "\n" unless @output.empty? || @output.end_with?("\n") + heredocs = @pending_heredocs + @pending_heredocs = [] + heredocs.each do |heredoc| + @output << @code[heredoc.content_loc.start_offset...heredoc.content_loc.end_offset] + @output << @code[heredoc.closing_loc.start_offset...heredoc.closing_loc.end_offset] + end + @column = 0 + @indent_pending = true + end + def emit_comment(comment) line_start = @code.rindex("\n", comment.location.start_offset - 1) line_start = line_start ? line_start + 1 : 0 diff --git a/spec/lib/rufo/prism_formatter_source_specs/heredoc.rb.spec b/spec/lib/rufo/prism_formatter_source_specs/heredoc.rb.spec new file mode 100644 index 00000000..28ae8bb4 --- /dev/null +++ b/spec/lib/rufo/prism_formatter_source_specs/heredoc.rb.spec @@ -0,0 +1,71 @@ +#~# ORIGINAL bare heredoc + +< Date: Thu, 28 May 2026 21:56:21 +0900 Subject: [PATCH 25/30] remove debug_log scaffolding DEBUG and both debug_log methods were development-only print helpers. The env-gated form added in 6a was still dead weight in the shipped formatter; drop the constant, the two method definitions, and the lone call site. Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/rufo/prism_formatter.rb | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/lib/rufo/prism_formatter.rb b/lib/rufo/prism_formatter.rb index 5ccf7508..69b861d0 100644 --- a/lib/rufo/prism_formatter.rb +++ b/lib/rufo/prism_formatter.rb @@ -5,7 +5,6 @@ class Rufo::PrismFormatter include Rufo::Settings - DEBUG = !ENV["RUFO_PRISM_DEBUG"].to_s.empty? INDENT_SIZE = 2 def self.format(code, **options) @@ -26,7 +25,6 @@ def initialize(code, **options) end def format - debug_log @parse_result visitor = FormatVisitor.new(@code, @parse_result.comments) @parse_result.value.accept(visitor) visitor.finish @@ -41,12 +39,6 @@ def result @output end - def debug_log(object) - if DEBUG - p [:debug, object] - end - end - class FormatVisitor < Prism::Visitor attr_reader :output @@ -296,11 +288,5 @@ def emit_comment(comment) end @source_offset = comment.location.end_offset end - - def debug_log(object) - if Rufo::PrismFormatter::DEBUG - p [:debug, object] - end - end end end From a5abb32cce41ae7bcc223a03a848c2da30ab84d6 Mon Sep 17 00:00:00 2001 From: Kazuki Nishikawa Date: Thu, 28 May 2026 22:04:34 +0900 Subject: [PATCH 26/30] add redo, retry, alias visitors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RedoNode / RetryNode are bare keywords — write_code_at(node.location). AliasMethodNode and AliasGlobalVariableNode share the same shape (keyword, new_name, old_name with single spaces); share a private visit_alias helper. Prism flags top-level redo/retry as :syntax-level errors even though it still builds a complete AST (semantic validity, not parse failure). The existing Ripper-based formatter formats these inputs fine. Add a NON_FATAL_ERROR_TYPES list so PrismFormatter skips :invalid_block_exit and :invalid_retry_without_rescue when deciding whether to raise. Other :syntax errors (e.g. dynamic constant assignment in def) still raise, preserving the existing behavior. Specs are copied verbatim from formatter_source_specs/. Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/rufo/prism_formatter.rb | 39 ++++++++++++++++++- .../alias.rb.spec | 27 +++++++++++++ .../prism_formatter_source_specs/redo.rb.spec | 6 +++ .../retry.rb.spec | 6 +++ 4 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 spec/lib/rufo/prism_formatter_source_specs/alias.rb.spec create mode 100644 spec/lib/rufo/prism_formatter_source_specs/redo.rb.spec create mode 100644 spec/lib/rufo/prism_formatter_source_specs/retry.rb.spec diff --git a/lib/rufo/prism_formatter.rb b/lib/rufo/prism_formatter.rb index 69b861d0..680738f4 100644 --- a/lib/rufo/prism_formatter.rb +++ b/lib/rufo/prism_formatter.rb @@ -7,6 +7,15 @@ class Rufo::PrismFormatter INDENT_SIZE = 2 + # Prism reports some semantic-validity issues with :syntax level even + # though it still builds a complete AST. The formatter can handle these + # inputs (matching the existing Ripper-based formatter, which formats + # syntactically-well-formed-but-semantically-invalid code). + NON_FATAL_ERROR_TYPES = [ + :invalid_block_exit, # redo / break / next outside a loop + :invalid_retry_without_rescue, # retry outside rescue + ].freeze + def self.format(code, **options) formatter = new(code, **options) formatter.format @@ -16,8 +25,9 @@ def self.format(code, **options) def initialize(code, **options) @code = code @parse_result = Prism.parse(code) - unless @parse_result.errors.empty? - error = @parse_result.errors.first + fatal_errors = @parse_result.errors.reject { |e| NON_FATAL_ERROR_TYPES.include?(e.type) } + unless fatal_errors.empty? + error = fatal_errors.first raise Rufo::SyntaxError.new(error.message, error.location.start_line) end @@ -148,6 +158,22 @@ def visit_undef_node(node) end end + def visit_redo_node(node) + write_code_at(node.location) + end + + def visit_retry_node(node) + write_code_at(node.location) + end + + def visit_alias_method_node(node) + visit_alias(node) + end + + def visit_alias_global_variable_node(node) + visit_alias(node) + end + def visit_parentheses_node(node) write_code_at(node.opening_loc) node.body.accept(self) @@ -191,6 +217,15 @@ def visit_statements_node(node) private + def visit_alias(node) + consume_source_up_to(node.location.start_offset) + write_code_at(node.keyword_loc) + write(" ") + node.new_name.accept(self) + write(" ") + node.old_name.accept(self) + end + # Append `value` to the output. Emits the pending indent first if we are # at the start of a line. `value` is assumed not to contain "\n" — use # `write_newline` to end a line. diff --git a/spec/lib/rufo/prism_formatter_source_specs/alias.rb.spec b/spec/lib/rufo/prism_formatter_source_specs/alias.rb.spec new file mode 100644 index 00000000..2301cc5a --- /dev/null +++ b/spec/lib/rufo/prism_formatter_source_specs/alias.rb.spec @@ -0,0 +1,27 @@ +#~# ORIGINAL + +alias foo bar + +#~# EXPECTED +alias foo bar + +#~# ORIGINAL + +alias :foo :bar + +#~# EXPECTED +alias :foo :bar + +#~# ORIGINAL + +alias store []= + +#~# EXPECTED +alias store []= + +#~# ORIGINAL + +alias $foo $bar + +#~# EXPECTED +alias $foo $bar diff --git a/spec/lib/rufo/prism_formatter_source_specs/redo.rb.spec b/spec/lib/rufo/prism_formatter_source_specs/redo.rb.spec new file mode 100644 index 00000000..1109b983 --- /dev/null +++ b/spec/lib/rufo/prism_formatter_source_specs/redo.rb.spec @@ -0,0 +1,6 @@ +#~# ORIGINAL redo + +redo + +#~# EXPECTED +redo diff --git a/spec/lib/rufo/prism_formatter_source_specs/retry.rb.spec b/spec/lib/rufo/prism_formatter_source_specs/retry.rb.spec new file mode 100644 index 00000000..b57d483f --- /dev/null +++ b/spec/lib/rufo/prism_formatter_source_specs/retry.rb.spec @@ -0,0 +1,6 @@ +#~# ORIGINAL retry + +retry + +#~# EXPECTED +retry From 51db0fe9193a2deb919536726317a4ddf333f387 Mon Sep 17 00:00:00 2001 From: Kazuki Nishikawa Date: Thu, 28 May 2026 22:07:57 +0900 Subject: [PATCH 27/30] add UnlessNode visitor with else clause support visit_unless_node mirrors visit_if_node but also dispatches the optional else_clause (an ElseNode) before writing end. Add visit_else_node as a shared helper that emits "else", a newline, the indented body, and a trailing newline; visit_if_node will reuse it once elsif chains are designed. Both unless cases from formatter_source_specs (with and without an empty else) format and round-trip correctly. Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/rufo/prism_formatter.rb | 23 +++++++++++++++++++ .../unless.rb.spec | 23 +++++++++++++++++++ 2 files changed, 46 insertions(+) create mode 100644 spec/lib/rufo/prism_formatter_source_specs/unless.rb.spec diff --git a/lib/rufo/prism_formatter.rb b/lib/rufo/prism_formatter.rb index 680738f4..6fcb88d6 100644 --- a/lib/rufo/prism_formatter.rb +++ b/lib/rufo/prism_formatter.rb @@ -207,6 +207,29 @@ def visit_if_node(node) write_code_at(node.end_keyword_loc) end + def visit_unless_node(node) + consume_source_up_to(node.location.start_offset) + write_code_at(node.keyword_loc) + write(" ") + node.predicate.accept(self) + write_newline + indent_by(Rufo::PrismFormatter::INDENT_SIZE) do + node.statements&.accept(self) + end + write_newline_unless_pending + node.else_clause&.accept(self) + write_code_at(node.end_keyword_loc) + end + + def visit_else_node(node) + write_code_at(node.else_keyword_loc) + write_newline + indent_by(Rufo::PrismFormatter::INDENT_SIZE) do + node.statements&.accept(self) + end + write_newline_unless_pending + end + def visit_statements_node(node) node.body.each_with_index do |child, i| consume_source_up_to(child.location.start_offset) diff --git a/spec/lib/rufo/prism_formatter_source_specs/unless.rb.spec b/spec/lib/rufo/prism_formatter_source_specs/unless.rb.spec new file mode 100644 index 00000000..b429c290 --- /dev/null +++ b/spec/lib/rufo/prism_formatter_source_specs/unless.rb.spec @@ -0,0 +1,23 @@ +#~# ORIGINAL + +unless 1 +2 +end + +#~# EXPECTED +unless 1 + 2 +end + +#~# ORIGINAL + +unless 1 +2 +else +end + +#~# EXPECTED +unless 1 + 2 +else +end From ce5c07d9e5bf8f73359bdbc78e34d42e30aa60ee Mon Sep 17 00:00:00 2001 From: Kazuki Nishikawa Date: Thu, 28 May 2026 22:10:35 +0900 Subject: [PATCH 28/30] adopt lonely, backtick, regex specs (literal-copy nodes) Safe-navigation cases (foo &. bar / foo&. bar) round-trip through the existing visit_call_node with no new code. XStringNode, RegularExpressionNode, and InterpolatedRegularExpressionNode are all "copy node.location verbatim" cases for the topics covered in the specs; add the three visitor methods and copy the corresponding spec files. Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/rufo/prism_formatter.rb | 12 +++++++ .../backtick_strings.rb.spec | 13 +++++++ .../lonely.rb.spec | 6 ++++ .../lonely_operator.rb.spec | 6 ++++ .../regex.rb.spec | 34 +++++++++++++++++++ 5 files changed, 71 insertions(+) create mode 100644 spec/lib/rufo/prism_formatter_source_specs/backtick_strings.rb.spec create mode 100644 spec/lib/rufo/prism_formatter_source_specs/lonely.rb.spec create mode 100644 spec/lib/rufo/prism_formatter_source_specs/lonely_operator.rb.spec create mode 100644 spec/lib/rufo/prism_formatter_source_specs/regex.rb.spec diff --git a/lib/rufo/prism_formatter.rb b/lib/rufo/prism_formatter.rb index 6fcb88d6..6572fe4b 100644 --- a/lib/rufo/prism_formatter.rb +++ b/lib/rufo/prism_formatter.rb @@ -116,6 +116,18 @@ def visit_string_node(node) end end + def visit_x_string_node(node) + write_code_at(node.location) + end + + def visit_regular_expression_node(node) + write_code_at(node.location) + end + + def visit_interpolated_regular_expression_node(node) + write_code_at(node.location) + end + def visit_class_variable_read_node(node) write_code_at(node.location) end diff --git a/spec/lib/rufo/prism_formatter_source_specs/backtick_strings.rb.spec b/spec/lib/rufo/prism_formatter_source_specs/backtick_strings.rb.spec new file mode 100644 index 00000000..b485ea2f --- /dev/null +++ b/spec/lib/rufo/prism_formatter_source_specs/backtick_strings.rb.spec @@ -0,0 +1,13 @@ +#~# ORIGINAL + +`cat meow` + +#~# EXPECTED +`cat meow` + +#~# ORIGINAL + + %x( cat meow ) + +#~# EXPECTED +%x( cat meow ) diff --git a/spec/lib/rufo/prism_formatter_source_specs/lonely.rb.spec b/spec/lib/rufo/prism_formatter_source_specs/lonely.rb.spec new file mode 100644 index 00000000..b275da97 --- /dev/null +++ b/spec/lib/rufo/prism_formatter_source_specs/lonely.rb.spec @@ -0,0 +1,6 @@ +#~# ORIGINAL + +foo &. bar + +#~# EXPECTED +foo&.bar diff --git a/spec/lib/rufo/prism_formatter_source_specs/lonely_operator.rb.spec b/spec/lib/rufo/prism_formatter_source_specs/lonely_operator.rb.spec new file mode 100644 index 00000000..5016ff81 --- /dev/null +++ b/spec/lib/rufo/prism_formatter_source_specs/lonely_operator.rb.spec @@ -0,0 +1,6 @@ +#~# ORIGINAL + +foo&. bar + +#~# EXPECTED +foo&.bar diff --git a/spec/lib/rufo/prism_formatter_source_specs/regex.rb.spec b/spec/lib/rufo/prism_formatter_source_specs/regex.rb.spec new file mode 100644 index 00000000..3ec27241 --- /dev/null +++ b/spec/lib/rufo/prism_formatter_source_specs/regex.rb.spec @@ -0,0 +1,34 @@ +#~# ORIGINAL + +// + +#~# EXPECTED +// + +#~# ORIGINAL + +//ix + +#~# EXPECTED +//ix + +#~# ORIGINAL + +/foo/ + +#~# EXPECTED +/foo/ + +#~# ORIGINAL + +/foo #{1 + 2} / + +#~# EXPECTED +/foo #{1 + 2} / + +#~# ORIGINAL + +%r( foo ) + +#~# EXPECTED +%r( foo ) From 2a6cc9930899f4e4ee958569a85872dfa9ba2e12 Mon Sep 17 00:00:00 2001 From: Kazuki Nishikawa Date: Thu, 28 May 2026 22:12:03 +0900 Subject: [PATCH 29/30] document prism spec copy policy Add a header comment explaining the relationship between prism_formatter_source_specs/ and formatter_source_specs/: verbatim copies when fully supported, hand-curated subsets when partial, no file when unsupported. Syncing is manual until PrismFormatter approaches parity and a per-engine PENDING marker can replace the dual-directory arrangement. This is the lower-impact half of the original 6b plan. A true consolidation (single spec dir + per-engine pending markers) would require extending the spec parser and is deferred until the divergence between engines shrinks. Co-Authored-By: Claude Opus 4.7 (1M context) --- spec/lib/rufo/prism_formatter_spec.rb | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/spec/lib/rufo/prism_formatter_spec.rb b/spec/lib/rufo/prism_formatter_spec.rb index 53b93d9e..fc3d99f3 100644 --- a/spec/lib/rufo/prism_formatter_spec.rb +++ b/spec/lib/rufo/prism_formatter_spec.rb @@ -73,6 +73,19 @@ def assert_format(code, expected = code, **options) ex.metadata[:line_number] = line end +# Specs that PrismFormatter (engine: :prism) is expected to pass. +# +# Files in spec/lib/rufo/prism_formatter_source_specs/ mirror the layout of +# spec/lib/rufo/formatter_source_specs/ (the legacy Ripper-based engine). +# When every case in a legacy file passes, the prism copy is verbatim; +# when only some pass, the prism file is a hand-curated subset (and may +# add prism-specific cases). No prism file exists for topics PrismFormatter +# does not yet handle — the PR checklist tracks adoption. +# +# Syncing is manual: a new test case added to a legacy file does not +# automatically appear here. Until PrismFormatter reaches feature parity +# and a per-engine PENDING marker is introduced, the prism owner reviews +# legacy changes and either mirrors the case or records the gap. RSpec.describe Rufo::PrismFormatter do Dir[File.join(FILE_PATH, "/prism_formatter_source_specs/*")].each do |source_specs| assert_source_specs(source_specs) if File.file?(source_specs) From c90adbd9a63ae8213c6164127de0fa5acf4704f4 Mon Sep 17 00:00:00 2001 From: Kazuki Nishikawa Date: Thu, 28 May 2026 22:47:50 +0900 Subject: [PATCH 30/30] trim PrismFormatter helpers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Remove unused code_at — every caller already uses write_code_at. - Drop the amount parameter on indent_by and rename to indent. The only argument ever passed was the INDENT_SIZE constant; pull the constant inside the helper. Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/rufo/prism_formatter.rb | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/lib/rufo/prism_formatter.rb b/lib/rufo/prism_formatter.rb index 6572fe4b..a728c60d 100644 --- a/lib/rufo/prism_formatter.rb +++ b/lib/rufo/prism_formatter.rb @@ -212,7 +212,7 @@ def visit_if_node(node) write(" ") node.predicate.accept(self) write_newline - indent_by(Rufo::PrismFormatter::INDENT_SIZE) do + indent do node.statements&.accept(self) end write_newline_unless_pending @@ -225,7 +225,7 @@ def visit_unless_node(node) write(" ") node.predicate.accept(self) write_newline - indent_by(Rufo::PrismFormatter::INDENT_SIZE) do + indent do node.statements&.accept(self) end write_newline_unless_pending @@ -236,7 +236,7 @@ def visit_unless_node(node) def visit_else_node(node) write_code_at(node.else_keyword_loc) write_newline - indent_by(Rufo::PrismFormatter::INDENT_SIZE) do + indent do node.statements&.accept(self) end write_newline_unless_pending @@ -293,15 +293,11 @@ def write_code_at(location) @source_offset = location.end_offset end - def indent_by(amount) - @indent += amount + def indent + @indent += Rufo::PrismFormatter::INDENT_SIZE yield ensure - @indent -= amount - end - - def code_at(location) - @code[location.start_offset...location.end_offset] + @indent -= Rufo::PrismFormatter::INDENT_SIZE end # Drain comments that occur before `offset` and advance the source cursor.