Skip to content

Commit cdfaf83

Browse files
authored
Merge pull request mbj#70 from naw/file-line-emitters
Add emitters for s(:__LINE__) and s(:__FILE__)
2 parents bf07696 + 1739fd8 commit cdfaf83

4 files changed

Lines changed: 120 additions & 22 deletions

File tree

lib/unparser.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ def self.unparse(node, comment_array = [])
5050
require 'unparser/emitter/literal/range'
5151
require 'unparser/emitter/literal/dynamic_body'
5252
require 'unparser/emitter/literal/execute_string'
53+
require 'unparser/emitter/meta'
5354
require 'unparser/emitter/send'
5455
require 'unparser/emitter/send/unary'
5556
require 'unparser/emitter/send/binary'

lib/unparser/emitter/meta.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
module Unparser
2+
class Emitter
3+
# Namespace class for meta emitters
4+
class Meta < self
5+
include Terminated
6+
7+
handle(:__FILE__, :__LINE__)
8+
9+
def dispatch
10+
write(node.type.to_s) # (e.g. literally write '__FILE__' or '__LINE__')
11+
end
12+
end # Meta
13+
end # Emitter
14+
end # Unparser
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
module ParserClassGenerator
2+
def self.generate_with_options(base_parser_class, builder_options)
3+
# This builds a dynamic subclass of the base_parser_class (e.g. Parser::Ruby23)
4+
# and overrides the default_parser method to return a parser whose builder
5+
# has various options set.
6+
#
7+
# Currently the only builder option is :emit_file_line_as_literals
8+
9+
Class.new(base_parser_class) do
10+
define_singleton_method(:default_parser) do |*args|
11+
super(*args).tap do |parser|
12+
parser.builder.emit_file_line_as_literals = builder_options[:emit_file_line_as_literals]
13+
end
14+
end
15+
16+
define_singleton_method(:inspect) do
17+
"#{base_parser_class.inspect} with builder options: #{builder_options.inspect}"
18+
end
19+
end
20+
end
21+
end

spec/unit/unparser_spec.rb

Lines changed: 84 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,76 @@
11
require 'spec_helper'
22
require 'parser/all'
3+
require 'support/parser_class_generator'
34

45
describe Unparser, mutant_expression: 'Unparser::Emitter*' do
56
describe '.unparse' do
67

7-
PARSERS = IceNine.deep_freeze(
8+
RUBY_VERSION_PARSERS = IceNine.deep_freeze(
89
'2.1' => Parser::Ruby21,
910
'2.2' => Parser::Ruby22,
1011
'2.3' => Parser::Ruby23
1112
)
1213

13-
RUBIES = PARSERS.keys.freeze
14+
RUBY_VERSIONS = RUBY_VERSION_PARSERS.keys.freeze
1415

15-
def self.parser_for_ruby_version(version)
16-
PARSERS.fetch(version) do
17-
raise "Unrecognized Ruby version #{version}"
16+
def self.builder_options
17+
@builder_options ||= {}
18+
end
19+
20+
def self.builder_options=(options)
21+
@builder_options = options
22+
end
23+
24+
def self.ruby_versions
25+
@ruby_versions ||= RUBY_VERSIONS
26+
end
27+
28+
def self.ruby_versions=(versions)
29+
@ruby_versions = versions
30+
end
31+
32+
def self.with_ruby_versions(beginning_at: nil, ending_at: nil, only: nil)
33+
original_ruby_versions = ruby_versions
34+
if only
35+
self.ruby_versions = only & ruby_versions # intersection
36+
else
37+
if ending_at
38+
idx = ruby_versions.index(ending_at) || fail('Invalid Ruby specified')
39+
self.ruby_versions = ruby_versions[0..idx]
40+
end
41+
if beginning_at
42+
idx = ruby_versions.index(beginning_at) || fail('Invalid Ruby specified')
43+
self.ruby_versions = ruby_versions[idx..-1]
44+
end
1845
end
46+
47+
yield
48+
49+
self.ruby_versions = original_ruby_versions
1950
end
2051

21-
def self.with_versions(versions)
22-
versions.each do |version|
23-
yield parser_for_ruby_version(version)
52+
def self.current_parsers
53+
ruby_versions.map do |ruby_version|
54+
if builder_options != {}
55+
ParserClassGenerator.generate_with_options(parser_for_ruby_version(ruby_version), builder_options)
56+
else
57+
parser_for_ruby_version(ruby_version)
58+
end
59+
end
60+
end
61+
62+
def self.with_builder_options(options)
63+
original_options = builder_options
64+
self.builder_options = builder_options.merge(options)
65+
66+
yield
67+
68+
self.builder_options = original_options
69+
end
70+
71+
def self.parser_for_ruby_version(version)
72+
RUBY_VERSION_PARSERS.fetch(version) do
73+
raise "Unrecognized Ruby version #{version}"
2474
end
2575
end
2676

@@ -50,14 +100,14 @@ def self.assert_unterminated(expression)
50100
assert_source("(#{expression}).foo")
51101
end
52102

53-
def self.assert_terminated(expression, rubies = RUBIES)
54-
assert_source(expression, rubies)
55-
assert_source("foo(#{expression})", rubies)
56-
assert_source("#{expression}.foo", rubies)
103+
def self.assert_terminated(expression)
104+
assert_source(expression)
105+
assert_source("foo(#{expression})")
106+
assert_source("#{expression}.foo")
57107
end
58108

59-
def self.assert_generates(ast_or_string, expected, versions = RUBIES)
60-
with_versions(versions) do |parser|
109+
def self.assert_generates(ast_or_string, expected)
110+
current_parsers.each do |parser|
61111
it "should generate #{ast_or_string} as #{expected} under #{parser.inspect}" do
62112
if ast_or_string.is_a?(String)
63113
expected = strip(expected)
@@ -69,16 +119,16 @@ def self.assert_generates(ast_or_string, expected, versions = RUBIES)
69119
end
70120
end
71121

72-
def self.assert_round_trip(input, versions = RUBIES)
73-
with_versions(versions) do |parser|
122+
def self.assert_round_trip(input)
123+
current_parsers.each do |parser|
74124
it "should round trip #{input} under #{parser.inspect}" do
75125
assert_round_trip(input, parser)
76126
end
77127
end
78128
end
79129

80-
def self.assert_source(input, versions = RUBIES)
81-
assert_round_trip(strip(input), versions)
130+
def self.assert_source(input)
131+
assert_round_trip(strip(input))
82132
end
83133

84134
context 'kwargs' do
@@ -273,8 +323,18 @@ def foo(bar:, baz: "value")
273323

274324
context 'magic keywords' do
275325
assert_generates '__ENCODING__', 'Encoding::UTF_8'
276-
assert_generates '__FILE__', '"(string)"'
277-
assert_generates '__LINE__', '1'
326+
327+
# These two assertions don't actually need to be wrapped in this block since `true` is the default,
328+
# but it is helpful to contrast with the assertions farther down.
329+
with_builder_options(emit_file_line_as_literals: true) do
330+
assert_generates '__FILE__', '"(string)"'
331+
assert_generates '__LINE__', '1'
332+
end
333+
334+
with_builder_options(emit_file_line_as_literals: false) do
335+
assert_source '__FILE__'
336+
assert_source '__LINE__'
337+
end
278338
end
279339

280340
context 'assignment' do
@@ -408,8 +468,10 @@ def foo
408468
end
409469

410470
context 'conditional send (csend)' do
411-
assert_terminated 'a&.b', %w(2.3)
412-
assert_terminated 'a&.b(c)', %w(2.3)
471+
with_ruby_versions(beginning_at: '2.3') do
472+
assert_terminated 'a&.b'
473+
assert_terminated 'a&.b(c)'
474+
end
413475
end
414476

415477
context 'send' do

0 commit comments

Comments
 (0)