diff --git a/Gemfile b/Gemfile index e2eca463..cb665c99 100644 --- a/Gemfile +++ b/Gemfile @@ -2,3 +2,5 @@ source "https://rubygems.org" # Specify your gem's dependencies in rufo.gemspec gemspec + +gem 'prism' diff --git a/lib/rufo/erb_formatter.rb b/lib/rufo/erb_formatter.rb index 0bc56dbe..774400ea 100644 --- a/lib/rufo/erb_formatter.rb +++ b/lib/rufo/erb_formatter.rb @@ -24,6 +24,8 @@ def self.format(code, **options) attr_reader :result def initialize(code, **options) + parser_engine = options.delete(:parser_engine) + @parser = Rufo::Parser.new(parser_engine) @options = options @scanner = CustomScanner.new(code) @code_mode = false @@ -50,7 +52,7 @@ def format private - attr_reader :scanner, :code_mode + attr_reader :scanner, :code_mode, :parser attr_accessor :current_lineno, :current_column def update_lineno(token) @@ -90,7 +92,7 @@ def process_erb end def process_code(code_str) - sexps = Ripper.sexp(code_str) + sexps = parser.sexp(code_str) if sexps.nil? prefix, suffix = determine_code_wrappers(code_str) end @@ -133,7 +135,7 @@ def code_block_token?(token) end def determine_code_wrappers(code_str) - keywords = Ripper.lex("#{code_str}").filter { |lex_token| code_block_token?(lex_token) } + keywords = parser.lex("#{code_str}").filter { |lex_token| code_block_token?(lex_token) } lexical_tokens = keywords.map { |lex_token| lex_token[3].to_s } state_tally = lexical_tokens.group_by(&:itself).transform_values(&:count) beg_token = state_tally["BEG"] || state_tally["EXPR_BEG"] || 0 @@ -142,20 +144,20 @@ def determine_code_wrappers(code_str) if depth > 0 affix = format_affix("end", depth.abs, :suffix) - return nil, affix if Ripper.sexp("#{code_str}#{affix}") + return nil, affix if parser.sexp("#{code_str}#{affix}") end - return nil, "}" if Ripper.sexp("#{code_str} }") - return "{", nil if Ripper.sexp("{ #{code_str}") + return nil, "}" if parser.sexp("#{code_str} }") + return "{", nil if parser.sexp("{ #{code_str}") if depth < 0 affix = format_affix("begin", depth.abs, :prefix) - return affix, nil if Ripper.sexp("#{affix}#{code_str}") + return affix, nil if parser.sexp("#{affix}#{code_str}") end - return "begin\n", "\nend" if Ripper.sexp("begin\n#{code_str}\nend") - return "if a\n", "\nend" if Ripper.sexp("if a\n#{code_str}\nend") - return "case a\n", "\nend" if Ripper.sexp("case a\n#{code_str}\nend") + return "begin\n", "\nend" if parser.sexp("begin\n#{code_str}\nend") + return "if a\n", "\nend" if parser.sexp("if a\n#{code_str}\nend") + return "case a\n", "\nend" if parser.sexp("case a\n#{code_str}\nend") raise_syntax_error!(code_str) end @@ -166,7 +168,8 @@ def raise_syntax_error!(code_str) end def format_code(str) - Rufo::Formatter.format(str, **@options).chomp + formatter_options = @options.merge(parser_engine: parser.parser_engine) + Rufo::Formatter.format(str, **formatter_options).chomp end def enable_code_mode diff --git a/lib/rufo/formatter.rb b/lib/rufo/formatter.rb index b4a7f614..87157e45 100644 --- a/lib/rufo/formatter.rb +++ b/lib/rufo/formatter.rb @@ -16,14 +16,16 @@ def self.format(code, **options) def initialize(code, **options) @code = code - @tokens = Rufo::Parser.lex(code).reverse! - @sexp = Rufo::Parser.sexp(code) - @sexp ||= Rufo::Parser.sexp_unparsable_code(code) + parser_engine = options.delete(:parser_engine) + parser = Rufo::Parser.new(parser_engine) + @tokens = parser.lex(code).reverse! + @sexp = parser.sexp(code) + @sexp ||= parser.sexp_unparsable_code(code) # sexp being nil means that the code is not valid. # Parse the code so we get better error messages. if @sexp.nil? - Rufo::Parser.parse(code) + parser.parse(code) raise Rufo::UnknownSyntaxError # Sometimes parsing does not raise an error end diff --git a/lib/rufo/parser.rb b/lib/rufo/parser.rb index f49c9e02..f1069e99 100644 --- a/lib/rufo/parser.rb +++ b/lib/rufo/parser.rb @@ -1,17 +1,38 @@ # frozen_string_literal: true -require "ripper" +class Rufo::Parser + DEFAULT_PARSER_ENGINE = :ripper -class Rufo::Parser < Ripper - def compile_error(msg) - raise ::Rufo::SyntaxError.new(msg, lineno) + attr_reader :parser_engine + + def initialize(parser_engine = nil) + parser_engine ||= :ripper + @parser_engine = parser_engine + @engine = case parser_engine + when :ripper + require_relative 'parser/ripper' + Rufo::Parser::Ripper + when :prism + require_relative 'parser/prism' + Rufo::Parser::Prism + else + raise ArgumentError, 'unsupported parser engine' + end + end + + def lex(code) + @engine.lex(code) end - def on_parse_error(msg) - raise ::Rufo::SyntaxError.new(msg, lineno) + def sexp(code) + @engine.sexp(code) end - def self.sexp_unparsable_code(code) + def parse(code) + @engine.parse(code) + end + + def sexp_unparsable_code(code) code_type = detect_unparsable_code_type(code) case code_type @@ -33,7 +54,9 @@ def self.sexp_unparsable_code(code) end end - def self.detect_unparsable_code_type(code) + private + + def detect_unparsable_code_type(code) tokens = self.lex(code) token = tokens.find { |_, kind| kind != :on_sp && kind != :on_ignored_nl } @@ -45,7 +68,7 @@ def self.detect_unparsable_code_type(code) end end - def self.extract_original_code_sexp(decorated_code, extractor) + def extract_original_code_sexp(decorated_code, extractor) sexp = self.sexp(decorated_code) return nil unless sexp diff --git a/lib/rufo/parser/prism.rb b/lib/rufo/parser/prism.rb new file mode 100644 index 00000000..47337bf8 --- /dev/null +++ b/lib/rufo/parser/prism.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require "prism" + +class Rufo::Parser::Prism < ::Prism::Translation::Ripper + def compile_error(msg) + raise ::Rufo::SyntaxError.new(msg, lineno) + end + + def on_parse_error(msg) + raise ::Rufo::SyntaxError.new(msg, lineno) + end +end diff --git a/lib/rufo/parser/ripper.rb b/lib/rufo/parser/ripper.rb new file mode 100644 index 00000000..6338c649 --- /dev/null +++ b/lib/rufo/parser/ripper.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +require "ripper" + +class Rufo::Parser::Ripper < ::Ripper + def compile_error(msg) + raise ::Rufo::SyntaxError.new(msg, lineno) + end + + def on_parse_error(msg) + raise ::Rufo::SyntaxError.new(msg, lineno) + end +end diff --git a/spec/lib/rufo/erb_formatter_spec.rb b/spec/lib/rufo/erb_formatter_spec.rb index e45e20e3..17083996 100644 --- a/spec/lib/rufo/erb_formatter_spec.rb +++ b/spec/lib/rufo/erb_formatter_spec.rb @@ -1,93 +1,109 @@ RSpec.describe Rufo::ErbFormatter do subject { described_class } - describe ".format" do - it "removes unnecessary spaces from code sections" do - result = subject.format("Example <%= a + 5%>") - expect(result).to eql("Example <%= a + 5 %>") - end - - it "handles do end blocks" do - result = subject.format("<% a do |b| %>\nabc\n<% end %>") - expect(result).to eql("<% a do |b| %>\nabc\n<% end %>") - end - - it "handles {} blocks" do - result = subject.format("<% a { |b| %>\nabc\n<% } %>") - expect(result).to eql("<% a { |b| %>\nabc\n<% } %>") - end - - it "handles rescue statements" do - result = subject.format("<% begin %>\na\n<% rescue %>\n<% end %>") - expect(result).to eql("<% begin %>\na\n<% rescue %>\n<% end %>") - end - - it "handles if statements" do - result = subject.format("<% if a %>\na\n<% elsif b %>\n<% end %>") - expect(result).to eql("<% if a %>\na\n<% elsif b %>\n<% end %>") - end - - it "handles case statements" do - result = subject.format("<% case a when a %>\na\n<% when b %>\n<% end %>") - expect(result).to eql("<% case a\n when a %>\na\n<% when b %>\n<% end %>") - end - - it "handles multiline statements" do - result = subject.format("<% link_to :a,\n:b %>") - expect(result).to eql("<% link_to :a,\n :b %>") - end - - it "handles indented multiline statements" do - result = subject.format(" <% link_to :a,\n:b %>") - expect(result).to eql(" <% link_to :a,\n :b %>") - end - - it "handles trim mode templates" do - result = subject.format("<% link_to :a -%>") - expect(result).to eql("<% link_to :a -%>") - end - - it "handles rails raw mode templates" do - result = subject.format("<%== link_to :a %>") - expect(result).to eql("<%== link_to :a %>") - end - - it "handles escaped erb templates" do - result = subject.format("<%%= puts :foo %>") - expect(result).to eql("<%%= puts :foo %>") - end - - it "handles escaped rails raw mode erb templates" do - result = subject.format("<%%== puts :foo %>") - expect(result).to eql("<%%== puts :foo %>") - end - - it "handles invalid code" do - expect { subject.format("\n\n<% a+ %>") }.to raise_error { |error| - expect(error).to be_a(Rufo::SyntaxError) - expect(error.lineno).to eql(3) - } - end - - it "formats with options" do - result = subject.format(%(<%= "hello" + ', world' %>), quote_style: :single) - expect(result).to eql("<%= 'hello' + ', world' %>") + shared_examples_for 'erb_formatter is works' do + describe ".format" do + let(:options) { { parser_engine: parser_engine } } + + it "removes unnecessary spaces from code sections" do + result = subject.format("Example <%= a + 5%>", **options) + expect(result).to eql("Example <%= a + 5 %>") + end + + it "handles do end blocks" do + result = subject.format("<% a do |b| %>\nabc\n<% end %>", **options) + expect(result).to eql("<% a do |b| %>\nabc\n<% end %>") + end + + it "handles {} blocks" do + result = subject.format("<% a { |b| %>\nabc\n<% } %>", **options) + expect(result).to eql("<% a { |b| %>\nabc\n<% } %>") + end + + it "handles rescue statements" do + result = subject.format("<% begin %>\na\n<% rescue %>\n<% end %>", **options) + expect(result).to eql("<% begin %>\na\n<% rescue %>\n<% end %>") + end + + it "handles if statements" do + result = subject.format("<% if a %>\na\n<% elsif b %>\n<% end %>", **options) + expect(result).to eql("<% if a %>\na\n<% elsif b %>\n<% end %>") + end + + it "handles case statements" do + result = subject.format("<% case a when a %>\na\n<% when b %>\n<% end %>", **options) + expect(result).to eql("<% case a\n when a %>\na\n<% when b %>\n<% end %>") + end + + it "handles multiline statements" do + result = subject.format("<% link_to :a,\n:b %>", **options) + expect(result).to eql("<% link_to :a,\n :b %>") + end + + it "handles indented multiline statements" do + result = subject.format(" <% link_to :a,\n:b %>", **options) + expect(result).to eql(" <% link_to :a,\n :b %>") + end + + it "handles trim mode templates" do + result = subject.format("<% link_to :a -%>", **options) + expect(result).to eql("<% link_to :a -%>") + end + + it "handles rails raw mode templates" do + result = subject.format("<%== link_to :a %>", **options) + expect(result).to eql("<%== link_to :a %>") + end + + it "handles escaped erb templates" do + result = subject.format("<%%= puts :foo %>", **options) + expect(result).to eql("<%%= puts :foo %>") + end + + it "handles escaped rails raw mode erb templates" do + result = subject.format("<%%== puts :foo %>", **options) + expect(result).to eql("<%%== puts :foo %>") + end + + it "handles invalid code" do + expect { subject.format("\n\n<% a+ %>", **options) }.to raise_error { |error| + expect(error).to be_a(Rufo::SyntaxError) + expect(error.lineno).to eql(3) + } + end + + it "formats with options" do + result = subject.format(%(<%= "hello" + ', world' %>), **options.merge(quote_style: :single)) + expect(result).to eql("<%= 'hello' + ', world' %>") + end + + it "handles literal keywords with code block" do + result = subject.format("<% if true %>\nabc\n<% end %>", **options) + expect(result).to eql("<% if true %>\nabc\n<% end %>") + + result = subject.format("<% a(true) do %>\nabc\n<% end %>", **options) + expect(result).to eql("<% a(true) do %>\nabc\n<% end %>") + + result = subject.format("<% a(nil) { %>\nabc\n<% } %>", **options) + expect(result).to eql("<% a(nil) { %>\nabc\n<% } %>") + end + + it "formats standalone 'yield'" do + result = subject.format("<%=yield%>", **options) + expect(result).to eql("<%= yield %>") + end end + end - it "handles literal keywords with code block" do - result = subject.format("<% if true %>\nabc\n<% end %>") - expect(result).to eql("<% if true %>\nabc\n<% end %>") + context 'ripper' do + let(:parser_engine) { :ripper } - result = subject.format("<% a(true) do %>\nabc\n<% end %>") - expect(result).to eql("<% a(true) do %>\nabc\n<% end %>") + it_behaves_like 'erb_formatter is works' + end - result = subject.format("<% a(nil) { %>\nabc\n<% } %>") - expect(result).to eql("<% a(nil) { %>\nabc\n<% } %>") - end + context 'prism' do + let(:parser_engine) { :prism } - it "formats standalone 'yield'" do - result = subject.format("<%=yield%>") - expect(result).to eql("<%= yield %>") - end + it_behaves_like 'erb_formatter is works' end end diff --git a/spec/lib/rufo/formatter_spec.rb b/spec/lib/rufo/formatter_spec.rb index 182ec9b7..62d0b1d7 100644 --- a/spec/lib/rufo/formatter_spec.rb +++ b/spec/lib/rufo/formatter_spec.rb @@ -40,10 +40,12 @@ def assert_source_specs(source_specs) 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]) + + options = test[:options].merge(parser_engine: parser_engine) + formatted = described_class.format(test[:original], **options) expected = test[:expected].rstrip + "\n" expect(formatted).to eq(expected) - idempotency_check = described_class.format(formatted, **test[:options]) + idempotency_check = described_class.format(formatted, **options) expect(idempotency_check).to eq(formatted) end end @@ -56,12 +58,13 @@ def assert_format(code, expected = code, **options) line = caller_locations[0].lineno ex = it "formats #{code.inspect} (line: #{line})" do - actual = Rufo.format(code, **options) + opts = options.merge(parser_engine:) + 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, **options) + 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 @@ -73,42 +76,55 @@ def assert_format(code, expected = code, **options) end RSpec.describe Rufo::Formatter do - Dir[File.join(FILE_PATH, "/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, "/formatter_source_specs/3.0/*")].each do |source_specs| + shared_examples_for 'formatter is works' do + Dir[File.join(FILE_PATH, "/formatter_source_specs/*")].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, "/formatter_source_specs/3.1/*")].each do |source_specs| - assert_source_specs(source_specs) if File.file?(source_specs) + if VERSION >= Gem::Version.new("3.0") + Dir[File.join(FILE_PATH, "/formatter_source_specs/3.0/*")].each do |source_specs| + assert_source_specs(source_specs) if File.file?(source_specs) + end end - end - if VERSION >= Gem::Version.new("3.2") - Dir[File.join(FILE_PATH, "/formatter_source_specs/3.2/*")].each do |source_specs| - assert_source_specs(source_specs) if File.file?(source_specs) + if VERSION >= Gem::Version.new("3.1") + Dir[File.join(FILE_PATH, "/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, "/formatter_source_specs/3.2/*")].each do |source_specs| + assert_source_specs(source_specs) if File.file?(source_specs) + end + end + + 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 Ripper" do + it "raises an unknown syntax error" do + expect { + Rufo.format("def foo; FOO = 1; end", parser_engine: parser_engine) + }.to raise_error(Rufo::UnknownSyntaxError) + end end end - # Empty - describe "empty" do - assert_format "", "" - assert_format " ", " " - assert_format "\n", "" - assert_format "\n\n", "" - assert_format "\n\n\n", "" + context 'ripper' do + let(:parser_engine) { :ripper } + + it_behaves_like 'formatter is works' end - describe "Syntax errors not handled by Ripper" do - it "raises an unknown syntax error" do - expect { - Rufo.format("def foo; FOO = 1; end") - }.to raise_error(Rufo::UnknownSyntaxError) - end + context 'prism' do + let(:parser_engine) { :prism } + + it_behaves_like 'formatter is works' end end diff --git a/spec/lib/rufo/parser_spec.rb b/spec/lib/rufo/parser_spec.rb index acf19870..f92e9c65 100644 --- a/spec/lib/rufo/parser_spec.rb +++ b/spec/lib/rufo/parser_spec.rb @@ -1,18 +1,42 @@ RSpec.describe Rufo::Parser do - subject { described_class } + subject { described_class.new(engine) } - it "parses valid code" do - subject.parse("a = 6") + context 'ripper' do + let(:engine) { :ripper } + + it "parses valid code" do + subject.parse("a = 6") + end + + context "code is invalid" do + let(:code) { "a do" } + + it "raises an error with line number information" do + expect { subject.parse(code) }.to raise_error do |error| + expect(error).to be_a(Rufo::SyntaxError) + expect(error.lineno).to be(1) + expect(error.message).to eql("syntax error, unexpected end-of-input") + end + end + end end - context "code is invalid" do - let(:code) { "a do" } + context 'prism' do + let(:engine) { :prism } + + it "parses valid code" do + subject.parse("a = 6") + end + + context "code is invalid" do + let(:code) { "a do" } - it "raises an error with line number information" do - expect { subject.parse(code) }.to raise_error do |error| - expect(error).to be_a(Rufo::SyntaxError) - expect(error.lineno).to be(1) - expect(error.message).to eql("syntax error, unexpected end-of-input") + it "raises an error with line number information" do + expect { subject.parse(code) }.to raise_error do |error| + expect(error).to be_a(Rufo::SyntaxError) + expect(error.lineno).to be(1) + expect(error.message).to eql("unexpected end-of-input, assuming it is closing the parent top level context") + end end end end