Skip to content
This repository was archived by the owner on Nov 30, 2024. It is now read-only.

Commit 9bba372

Browse files
committed
Introduce Source, Source::Node, Source::Token and Source::Location
1 parent f6c4430 commit 9bba372

File tree

7 files changed

+506
-0
lines changed

7 files changed

+506
-0
lines changed

lib/rspec/core/source.rb

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
RSpec::Support.require_rspec_core "source/node"
2+
RSpec::Support.require_rspec_core "source/token"
3+
4+
module RSpec
5+
module Core
6+
# @private
7+
class Source
8+
attr_reader :source, :path
9+
10+
def self.from_file(path)
11+
source = File.read(path)
12+
new(source, path)
13+
end
14+
15+
def initialize(source_string, path=nil)
16+
@source = source_string
17+
@path = path ? File.expand_path(path) : '(string)'
18+
end
19+
20+
def lines
21+
@lines ||= source.split("\n")
22+
end
23+
24+
def ast
25+
@ast ||= begin
26+
require 'ripper'
27+
sexp = Ripper.sexp(source)
28+
Node.new(sexp)
29+
end
30+
end
31+
32+
def tokens
33+
@tokens ||= begin
34+
require 'ripper'
35+
tokens = Ripper.lex(source)
36+
Token.tokens_from_ripper_tokens(tokens)
37+
end
38+
end
39+
40+
def nodes_by_line_number
41+
@nodes_by_line_number ||= begin
42+
nodes_by_line_number = ast.select(&:location).group_by { |node| node.location.line }
43+
Hash.new { |hash, key| hash[key] = [] }.merge(nodes_by_line_number)
44+
end
45+
end
46+
47+
def tokens_by_line_number
48+
@tokens_by_line_number ||= begin
49+
nodes_by_line_number = tokens.group_by { |token| token.location.line }
50+
Hash.new { |hash, key| hash[key] = [] }.merge(nodes_by_line_number)
51+
end
52+
end
53+
54+
def inspect
55+
"#<#{self.class} #{path}>"
56+
end
57+
end
58+
end
59+
end

lib/rspec/core/source/location.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
module RSpec
2+
module Core
3+
class Source
4+
# @private
5+
Location = Struct.new(:line, :column) do
6+
def self.location?(array)
7+
array.is_a?(Array) && array.size == 2 && array.all? { |e| e.is_a?(Integer) }
8+
end
9+
end
10+
end
11+
end
12+
end

lib/rspec/core/source/node.rb

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
RSpec::Support.require_rspec_core "source/location"
2+
3+
module RSpec
4+
module Core
5+
class Source
6+
# @private
7+
class Node
8+
include Enumerable
9+
10+
attr_reader :sexp, :parent
11+
12+
def self.sexp?(array)
13+
array.is_a?(Array) && array.first.is_a?(Symbol)
14+
end
15+
16+
def initialize(ripper_sexp, parent=nil)
17+
@sexp = ripper_sexp.freeze
18+
@parent = parent
19+
end
20+
21+
def type
22+
sexp[0]
23+
end
24+
25+
def args
26+
@args ||= raw_args.map do |raw_arg|
27+
if Node.sexp?(raw_arg)
28+
Node.new(raw_arg, self)
29+
elsif Location.location?(raw_arg)
30+
Location.new(*raw_arg)
31+
elsif raw_arg.is_a?(Array)
32+
GroupNode.new(raw_arg, self)
33+
else
34+
raw_arg
35+
end
36+
end.freeze
37+
end
38+
39+
def children
40+
@children ||= args.select { |arg| arg.is_a?(Node) }.freeze
41+
end
42+
43+
def location
44+
@location ||= args.find { |arg| arg.is_a?(Location) }
45+
end
46+
47+
def each(&block)
48+
return to_enum(__method__) unless block_given?
49+
50+
yield self
51+
52+
children.each do |child|
53+
child.each(&block)
54+
end
55+
end
56+
57+
def each_ancestor
58+
return to_enum(__method__) unless block_given?
59+
60+
last_node = self
61+
62+
while (current_node = last_node.parent)
63+
yield current_node
64+
last_node = current_node
65+
end
66+
end
67+
68+
def inspect
69+
"#<#{self.class} #{type}>"
70+
end
71+
72+
private
73+
74+
def raw_args
75+
sexp[1..-1] || []
76+
end
77+
end
78+
79+
# @private
80+
class GroupNode < Node
81+
def type
82+
:group
83+
end
84+
85+
private
86+
87+
def raw_args
88+
sexp
89+
end
90+
end
91+
end
92+
end
93+
end

lib/rspec/core/source/token.rb

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
RSpec::Support.require_rspec_core "source/location"
2+
3+
module RSpec
4+
module Core
5+
class Source
6+
# @private
7+
class Token
8+
attr_reader :token
9+
10+
def self.tokens_from_ripper_tokens(ripper_tokens)
11+
ripper_tokens.map { |ripper_token| new(ripper_token) }.freeze
12+
end
13+
14+
def initialize(ripper_token)
15+
@token = ripper_token.freeze
16+
end
17+
18+
def location
19+
@location ||= Location.new(*token[0])
20+
end
21+
22+
def type
23+
token[1]
24+
end
25+
26+
def string
27+
token[2]
28+
end
29+
30+
def ==(other)
31+
token == other.token
32+
end
33+
34+
alias_method :eql?, :==
35+
36+
def inspect
37+
"#<#{self.class} #{type} #{string.inspect}>"
38+
end
39+
end
40+
end
41+
end
42+
end

spec/rspec/core/source/node_spec.rb

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
require 'rspec/core/source/node'
2+
3+
class RSpec::Core::Source
4+
RSpec.describe Node, :if => RSpec::Support::RubyFeatures.ripper_supported? do
5+
let(:root_node) do
6+
Node.new(sexp)
7+
end
8+
9+
let(:sexp) do
10+
require 'ripper'
11+
Ripper.sexp(source)
12+
end
13+
14+
let(:source) { <<-END }
15+
variable = do_something(1, 2)
16+
variable.do_anything do |arg|
17+
puts arg
18+
end
19+
END
20+
21+
# [:program,
22+
# [[:assign,
23+
# [:var_field, [:@ident, "variable", [1, 6]]],
24+
# [:method_add_arg,
25+
# [:fcall, [:@ident, "do_something", [1, 17]]],
26+
# [:arg_paren,
27+
# [:args_add_block,
28+
# [[:@int, "1", [1, 30]], [:@int, "2", [1, 33]]],
29+
# false]]]],
30+
# [:method_add_block,
31+
# [:call,
32+
# [:var_ref, [:@ident, "variable", [2, 6]]],
33+
# :".",
34+
# [:@ident, "do_anything", [2, 15]]],
35+
# [:do_block,
36+
# [:block_var,
37+
# [:params, [[:@ident, "arg", [2, 31]]], nil, nil, nil, nil, nil, nil],
38+
# false],
39+
# [[:command,
40+
# [:@ident, "puts", [3, 8]],
41+
# [:args_add_block, [[:var_ref, [:@ident, "arg", [3, 13]]]], false]]]]]]]
42+
43+
describe '#args' do
44+
context 'when the sexp args consist of direct child sexps' do
45+
let(:target_node) do
46+
root_node.find { |node| node.type == :method_add_arg }
47+
end
48+
49+
it 'returns the child nodes' do
50+
expect(target_node.args).to match([
51+
an_object_having_attributes(:type => :fcall),
52+
an_object_having_attributes(:type => :arg_paren)
53+
])
54+
end
55+
end
56+
57+
context 'when the sexp args include an array of sexps' do
58+
let(:target_node) do
59+
root_node.find { |node| node.type == :args_add_block }
60+
end
61+
62+
it 'returns pseudo group node for the array' do
63+
expect(target_node.args).to match([
64+
an_object_having_attributes(:type => :group),
65+
false
66+
])
67+
end
68+
end
69+
end
70+
71+
describe '#each_ancestor' do
72+
let(:target_node) do
73+
root_node.find { |node| node.type == :arg_paren }
74+
end
75+
76+
it 'yields ancestor nodes from parent to root' do
77+
expect { |b| target_node.each_ancestor(&b) }.to yield_successive_args(
78+
an_object_having_attributes(:type => :method_add_arg),
79+
an_object_having_attributes(:type => :assign),
80+
an_object_having_attributes(:type => :group),
81+
an_object_having_attributes(:type => :program)
82+
)
83+
end
84+
end
85+
86+
describe '#location' do
87+
context 'with identifier type node' do
88+
let(:target_node) do
89+
root_node.find { |node| node.type == :@ident }
90+
end
91+
92+
it 'returns a Location object with line and column numbers' do
93+
expect(target_node.location).to have_attributes(:line => 1, :column => 6)
94+
end
95+
end
96+
97+
context 'with non-identifier type node' do
98+
let(:target_node) do
99+
root_node.find { |node| node.type == :assign }
100+
end
101+
102+
it 'returns nil' do
103+
expect(target_node.location).to be_nil
104+
end
105+
end
106+
end
107+
108+
describe '#inspect' do
109+
it 'returns a string including class name and node type' do
110+
expect(root_node.inspect).to eq('#<RSpec::Core::Source::Node program>')
111+
end
112+
end
113+
end
114+
end

spec/rspec/core/source/token_spec.rb

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
require 'rspec/core/source/token'
2+
3+
class RSpec::Core::Source
4+
RSpec.describe Token, :if => RSpec::Support::RubyFeatures.ripper_supported? do
5+
let(:target_token) do
6+
tokens.first
7+
end
8+
9+
let(:tokens) do
10+
Token.tokens_from_ripper_tokens(ripper_tokens)
11+
end
12+
13+
let(:ripper_tokens) do
14+
require 'ripper'
15+
Ripper.lex(source)
16+
end
17+
18+
let(:source) do
19+
'puts :foo'
20+
end
21+
22+
# [
23+
# [[1, 0], :on_ident, "puts"],
24+
# [[1, 4], :on_sp, " "],
25+
# [[1, 5], :on_symbeg, ":"],
26+
# [[1, 6], :on_ident, "foo"]
27+
# ]
28+
29+
describe '#location' do
30+
it 'returns a Location object with line and column numbers' do
31+
expect(target_token.location).to have_attributes(:line => 1, :column => 0)
32+
end
33+
end
34+
35+
describe '#type' do
36+
it 'returns a type of the token' do
37+
expect(target_token.type).to eq(:on_ident)
38+
end
39+
end
40+
41+
describe '#string' do
42+
it 'returns a source string corresponding to the token' do
43+
expect(target_token.string).to eq('puts')
44+
end
45+
end
46+
47+
describe '#==' do
48+
context 'when both tokens have same Ripper token' do
49+
it 'returns true' do
50+
expect(Token.new(ripper_tokens[0]) == Token.new(ripper_tokens[0])).to be true
51+
end
52+
end
53+
54+
context 'when both tokens have different Ripper token' do
55+
it 'returns false' do
56+
expect(Token.new(ripper_tokens[0]) == Token.new(ripper_tokens[1])).to be false
57+
end
58+
end
59+
end
60+
61+
describe '#inspect' do
62+
it 'returns a string including class name, token type and source string' do
63+
expect(target_token.inspect).to eq('#<RSpec::Core::Source::Token on_ident "puts">')
64+
end
65+
end
66+
end
67+
end

0 commit comments

Comments
 (0)