-
Notifications
You must be signed in to change notification settings - Fork 1.7k
SSR: Support statement matching and replacing #6587
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
Conversation
Marking this PR ready for review to capture suggestions and a general (n)ack on the way this is implemented. Note that the code and commit history are still messy, that will be cleaned up in due time when the path forward is cleared up 🙂 |
Nice to see this being worked on! You're right that the optional semicolon handling is adding quite a bit of complexity. My personal feeling would be that making semicolons non-optional would be reasonable for a first cut of the feature. If you do want to make semicolons optional, I think it could be done more simply. I'd be inclined to allow You'd then need a little extra code in |
@davidlattimore That suggestion is implemented in the first 5 commits, up to and including be6b4c3. That last commit makes sure optional trailing semicolons are only allowed for With that, as you say the replacement code needs an update to insert missing trailing semicolons where necessary. I'm hesitant to implement it that way though because we're going to need partial match ranges anyway for statements lists - where one usually only wants to match a few consecutive statements in a block. At that point this arguably hacky approach can be stripped out again, and have a consistent user experience as well (ie. replacing In the end we should start with some test cases covering these, especially where the user intends to change trailing colon rules - and compare the simplified approach with match ranges. |
Do you think it might make sense then to leave optional semicolons until after multi-statement support is implemented? |
574db30
to
f8bc518
Compare
@davidlattimore I am not sure. Implementing it the way you suggested turns out fairly trivial, but the aforementioned hypothetical test/usecases fail as expected: Afaik this should be fixable earlier in the pipeline, when having the search pattern and template side-by-side: if the search pattern includes a trailing semicolon, the replacement statement should receive one as well. Unsurprisingly the partial match implementation implicitly succeeds all these tests, see the cleaned up version here: https://github.com/MarijnS95/rust-analyzer/compare/stmt..let-stmt-partial Do you think these theoretical usecases are worth fixing? Otherwise, we might do this in 3 stages:
|
I probably wouldn't worry about the hypothetical use cases. SSR is really intended for transforming syntactically valid code into different syntactically valid code. I struggle to think of a case where removing semicolons from valid code would leave the code valid. Even if there is a case, it's probably pretty obscure, so probably isn't a high priority to support. I assume the motivation for wanting to support optional trailing semicolons is to make writing the rule easier, since you don't need to remember to add the semicolons? Assuming that's the case, you could make it an error to supply a rule that has a trailing semicolon on one side, but not the other. Not sure if it's worth enforcing such a rule though. I'd probably just treat rules with / without the trailing semicolon as semantically equivalent. |
@davidlattimore I mostly care about consistency to the user. If they specify a semicolon on one side but not the other there must be a meaning in that. I won't be surprised if someone ends up in a situation with a single As for making it optional, yes, just convenience. Just as it might be convenient to match just the In the end, unlike expressions a let binding without trailing semicolon is invalid so we might as well treat it as such: simply disallow it from being specified (parsed, really) in the search pattern nor replacement template. |
Disallowing let expressions without a trailing semicolon sounds good to me :) |
@davidlattimore This should have been easy to solve by converting the statement parser to |
Unsurprisingly this breaks macro calls, where |
@@ -212,6 +212,13 @@ impl ast::Attr { | |||
} | |||
} | |||
|
|||
impl ast::Stmt { | |||
/// Returns `text`, parsed as statement, but only if it has no errors. | |||
pub fn parse(text: &str) -> Result<Self, ()> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It'd be nice to add a test for this function. The other parse functions in this file have tests - see test.rs in this directory
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair point. Doesn't seem like every type (ast::Attr
right above) has an explicit test though.
Not sure what to add for the err
case. Could add some garbage, but I guess we rather want bits of code that should explicitly not be parseable as Stmt
. Suggestions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, I hadn't seen the parser for ast::Attr
, that must have been added since I implemented the others. I was thinking of the ast::Item::parse
etc, which have tests - e.g. item_parser_tests
.
For error cases, I guess:
- Something that isn't a statement. e.g. and attribute
#[foo]
- Multiple statements:
a(); b();
- Something unparsable:
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just letting you know in case you feel like adding more tests to the other parsers.
Apart your suggestions I've added a bunch of items, expressions and blocks to the mix that can be parsed as Stmt
.
380b872
to
253d651
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How fast are all those parser tests? Given that the parser is already pretty well tested elsewhere and the new API is just adding a thin layer of extra code, I'm wondering if that many tests might be too much. But if they run quickly, perhaps it doesn't matter.
Otherwise, this looks good to me.
Adjusting `grammar::fragments::stmt` to Optional or Yes will break original functionality and tests.
@davidlattimore The timing difference is pretty much negligible and within margin of error:
(Measurements taken after building the testing binary once, of course) EDIT: However, fair to say littering tests around when they have little significance only confuses the reader above all. In the end I guess these serve to test the parser "setup" like whether trailing semicolons are allowed (which doesn't make sense on |
Now that statements can be matched and replaced (rust-lang#6587) some usecases require expressions to be replaced with statements as well. This happens when something that can ambiguously be an expression or statement like `if` blocks and loops appear in the last position of a block: in this case a replacement pattern of the form `if foo(){$a();}==>>$a();` will only substitute `if` blocks in the list of statements but not if they (implicitly) end up in the trailing expression, where they are not wrapped by an EXPR_STMT (but the pattern and template are as only succeeds for the `stmt ==>> stmt` case). Instead of adding two rules that match an expression - and emit duplicate matching errors - allow the template for expressions to be a statement if it fails to parse as an expression.
Now that statements can be matched and replaced (rust-lang#6587) some usecases require expressions to be replaced with statements as well. This happens when something that can ambiguously be an expression or statement like `if` and loop blocks appear in the last position of a block: in this case a replacement pattern of the form `if foo(){$a();}==>>$a();` will only substitute `if` blocks in the list of statements but not if they (implicitly) end up in the trailing expression, where they are not wrapped by an EXPR_STMT (but the pattern and template are as only succeeds for the `stmt ==>> stmt` case). Instead of adding two rules that match an expression - and emit duplicate matching errors - allow the template for expressions to be a statement if it fails to parse as an expression.
Now that statements can be matched and replaced (rust-lang#6587) some usecases require expressions to be replaced with statements as well. This happens when something that can ambiguously be an expression or statement like `if` and loop blocks appear in the last position of a block, as trailing expression. In this case a replacement pattern of the form `if foo(){$a();}==>>$a();` will only substitute `if` blocks in the list of statements but not if they (implicitly) end up in the trailing expression, where they are not wrapped by an EXPR_STMT (but the pattern and template are, as parsing only succeeds for the `stmt ==>> stmt` case). Instead of adding two rules that match an expression - and emit duplicate matching errors - allow the template for expressions to be a statement if it fails to parse as an expression.
let me bors d=davidlattimore |
✌️ davidlattimore can now approve this pull request. To approve and merge a pull request, simply reply with |
bors r+ |
7147: ssr: Allow replacing expressions with statements r=davidlattimore a=MarijnS95 Depends on #6587 Until that is merged, the diff is https://github.com/MarijnS95/rust-analyzer/compare/stmt..replace-expr-with-stmt --- Now that statements can be matched and replaced (#6587) some usecases require expressions to be replaced with statements as well. This happens when something that can ambiguously be an expression or statement like `if` and loop blocks appear in the last position of a block, as trailing expression. In this case a replacement pattern of the form `if foo(){$a();}==>>$a();` will only substitute `if` blocks in the list of statements but not if they (implicitly) end up in the trailing expression, where they are not wrapped by an EXPR_STMT (but the pattern and template are, as parsing only succeeds for the `stmt ==>> stmt` case). Instead of adding two rules that match an expression - and emit duplicate matching errors - allow the template for expressions to be a statement if it fails to parse as an expression. --- Another gross change that does not seem to break any tests currently, but perhaps a safeguard should be added to only allow this kind of replacement in blocks by "pushing" the replacement template to the statement list and clearing the trailing expression? CC @davidlattimore Co-authored-by: Marijn Suijten <[email protected]>
Now that statements can be matched and replaced (rust-lang#6587) some usecases require expressions to be replaced with statements as well. This happens when something that can ambiguously be an expression or statement like `if` and loop blocks appear in the last position of a block, as trailing expression. In this case a replacement pattern of the form `if foo(){$a();}==>>$a();` will only substitute `if` blocks in the list of statements but not if they (implicitly) end up in the trailing expression, where they are not wrapped by an EXPR_STMT (but the pattern and template are, as parsing only succeeds for the `stmt ==>> stmt` case). Instead of adding two rules that match an expression - and emit duplicate matching errors - allow the template for expressions to be a statement if it fails to parse as an expression.
Now that statements can be matched and replaced (rust-lang#6587) some usecases require expressions to be replaced with statements as well. This happens when something that can ambiguously be an expression or statement like `if` and loop blocks appear in the last position of a block, as trailing expression. In this case a replacement pattern of the form `if foo(){$a();}==>>$a();` will only substitute `if` blocks in the list of statements but not if they (implicitly) end up in the trailing expression, where they are not wrapped by an EXPR_STMT (but the pattern and template are, as parsing only succeeds for the `stmt ==>> stmt` case). Instead of adding two rules that match an expression - and emit duplicate matching errors - allow the template for expressions to be a statement if it fails to parse as an expression.
For #3186
Hi!
This is a smaller initial patchset that came up while working on support for statement lists (and my first time working on RA 😁). It has me stuck on trailing semicolons for which I hope to receive some feedback. Matching (and replacing)
let
bindings with a trailing semicolon works fine, but trying to omit these (to make patterns more ergonomic) turns out more complex than expected.The "optional trailing semicolon solution" implemented in this PR is ugly because
Matcher::attempt_match_token
should only consume a trailing;
when parsinglet
bindings to prevent other code from breaking. That at the same time has a nasty side-effect of;
ending up in the matched code: any replacements on that should include the trailing semicolon as well even if it was not in the pattern. A better example is in the tests:https://github.com/rust-analyzer/rust-analyzer/blob/3ae1649c24a689473b874c331f5f176e5839978e/crates/ssr/src/tests.rs#L178-L184
The end result to achieve is (I guess) allowing replacement of let bindings without trailing semicolon like
let x = $a ==>> let x = 1
(but including them on both sides is still fine), and should make replacement in a macro call (wherefoo!(let a = 2;)
for a$x:stmt
is invalid syntax) possible as well. That should allow to enable/fix these tests:https://github.com/rust-analyzer/rust-analyzer/blob/3ae1649c24a689473b874c331f5f176e5839978e/crates/ssr/src/tests.rs#L201-L214
A possible MVP of this PR might be to drop this optional `;' handling entirely and only allow an SSR pattern/template with semicolons on either side.