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

Add ProcessedSource#tokens_within, ProcessedSource#first_token_of and ProcessedSource#last_token_of #92

Merged
merged 1 commit into from Aug 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -4,6 +4,7 @@

### New features

* [#92](https://github.com/rubocop-hq/rubocop-ast/pull/92): Add `ProcessedSource#tokens_within`, `ProcessedSource#first_token_of` and `ProcessedSource#last_token_of`. ([@fatkodima][])
* [#88](https://github.com/rubocop-hq/rubocop-ast/pull/88): Add `RescueNode`. Add `ResbodyNode#exceptions` and `ResbodyNode#branch_index`. ([@fatkodima][])
* [#89](https://github.com/rubocop-hq/rubocop-ast/pull/89): Support right hand assignment for Ruby 2.8 (3.0) parser. ([@koic][])

Expand Down
39 changes: 39 additions & 0 deletions lib/rubocop/ast/processed_source.rb
Expand Up @@ -159,6 +159,20 @@ def line_indentation(line_number)
.length
end

def tokens_within(range_or_node)
begin_index = first_token_index(range_or_node)
end_index = last_token_index(range_or_node)
sorted_tokens[begin_index..end_index]
end

def first_token_of(range_or_node)
sorted_tokens[first_token_index(range_or_node)]
end

def last_token_of(range_or_node)
sorted_tokens[last_token_index(range_or_node)]
end

private

def comment_index
Expand Down Expand Up @@ -240,6 +254,31 @@ def create_parser(ruby_version)
end
end
end

def first_token_index(range_or_node)
begin_pos = source_range(range_or_node).begin_pos
sorted_tokens.bsearch_index { |token| token.begin_pos >= begin_pos }
end

def last_token_index(range_or_node)
end_pos = source_range(range_or_node).end_pos
sorted_tokens.bsearch_index { |token| token.end_pos >= end_pos }
end

# The tokens list is always sorted by token position, except for cases when heredoc
# is passed as a method argument. In this case tokens are interleaved by
# heredoc contents' tokens.
def sorted_tokens
@sorted_tokens ||= tokens.sort_by(&:begin_pos)
end

def source_range(range_or_node)
if range_or_node.respond_to?(:source_range)
range_or_node.source_range
else
range_or_node
end
end
end
end
end
Expand Down
87 changes: 86 additions & 1 deletion spec/rubocop/ast/processed_source_spec.rb
Expand Up @@ -10,6 +10,7 @@ def some_method
end
some_method
RUBY
let(:ast) { processed_source.ast }
let(:path) { 'ast/and_node_spec.rb' }

shared_context 'invalid encoding source' do
Expand Down Expand Up @@ -292,7 +293,6 @@ def some_method
describe '#contains_comment?' do
subject(:commented) { processed_source.contains_comment?(range) }

let(:ast) { processed_source.ast }
let(:array) { ast }
let(:hash) { array.children[1] }

Expand Down Expand Up @@ -464,4 +464,89 @@ def some_method
expect(processed_source.following_line(brace_token)).to eq '# line 3'
end
end

describe '#tokens_within' do
let(:source) { <<~RUBY }
foo(1, 2)
bar(3)
RUBY

it 'returns tokens for node' do
node = ast.children[1]
tokens = processed_source.tokens_within(node.source_range)

expect(tokens.map(&:text)).to eq(['bar', '(', '3', ')'])
end

it 'accepts Node as an argument' do
node = ast.children[1]
tokens = processed_source.tokens_within(node)

expect(tokens.map(&:text)).to eq(['bar', '(', '3', ')'])
end

context 'when heredoc as argument is present' do
let(:source) { <<~RUBY }
foo(1, [before], <<~DOC, [after])
inside heredoc.
DOC
bar(2)
RUBY

it 'returns tokens for node before heredoc' do
node = ast.children[0].arguments[1]
tokens = processed_source.tokens_within(node.source_range)

expect(tokens.map(&:text)).to eq(['[', 'before', ']'])
end

it 'returns tokens for heredoc node' do
node = ast.children[0].arguments[2]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aren't the heredoc's tokens a bigger issue for #{} within the heredoc?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't get the point. Can you reword/explain more?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was wondering: would your test fail if the tokens were not sorted? I thought it was for cases where there's a heredoc with for #{} that doing the bsearch on unsorted tokens might fail.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When tokens are not sorted, for some cases, depending on the source, it will fail, sometimes not. It depends on how lucky bsearch will be to find correct locations.
When they are sorted, it will always succeed.

tokens = processed_source.tokens_within(node.source_range)

expect(tokens.map(&:text)).to eq(['<<"'])
end

it 'returns tokens for node after heredoc' do
node = ast.children[0].arguments[3]
tokens = processed_source.tokens_within(node.source_range)

expect(tokens.map(&:text)).to eq(['[', 'after', ']'])
end
end
end

describe '#first_token_of' do
let(:source) { <<~RUBY }
foo(1, 2)
bar(3)
RUBY

it 'returns first token for node' do
node = ast.children[1]
expect(processed_source.first_token_of(node.source_range).text).to eq('bar')
end

it 'accepts Node as an argument' do
node = ast.children[1]
expect(processed_source.first_token_of(node).text).to eq('bar')
end
end

describe '#last_token_of' do
let(:source) { <<~RUBY }
foo(1, 2)
bar = baz
RUBY

it 'returns last token for node' do
node = ast.children[1]
expect(processed_source.last_token_of(node.source_range).text).to eq('baz')
end

it 'accepts Node as an argument' do
node = ast.children[1]
expect(processed_source.last_token_of(node).text).to eq('baz')
end
end
end