Skip to content
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

Improve diagnostics to explain why an identifier isn't a valid #893

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions Sources/SwiftDriver/Driver/Driver.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2137,10 +2137,19 @@ extension Driver {
moduleName = "__bad__"
}
}

if !moduleName.sd_isSwiftIdentifier {
fallbackOrDiagnose(.error_bad_module_name(moduleName: moduleName, explicitModuleName: parsedOptions.contains(.moduleName)))
} else if moduleName == "Swift" && !parsedOptions.contains(.parseStdlib) {

do {
try moduleName.sd_validateSwiftIdentifier()
}
catch let validationFailure as InvalidSwiftIdentifierError {
fallbackOrDiagnose(.error_bad_module_name(
moduleName: moduleName,
explicitModuleName: parsedOptions.contains(.moduleName),
validationFailureReason: validationFailure.reason
))
}

if moduleName == "Swift" && !parsedOptions.contains(.parseStdlib) {
fallbackOrDiagnose(.error_stdlib_module_name(moduleName: moduleName, explicitModuleName: parsedOptions.contains(.moduleName)))
}

Expand Down
7 changes: 4 additions & 3 deletions Sources/SwiftDriver/Utilities/Diagnostics.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,16 +63,17 @@ extension Diagnostic.Message {

static func error_bad_module_name(
moduleName: String,
explicitModuleName: Bool
explicitModuleName: Bool,
validationFailureReason reason: String
) -> Diagnostic.Message {
let suffix: String
if explicitModuleName {
suffix = ""
} else {
suffix = "; use -module-name flag to specify an alternate name"
suffix = " Use -module-name flag to specify an alternate name"
}

return .error("module name \"\(moduleName)\" is not a valid identifier\(suffix)")
return .error("module name \"\(moduleName)\" is not a valid Swift identifier, because \(reason).\(suffix)")
Copy link
Contributor

Choose a reason for hiding this comment

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

The existing style of having separate clauses is intentional as it matches clang.

}

static func error_stdlib_module_name(
Expand Down
23 changes: 15 additions & 8 deletions Sources/SwiftDriver/Utilities/StringAdditions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,24 @@
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

public struct InvalidSwiftIdentifierError: Error {
let reason: String
}

extension String {
/// Whether this string is a Swift identifier.
public var sd_isSwiftIdentifier: Bool {
guard let start = unicodeScalars.first else {
return false
}

let continuation = unicodeScalars.dropFirst()

return start.isValidSwiftIdentifierStart &&
continuation.allSatisfy { $0.isValidSwiftIdentifierContinuation }
do {
try sd_validateSwiftIdentifier()
return true
} catch {
return false
}
}

public func sd_validateSwiftIdentifier() throws {
// TODO: implement me
}
}

Expand Down
81 changes: 62 additions & 19 deletions Tests/SwiftDriverTests/StringAdditionsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import XCTest
import SwiftDriver
import TSCTestSupport

final class StringAdditionsTests: XCTestCase {

Expand All @@ -20,25 +21,52 @@ final class StringAdditionsTests: XCTestCase {
XCTAssertTrue("_startsWithUnderscore".sd_isSwiftIdentifier)
XCTAssertTrue("contains_Number5".sd_isSwiftIdentifier)
XCTAssertTrue("_1".sd_isSwiftIdentifier)
XCTAssertFalse("5startsWithNumber".sd_isSwiftIdentifier)
XCTAssertFalse("contains space".sd_isSwiftIdentifier)
XCTAssertFalse("contains\nnewline".sd_isSwiftIdentifier)
XCTAssertFalse("contains\ttab".sd_isSwiftIdentifier)
XCTAssertFalse("contains_punctuation,.!?#".sd_isSwiftIdentifier)
XCTAssertTrue("contains$dollar".sd_isSwiftIdentifier)
XCTAssertFalse("$startsWithDollar".sd_isSwiftIdentifier)
XCTAssertFalse("operators+-=*/^".sd_isSwiftIdentifier)
XCTAssertFalse("braces{}".sd_isSwiftIdentifier)
XCTAssertFalse("angleBrackets<>".sd_isSwiftIdentifier)
XCTAssertFalse("parens()".sd_isSwiftIdentifier)
XCTAssertFalse("squareBrackets[]".sd_isSwiftIdentifier)

XCTAssertFalse("<#some name#>".sd_isSwiftIdentifier,
"Placeholders are not valid identifiers")

XCTAssertFalse("".sd_isSwiftIdentifier)
XCTAssertFalse("`$`".sd_isSwiftIdentifier)
XCTAssertFalse("backtick`".sd_isSwiftIdentifier)

assertInvalidIdentifier("5startsWithNumber", expectedMessage:
"Swift identifiers cannot start with a number")

assertInvalidIdentifier("contains space", expectedMessage:
"Swift identifiers cannot contains spaces")

assertInvalidIdentifier("contains\nnewline", expectedMessage:
"Swift identifiers cannot contain new line characters")

assertInvalidIdentifier("contains\ttab", expectedMessage:
"Swift identifiers cannot contain tab characters")

assertInvalidIdentifier("contains_punctuation,.!?#", expectedMessage:
##"Swift identifiers cannot contain punctuation (like ",", ".", "!", "?", or "#")"##)

assertInvalidIdentifier("$startsWithDollar", expectedMessage:
#"Swift identifiers cannot start with a dollar sign ("$")"#)

assertInvalidIdentifier("operators+-=*/^", expectedMessage:
#"Swift identifiers cannot contain operators (like "+", "-", "=", "*", "/", or "^")"#)

assertInvalidIdentifier("braces{}", expectedMessage:
#"Swift identifiers cannot contain curly braces ("{" or "}")"#)

assertInvalidIdentifier("angleBrackets<>", expectedMessage:
#"Swift identifiers cannot contain parenthesis ("<" or ">")"#)

assertInvalidIdentifier("parens()", expectedMessage:
#"Swift identifiers cannot contain parenthesis ("(" or ")")"#)

assertInvalidIdentifier("squareBrackets[]", expectedMessage:
#"Swift identifiers cannot contain square brackets ("[" or "]")"#)

assertInvalidIdentifier("<#some name#>", expectedMessage:
"Swift identifiers cannot contain Xcode placeholders")

assertInvalidIdentifier("", expectedMessage:
"Swift identifiers cannot be empty")

assertInvalidIdentifier("`$`", expectedMessage:
"Swift identifiers cannot be only a dollar sign") // TODO: what's the exact rule here?

assertInvalidIdentifier("backtick`", expectedMessage:
"Swift identifiers cannot contain mis-matched backticks") // TODO: what's the exact rule here?
}

func testSwiftKeywordsAsIdentifiers() {
Expand All @@ -58,7 +86,7 @@ final class StringAdditionsTests: XCTestCase {

func testUnicodeCharacters() {
XCTAssertTrue("👨".sd_isSwiftIdentifier)
XCTAssertFalse("❤️".sd_isSwiftIdentifier)
XCTAssertFalse("❤️".sd_isSwiftIdentifier) // Multiple codepoints
XCTAssertTrue("💑".sd_isSwiftIdentifier) // Single codepoint
XCTAssertFalse("🙍🏻‍♂️".sd_isSwiftIdentifier) // Multiple codepoints
XCTAssertTrue("你好".sd_isSwiftIdentifier)
Expand All @@ -76,4 +104,19 @@ final class StringAdditionsTests: XCTestCase {
XCTAssertFalse("".sd_isSwiftIdentifier,
"Private-use characters aren't valid Swift identifiers")
}

private func assertInvalidIdentifier(
_ invalidIdentifier: String,
expectedMessage: String,
file: StaticString = #file,
line: UInt = #line
) {
XCTAssertThrows(try invalidIdentifier.sd_validateSwiftIdentifier()) { e in
guard let e = e as? InvalidSwiftIdentifierError else {
XCTFail()
return
}
XCTAssertEqual(e.message, expectedMesssage)
}
}
}