diff --git a/Sources/SwiftDriver/Driver/Driver.swift b/Sources/SwiftDriver/Driver/Driver.swift index 704442d0d..2cab2a481 100644 --- a/Sources/SwiftDriver/Driver/Driver.swift +++ b/Sources/SwiftDriver/Driver/Driver.swift @@ -2973,8 +2973,10 @@ extension Driver { with moduleName: String, onError diagnosticsEngine: DiagnosticsEngine) -> [String: String]? { var moduleAliases: [String: String]? = nil - let validate = { (_ arg: String, allowModuleName: Bool) -> Bool in - if !arg.sd_isSwiftIdentifier { + // validatingModuleName should be true when validating the alias target (an actual module + // name), or false when validating the alias name (which can be a raw identifier). + let validate = { (_ arg: String, validatingModuleName: Bool) -> Bool in + if (validatingModuleName && !arg.sd_isSwiftIdentifier) || !arg.sd_isValidAsRawIdentifier { diagnosticsEngine.emit(.error_bad_module_name(moduleName: arg, explicitModuleName: true)) return false } @@ -2982,7 +2984,7 @@ extension Driver { diagnosticsEngine.emit(.error_stdlib_module_name(moduleName: arg, explicitModuleName: true)) return false } - if !allowModuleName, arg == moduleName { + if !validatingModuleName, arg == moduleName { diagnosticsEngine.emit(.error_bad_module_alias(arg, moduleName: moduleName)) return false } diff --git a/Sources/SwiftDriver/Utilities/StringAdditions.swift b/Sources/SwiftDriver/Utilities/StringAdditions.swift index 9feabd5b1..c84bd982a 100644 --- a/Sources/SwiftDriver/Utilities/StringAdditions.swift +++ b/Sources/SwiftDriver/Utilities/StringAdditions.swift @@ -21,6 +21,15 @@ extension String { return start.isValidSwiftIdentifierStart && continuation.allSatisfy { $0.isValidSwiftIdentifierContinuation } } + + /// Whether this string is a valid Swift raw identifier. + public var sd_isValidAsRawIdentifier: Bool { + guard !isEmpty else { + return false + } + return unicodeScalars.allSatisfy { $0.isValidRawIdentifierScalar } + && !unicodeScalars.allSatisfy { $0.isPermittedRawIdentifierWhitespace } + } } extension DefaultStringInterpolation { @@ -120,6 +129,43 @@ extension Unicode.Scalar { /// `true` if this character is an ASCII digit: [0-9] var isASCIIDigit: Bool { (0x30...0x39).contains(value) } + /// `true` if this scalar is valid in a Swift raw identifier. + var isValidRawIdentifierScalar: Bool { + // A raw identifier is terminated by a backtick, and the backslash is reserved for possible + // future escaping. + if self == "`" || self == "\\" { + return false + } + // Unprintable ASCII control characters are forbidden. + if (0x0000...0x001F).contains(value) || value == 0x007F { + return false; + } + return !isForbiddenRawIdentifierWhitespace + } + + var isForbiddenRawIdentifierWhitespace: Bool { + // This is the set of code points satisfying the `White_Space` property, excluding the set + // satisfying the `Pattern_White_Space` property, and excluding any other ASCII non-printables + // and Unicode separators. In other words, the only whitespace code points allowed in a raw + // identifier are U+0020, and U+200E/200F (LTR/RTL marks) (see + // `isPermittedRawIdentifierWhitespace` below). + return (0x0009...0x000D).contains(value) || + value == 0x0085 || + value == 0x00A0 || + value == 0x1680 || + (0x2000...0x200A).contains(value) || + (0x2028...0x2029).contains(value) || + value == 0x202F || + value == 0x205F || + value == 0x3000 + } + + var isPermittedRawIdentifierWhitespace: Bool { + return value == 0x0020 || + value == 0x200E || + value == 0x200F + } + /// `true` if this is a body character of a C identifier, /// which is [a-zA-Z0-9_]. func isCIdentifierBody(allowDollar: Bool = false) -> Bool { diff --git a/Tests/SwiftDriverTests/StringAdditionsTests.swift b/Tests/SwiftDriverTests/StringAdditionsTests.swift index 0d0377375..d37231431 100644 --- a/Tests/SwiftDriverTests/StringAdditionsTests.swift +++ b/Tests/SwiftDriverTests/StringAdditionsTests.swift @@ -76,4 +76,16 @@ final class StringAdditionsTests: XCTestCase { XCTAssertFalse("".sd_isSwiftIdentifier, "Private-use characters aren't valid Swift identifiers") } + + func testRawIdentifiers() { + XCTAssertTrue("plain".sd_isValidAsRawIdentifier) + XCTAssertTrue("has spaces".sd_isValidAsRawIdentifier) + XCTAssertTrue("$^has/other!characters@#".sd_isValidAsRawIdentifier) + + XCTAssertFalse("has`backtick".sd_isValidAsRawIdentifier) + XCTAssertFalse("has\\backslash".sd_isValidAsRawIdentifier) + XCTAssertFalse("has\u{0000}control\u{007F}characters".sd_isValidAsRawIdentifier) + XCTAssertFalse("has\u{00A0}forbidden\u{2028}whitespace".sd_isValidAsRawIdentifier) + XCTAssertFalse(" ".sd_isValidAsRawIdentifier) + } } diff --git a/Tests/SwiftDriverTests/SwiftDriverTests.swift b/Tests/SwiftDriverTests/SwiftDriverTests.swift index 1ae33d985..448029a15 100644 --- a/Tests/SwiftDriverTests/SwiftDriverTests.swift +++ b/Tests/SwiftDriverTests/SwiftDriverTests.swift @@ -3358,11 +3358,22 @@ final class SwiftDriverTests: XCTestCase { $1.expect(.error("module alias \"Foo\" should be different from the module name \"Foo\"")) } + // A module alias is allowed to be a valid raw identifier, not just a regular Swift identifier. + try assertNoDriverDiagnostics( + args: "swiftc", "foo.swift", "-module-name", "Foo", "-module-alias", "//car/far:par=Bar", "-emit-module", "-emit-module-path", "/tmp/dir/Foo.swiftmodule" + ) + // The alias target (an actual module name), however, may not be a raw identifier. try assertDriverDiagnostics( - args: ["swiftc", "foo.swift", "-module-name", "Foo", "-module-alias", "C-ar=Bar", "-emit-module", "-emit-module-path", "/tmp/dir/Foo.swiftmodule"] + args: ["swiftc", "foo.swift", "-module-name", "Foo", "-module-alias", "Bar=C-ar", "-emit-module", "-emit-module-path", "/tmp/dir/Foo.swiftmodule"] ) { $1.expect(.error("module name \"C-ar\" is not a valid identifier")) } + // We should still diagnose names that are not valid raw identifiers. + try assertDriverDiagnostics( + args: ["swiftc", "foo.swift", "-module-name", "Foo", "-module-alias", "C`ar=Bar", "-emit-module", "-emit-module-path", "/tmp/dir/Foo.swiftmodule"] + ) { + $1.expect(.error("module name \"C`ar\" is not a valid identifier")) + } try assertDriverDiagnostics( args: ["swiftc", "foo.swift", "-module-name", "Foo", "-module-alias", "Car=Bar", "-module-alias", "Train=Car", "-emit-module", "-emit-module-path", "/tmp/dir/Foo.swiftmodule"]