Skip to content

Be more defensive in the parser translator lexer#3551

Merged
kddnewton merged 1 commit into
ruby:mainfrom
Earlopain:parser-translator-lexer-invalid-syntax
Aug 14, 2025
Merged

Be more defensive in the parser translator lexer#3551
kddnewton merged 1 commit into
ruby:mainfrom
Earlopain:parser-translator-lexer-invalid-syntax

Conversation

@Earlopain

@Earlopain Earlopain commented Apr 17, 2025

Copy link
Copy Markdown
Collaborator

Fixes rubocop/rubocop#14100
Fixes #3550

Generally I have been good about safely accessing the tokens but failed to properly guard against no tokens in places
where it could theoretically happen through invalid syntax.

I added a test case for one occurance, other changes are theoretical only.

Also, TIL that this works:

a = [[1,2], [3,4]]
i = 0

while (foo, _ = a[i])
  puts foo
  i += 1
end
# => 1
# => 3

I make use of that construct to safely access the data without much changes. Makes some code easier actually.

@Earlopain

Copy link
Copy Markdown
Collaborator Author

Hi @kddnewton, sorry for the bother but can you check this out?

Comment thread lib/prism/translation/parser/lexer.rb Outdated

if percent_array?(quote_stack.pop)
prev_token = lexed[index - 2][0] if index - 2 >= 0
prev_token, _ = lexed[index - 2]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is going to wrap around now, but I don't think it should

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Oops, indeed. I should not have removed that

Comment thread lib/prism/translation/parser/lexer.rb Outdated

prev_token = lexed[index - 2][0] if index - 2 >= 0
next_token = lexed[index][0]
prev_token, _ = lexed[index - 2]

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Same here, we don't want it to wrap

@Earlopain
Earlopain force-pushed the parser-translator-lexer-invalid-syntax branch from 329bd6e to b1a9b32 Compare June 30, 2025 12:46
Comment thread lib/prism/translation/parser/lexer.rb Outdated
current_length += token.value.bytesize
# Heredoc interpolation can have multiple STRING_CONTENT nodes on the same line.
is_first_token_on_line = lexed[index - 1] && token.location.start_line != lexed[index - 2][0].location&.start_line
is_first_token_on_line = lexed[index - 1] && token.location.start_line != lexed[index - 2]&.first.location&.start_line

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is it possible for lexed[index - 2] to be nil here? If it is, then we need &.location as well. If it's not, then we shouldn't use &.first

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I meant this to be "safe" safe navigation. I don't believe this matters for valid syntax, but with invalid one I can't say so wanted to be on the safe side.

@Earlopain
Earlopain force-pushed the parser-translator-lexer-invalid-syntax branch from b1a9b32 to 63e30a1 Compare July 8, 2025 18:15
@Earlopain

Copy link
Copy Markdown
Collaborator Author

Any chance to get this merged? This is also a problem for rubocop extensions that try to extract ruby code from templating like erb or slim (r7kamura/rubocop-slim#31)

Comment on lines 277 to -280

while !((next_token = lexed[index][0]) && next_token.type == :EMBDOC_END) && (index < length - 1)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I think you can also do the same line 284?

@Earlopain Earlopain Aug 11, 2025

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, that works (if you meant what I changed that is)

Comment thread lib/prism/translation/parser/lexer.rb Outdated
current_length += token.value.bytesize
# Heredoc interpolation can have multiple STRING_CONTENT nodes on the same line.
is_first_token_on_line = lexed[index - 1] && token.location.start_line != lexed[index - 2][0].location&.start_line
is_first_token_on_line = lexed[index - 1] && token.location.start_line != lexed[index - 2]&.first&.location&.start_line

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we avoid the mix of index accesses and first with something like this?

Suggested change
is_first_token_on_line = lexed[index - 1] && token.location.start_line != lexed[index - 2]&.first&.location&.start_line
prev_prev_token, _ = lexed[index - 2] if index - 2 >= 0
is_first_token_on_line = lexed[index - 1] && token.location.start_line != prev_prev_token&.location&.start_line

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, sure. It's a bit better. It's actually just prev_token, even if it is[index -2]. Right at the top of the loop index already gets incremented so you need to unintuitively look back a bit further.

I should move most of the state from the huge to_a into the class itself to make writing helper methods more convenient sometime.

@Earlopain
Earlopain force-pushed the parser-translator-lexer-invalid-syntax branch 2 times, most recently from 247e841 to a932a4f Compare August 11, 2025 19:25
Comment thread lib/prism/translation/parser/lexer.rb Outdated
result = Float::MAX

while (lexed[next_token_index] && next_token = lexed[next_token_index][0])
while (next_token, _ = lexed[next_token_index])

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I don't think the behaviour of these two constructs (old and new) will be the same in every instance. For example:

irb> foo = [nil, 1]
#=> [nil, 1]

irb> while (i, _ = foo); puts "42"; break; end
42
#=> nil

irb> while (i = foo&.first); puts "42"; break; end
#=> nil

The same would be true for any assignments in if conditions as well.

I would very much like to see &.first instead of destructuring used to safely get these values, since its behaviour would be the same as [0] (but safe against nil values) and I also find that it is more readable.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Good catch, I was expecting the destructure to be falsy

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

The token being nil would be a bug in prism I believe (and probably blow up in a different place later). I can see now how this isn't quite the same but practially the difference should be zero.

Anyways, I changed it.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for changing it, but I don't get why you think it would be a bug. The loop is explicitly conditional on the next_token being truthy, and if it isn't it should break.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

I was just saying that this difference shouldn't matter for the data that prism provides. It's always [token, some_internal_state], if there's no token (nil, then that should not have been part of lexed in the first place.

Generally I have been good about safely accessing the tokens but failed
to properly guard against no tokens in places
where it could theoretically happen through invalid syntax.

I added a test case for one occurance, other changes are theoretical only.
@Earlopain
Earlopain force-pushed the parser-translator-lexer-invalid-syntax branch from a932a4f to 4a3866a Compare August 14, 2025 15:06
@kddnewton
kddnewton merged commit 0984117 into ruby:main Aug 14, 2025
60 checks passed
@Earlopain

Copy link
Copy Markdown
Collaborator Author

Thanks everyone

@kddnewton

Copy link
Copy Markdown
Collaborator

Thanks for your patience @Earlopain — I'll be much faster when I'm back from paternity leave.

@Earlopain

Copy link
Copy Markdown
Collaborator Author

@kddnewton don't worry about it at all. Your labour on the previous PRs more than makes up for it

@Earlopain
Earlopain deleted the parser-translator-lexer-invalid-syntax branch October 3, 2025 10:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Need another review Need an extra review

Projects

None yet

Development

Successfully merging this pull request may close these issues.

undefined method '[]' for nil with (incomplete) case statement Parser fails to tokenize ERB with two subsequent returning expressions

5 participants