Skip to content

Check that signatures of @usableFromInline declarations only reference @usableFromInline or public types #16586

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

Conversation

slavapestov
Copy link
Contributor

@slavapestov slavapestov commented May 14, 2018

We were performing these checks for references from the body of inlinable functions as part of availability checking, but we also have to do similar checks in access level checking for types that are referenced from the signature of the function itself.

Also, allow type aliases to be @usableFromInline, and ensure that only @usableFromInline type aliases are referenced from inlinable bodies.

This plugs some holes in the model that could cause linker errors on invalid code. Fixes rdar://problem/32722385, rdar://problem/39532028,

@slavapestov slavapestov force-pushed the usable-from-inline-type-alias branch 4 times, most recently from ac2d25b to a0c6ca0 Compare May 18, 2018 08:13
@slavapestov slavapestov changed the title [WIP] Check that signatures of @usableFromInline declarations only reference @usableFromInline or public types Check that signatures of @usableFromInline declarations only reference @usableFromInline or public types May 18, 2018
@slavapestov
Copy link
Contributor Author

@swift-ci Please test

@swift-ci
Copy link
Contributor

Build failed
Swift Test OS X Platform
Git Sha - a0c6ca0393c053e4a6061733da9db511141ad1fb

@swift-ci
Copy link
Contributor

Build failed
Swift Test Linux Platform
Git Sha - a0c6ca0393c053e4a6061733da9db511141ad1fb

@slavapestov slavapestov force-pushed the usable-from-inline-type-alias branch from a0c6ca0 to 6f9dc2c Compare May 18, 2018 09:04
@slavapestov
Copy link
Contributor Author

@swift-ci Please test

1 similar comment
@slavapestov
Copy link
Contributor Author

@swift-ci Please test

@slavapestov slavapestov requested a review from jrose-apple May 18, 2018 09:23
@slavapestov
Copy link
Contributor Author

@swift-ci Please test source compatibility

1 similar comment
@slavapestov
Copy link
Contributor Author

@swift-ci Please test source compatibility

@swift-ci
Copy link
Contributor

Build failed
Swift Test Linux Platform
Git Sha - 6f9dc2c3f278f2e434fa74ca4aeb7d3b6242b4ae

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.

Whew, that was more than I thought it would be! I do have some concerns about this implementation, though.

@@ -1317,20 +1328,25 @@ ERROR(unsupported_nested_protocol,none,
// Type aliases
ERROR(type_alias_underlying_type_access,none,
"type alias %select{must be declared %select{"
"%select{private|fileprivate|internal|%error|%error}2|private or fileprivate}3"
"%select{private|fileprivate|internal|%error|%error}1|private or fileprivate}3"
Copy link
Contributor

Choose a reason for hiding this comment

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

(here and below) This was correct the way it was before. Argument 1 is the current access; argument 2 is the required access.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Are you sure? When isExplicit is true, %1 is the decl's getFormalAccess(), when isExplicit is false, %1 is the AccessScope's requiredAccessForDiagnostics(). On the other hand, %2 is the AccessScope's accessLevelForDiagnostics().

I noticed some inconsistency here and cleaned it up. Some diagnostics were using %2 here for the isExplicit == false case, but this appeared to generate a wrong diagnostic because of the distinction between requiredAccessForDiagnostics() and accessLevelForDiagnostics() with private vs fileprivate. This change and tests for it was in a separate patch so it should be easier to review that way.

Copy link
Contributor

Choose a reason for hiding this comment

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

Note the difference in messages: "must be declared X" vs. "cannot be declared Y". There might be mistakes at some of the use sites, but they're definitely not supposed to be the same value.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

"When isExplicit is true, %1 is the decl's getFormalAccess(), when isExplicit is false, %1 is the AccessScope's requiredAccessForDiagnostics()" -- so they're different values :)

So using %1 vs %2 in the !isExplicit case is almost equivalent, except for private vs fileprivate.

Copy link
Contributor

Choose a reason for hiding this comment

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

I see: you changed the call site too. Please don't do this; it muddles the notion of what each parameter is used for. At that point I'd rather just use different diagnostics.

TypeChecker::TypeAccessScopeCacheMap &Cache;

protected:
ASTContext &Context;
Optional<AccessScope> Scope = AccessScope::getPublic();

AccessScopeChecker(const DeclContext *useDC,
bool treatUsableFromInlineAsPublic,
Copy link
Contributor

Choose a reason for hiding this comment

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

You're definitely starting to convince me that UsableFromInline is really a new access level.

AccessScope accessScope,
AccessLevel contextAccess) {
if (!params)
return;

if (checkUsableFromInline) {
if (auto *VD = dyn_cast<ValueDecl>(owner)) {
if (VD->getFormalAccess() != AccessLevel::Internal)
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 suspicious condition—this could be above internal or it could be below internal.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

checkUsableFromInline means only check @usableFromInline and nothing else. So we call this function once with checkUsableFromInline=false if the decl is not @usableFromInline; if it is, we call it twice, first with false then with true.

!isa<ModuleDecl>(accessScope.getDeclContext()) &&
TC.getLangOpts().isSwiftVersion3()) {
downgradeToWarning = DowngradeToWarning::Yes;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

What happened to this code?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This was a separate patch -- it was duplicated twice so I moved it.

Copy link
Contributor

Choose a reason for hiding this comment

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

…and now it's applied to more places. Maybe it doesn't matter, though.

}

auto minAccess = minAccessScope.accessLevelForDiagnostics();

bool isExplicit =
owner->getAttrs().hasAttribute<AccessControlAttr>() ||
owner->getDeclContext()->getAsProtocolOrProtocolExtensionContext();
isa<ProtocolDecl>(owner->getDeclContext());
Copy link
Contributor

Choose a reason for hiding this comment

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

Oops, good catch! Is there a test for this?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@@ -2175,14 +2223,16 @@ static void checkAccessControl(TypeChecker &TC, const Decl *D) {

case DeclKind::Struct: {
auto SD = cast<StructDecl>(D);
checkGenericParamAccess(TC, SD->getGenericParams(), SD);
checkGenericParamAccess(TC, SD->getGenericParams(), SD,
/*checkUsableFromInline=*/false);
Copy link
Contributor

Choose a reason for hiding this comment

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

(here and elsewhere) Why is this false and not inherited? Can you leave a comment to that effect?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's correct. checkAccessControl() only checks access control rules. The separate checkUsableFromInline() performs the same visitation of all TypeLocs, calling checkTypeAccess() with checkUsableFromInline=true.

if (anyVar->getFormalAccess() != AccessLevel::Internal)
return;
if (!anyVar->isUsableFromInline())
return;
Copy link
Contributor

Choose a reason for hiding this comment

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

This "internal and usable from inline" check shows up a lot. Maybe worth a helper function, even though it's so simple?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah. I'm going to drop the assertion from isUsableFromInline() that the access level is internal, and just return false if its not.

};

std::for_each(assocType->getInherited().begin(),
assocType->getInherited().end(),
Copy link
Contributor

Choose a reason for hiding this comment

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

This needs to check requirements, not just the inheritance clause, right? (Also, llvm::for_each.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's a good point. As you correctly noted this is copy and pasted from checkAccessControl() so probably the existing code was wrong too.

Type rawType = ED->getRawType();
auto rawTypeLocIter = std::find_if(ED->getInherited().begin(),
ED->getInherited().end(),
[&](TypeLoc inherited) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Okay this is really starting to seem like too much code duplicated from the regular access checking.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah. Perhaps we can factor out a visitor that walks the TypeLocs in a Decl?

@@ -4508,6 +4934,7 @@ class DeclChecker : public DeclVisitor<DeclChecker> {

TC.checkDeclAttributes(SD);
checkAccessControl(TC, SD);
checkUsableFromInline(TC, SD);
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to do both? Can't checkAccessControl handle 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.

Yes, I could merge the two into one function. It would eliminate the duplicated logic for visting the TypeLocs, at the cost of making the whole function longer. However, we could then split it up into smaller functions for each Decl kind or something, maybe even turn it into a visitor. Sounds good?

@slavapestov
Copy link
Contributor Author

Source compat failure:

/Users/buildnode/jenkins/workspace-private/swift-PR-source-compat-suite/project_cache/Chatto/ChattoAdditions/Source/Chat Items/PhotoMessages/Views/PhotoMessageCollectionViewCellDefaultStyle.swift:92:36: error: type alias 'Class' is internal and cannot be referenced from a default argument value
        bubbleMasks: BubbleMasks = Class.createDefaultBubbleMasks(),
                                   ^
/Users/buildnode/jenkins/workspace-private/swift-PR-source-compat-suite/project_cache/Chatto/ChattoAdditions/Source/Chat Items/PhotoMessages/Views/PhotoMessageCollectionViewCellDefaultStyle.swift:28:15: note: type alias 'Class' is not public
    typealias Class = PhotoMessageCollectionViewCellDefaultStyle
              ^

Looks like I need to disable the new checks in -swift-version < 4.2.

@jrose-apple
Copy link
Contributor

Less than 5, please. We don't currently have any language changes that are gated on 4.2 besides SDK changes, right?

@slavapestov slavapestov force-pushed the usable-from-inline-type-alias branch 5 times, most recently from 14ed713 to a68baa4 Compare June 3, 2018 08:37
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.

Comments for everything except the last Sema change.

@@ -2419,7 +2410,7 @@ static void checkAccessControl(TypeChecker &TC, const Decl *D) {
if (!EED->hasAssociatedValues())
return;
for (auto &P : *EED->getParameterList()) {
checkTypeAccess(TC, P->getTypeLoc(), P,
checkTypeAccess(TC, P->getTypeLoc(), EED->getParentEnum(),
Copy link
Contributor

Choose a reason for hiding this comment

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

This one seems wrong; it should just be EED, right? (Because some day we might have enum cases less visible than the enum.)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Right. Fixed

@@ -1317,20 +1328,25 @@ ERROR(unsupported_nested_protocol,none,
// Type aliases
ERROR(type_alias_underlying_type_access,none,
"type alias %select{must be declared %select{"
"%select{private|fileprivate|internal|%error|%error}2|private or fileprivate}3"
"%select{private|fileprivate|internal|%error|%error}1|private or fileprivate}3"
Copy link
Contributor

Choose a reason for hiding this comment

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

I see: you changed the call site too. Please don't do this; it muddles the notion of what each parameter is used for. At that point I'd rather just use different diagnostics.

!isa<ModuleDecl>(accessScope.getDeclContext()) &&
TC.getLangOpts().isSwiftVersion3()) {
downgradeToWarning = DowngradeToWarning::Yes;
}
Copy link
Contributor

Choose a reason for hiding this comment

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

…and now it's applied to more places. Maybe it doesn't matter, though.

lib/AST/Decl.cpp Outdated
@@ -2082,7 +2082,8 @@ SourceLoc ValueDecl::getAttributeInsertionLoc(bool forModifier) const {
/// Returns true if \p VD needs to be treated as publicly-accessible
/// at the SIL, LLVM, and machine levels due to being @usableFromInline.
bool ValueDecl::isUsableFromInline() const {
assert(getFormalAccess() == AccessLevel::Internal);
if (getFormalAccess() != AccessLevel::Internal)
return false;
Copy link
Contributor

Choose a reason for hiding this comment

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

Unhappy with this. If you're going to make this an API that can be called everywhere, shouldn't it return true for open and public decls as well?

Make this method public and fix it to do the right thing for
associated types.
@slavapestov slavapestov force-pushed the usable-from-inline-type-alias branch from c241e38 to 384aad4 Compare June 26, 2018 06:10
Previously we made sure that inlinable function bodies only
reference @usableFromInline or public declarations, but we
also have to make sure that @usableFromInline declarations
only reference other declarations that are at least as
visible.

Otherwise, you could, for example, define a @usableFromInline
typealias which referenced an internal type, and then
reference the typealias from a @usableFromInline function body.
AccessControlCheckerBase
|- AccessControlChecker
|- UsableFromInlineChecker

This factors out the checkUsableFromInline parameter that
is passed around all over the place.
@slavapestov slavapestov force-pushed the usable-from-inline-type-alias branch from 384aad4 to 9b43f26 Compare June 26, 2018 08:34
@slavapestov
Copy link
Contributor Author

@swift-ci Please test

@slavapestov
Copy link
Contributor Author

@swift-ci Please test source compatibility

@slavapestov
Copy link
Contributor Author

This has been open for far too long. Since the latest iteration moves a lot of code to a new source file, I'm going to merge it now to avoid more merge conflicts. I'm not quite done extracting out the common visitor, but if you squint you will be able to guess where it's going to go.

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.

3 participants