diff --git a/Sources/SwiftParser/Attributes.swift b/Sources/SwiftParser/Attributes.swift index cdd9765331f..2d02f58db4b 100644 --- a/Sources/SwiftParser/Attributes.swift +++ b/Sources/SwiftParser/Attributes.swift @@ -176,7 +176,14 @@ extension Parser { argumentMode: AttributeArgumentMode, parseArguments: (inout Parser) -> RawAttributeSyntax.Arguments ) -> RawAttributeListSyntax.Element { - let (unexpectedBeforeAtSign, atSign) = self.expect(.atSign) + var (unexpectedBeforeAtSign, atSign) = self.expect(.atSign) + if atSign.trailingTriviaByteLength > 0 || self.currentToken.leadingTriviaByteLength > 0 { + let diagnostic = TokenDiagnostic( + self.swiftVersion < .v6 ? .extraneousTrailingWhitespaceWarning : .extraneousTrailingWhitespaceError, + byteOffset: atSign.leadingTriviaByteLength + atSign.tokenText.count + ) + atSign = atSign.tokenView.withTokenDiagnostic(tokenDiagnostic: diagnostic, arena: self.arena) + } let attributeName = self.parseAttributeName() let shouldParseArgument: Bool switch argumentMode { @@ -190,7 +197,14 @@ extension Parser { shouldParseArgument = false } if shouldParseArgument { - let (unexpectedBeforeLeftParen, leftParen) = self.expect(.leftParen) + var (unexpectedBeforeLeftParen, leftParen) = self.expect(.leftParen) + if unexpectedBeforeLeftParen == nil && (attributeName.raw.trailingTriviaByteLength > 0 || leftParen.leadingTriviaByteLength > 0) { + let diagnostic = TokenDiagnostic( + self.swiftVersion < .v6 ? .extraneousLeadingWhitespaceWarning : .extraneousLeadingWhitespaceError, + byteOffset: 0 + ) + leftParen = leftParen.tokenView.withTokenDiagnostic(tokenDiagnostic: diagnostic, arena: self.arena) + } let argument = parseArguments(&self) let (unexpectedBeforeRightParen, rightParen) = self.expect(.rightParen) return .attribute( diff --git a/Sources/SwiftParser/CMakeLists.txt b/Sources/SwiftParser/CMakeLists.txt index 15df1bb8198..b27e167f87a 100644 --- a/Sources/SwiftParser/CMakeLists.txt +++ b/Sources/SwiftParser/CMakeLists.txt @@ -30,8 +30,9 @@ add_swift_syntax_library(SwiftParser Recovery.swift Specifiers.swift Statements.swift - StringLiterals.swift StringLiteralRepresentedLiteralValue.swift + StringLiterals.swift + SwiftVersion.swift SyntaxUtils.swift TokenConsumer.swift TokenPrecedence.swift diff --git a/Sources/SwiftParser/Declarations.swift b/Sources/SwiftParser/Declarations.swift index 07b9d3293a0..45877fc89d2 100644 --- a/Sources/SwiftParser/Declarations.swift +++ b/Sources/SwiftParser/Declarations.swift @@ -1928,19 +1928,18 @@ extension Parser { ) -> RawMacroExpansionDeclSyntax { var (unexpectedBeforePound, pound) = self.eat(handle) - if pound.trailingTriviaByteLength != 0 { - // `#` and the macro name must not be separated by a newline. - unexpectedBeforePound = RawUnexpectedNodesSyntax(combining: unexpectedBeforePound, pound, arena: self.arena) - pound = RawTokenSyntax(missing: .pound, text: "#", leadingTriviaPieces: pound.leadingTriviaPieces, arena: self.arena) + if pound.trailingTriviaByteLength > 0 || currentToken.leadingTriviaByteLength > 0 { + // If there are whitespaces after '#' diagnose. + let diagnostic = TokenDiagnostic( + .extraneousTrailingWhitespaceError, + byteOffset: pound.leadingTriviaByteLength + pound.tokenText.count + ) + pound = pound.tokenView.withTokenDiagnostic(tokenDiagnostic: diagnostic, arena: self.arena) } - var unexpectedBeforeMacro: RawUnexpectedNodesSyntax? - var macro: RawTokenSyntax + let unexpectedBeforeMacro: RawUnexpectedNodesSyntax? + let macro: RawTokenSyntax if !self.atStartOfLine { (unexpectedBeforeMacro, macro) = self.expectIdentifier(allowKeywordsAsIdentifier: true) - if macro.leadingTriviaByteLength != 0 { - unexpectedBeforeMacro = RawUnexpectedNodesSyntax(combining: unexpectedBeforeMacro, macro, arena: self.arena) - pound = self.missingToken(.identifier, text: macro.tokenText) - } } else { unexpectedBeforeMacro = nil macro = self.missingToken(.identifier) diff --git a/Sources/SwiftParser/Expressions.swift b/Sources/SwiftParser/Expressions.swift index 462c5f728dd..d60c53b9afb 100644 --- a/Sources/SwiftParser/Expressions.swift +++ b/Sources/SwiftParser/Expressions.swift @@ -1283,20 +1283,18 @@ extension Parser { flavor: ExprFlavor ) -> RawMacroExpansionExprSyntax { var (unexpectedBeforePound, pound) = self.expect(.pound) - if pound.trailingTriviaByteLength != 0 { + if pound.trailingTriviaByteLength > 0 || currentToken.leadingTriviaByteLength > 0 { // If there are whitespaces after '#' diagnose. - unexpectedBeforePound = RawUnexpectedNodesSyntax(combining: unexpectedBeforePound, pound, arena: self.arena) - pound = self.missingToken(.pound) + let diagnostic = TokenDiagnostic( + .extraneousTrailingWhitespaceError, + byteOffset: pound.leadingTriviaByteLength + pound.tokenText.count + ) + pound = pound.tokenView.withTokenDiagnostic(tokenDiagnostic: diagnostic, arena: self.arena) } - var unexpectedBeforeMacroName: RawUnexpectedNodesSyntax? - var macroName: RawTokenSyntax + let unexpectedBeforeMacroName: RawUnexpectedNodesSyntax? + let macroName: RawTokenSyntax if !self.atStartOfLine { (unexpectedBeforeMacroName, macroName) = self.expectIdentifier(allowKeywordsAsIdentifier: true) - if macroName.leadingTriviaByteLength != 0 { - // If there're whitespaces after '#' diagnose. - unexpectedBeforeMacroName = RawUnexpectedNodesSyntax(combining: unexpectedBeforeMacroName, macroName, arena: self.arena) - pound = self.missingToken(.identifier, text: macroName.tokenText) - } } else { unexpectedBeforeMacroName = nil macroName = self.missingToken(.identifier) diff --git a/Sources/SwiftParser/Lexer/Lexeme.swift b/Sources/SwiftParser/Lexer/Lexeme.swift index dab116d9ec3..bb185d408fc 100644 --- a/Sources/SwiftParser/Lexer/Lexeme.swift +++ b/Sources/SwiftParser/Lexer/Lexeme.swift @@ -102,7 +102,8 @@ extension Lexer { SyntaxText(baseAddress: start, count: byteLength) } - var textRange: Range { + @_spi(Testing) + public var textRange: Range { leadingTriviaByteLength.., + swiftVersion: SwiftVersion? = nil, experimentalFeatures: ExperimentalFeatures ) -> SourceFileSyntax { - var parser = Parser(source, experimentalFeatures: experimentalFeatures) + var parser = Parser(source, swiftVersion: swiftVersion, experimentalFeatures: experimentalFeatures) return SourceFileSyntax.parse(from: &parser) } @@ -36,9 +37,10 @@ extension Parser { /// `Parser.init` for more details. public static func parse( source: UnsafeBufferPointer, - maximumNestingLevel: Int? = nil + maximumNestingLevel: Int? = nil, + swiftVersion: SwiftVersion? = nil ) -> SourceFileSyntax { - var parser = Parser(source, maximumNestingLevel: maximumNestingLevel) + var parser = Parser(source, maximumNestingLevel: maximumNestingLevel, swiftVersion: swiftVersion) return SourceFileSyntax.parse(from: &parser) } diff --git a/Sources/SwiftParser/Parser.swift b/Sources/SwiftParser/Parser.swift index 7c0814d6923..fe5a921e5c5 100644 --- a/Sources/SwiftParser/Parser.swift +++ b/Sources/SwiftParser/Parser.swift @@ -117,6 +117,9 @@ public struct Parser { /// Parser should own a ``LookaheadTracker`` so that we can share one `furthestOffset` in a parse. let lookaheadTrackerOwner: LookaheadTrackerOwner + /// The Swift version as which this source file should be parsed. + let swiftVersion: SwiftVersion + /// The experimental features that have been enabled. let experimentalFeatures: ExperimentalFeatures @@ -129,6 +132,9 @@ public struct Parser { static let defaultMaximumNestingLevel = 256 #endif + /// The Swift version as which source files should be parsed if no Swift version is explicitly specified in the parser. + static let defaultSwiftVersion: SwiftVersion = .v6 + var _emptyRawMultipleTrailingClosureElementListSyntax: RawMultipleTrailingClosureElementListSyntax? /// Create an empty collection of the given type. @@ -187,12 +193,15 @@ public struct Parser { /// arena is created automatically, and `input` copied into the /// arena. If non-`nil`, `input` must be within its registered /// source buffer or allocator. + /// - swiftVersion: The version of Swift using which the file should be parsed. + /// Defaults to the latest version. /// - experimentalFeatures: The experimental features enabled for the parser. private init( buffer input: UnsafeBufferPointer, maximumNestingLevel: Int?, parseTransition: IncrementalParseTransition?, arena: ParsingSyntaxArena?, + swiftVersion: SwiftVersion?, experimentalFeatures: ExperimentalFeatures ) { var input = input @@ -200,13 +209,12 @@ public struct Parser { self.arena = arena precondition(arena.contains(text: SyntaxText(baseAddress: input.baseAddress, count: input.count))) } else { - self.arena = ParsingSyntaxArena( - parseTriviaFunction: TriviaParser.parseTrivia(_:position:) - ) + self.arena = ParsingSyntaxArena(parseTriviaFunction: TriviaParser.parseTrivia) input = self.arena.internSourceBuffer(input) } self.maximumNestingLevel = maximumNestingLevel ?? Self.defaultMaximumNestingLevel + self.swiftVersion = swiftVersion ?? Self.defaultSwiftVersion self.experimentalFeatures = experimentalFeatures self.lookaheadTrackerOwner = LookaheadTrackerOwner() @@ -224,6 +232,7 @@ public struct Parser { string input: String, maximumNestingLevel: Int?, parseTransition: IncrementalParseTransition?, + swiftVersion: SwiftVersion?, experimentalFeatures: ExperimentalFeatures ) { var input = input @@ -234,6 +243,7 @@ public struct Parser { maximumNestingLevel: maximumNestingLevel, parseTransition: parseTransition, arena: nil, + swiftVersion: swiftVersion, experimentalFeatures: experimentalFeatures ) } @@ -243,13 +253,15 @@ public struct Parser { public init( _ input: String, maximumNestingLevel: Int? = nil, - parseTransition: IncrementalParseTransition? = nil + parseTransition: IncrementalParseTransition? = nil, + swiftVersion: SwiftVersion? = nil ) { // Chain to the private String initializer. self.init( string: input, maximumNestingLevel: maximumNestingLevel, parseTransition: parseTransition, + swiftVersion: swiftVersion, experimentalFeatures: [] ) } @@ -277,7 +289,8 @@ public struct Parser { _ input: UnsafeBufferPointer, maximumNestingLevel: Int? = nil, parseTransition: IncrementalParseTransition? = nil, - arena: ParsingSyntaxArena? = nil + arena: ParsingSyntaxArena? = nil, + swiftVersion: SwiftVersion? = nil ) { // Chain to the private buffer initializer. self.init( @@ -285,6 +298,7 @@ public struct Parser { maximumNestingLevel: maximumNestingLevel, parseTransition: parseTransition, arena: arena, + swiftVersion: swiftVersion, experimentalFeatures: [] ) } @@ -296,6 +310,7 @@ public struct Parser { _ input: String, maximumNestingLevel: Int? = nil, parseTransition: IncrementalParseTransition? = nil, + swiftVersion: SwiftVersion? = nil, experimentalFeatures: ExperimentalFeatures ) { // Chain to the private String initializer. @@ -303,6 +318,7 @@ public struct Parser { string: input, maximumNestingLevel: maximumNestingLevel, parseTransition: parseTransition, + swiftVersion: swiftVersion, experimentalFeatures: experimentalFeatures ) } @@ -315,6 +331,7 @@ public struct Parser { maximumNestingLevel: Int? = nil, parseTransition: IncrementalParseTransition? = nil, arena: ParsingSyntaxArena? = nil, + swiftVersion: SwiftVersion? = nil, experimentalFeatures: ExperimentalFeatures ) { // Chain to the private buffer initializer. @@ -323,6 +340,7 @@ public struct Parser { maximumNestingLevel: maximumNestingLevel, parseTransition: parseTransition, arena: arena, + swiftVersion: swiftVersion, experimentalFeatures: experimentalFeatures ) } diff --git a/Sources/SwiftParser/StringLiterals.swift b/Sources/SwiftParser/StringLiterals.swift index 4224e5dd361..d6fc5163236 100644 --- a/Sources/SwiftParser/StringLiterals.swift +++ b/Sources/SwiftParser/StringLiterals.swift @@ -64,10 +64,11 @@ fileprivate class StringLiteralExpressionIndentationChecker { // error is fixed return nil } - return token.tokenView.withTokenDiagnostic( + let tokenWithDiagnostic = token.tokenView.withTokenDiagnostic( tokenDiagnostic: TokenDiagnostic(.insufficientIndentationInMultilineStringLiteral, byteOffset: 0), arena: arena ) + return RawSyntax(tokenWithDiagnostic) } private func visitLayoutNode(node: RawSyntax) -> RawSyntax? { diff --git a/Sources/SwiftParser/SwiftVersion.swift b/Sources/SwiftParser/SwiftVersion.swift new file mode 100644 index 00000000000..7867bd72fc2 --- /dev/null +++ b/Sources/SwiftParser/SwiftVersion.swift @@ -0,0 +1,20 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2024 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors +// +//===----------------------------------------------------------------------===// + +extension Parser { + /// A Swift language version. + public enum SwiftVersion: Comparable { + case v4 + case v5 + case v6 + } +} diff --git a/Sources/SwiftParser/TokenConsumer.swift b/Sources/SwiftParser/TokenConsumer.swift index a349aa8ed3b..0f36e26c4a3 100644 --- a/Sources/SwiftParser/TokenConsumer.swift +++ b/Sources/SwiftParser/TokenConsumer.swift @@ -18,6 +18,8 @@ protocol TokenConsumer { /// The current token syntax being examined by the consumer var currentToken: Lexer.Lexeme { get } + var swiftVersion: Parser.SwiftVersion { get } + /// The experimental features that have been enabled. var experimentalFeatures: Parser.ExperimentalFeatures { get } diff --git a/Sources/SwiftParserDiagnostics/LexerDiagnosticMessages.swift b/Sources/SwiftParserDiagnostics/LexerDiagnosticMessages.swift index 3fa4c124828..f8ec3b079ba 100644 --- a/Sources/SwiftParserDiagnostics/LexerDiagnosticMessages.swift +++ b/Sources/SwiftParserDiagnostics/LexerDiagnosticMessages.swift @@ -35,6 +35,25 @@ public extension TokenError { } } +/// A warning diagnostic whose ID is determined by the diagnostic's type. +public protocol TokenWarning: DiagnosticMessage { + var diagnosticID: MessageID { get } +} + +public extension TokenWarning { + static var diagnosticID: MessageID { + return MessageID(domain: diagnosticDomain, id: "\(self)") + } + + var diagnosticID: MessageID { + return Self.diagnosticID + } + + var severity: DiagnosticSeverity { + return .warning + } +} + // MARK: - Errors (please sort alphabetically) /// Please order the cases in this enum alphabetically by case name. @@ -85,6 +104,24 @@ public enum StaticTokenWarning: String, DiagnosticMessage { public var severity: DiagnosticSeverity { .warning } } +@_spi(SyntaxText) +public struct ExtraneousLeadingWhitespaceError: TokenError { + public let tokenText: SyntaxText + + public var message: String { + return "extraneous whitespace before '\(tokenText)' is not permitted" + } +} + +@_spi(SyntaxText) +public struct ExtraneousTrailingWhitespaceError: TokenError { + public let tokenText: SyntaxText + + public var message: String { + return "extraneous whitespace after '\(tokenText)' is not permitted" + } +} + public struct InvalidFloatingPointExponentDigit: TokenError { public enum Kind: Sendable { case digit(Unicode.Scalar) @@ -126,17 +163,26 @@ public struct InvalidDigitInIntegerLiteral: TokenError { } } +/// Downgrades a ``TokenError`` to a ``TokenWarning`` until Swift 6. +public struct ErrorToWarningDowngrade: TokenWarning { + public let error: TokenError + + public var message: String { + return "\(error.message); this is an error in Swift 6" + } +} + // MARK: - Convert TokenDiagnostic from SwiftSyntax to error messages public extension SwiftSyntax.TokenDiagnostic { /// `tokenText` is the entire text of the token in which the ``TokenDiagnostic`` /// occurred, including trivia. @_spi(RawSyntax) - func diagnosticMessage(wholeTextBytes: [UInt8]) -> DiagnosticMessage { + func diagnosticMessage(in token: TokenSyntax) -> DiagnosticMessage { var scalarAtErrorOffset: UnicodeScalar { // Fall back to the Unicode replacement character U+FFFD in case we can't // lex the unicode character at `byteOffset`. It's the best we can do - Unicode.Scalar.lexing(from: wholeTextBytes[Int(self.byteOffset)...]) ?? UnicodeScalar("�") + Unicode.Scalar.lexing(from: token.syntaxTextBytes[Int(self.byteOffset)...]) ?? UnicodeScalar("�") } switch self.kind { @@ -147,6 +193,10 @@ public extension SwiftSyntax.TokenDiagnostic { case .expectedDigitInFloatLiteral: return StaticTokenError.expectedDigitInFloatLiteral case .expectedHexCodeInUnicodeEscape: return StaticTokenError.expectedHexCodeInUnicodeEscape case .expectedHexDigitInHexLiteral: return StaticTokenError.expectedHexDigitInHexLiteral + case .extraneousLeadingWhitespaceError: return ExtraneousLeadingWhitespaceError(tokenText: token.rawText) + case .extraneousLeadingWhitespaceWarning: return ErrorToWarningDowngrade(error: ExtraneousLeadingWhitespaceError(tokenText: token.rawText)) + case .extraneousTrailingWhitespaceError: return ExtraneousTrailingWhitespaceError(tokenText: token.rawText) + case .extraneousTrailingWhitespaceWarning: return ErrorToWarningDowngrade(error: ExtraneousTrailingWhitespaceError(tokenText: token.rawText)) case .insufficientIndentationInMultilineStringLiteral: // This should be diagnosed when visiting the `StringLiteralExprSyntax` // inside `ParseDiagnosticsGenerator` but fall back to an error message @@ -177,8 +227,16 @@ public extension SwiftSyntax.TokenDiagnostic { } } - func diagnosticMessage(in token: TokenSyntax) -> DiagnosticMessage { - return self.diagnosticMessage(wholeTextBytes: token.syntaxTextBytes) + func position(in token: TokenSyntax) -> AbsolutePosition { + switch kind { + case .extraneousLeadingWhitespaceError, .extraneousLeadingWhitespaceWarning: + if let previousToken = token.previousToken(viewMode: .all) { + return previousToken.endPositionBeforeTrailingTrivia + } + default: + break + } + return token.position.advanced(by: Int(byteOffset)) } func fixIts(in token: TokenSyntax) -> [FixIt] { @@ -232,6 +290,20 @@ public extension SwiftSyntax.TokenDiagnostic { return [ FixIt(message: .insertWhitespace, changes: changes) ] + case .extraneousLeadingWhitespaceError, .extraneousLeadingWhitespaceWarning: + var changes: [FixIt.Change] = [] + changes.append(.replaceLeadingTrivia(token: token, newTrivia: [])) + if let previousToken = token.previousToken(viewMode: .sourceAccurate) { + changes.append(.replaceTrailingTrivia(token: previousToken, newTrivia: [])) + } + return [FixIt(message: .removeExtraneousWhitespace, changes: changes)] + case .extraneousTrailingWhitespaceError, .extraneousTrailingWhitespaceWarning: + var changes: [FixIt.Change] = [] + changes.append(.replaceTrailingTrivia(token: token, newTrivia: [])) + if let nextToken = token.nextToken(viewMode: .sourceAccurate) { + changes.append(.replaceLeadingTrivia(token: nextToken, newTrivia: [])) + } + return [FixIt(message: .removeExtraneousWhitespace, changes: changes)] default: return [] } diff --git a/Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift b/Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift index 35b864b4d62..e73ea487fde 100644 --- a/Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift +++ b/Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift @@ -314,47 +314,6 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { return .visitChildren } - /// If `unexpectedBefore` only contains a single token with the same kind as `token`, - /// `unexpectedBefore` has trailing trivia and `token` is missing, emit a diagnostic - /// that `unexpectedBefore` must not be followed by whitespace. - /// The Fix-It of that diagnostic removes the trailing trivia from `unexpectedBefore`. - func handleExtraneousWhitespaceError(unexpectedBefore: UnexpectedNodesSyntax?, token: TokenSyntax) { - if let unexpected = unexpectedBefore?.onlyPresentToken(where: { $0.tokenKind == token.tokenKind }), - !unexpected.trailingTrivia.isEmpty, - token.isMissing - { - let changes: [FixIt.MultiNodeChange] = [ - .makeMissing(unexpected, transferTrivia: false), // don't transfer trivia because trivia is the issue here - .makePresent(token, leadingTrivia: unexpected.leadingTrivia), - ] - if let nextToken = token.nextToken(viewMode: .all), - nextToken.isMissing - { - // If the next token is missing, the problem here isn’t actually the - // space after token but that the missing token should be added after - // `token` without a space. Generate a diagnostic for that. - _ = handleMissingSyntax( - nextToken, - overridePosition: unexpected.endPositionBeforeTrailingTrivia, - additionalChanges: changes, - additionalHandledNodes: [unexpected.id, token.id] - ) - } else { - let fixIt = FixIt( - message: .removeExtraneousWhitespace, - changes: changes - ) - addDiagnostic( - token, - position: unexpected.endPositionBeforeTrailingTrivia, - ExtraneousWhitespace(tokenWithWhitespace: unexpected), - fixIts: [fixIt], - handledNodes: [token.id, unexpected.id] - ) - } - } - } - // MARK: - Generic diagnostic generation public override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind { @@ -456,11 +415,27 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { handleMissingToken(token) } else { if let tokenDiagnostic = token.tokenDiagnostic { + switch tokenDiagnostic.kind { + case .extraneousLeadingWhitespaceError, .extraneousLeadingWhitespaceWarning: + if token.previousToken(viewMode: .fixedUp)?.isMissing ?? false { + // If the previous token is missing, it doesn't make sense to complain about extraneous whitespace between + // the previous token and this one. + return .skipChildren + } + case .extraneousTrailingWhitespaceError, .extraneousTrailingWhitespaceWarning: + if token.nextToken(viewMode: .fixedUp)?.isMissing ?? false { + // If the next token is missing, it doesn't make sense to complain about extraneous whitespace between + // this token and the next. + return .skipChildren + } + default: + break + } let message = tokenDiagnostic.diagnosticMessage(in: token) precondition(message.severity.matches(tokenDiagnostic.severity)) self.addDiagnostic( token, - position: token.position.advanced(by: Int(tokenDiagnostic.byteOffset)), + position: tokenDiagnostic.position(in: token), message, fixIts: tokenDiagnostic.fixIts(in: token) ) @@ -1261,32 +1236,6 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { return .visitChildren } - public override func visit(_ node: MacroExpansionDeclSyntax) -> SyntaxVisitorContinueKind { - if shouldSkip(node) { - return .skipChildren - } - - handleExtraneousWhitespaceError( - unexpectedBefore: node.unexpectedBetweenModifiersAndPound, - token: node.pound - ) - - return .visitChildren - } - - public override func visit(_ node: MacroExpansionExprSyntax) -> SyntaxVisitorContinueKind { - if shouldSkip(node) { - return .skipChildren - } - - handleExtraneousWhitespaceError( - unexpectedBefore: node.unexpectedBeforePound, - token: node.pound - ) - - return .visitChildren - } - public override func visit(_ node: MemberBlockItemSyntax) -> SyntaxVisitorContinueKind { if shouldSkip(node) { return .skipChildren diff --git a/Sources/SwiftSyntax/Raw/RawSyntaxTokenView.swift b/Sources/SwiftSyntax/Raw/RawSyntaxTokenView.swift index 61841ddd55b..999464ba131 100644 --- a/Sources/SwiftSyntax/Raw/RawSyntaxTokenView.swift +++ b/Sources/SwiftSyntax/Raw/RawSyntaxTokenView.swift @@ -281,28 +281,28 @@ public struct RawSyntaxTokenView: Sendable { } @_spi(RawSyntax) - public func withTokenDiagnostic(tokenDiagnostic: TokenDiagnostic?, arena: SyntaxArena) -> RawSyntax { + public func withTokenDiagnostic(tokenDiagnostic: TokenDiagnostic?, arena: SyntaxArena) -> RawTokenSyntax { arena.addChild(self.raw.arenaReference) switch raw.rawData.payload { case .parsedToken(var dat): if arena == self.raw.arenaReference { dat.tokenDiagnostic = tokenDiagnostic - return RawSyntax(arena: arena, payload: .parsedToken(dat)) + return RawSyntax(arena: arena, payload: .parsedToken(dat)).cast(RawTokenSyntax.self) } // If the modified token is allocated in a different arena, it might have // a different or no `parseTrivia` function. We thus cannot use a // `parsedToken` anymore. - return .makeMaterializedToken( + return RawSyntax.makeMaterializedToken( kind: formKind(), leadingTrivia: formLeadingTrivia(), trailingTrivia: formTrailingTrivia(), presence: presence, tokenDiagnostic: tokenDiagnostic, arena: arena - ) + ).cast(RawTokenSyntax.self) case .materializedToken(var dat): dat.tokenDiagnostic = tokenDiagnostic - return RawSyntax(arena: arena, payload: .materializedToken(dat)) + return RawSyntax(arena: arena, payload: .materializedToken(dat)).cast(RawTokenSyntax.self) default: preconditionFailure("'withTokenDiagnostic' is not available for non-token node") } diff --git a/Sources/SwiftSyntax/TokenDiagnostic.swift b/Sources/SwiftSyntax/TokenDiagnostic.swift index c74f24dae25..07a1bd2f812 100644 --- a/Sources/SwiftSyntax/TokenDiagnostic.swift +++ b/Sources/SwiftSyntax/TokenDiagnostic.swift @@ -30,6 +30,10 @@ public struct TokenDiagnostic: Hashable, Sendable { case expectedDigitInFloatLiteral case expectedHexCodeInUnicodeEscape case expectedHexDigitInHexLiteral + case extraneousLeadingWhitespaceError + case extraneousLeadingWhitespaceWarning + case extraneousTrailingWhitespaceError + case extraneousTrailingWhitespaceWarning case insufficientIndentationInMultilineStringLiteral case invalidBinaryDigitInIntegerLiteral case invalidCharacter @@ -65,6 +69,10 @@ public struct TokenDiagnostic: Hashable, Sendable { case .expectedDigitInFloatLiteral: return .error case .expectedHexCodeInUnicodeEscape: return .error case .expectedHexDigitInHexLiteral: return .error + case .extraneousLeadingWhitespaceError: return .error + case .extraneousLeadingWhitespaceWarning: return .warning + case .extraneousTrailingWhitespaceError: return .error + case .extraneousTrailingWhitespaceWarning: return .warning case .insufficientIndentationInMultilineStringLiteral: return .error case .invalidBinaryDigitInIntegerLiteral: return .error case .invalidCharacter: return .error diff --git a/Tests/SwiftParserTest/Assertions.swift b/Tests/SwiftParserTest/Assertions.swift index 29216e25c02..7a19ff2c110 100644 --- a/Tests/SwiftParserTest/Assertions.swift +++ b/Tests/SwiftParserTest/Assertions.swift @@ -142,8 +142,19 @@ private func assertTokens( line: expectedLexeme.line ) case (let actualError?, let expectedError?): + // Create a token from the lexeme so we can pass it to `TokenDiagnostic.diagnosticMessage(in:)` + let arena = ParsingSyntaxArena(parseTriviaFunction: TriviaParser.parseTrivia) + let rawToken = RawTokenSyntax( + kind: actualLexeme.rawTokenKind, + wholeText: arena.intern(actualLexeme.wholeText), + textRange: actualLexeme.textRange, + presence: .present, + tokenDiagnostic: actualLexeme.diagnostic, + arena: arena + ) + let token = Syntax(raw: RawSyntax(rawToken), rawNodeArena: arena).cast(TokenSyntax.self) assertStringsEqualWithDiff( - actualError.diagnosticMessage(wholeTextBytes: Array(actualLexeme.wholeText)).message, + actualError.diagnosticMessage(in: token).message, expectedError, file: expectedLexeme.file, line: expectedLexeme.line @@ -482,6 +493,7 @@ extension ParserTestCase { fileprivate func assertMutationRoundTrip( source: [UInt8], _ parse: (inout Parser) -> S, + swiftVersion: Parser.SwiftVersion?, experimentalFeatures: Parser.ExperimentalFeatures, file: StaticString, line: UInt @@ -490,7 +502,7 @@ extension ParserTestCase { let mutatedSource = String(decoding: buf, as: UTF8.self) // Check that we don't hit any assertions in the parser while parsing // the mutated source and that it round-trips - var mutatedParser = Parser(buf, experimentalFeatures: experimentalFeatures) + var mutatedParser = Parser(buf, swiftVersion: swiftVersion, experimentalFeatures: experimentalFeatures) let mutatedTree = parse(&mutatedParser) // Run the diagnostic generator to make sure it doesn’t crash _ = ParseDiagnosticsGenerator.diagnostics(for: mutatedTree) @@ -548,6 +560,8 @@ extension ParserTestCase { /// - applyFixIts: Applies only the fix-its with these messages. /// - expectedFixedSource: Asserts that the source after applying fix-its matches /// this string. + /// - swiftVersion: The version of Swift using which the file should be parsed. + /// Defaults to the latest version. /// - experimentalFeatures: A list of experimental features to enable, or /// `nil` to enable the default set of features provided by the test case. func assertParse( @@ -559,6 +573,7 @@ extension ParserTestCase { applyFixIts: [String]? = nil, fixedSource expectedFixedSource: String? = nil, options: AssertParseOptions = [], + swiftVersion: Parser.SwiftVersion? = nil, experimentalFeatures: Parser.ExperimentalFeatures? = nil, file: StaticString = #file, line: UInt = #line @@ -569,7 +584,7 @@ extension ParserTestCase { var (markerLocations, source) = extractMarkers(markedSource) markerLocations["START"] = 0 - var parser = Parser(source, experimentalFeatures: experimentalFeatures) + var parser = Parser(source, swiftVersion: swiftVersion, experimentalFeatures: experimentalFeatures) #if SWIFTPARSER_ENABLE_ALTERNATE_TOKEN_INTROSPECTION if !longTestsDisabled { parser.enableAlternativeTokenChoices() @@ -649,14 +664,21 @@ extension ParserTestCase { } if expectedDiagnostics.isEmpty && diags.isEmpty { - assertBasicFormat(source: source, parse: parse, experimentalFeatures: experimentalFeatures, file: file, line: line) + assertBasicFormat(source: source, parse: parse, swiftVersion: swiftVersion, experimentalFeatures: experimentalFeatures, file: file, line: line) } if !longTestsDisabled { DispatchQueue.concurrentPerform(iterations: Array(tree.tokens(viewMode: .all)).count) { tokenIndex in let flippedTokenTree = TokenPresenceFlipper(flipTokenAtIndex: tokenIndex).rewrite(Syntax(tree)) _ = ParseDiagnosticsGenerator.diagnostics(for: flippedTokenTree) - assertMutationRoundTrip(source: flippedTokenTree.syntaxTextBytes, parse, experimentalFeatures: experimentalFeatures, file: file, line: line) + assertMutationRoundTrip( + source: flippedTokenTree.syntaxTextBytes, + parse, + swiftVersion: swiftVersion, + experimentalFeatures: experimentalFeatures, + file: file, + line: line + ) } #if SWIFTPARSER_ENABLE_ALTERNATE_TOKEN_INTROSPECTION @@ -666,7 +688,7 @@ extension ParserTestCase { DispatchQueue.concurrentPerform(iterations: mutations.count) { index in let mutation = mutations[index] let alternateSource = MutatedTreePrinter.print(tree: Syntax(tree), mutations: [mutation.offset: mutation.replacement]) - assertMutationRoundTrip(source: alternateSource, parse, experimentalFeatures: experimentalFeatures, file: file, line: line) + assertMutationRoundTrip(source: alternateSource, parse, swiftVersion: swiftVersion, experimentalFeatures: experimentalFeatures, file: file, line: line) } #endif } @@ -704,16 +726,17 @@ class TriviaRemover: SyntaxRewriter { func assertBasicFormat( source: String, parse: (inout Parser) -> S, + swiftVersion: Parser.SwiftVersion?, experimentalFeatures: Parser.ExperimentalFeatures, file: StaticString = #file, line: UInt = #line ) { - var parser = Parser(source, experimentalFeatures: experimentalFeatures) + var parser = Parser(source, swiftVersion: swiftVersion, experimentalFeatures: experimentalFeatures) let sourceTree = parse(&parser) let withoutTrivia = TriviaRemover(viewMode: .sourceAccurate).rewrite(sourceTree) let formatted = withoutTrivia.formatted() - var formattedParser = Parser(formatted.description, experimentalFeatures: experimentalFeatures) + var formattedParser = Parser(formatted.description, swiftVersion: swiftVersion, experimentalFeatures: experimentalFeatures) let formattedReparsed = Syntax(parse(&formattedParser)) do { diff --git a/Tests/SwiftParserTest/AttributeTests.swift b/Tests/SwiftParserTest/AttributeTests.swift index 8816917987c..57c77124a01 100644 --- a/Tests/SwiftParserTest/AttributeTests.swift +++ b/Tests/SwiftParserTest/AttributeTests.swift @@ -947,4 +947,71 @@ final class AttributeTests: ParserTestCase { """ ) } + + func testSpaceBetweenAtAndAttribute() { + assertParse( + "@1️⃣ custom func foo() {}", + diagnostics: [ + DiagnosticSpec(message: "extraneous whitespace after '@' is not permitted", fixIts: ["remove whitespace"]) + ], + fixedSource: "@custom func foo() {}" + ) + + assertParse( + "@1️⃣ custom func foo() {}", + diagnostics: [ + DiagnosticSpec( + message: "extraneous whitespace after '@' is not permitted; this is an error in Swift 6", + severity: .warning, + fixIts: ["remove whitespace"] + ) + ], + fixedSource: "@custom func foo() {}", + swiftVersion: .v5 + ) + + assertParse( + """ + @1️⃣ + custom func foo() {} + """, + diagnostics: [ + DiagnosticSpec(message: "extraneous whitespace after '@' is not permitted", fixIts: ["remove whitespace"]) + ], + fixedSource: "@custom func foo() {}" + ) + } + + func testSpaceBetweenAttributeNameAndLeftParen() { + assertParse( + "@custom1️⃣ (1) func foo() {}", + diagnostics: [ + DiagnosticSpec(message: "extraneous whitespace before '(' is not permitted", fixIts: ["remove whitespace"]) + ], + fixedSource: "@custom(1) func foo() {}" + ) + + assertParse( + "@custom1️⃣ (1) func foo() {}", + diagnostics: [ + DiagnosticSpec( + message: "extraneous whitespace before '(' is not permitted; this is an error in Swift 6", + severity: .warning, + fixIts: ["remove whitespace"] + ) + ], + fixedSource: "@custom(1) func foo() {}", + swiftVersion: .v5 + ) + + assertParse( + """ + @custom + 1️⃣(1) func foo() {} + """, + diagnostics: [ + DiagnosticSpec(message: "unexpected code '(1)' in function") + ] + ) + } }