Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Prism as a Ruby parser #330

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,5 @@ source "https://rubygems.org"

# Specify your gem's dependencies in rufo.gemspec
gemspec

gem 'prism'
25 changes: 14 additions & 11 deletions lib/rufo/erb_formatter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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

Expand All @@ -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
Expand Down
10 changes: 6 additions & 4 deletions lib/rufo/formatter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
41 changes: 32 additions & 9 deletions lib/rufo/parser.rb
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 }

Expand All @@ -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

Expand Down
13 changes: 13 additions & 0 deletions lib/rufo/parser/prism.rb
Original file line number Diff line number Diff line change
@@ -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
13 changes: 13 additions & 0 deletions lib/rufo/parser/ripper.rb
Original file line number Diff line number Diff line change
@@ -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
182 changes: 99 additions & 83 deletions spec/lib/rufo/erb_formatter_spec.rb
Original file line number Diff line number Diff line change
@@ -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
Loading
Loading