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" 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..4f917fa8 --- /dev/null +++ b/lib/rufo/prism_formatter.rb @@ -0,0 +1,182 @@ +# frozen_string_literal: true + +require "prism" + +class Rufo::PrismFormatter + include Rufo::Settings + + DEBUG = true + + 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 + debug_log @parse_result + visitor = FormatVisitor.new(@code) + @parse_result.value.accept(visitor) + @output = visitor.output + + @output.chomp! if @output.end_with?("\n\n") + @output.lstrip! + @output << "\n" unless @output.end_with?("\n") + end + + def result + @output + end + + def debug_log(object) + if DEBUG + p [:debug, object] + end + 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_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 + + 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_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| + if previous&.newline? + write "\n" + end + + child.accept(self) + previous = child + end + 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 + + 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/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 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 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 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 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 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 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 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_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 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 } + 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 +$@ 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 +:* 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) 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 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 diff --git a/spec/lib/rufo/prism_formatter_spec.rb b/spec/lib/rufo/prism_formatter_spec.rb new file mode 100644 index 00000000..53b93d9e --- /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