Skip to content

Commit a68347f

Browse files
committed
[Sema] Validate names of @objc declarations with raw identifiers.
If a decl is exported to Objective-C (explicitly or implicitly), it must be given an explicit name that is a valid Objective-C identifier.
1 parent 0844e27 commit a68347f

File tree

3 files changed

+134
-7
lines changed

3 files changed

+134
-7
lines changed

include/swift/AST/DiagnosticsSema.def

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6417,6 +6417,14 @@ ERROR(objc_enum_case_multi,none,
64176417
ERROR(objc_extension_not_class,none,
64186418
"'@objc' can only be applied to an extension of a class", ())
64196419

6420+
ERROR(objc_name_not_valid_identifier,none,
6421+
"'@objc' %0 name is not a valid Objective-C identifier",
6422+
(DescriptiveDeclKind))
6423+
ERROR(objc_cannot_infer_name_raw_identifier,none,
6424+
"cannot infer '@objc' %0 name because the Swift name is not a valid "
6425+
"Objective-C identifier; specify the name in '@objc' explicitly",
6426+
(DescriptiveDeclKind))
6427+
64206428
// If you change this, also change enum ObjCReason
64216429
#define OBJC_ATTR_SELECT "select{marked @_cdecl|marked dynamic|marked @objc|marked @objcMembers|marked @IBOutlet|marked @IBAction|marked @IBSegueAction|marked @NSManaged|a member of an @objc protocol|implicitly @objc|an @objc override|an implementation of an @objc requirement|marked @IBInspectable|marked @GKInspectable|in an @objc extension of a class (without @nonobjc)|in an @objc @implementation extension of a class (without final or @nonobjc)|marked @objc by an access note}"
64226430

lib/Sema/TypeCheckDeclObjC.cpp

Lines changed: 74 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1635,6 +1635,56 @@ static std::optional<ObjCReason> shouldMarkAsObjC(const ValueDecl *VD,
16351635
return currentlyObjC;
16361636
}
16371637

1638+
/// Emits a diagnostic for an invalid `@objc` name.
1639+
static void diagnoseInvalidObjCName(ValueDecl *VD, ObjCAttr *attr) {
1640+
if (attr && !attr->getNameLocs().empty()) {
1641+
// Locate the diagnostic on the name itself if it was explicitly provided.
1642+
VD->getDiags().diagnose(attr->getNameLocs().front(),
1643+
diag::objc_name_not_valid_identifier,
1644+
VD->getDescriptiveKind());
1645+
} else {
1646+
switch (VD->getDescriptiveKind()) {
1647+
case DescriptiveDeclKind::Getter:
1648+
case DescriptiveDeclKind::Setter:
1649+
// If the name of the getter or setter isn't provided explicitly
1650+
// and the inferred name is invalid, then the property name must
1651+
// also be invalid and we'll diagnose that. Don't emit redundant
1652+
// diagnostics for the getter/setter.
1653+
break;
1654+
default:
1655+
VD->diagnose(diag::objc_cannot_infer_name_raw_identifier,
1656+
VD->getDescriptiveKind());
1657+
}
1658+
}
1659+
}
1660+
1661+
// Returns true if all of the selector pieces are Obj-C compatible (i.e., no
1662+
// raw identifiers with non-identifier characters).
1663+
static bool isObjCSelectorValid(ObjCSelector selector) {
1664+
for (const auto &piece : selector.getSelectorPieces()) {
1665+
if (!piece.empty() && piece.mustAlwaysBeEscaped()) {
1666+
return false;
1667+
}
1668+
}
1669+
return true;
1670+
}
1671+
1672+
/// Checks whether the `@objc` name of a declaration is valid and emits a
1673+
/// diagnostic if not.
1674+
static void checkObjCNameValidity(ValueDecl *VD, ObjCAttr *attr) {
1675+
if (attr && attr->hasName()) {
1676+
// The name was explicitly provided by the attribute, so check that.
1677+
if (!isObjCSelectorValid(*attr->getName())) {
1678+
diagnoseInvalidObjCName(VD, attr);
1679+
}
1680+
} else {
1681+
// Check the original name of the declaration.
1682+
if (VD->getName().mustAlwaysBeEscaped()) {
1683+
diagnoseInvalidObjCName(VD, attr);
1684+
}
1685+
}
1686+
}
1687+
16381688
/// Determine whether the given type is a C integer type.
16391689
static bool isCIntegerType(Type type) {
16401690
auto nominal = type->getAnyNominal();
@@ -1677,7 +1727,8 @@ static bool isEnumObjC(EnumDecl *enumDecl) {
16771727
// FIXME: Use shouldMarkAsObjC once it loses it's TypeChecker argument.
16781728

16791729
// If there is no @objc attribute, it's not @objc.
1680-
if (!enumDecl->getAttrs().hasAttribute<ObjCAttr>())
1730+
auto attr = enumDecl->getAttrs().getAttribute<ObjCAttr>();
1731+
if (!attr)
16811732
return false;
16821733

16831734
Type rawType = enumDecl->getRawType();
@@ -1706,7 +1757,8 @@ static bool isEnumObjC(EnumDecl *enumDecl) {
17061757
if (enumDecl->getAllElements().empty()) {
17071758
enumDecl->diagnose(diag::empty_enum_raw_type);
17081759
}
1709-
1760+
1761+
checkObjCNameValidity(enumDecl, attr);
17101762
return true;
17111763
}
17121764

@@ -1727,14 +1779,18 @@ bool IsObjCRequest::evaluate(Evaluator &evaluator, ValueDecl *VD) const {
17271779
// Members of classes can be @objc.
17281780
isObjC = shouldMarkAsObjC(VD, isa<ConstructorDecl>(VD));
17291781
}
1730-
else if (isa<ClassDecl>(VD)) {
1782+
else if (auto classDecl = dyn_cast<ClassDecl>(VD)) {
17311783
// Classes can be @objc.
17321784

17331785

17341786

17351787
// Protocols and enums can also be @objc, but this is covered by the
17361788
// isObjC() check at the beginning.
17371789
isObjC = shouldMarkAsObjC(VD, /*allowImplicit=*/false);
1790+
if (isObjC) {
1791+
checkObjCNameValidity(classDecl,
1792+
classDecl->getAttrs().getAttribute<ObjCAttr>());
1793+
}
17381794
} else if (auto enumDecl = dyn_cast<EnumDecl>(VD)) {
17391795
// Enums can be @objc so long as they have a raw type that is representable
17401796
// as an arithmetic type in C.
@@ -1744,13 +1800,17 @@ bool IsObjCRequest::evaluate(Evaluator &evaluator, ValueDecl *VD) const {
17441800
} else if (auto enumElement = dyn_cast<EnumElementDecl>(VD)) {
17451801
// Enum elements can be @objc so long as the containing enum is @objc.
17461802
if (enumElement->getParentEnum()->isObjC()) {
1747-
if (auto attr = enumElement->getAttrs().getAttribute<ObjCAttr>())
1803+
auto attr = enumElement->getAttrs().getAttribute<ObjCAttr>();
1804+
checkObjCNameValidity(enumElement, attr);
1805+
if (attr)
17481806
isObjC = objCReasonForObjCAttr(attr);
17491807
else
17501808
isObjC = ObjCReason::ElementOfObjCEnum;
17511809
}
17521810
} else if (auto proto = dyn_cast<ProtocolDecl>(VD)) {
1753-
if (auto attr = proto->getAttrs().getAttribute<ObjCAttr>()) {
1811+
auto attr = proto->getAttrs().getAttribute<ObjCAttr>();
1812+
if (attr) {
1813+
checkObjCNameValidity(proto, attr);
17541814
isObjC = objCReasonForObjCAttr(attr);
17551815

17561816
// If the protocol is @objc, it may only refine other @objc protocols and
@@ -1915,10 +1975,17 @@ static ObjCSelector inferObjCName(ValueDecl *decl) {
19151975
}
19161976
}
19171977

1978+
auto validateObjCSelector = [&](ObjCSelector selector) {
1979+
if (!isObjCSelectorValid(selector)) {
1980+
diagnoseInvalidObjCName(decl, attr);
1981+
}
1982+
return selector;
1983+
};
1984+
19181985
// If the decl already has a name, do nothing; the protocol conformance
19191986
// checker will handle any mismatches.
19201987
if (attr && attr->hasName())
1921-
return *attr->getName();
1988+
return validateObjCSelector(*attr->getName());
19221989

19231990
// When no override determined the Objective-C name, look for
19241991
// requirements for which this declaration is a witness.
@@ -1965,7 +2032,7 @@ static ObjCSelector inferObjCName(ValueDecl *decl) {
19652032
return *requirementObjCName;
19662033
}
19672034

1968-
return *decl->getObjCRuntimeName(true);
2035+
return validateObjCSelector(*decl->getObjCRuntimeName(true));
19692036
}
19702037

19712038
/// Mark the given declaration as being Objective-C compatible (or

test/Sema/objc_raw_identifiers.swift

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// RUN: %empty-directory(%t)
2+
// RUN: %target-swift-frontend(mock-sdk: %clang-importer-sdk) -verify -emit-module -o %t %s -module-name RawIdentifier -disable-objc-attr-requires-foundation-module
3+
4+
// REQUIRES: objc_interop
5+
6+
@objc(`Foo Bar`) public class FooBar { // expected-error {{'@objc' class name is not a valid Objective-C identifier}}
7+
@objc func `function with spaces`() {} // expected-error {{cannot infer '@objc' instance method name because the Swift name is not a valid Objective-C identifier; specify the name in '@objc' explicitly}}
8+
9+
@objc(for) func forFunction1() {}
10+
@objc(`while`) func whileFunction() {}
11+
public func `not exported`() {}
12+
}
13+
14+
@objc @objcMembers public class `Class with Spaces` { // expected-error {{cannot infer '@objc' class name because the Swift name is not a valid Objective-C identifier; specify the name in '@objc' explicitly}}
15+
public var `this is exported`: Int = 0 // expected-error {{cannot infer '@objc' property name because the Swift name is not a valid Objective-C identifier; specify the name in '@objc' explicitly}}
16+
17+
public var `another var`: Int { // expected-error {{cannot infer '@objc' property name because the Swift name is not a valid Objective-C identifier; specify the name in '@objc' explicitly}}
18+
get { 0 }
19+
set {}
20+
}
21+
22+
public var `yet another var`: Int { // expected-error {{cannot infer '@objc' property name because the Swift name is not a valid Objective-C identifier; specify the name in '@objc' explicitly}}
23+
@objc(`yet another getter`) get { 0 } // expected-error {{'@objc' getter name is not a valid Objective-C identifier}}
24+
@objc(`yet another setter`:) set {} // expected-error {{'@objc' setter name is not a valid Objective-C identifier}}
25+
}
26+
27+
public func `also exported`() {} // expected-error {{cannot infer '@objc' instance method name because the Swift name is not a valid Objective-C identifier; specify the name in '@objc' explicitly}}
28+
29+
public init() {}
30+
31+
public init(`label with space`: Int) {} // expected-error {{cannot infer '@objc' initializer name because the Swift name is not a valid Objective-C identifier; specify the name in '@objc' explicitly}}
32+
33+
public init(goodLabel `internal name`: Int) {}
34+
}
35+
36+
@objc class NormalObjCClass {}
37+
38+
class `Inherits from Objective-C Class`: NormalObjCClass { // expected-error {{cannot infer '@objc' class name because the Swift name is not a valid Objective-C identifier; specify the name in '@objc' explicitly}}
39+
}
40+
41+
@objc protocol `Some Protocol` { // expected-error {{cannot infer '@objc' protocol name because the Swift name is not a valid Objective-C identifier; specify the name in '@objc' explicitly}}
42+
}
43+
44+
@objc enum `Some Enum`: Int { // expected-error {{cannot infer '@objc' enum name because the Swift name is not a valid Objective-C identifier; specify the name in '@objc' explicitly}}
45+
case `some case A` // expected-error {{cannot infer '@objc' enum case name because the Swift name is not a valid Objective-C identifier; specify the name in '@objc' explicitly}}
46+
@objc(someCaseB) case `some case B`
47+
@objc(`some case C`) case `some case C` // expected-error {{'@objc' enum case name is not a valid Objective-C identifier}}
48+
}
49+
50+
@objc enum EnumNameIsOK: Int {
51+
case `raw identifier` // expected-error {{cannot infer '@objc' enum case name because the Swift name is not a valid Objective-C identifier; specify the name in '@objc' explicitly}}
52+
}

0 commit comments

Comments
 (0)