-
Notifications
You must be signed in to change notification settings - Fork 10.5k
[Diagnostics] Add fix-its for missing set
and )
after access modifier
#82058
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
base: main
Are you sure you want to change the base?
Conversation
auto diag = diagnose(Loc, diag::attr_access_expected_set, AttrName); | ||
|
||
if (Tok.is(tok::l_paren)) { | ||
return makeParserSuccess(); |
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.
Special handling private((
. Could you explain what this is for?
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.
The previous code already checked for the current token not being (
before consuming a )
as it wouldn't be closing the (
of the access modifier subject as expected in the recovery.
I thought I'd add the same check in the case of private(( <declaration>
but thinking about it again it might not be necessary and we can still just suggest to replace the extra (
with set)
(we can also do the same for private(()
, no?).
What do you think?
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 seems like the previous code was assuming if you had private( ()
then the ()
was something unrelated to the private(
, i.e the closing )
matches the (
in the ()
, not the private(
. That seems somewhat reasonable to me, although I doubt this case happens often in practice. It does more closely match what swift-syntax does though, so I think we should probably move this check to the if (peekToken().is(tok::r_paren))
condition, which would still allow us to correct private(( <declaration>
.
var unterminatedSubject = 0 | ||
|
||
private(42 // expected-error{{expected 'set' as subject of 'private' modifier}} expected-error{{expected declaration}} | ||
private(42 // expected-error{{expected 'set' as subject of 'private' modifier}} {{9-11=set)}} expected-error{{expected declaration}} |
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.
Should a test with more than one token e.g. private(42 45)
be added as well?
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.
There's this one already with multiple random tokens but I haven't added any assertions for the fix-its (as I didn't handle that case). What do you have in mind?
private(a bunch of random tokens)
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.
In that case should fix-it correct this case as well?
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.
I've been thinking about handling it by basically consuming tokens until we find a declaration and replace the whole range till the declaration with set
or set)
but there are some problems that we need to consider:
- In the worst case, we won't have a declaration until EOF so we'll end up scanning the remaining part of the file and checking
isStartOfSwiftDecl
with each token but that's an extreme case that won't happen with a lot of remaining code anyways I think. We can also limit how far ahead we search. <access-modifier>(
may not be followed by a declaration of its own (just by mistake maybe?) and so the fix-it will try to replace irrelevant but valid tokens until the first declaration withset
which doesn't make sense. Here's an example:private( // Fix-it would suggest replacing the code from here if isEnabled { startServiceA() startServiceB() performTaskC() } // till here with `set` var name: String = "Swift"
I'd love to hear your suggestions here.
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.
I don't think 2. should be necessary, maybe we could leave this case out as is, maybe only handle by consuming multiple tokens in same line e.g. until find \n
. I think my question was just to exercise thinking about other odd cases maybe something like private set((()))
. But at the end maybe those odd cases don't need to be handle as they are more unlikely.
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.
FWIW even if we don't handle these cases, I think it's still worth adding them as tests
@lucianopa-msft @rintaro I've updated the fix-its to include multiple invalid tokens (and invalid parentheses like |
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.
Thanks!
lib/Parse/ParseDecl.cpp
Outdated
int OpenParens = 1; | ||
while (!Tok.isAtStartOfLine() && !isStartOfSwiftDecl()) { |
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.
I'm not convinced we should have such an aggressive lookahead for this, consider the case where e.g the user has forgotten to write func
:
private( foo(_ x: Int) -> Bool
let x = 0
I don't think we want to suggest completely wiping out the function signature they wrote. Generally I think we only ought to be suggesting deleting code when it's very obviously bogus.
IMO the logic here should be:
- If we have
private()
, insertset
- If we have
private(<token>)
where<token>
isn'tset
, replace that token - If we have
private(
with no<token>)
, then insertset)
, such that the above case becomesprivate(set) foo
The swift-syntax parser has a slightly more advanced recovery where it can delete certain non-bracket tokens if they appear before a set
, but I don't think it's worth trying to emulate that here since we'll be switching over to that parser at some point in the future.
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.
Makes sense, I think that's similar to point #2 discussed previously.
What you're referring to is similar to the initial PR (commit 903496d), do you think we should revert back to that state and address issues from there?
And thanks for the note on switching to the swift-syntax parser, I'll keep that in mind for future changes.
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.
do you think we should revert back to that state and address issues from there?
Yeah I think that's probably best, we could still add special cases for things like private(foo bar)
and private(())
, but I'm not convinced those are particularly likely mistakes to make, so I think it's fine not to handle them. Though if we did, we'd also want to handle them in swift-syntax.
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.
I reverted back to the initial implementation.
If we have
private(
with no<token>)
, then insertset)
, such that the above case becomesprivate(set) foo
.
The initial implementation handles this case differently:
private( <declaration>
insertsset)
to becomeprivate(set) <declaration>
private(<unexpected> <declaration>
replaces<unexpected>
withset)
to becomeprivate(set) <declaration>
It depends on whether there's a declaration after (
or (<token>
or not, so it won't handle the private( foo(_: Int)
case since there's no valid declaration following (
or foo
.
We can add this case or replace the 2 cases above with it, or keep the 2 cases described as is.
I'd suggest keeping the 2 cases and suggesting set)
even if no declaration is found, this allows us to cover more cases like private(get var name
, private( var name
, and private( name
.
Removing the 2 cases can reduce the overhead of using isStartOfSwiftDecl
, but I feel like it's still helpful for providing a better fix-it.
What do you think?
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.
Thanks! I think it's fine to do both the general case where we don't have a declaration, as well as the private(<unexpected> <declaration>
case that checks isStartOfSwiftDecl
. The private( <declaration>
case should naturally fall out of the general case though I think?
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.
@hamishknight After some testing, replacing private( <not-declaration>
with private(set) <not-declaration>
has some pros and cons.
private( foo(...)
turns intoprivate(set) foo(...)
. ✅private((()))
turns intoprivate(set)(()))
which is not ideal. ❌private(some random tokens)
turns intoprivate(set) some random tokens)
which is not ideal. ❌
It's not very reliable in its simple form, though some cases can be solved by matching multiple tokens between the parentheses. Other cases may still yield weird results like private( some random token
turning into private(set) some random tokens
which we can leave as is to handle cases like number 1, or not handle cases without matching parentheses to guarantee reliable suggestions.
What do you think?
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.
private((()))
turns intoprivate(set)(()))
which is not ideal. ❌
This seems fine to me, we have no way of knowing what the user is trying to do with ((()))
and it's very unlikely to come up in practice, I'm happy to leave it be.
private(some random tokens)
turns intoprivate(set) some random tokens)
which is not ideal. ❌
This does seem a bit more unfortunate, but again I think it's pretty unlikely to come up in practice, so I'm okay not handling it for now. If we did want to handle it, we would also need to teach swift-syntax to handle it too.
2aa75e6
to
903496d
Compare
// Minimal recovery: if there's a single token and then an r_paren, | ||
// consume them both. If there's just an r_paren, consume that. |
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.
Nit: This comment is now outdated
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.
The recovery still hasn't changed, do you think we should update the comment to mention the fix-its as well?
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.
I think it's worth mentioning the isAtStartOfSwiftDecl
logic, but otherwise I think it's fine
lib/Parse/ParseDecl.cpp
Outdated
int OpenParens = 1; | ||
while (!Tok.isAtStartOfLine() && !isStartOfSwiftDecl()) { |
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.
Thanks! I think it's fine to do both the general case where we don't have a declaration, as well as the private(<unexpected> <declaration>
case that checks isStartOfSwiftDecl
. The private( <declaration>
case should naturally fall out of the general case though I think?
var unterminatedSubject = 0 | ||
|
||
private(42 // expected-error{{expected 'set' as subject of 'private' modifier}} expected-error{{expected declaration}} | ||
private(42 // expected-error{{expected 'set' as subject of 'private' modifier}} {{9-11=set)}} expected-error{{expected declaration}} |
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.
FWIW even if we don't handle these cases, I think it's still worth adding them as tests
auto diag = diagnose(Loc, diag::attr_access_expected_set, AttrName); | ||
|
||
if (Tok.is(tok::l_paren)) { | ||
return makeParserSuccess(); |
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 seems like the previous code was assuming if you had private( ()
then the ()
was something unrelated to the private(
, i.e the closing )
matches the (
in the ()
, not the private(
. That seems somewhat reasonable to me, although I doubt this case happens often in practice. It does more closely match what swift-syntax does though, so I think we should probably move this check to the if (peekToken().is(tok::r_paren))
condition, which would still allow us to correct private(( <declaration>
.
Summary
Added fix-its missing
set
subject and)
for access control modifiers. The added fix-its handle the following cases:private(set
inserts)
to becomeprivate(set)
private()
insertsset
to becomeprivate(set)
private(<unexpected>)
replaces<unexpected>
withset
to becomeprivate(set)
private( <declaration>
insertsset)
to becomeprivate(set) <declaration>
private(<unexpected> <declaration>
replaces<unexpected>
withset)
to becomeprivate(set) <declaration>