-
Notifications
You must be signed in to change notification settings - Fork 10.5k
warn on empty OptionSet static constants #27089
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
warn on empty OptionSet static constants #27089
Conversation
Welcome to the compiler! Your logic here is sound, but I think it would be easier to do this the other way around: check when declaring a constant if the current type is an OptionSet, rather than checking for all zero-value constants when handling the OptionSet conformance. @brentdax, did you have an implementation strategy in mind when you marked it as a Starter Bug? |
lib/Sema/TypeCheckProtocol.cpp
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.
If you're looking for advice how to make this more readable, the two biggest recommendations are
- Split it into a function, and
- Early-return
Something like this could help, with comments explaining the checks you're making:
void diagnoseEmptyOptionSetCase(/* args you need... */) {
auto protocol = conformance->getProtocol();
if (!protocol->isSpecificProtocol(KnownProtocolKind::OptionSet))
return;
for (auto member : idc->getMembers()) {
auto pbd = dyn_cast<PatternBindingDecl>(member);
if (!pbd || !pbd->isStatic()) continue;
// ... rest of the stuff, but early-returning/continuing when the condition is not met
}
}
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.
Or take Jordan's suggestion, which should simplify the implementation a bit 😄
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, thanks!
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.
Would you be able to point me in right direction in the codebase? Would that be done at parsing time or at type checking?
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.
This would happen during type checking, after we've assigned types to stored properties. visitBoundVariable
in TypeCheckDecl.cpp
might be a good place to put this check.
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.
Ok thanks!
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 took Jordan's and your own suggestions. I didn't know where to put the new function though. I saw there's a MiscDiagnostics file. Would it make sense to put it there? Thanks
@jrose-apple I don't think I did. This might be a little tougher than I anticipated—sorry, @Vercantez! |
lib/Sema/TypeCheckProtocol.cpp
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.
You're probably going to throw out this code, but if you don't, beware—this closing brace is indented incorrectly. It's indented to match the isStatic()
check, but it actually goes with the PatternBindingDecl
cast.
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'll fix it in case I keep any of it
lib/Sema/TypeCheckDecl.cpp
Outdated
@@ -2383,6 +2385,42 @@ class DeclChecker : public DeclVisitor<DeclChecker> { | |||
if (VD->getAttrs().hasAttribute<DynamicReplacementAttr>()) | |||
TC.checkDynamicReplacementAttribute(VD); | |||
} | |||
|
|||
void checkForEmptyOptionSet(VarDecl *VD) { |
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 sure where this should go
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 this is an okay place. Another option is to make a static
helper function above the definition of DeclChecker. That seems to be where most of the other check*
functions are that are only used in this file.
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.
This is looking good so far! I have a number of additional suggestions, but you're on the right track.
@@ -4423,6 +4423,10 @@ ERROR(property_delegate_type_not_usable_from_inline,none, | |||
"must be '@usableFromInline' or public", | |||
(bool, bool)) | |||
|
|||
WARNING(option_set_zero_constant,none, | |||
"'static let' constant inside %0 that conforms to OptionSet produces an empty option set", |
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 you can drop "that conforms to OptionSet". You've already got "option set" in the diagnostic text, and the type name probably has to do with options already.
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.
Might be simpler to say %0 %1 produces an empty option set
where %0
is DescriptiveDeclKind
and %1
is an Identifier
(property name). Example: static property 'foo' produces an empty option set
.
lib/Sema/TypeCheckDecl.cpp
Outdated
|
||
void checkForEmptyOptionSet(VarDecl *VD) { | ||
if (!VD->isStatic()) | ||
return; |
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 want this to apply to both let
and var
? If not, this would be a good place to check it too.
lib/Sema/TypeCheckDecl.cpp
Outdated
if (!VD->isStatic()) | ||
return; | ||
auto DC = VD->getDeclContext(); | ||
auto protocols = DC->getLocalProtocols(); |
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.
"Local" protocols means protocols declared in this same declaration, but if we're in an extension, we probably still want to do this check. Fortunately, I think you can still do it pretty easily:
auto *optionSetProto = TC.Context.getProtocol(KnownProtocolKind::OptionSet);
bool conformsToOptionalSet = TC.containsProtocol(DC->getSelfTypeInContext(), optionSetProto, DC, /*Flags*/None);
lib/Sema/TypeCheckDecl.cpp
Outdated
auto ctor = dyn_cast<CallExpr>(entry.getInit()); | ||
if (!ctor) continue; | ||
auto argLabels = ctor->getArgumentLabels(); | ||
if (!argLabels.front().is(StringRef("rawValue"))) continue; |
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.
Nitpick: There's an implicit conversion from string literals to StringRef, so you can drop 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.
Also I think this should use Id_rawValue
from ASTContext, rather than a hardcoded literal.
lib/Sema/TypeCheckDecl.cpp
Outdated
auto ctor = dyn_cast<CallExpr>(entry.getInit()); | ||
if (!ctor) continue; | ||
auto argLabels = ctor->getArgumentLabels(); | ||
if (!argLabels.front().is(StringRef("rawValue"))) continue; |
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's worth double-checking that there's exactly one label too! In particular, if there are zero labels this will crash.
lib/Sema/TypeCheckDecl.cpp
Outdated
auto PBD = VD->getParentPatternBinding(); | ||
if (!PBD) | ||
return; | ||
for (auto entry : PBD->getPatternList()) { |
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.
Since one PatternBindingDecl can bind multiple variables:
let (x, y) = foo(), z = bar()
…you should probably be a little more careful to only look at the one you want, so you don't end up emitting duplicate diagnostics. In this case limiting it to when entry.getPattern()->getSingleVar() == VD
is probably good enough! That'll ignore the more complex cases, which is what we want for a diagnostic like this.
lib/Sema/TypeCheckDecl.cpp
Outdated
@@ -2383,6 +2385,42 @@ class DeclChecker : public DeclVisitor<DeclChecker> { | |||
if (VD->getAttrs().hasAttribute<DynamicReplacementAttr>()) | |||
TC.checkDynamicReplacementAttribute(VD); | |||
} | |||
|
|||
void checkForEmptyOptionSet(VarDecl *VD) { |
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 this is an okay place. Another option is to make a static
helper function above the definition of DeclChecker. That seems to be where most of the other check*
functions are that are only used in this file.
lib/Sema/TypeCheckDecl.cpp
Outdated
auto val = intArg->getValue(); | ||
if (val == 0) { | ||
auto loc = VD->getLoc(); | ||
TC.diagnose(loc, diag::option_set_zero_constant, type); |
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 about adding a note with a fix-it as well? "use [] to silence this warning"
lib/Sema/TypeCheckDecl.cpp
Outdated
return; | ||
for (auto entry : PBD->getPatternList()) { | ||
auto ctor = dyn_cast<CallExpr>(entry.getInit()); | ||
if (!ctor) continue; |
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's also worth checking that the thing being called is a ConstructorDecl. ctor->getCalledValue()
will give you that.
lib/Sema/TypeCheckDecl.cpp
Outdated
@@ -2383,6 +2385,42 @@ class DeclChecker : public DeclVisitor<DeclChecker> { | |||
if (VD->getAttrs().hasAttribute<DynamicReplacementAttr>()) | |||
TC.checkDynamicReplacementAttribute(VD); | |||
} | |||
|
|||
void checkForEmptyOptionSet(VarDecl *VD) { |
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 might suggest making VD
const
just to make it clear that it's not going to modify the AST to fix any issues.
test/Sema/option-set-empty.swift
Outdated
@@ -0,0 +1,4 @@ | |||
struct MyOptions: OptionSet { | |||
var rawValue: Int | |||
static let none = MyOptions(rawValue: 0) // expected-warning {{'static let' constant inside 'MyOptions' that conforms to OptionSet produces an empty option set}} |
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.
Can you verify if the fix-it is correctly applied? You need to add it at the end of this diagnostic. The format is:
{{column_start-column_end=<text>}}
Example:
// expected-warning {{warning_text}}{{30-34=([])}}
Also, you need to update this diagnostic as you've changed the text!
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'd just noticed 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.
The fix-it would be a part of the note though right?
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.
Also, is there any way to run a specific unit test instead of the whole suite? I'm currently using ninja check-swift
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.
You can invoke lit
directly, it lives inside the LLVM checkout
$LLVM_DIR/utils/lit/lit.py -sv $BUILD_DIR/swift-macosx-x84_64/test-macosx-x86_64 --filter=option-set-empty
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.
Yeah, that was just an example. In your case, you need to do:
// expected-warning {{text}} expected-note {{text}}{{30-34=([])}}
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.
@harlanhaskins It's saying "Test has no run line!". Sorry if I'm missing something obvious.
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.
Nevermind. Figured it out.
lib/Sema/TypeCheckDecl.cpp
Outdated
if (val != 0) continue; | ||
|
||
auto loc = VD->getLoc(); | ||
tc.diagnose(loc, diag::option_set_zero_constant, DescriptiveDeclKind::StaticProperty, VD->getName()); |
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's better to pass VD->getDescriptiveKind()
rather than passing a kind directly
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.
Actually, I just realised that we want to emit this for static properties only, so you don't even need the DescriptiveDeclKind
- the diagnostic can directly mention it. It could simply be static property %0 produces an empty option set
. Although if you want to handle non-static properties as well then yeah you'd need the kind.
lib/Sema/TypeCheckDecl.cpp
Outdated
if (!intArg) continue; | ||
|
||
auto val = intArg->getValue(); | ||
if (val != 0) continue; |
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.
Some of these can be inlined as they're not being used later.
if (val != 0) continue; | |
if (intArg->getValue() != 0) continue; |
Also, seems like you now have a conflict! |
@@ -4623,6 +4623,12 @@ NOTE(previous_function_builder_here, none, | |||
ERROR(function_builder_arguments, none, | |||
"function builder attributes cannot have arguments", ()) | |||
|
|||
WARNING(option_set_zero_constant,none, | |||
"static property %0 produces an empty option set", |
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: Can you align the start with the text above?
"static property %0 produces an empty option set", | ||
(Identifier)) | ||
NOTE(option_set_empty_set_init,none, | ||
"use [] to silence this warning", ()) |
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: Same here 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.
Looking good!
lib/Sema/TypeCheckDecl.cpp
Outdated
auto DC = VD->getDeclContext(); | ||
|
||
auto *optionSetProto = tc.Context.getProtocol(KnownProtocolKind::OptionSet); | ||
auto protocolConformance = tc.containsProtocol(DC->getSelfTypeInContext(), optionSetProto, DC, /*Flags*/None); |
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.
Nitpick: We have an 80-column limit; please wrap and align to the first open paren.
Also nitpick: I'd probably just use bool
here, but if you want to use auto
please pick a name that sounds like a true/false value, like "conformsToOptionSet".
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.
Will do. It is actually returning an Optional of swift::ProtocolConformanceRef. Should I check if the optional is empty to produce a bool instead?
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.
Would it be ok to use a double negation to convert it into a bool?
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, right. I don't think we use the double negation thing too much in this codebase; an explicit call to Optional::hasValue would be most clear, but also longer.
Now that I know what the type is, leaving it the way it is is also okay. It does, in fact, represent a possibly-absent conformance.
lib/Sema/TypeCheckDecl.cpp
Outdated
if (!protocolConformance) | ||
return; | ||
|
||
if (!VD->getType()->isEqual(DC->getSelfTypeInContext())) |
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.
This is a cheaper check than the conformance one, so maybe it should go first. (Not like you should have known this ahead of time.)
|
||
struct MyOptions: OptionSet { | ||
var rawValue: Int | ||
static let none = MyOptions(rawValue: 0) // expected-warning {{static property 'none' produces an empty option set}} expected-note {{use [] to silence this warning}}{{32-45=([])}} |
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.
Can you add some test cases that shouldn't cause the error? I can think of a few:
- Other types
- Other constructors of MyOptions
- Values other than 0
var
s
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.
Non-static let
too, maybe! And anything else you can think of.
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.
Still would like you to add this last test too. :-)
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.
Sorry! I did, but then rolled it back to try to figure out where it was crashing. I think I figured it out now though. Just need to confirm it.
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 added the tests back but I had to make the non-static let of another type or else I'd get a build error because a struct cannot contain itself. Should I add that case anyways and expect the error?
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, right. And if it's for another type it's not a new test. I guess it's still good to add the test case and expect the one error to make sure the second one doesn't pop up.
…thub.com/Vercantez/swift into warn-on-empty-optionset-static-constants
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.
This is super close, thanks again @Vercantez!
lib/Sema/TypeCheckDecl.cpp
Outdated
DC, | ||
/*Flags*/None); | ||
|
||
if (!protocolConformance) |
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.
Small comment here would be nice
if (!protocolConformance) | |
// Make sure this type conforms to OptionSet | |
if (!protocolConformance) |
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.
For that matter, it might make sense to name the variable optionSetConformance
—it's more descriptive.
lib/Sema/TypeCheckDecl.cpp
Outdated
if (!PBD) | ||
return; | ||
|
||
for (auto entry : PBD->getPatternList()) { |
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.
You should be able to get the entry with PBD->getPatternEntryForVarDecl(VD)
instead of searching through the list yourself.
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.
Oops, I forgot about this API. This one's my bad. ><
lib/Sema/TypeCheckDecl.cpp
Outdated
for (auto entry : PBD->getPatternList()) { | ||
if (entry.getPattern()->getSingleVar() != VD) continue; | ||
|
||
auto ctor = dyn_cast<CallExpr>(entry.getInit()); |
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.
A comment specifying the pattern you're looking for would help here.
Let's see how we're doing. @swift-ci Please smoke test |
Oops. I'll try to fix the failures. |
I couldn't figure out why the smoke tests were failing. I'm trying to install Xcode 11 beta 6 so I can have a similar environment to the CI server but I can't seem to find a download. Should the release version of Xcode 11 be ok to run the test suite locally? |
It should be fine, yeah. I'm running with one of the Xcode 11 GMs myself (forget which one), which is very close to the release. |
@swift-ci please smoke test |
Seems like you need to rebase and update your implementation as there have been some API changes. |
lib/Sema/TypeCheckDecl.cpp
Outdated
// Make sure property is being set with a constructor | ||
auto ctor = dyn_cast<CallExpr>(entry.getInit()); | ||
auto ctor = dyn_cast<CallExpr>(init); |
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.
Hm, why the indirection through the index?
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.
@CodaFi removed the direct accessors for requestification
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.
Gotcha. Not that it's important, but you could also compact this a little by using dyn_cast_or_null
.
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.
Oh, that's useful! Didn't know about that. Done!
@swift-ci please smoke test |
I'm having trouble interpreting the error on the Mac server. Can somebody please give me a pointer? |
I think these LLDB tests were failing yesterday independently of your change. Let's try again. @swift-ci Please test macOS |
Build failed |
@harlanhaskins Any remaining concerns? |
…or not. @shahmishal, do we need a workspace clean?
|
No remaining concerns from me, this looks good to go. Thanks @Vercantez! |
|
@swift-ci Please test macOS |
Build failed |
Oops... we got rid of the Optional in #27949 so unfortunately you'll have to rebase again and adjust your usage. You'll have to do something like: bool conformsToOptionSet = !tc.containsProtocol(DC->getSelfTypeInContext(),
optionSetProto, DC, /*Flags*/None).isInvalid(); (might be worth breaking this check into two lines) |
It's bool-convertible now, I think, so you can drop the double negative. |
Yep (just checked) 👍 |
Done! |
@swift-ci Please test |
Build failed |
Build failed |
Harlan acknowledged no further changes.
Thank you, @Vercantez! |
This adds a warning when declaring a static 'none' property for an OptionSet-conforming type. e.g.:
struct MyOptions: OptionSet {
var rawValue: Int
static let none = MyOptions(rawValue: 0) // Warning: 'static let' constant inside MyOptions that conforms to OptionSet produces an empty option set
}
Resolves SR-10118.
This is my first PR for the compiler and it feels like I'm going about adding this warning the wrong way. There's a huge pyramid of ifs and for loops. At the bare minimum I think I should refactor this out into a function. Although I feel like there's a much simpler way to do this. Currently 5 tests are failing. Any pointers would be greatly appreciated. Thank you!