Skip to content

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

Merged
merged 31 commits into from
Oct 31, 2019
Merged

warn on empty OptionSet static constants #27089

merged 31 commits into from
Oct 31, 2019

Conversation

Vercantez
Copy link
Contributor

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!

@jrose-apple
Copy link
Contributor

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?

}
}
}
}
Copy link
Contributor

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
  }
}

Copy link
Contributor

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 😄

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ah, thanks!

Copy link
Contributor Author

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?

Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Ok thanks!

Copy link
Contributor Author

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

@beccadax
Copy link
Contributor

beccadax commented Sep 9, 2019

@jrose-apple I don't think I did. This might be a little tougher than I anticipated—sorry, @Vercantez!

}
}
}
}
Copy link
Contributor

@beccadax beccadax Sep 9, 2019

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.

Copy link
Contributor Author

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

@jrose-apple jrose-apple self-requested a review September 12, 2019 22:02
@@ -2383,6 +2385,42 @@ class DeclChecker : public DeclVisitor<DeclChecker> {
if (VD->getAttrs().hasAttribute<DynamicReplacementAttr>())
TC.checkDynamicReplacementAttribute(VD);
}

void checkForEmptyOptionSet(VarDecl *VD) {
Copy link
Contributor Author

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

Copy link
Contributor

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.

Copy link
Contributor

@jrose-apple jrose-apple left a 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",
Copy link
Contributor

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.

Copy link
Collaborator

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.


void checkForEmptyOptionSet(VarDecl *VD) {
if (!VD->isStatic())
return;
Copy link
Contributor

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.

if (!VD->isStatic())
return;
auto DC = VD->getDeclContext();
auto protocols = DC->getLocalProtocols();
Copy link
Contributor

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);

auto ctor = dyn_cast<CallExpr>(entry.getInit());
if (!ctor) continue;
auto argLabels = ctor->getArgumentLabels();
if (!argLabels.front().is(StringRef("rawValue"))) continue;
Copy link
Contributor

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.

Copy link
Collaborator

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.

auto ctor = dyn_cast<CallExpr>(entry.getInit());
if (!ctor) continue;
auto argLabels = ctor->getArgumentLabels();
if (!argLabels.front().is(StringRef("rawValue"))) continue;
Copy link
Contributor

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.

auto PBD = VD->getParentPatternBinding();
if (!PBD)
return;
for (auto entry : PBD->getPatternList()) {
Copy link
Contributor

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.

@@ -2383,6 +2385,42 @@ class DeclChecker : public DeclVisitor<DeclChecker> {
if (VD->getAttrs().hasAttribute<DynamicReplacementAttr>())
TC.checkDynamicReplacementAttribute(VD);
}

void checkForEmptyOptionSet(VarDecl *VD) {
Copy link
Contributor

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.

auto val = intArg->getValue();
if (val == 0) {
auto loc = VD->getLoc();
TC.diagnose(loc, diag::option_set_zero_constant, type);
Copy link
Contributor

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"

return;
for (auto entry : PBD->getPatternList()) {
auto ctor = dyn_cast<CallExpr>(entry.getInit());
if (!ctor) continue;
Copy link
Contributor

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.

@@ -2383,6 +2385,42 @@ class DeclChecker : public DeclVisitor<DeclChecker> {
if (VD->getAttrs().hasAttribute<DynamicReplacementAttr>())
TC.checkDynamicReplacementAttribute(VD);
}

void checkForEmptyOptionSet(VarDecl *VD) {
Copy link
Contributor

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.

@@ -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}}
Copy link
Collaborator

@theblixguy theblixguy Sep 18, 2019

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!

Copy link
Contributor Author

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.

Copy link
Contributor Author

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?

Copy link
Contributor Author

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

Copy link
Contributor

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

Copy link
Collaborator

@theblixguy theblixguy Sep 18, 2019

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=([])}}

Copy link
Contributor Author

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Nevermind. Figured it out.

if (val != 0) continue;

auto loc = VD->getLoc();
tc.diagnose(loc, diag::option_set_zero_constant, DescriptiveDeclKind::StaticProperty, VD->getName());
Copy link
Collaborator

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

Copy link
Collaborator

@theblixguy theblixguy Sep 18, 2019

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.

if (!intArg) continue;

auto val = intArg->getValue();
if (val != 0) continue;
Copy link
Collaborator

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.

Suggested change
if (val != 0) continue;
if (intArg->getValue() != 0) continue;

@theblixguy
Copy link
Collaborator

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",
Copy link
Collaborator

@theblixguy theblixguy Sep 18, 2019

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", ())
Copy link
Collaborator

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.

Copy link
Contributor

@jrose-apple jrose-apple left a comment

Choose a reason for hiding this comment

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

Looking good!

auto DC = VD->getDeclContext();

auto *optionSetProto = tc.Context.getProtocol(KnownProtocolKind::OptionSet);
auto protocolConformance = tc.containsProtocol(DC->getSelfTypeInContext(), optionSetProto, DC, /*Flags*/None);
Copy link
Contributor

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".

Copy link
Contributor Author

@Vercantez Vercantez Sep 18, 2019

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?

Copy link
Contributor Author

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?

Copy link
Contributor

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.

if (!protocolConformance)
return;

if (!VD->getType()->isEqual(DC->getSelfTypeInContext()))
Copy link
Contributor

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=([])}}
Copy link
Contributor

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
  • vars

Copy link
Contributor

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.

Copy link
Contributor

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. :-)

Copy link
Contributor Author

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.

Copy link
Contributor Author

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?

Copy link
Contributor

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.

Copy link
Contributor

@harlanhaskins harlanhaskins left a 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!

DC,
/*Flags*/None);

if (!protocolConformance)
Copy link
Contributor

@harlanhaskins harlanhaskins Sep 18, 2019

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

Suggested change
if (!protocolConformance)
// Make sure this type conforms to OptionSet
if (!protocolConformance)

Copy link
Contributor

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.

if (!PBD)
return;

for (auto entry : PBD->getPatternList()) {
Copy link
Contributor

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.

Copy link
Contributor

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. ><

for (auto entry : PBD->getPatternList()) {
if (entry.getPattern()->getSingleVar() != VD) continue;

auto ctor = dyn_cast<CallExpr>(entry.getInit());
Copy link
Contributor

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.

@jrose-apple
Copy link
Contributor

Let's see how we're doing.

@swift-ci Please smoke test

@Vercantez
Copy link
Contributor Author

Oops. I'll try to fix the failures.

@Vercantez
Copy link
Contributor Author

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?

@jrose-apple
Copy link
Contributor

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.

@theblixguy
Copy link
Collaborator

@swift-ci please smoke test

@theblixguy
Copy link
Collaborator

Seems like you need to rebase and update your implementation as there have been some API changes.

// Make sure property is being set with a constructor
auto ctor = dyn_cast<CallExpr>(entry.getInit());
auto ctor = dyn_cast<CallExpr>(init);
Copy link
Contributor

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?

Copy link
Contributor

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

Copy link
Contributor

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.

Copy link
Contributor Author

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!

@jrose-apple
Copy link
Contributor

@swift-ci please smoke test

@Vercantez
Copy link
Contributor Author

I'm having trouble interpreting the error on the Mac server. Can somebody please give me a pointer?

@jrose-apple
Copy link
Contributor

I think these LLDB tests were failing yesterday independently of your change. Let's try again.

@swift-ci Please test macOS

@swift-ci
Copy link
Contributor

Build failed
Swift Test OS X Platform
Git Sha - 06c0571

@jrose-apple
Copy link
Contributor

@harlanhaskins Any remaining concerns?

@jrose-apple
Copy link
Contributor

…or not. @shahmishal, do we need a workspace clean?

09:17:07 ======UPDATE FAILURES======
09:17:07 /Users/buildnode/jenkins/workspace/swift-PR-osx/branch-master/llvm-project failed (ret=1): ['git', 'checkout', u'swift/master']
09:17:07 error: Your local changes to the following files would be overwritten by checkout:
09:17:07 clang/test/Driver/flang/flang.F90
09:17:07 Please commit your changes or stash them before you switch branches.
09:17:07 Aborting

@harlanhaskins
Copy link
Contributor

No remaining concerns from me, this looks good to go. Thanks @Vercantez!

@shahmishal
Copy link
Member

…or not. @shahmishal, do we need a workspace clean?

09:17:07 ======UPDATE FAILURES======
09:17:07 /Users/buildnode/jenkins/workspace/swift-PR-osx/branch-master/llvm-project failed (ret=1): ['git', 'checkout', u'swift/master']
09:17:07 error: Your local changes to the following files would be overwritten by checkout:
09:17:07 clang/test/Driver/flang/flang.F90
09:17:07 Please commit your changes or stash them before you switch branches.
09:17:07 Aborting

swiftlang/llvm-project#63

@theblixguy
Copy link
Collaborator

@swift-ci Please test macOS

@swift-ci
Copy link
Contributor

Build failed
Swift Test OS X Platform
Git Sha - cba8684

@theblixguy
Copy link
Collaborator

theblixguy commented Oct 30, 2019

/Users/buildnode/jenkins/workspace/swift-PR-osx/branch-master/swift/lib/Sema/TypeCheckDecl.cpp:506:66: error: no member named 'hasValue' in 'swift::ProtocolConformanceRef'
20:09:13 /Flags/None).hasValue();
20:09:13 ~~~~~~~~~~~~~~ ^

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)

@jrose-apple
Copy link
Contributor

It's bool-convertible now, I think, so you can drop the double negative.

@theblixguy
Copy link
Collaborator

Yep (just checked) 👍

@Vercantez
Copy link
Contributor Author

Done!

@jrose-apple
Copy link
Contributor

@swift-ci Please test

@swift-ci
Copy link
Contributor

Build failed
Swift Test Linux Platform
Git Sha - 06c0571

@swift-ci
Copy link
Contributor

Build failed
Swift Test OS X Platform
Git Sha - cba8684

@jrose-apple jrose-apple dismissed harlanhaskins’s stale review October 31, 2019 20:32

Harlan acknowledged no further changes.

@jrose-apple jrose-apple merged commit 43d2904 into swiftlang:master Oct 31, 2019
@jrose-apple
Copy link
Contributor

Thank you, @Vercantez!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants