diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 47107e822b5..f764474c665 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -11,5 +11,5 @@ url: https://forums.swift.org/tags/c/development/tooling/39/swift-syntax about: Ask and answer questions about SwiftSyntax - name: SwiftSyntax Documentation - url: https://github.com/apple/swift-syntax/tree/main/Documentation + url: https://swiftpackageindex.com/apple/swift-syntax/documentation/swiftsyntax about: Learn about how to use the SwiftSyntax in your projects diff --git a/Sources/SwiftParser/Declarations.swift b/Sources/SwiftParser/Declarations.swift index 72d03adb975..fcfba32398b 100644 --- a/Sources/SwiftParser/Declarations.swift +++ b/Sources/SwiftParser/Declarations.swift @@ -30,16 +30,22 @@ extension DeclarationModifier { extension TokenConsumer { mutating func atStartOfFreestandingMacroExpansion() -> Bool { + // Check if "'#' " where the identifier is on the sameline. if !self.at(.pound) { return false } - if self.peek().rawTokenKind != .identifier && !self.peek().isLexerClassifiedKeyword { + if self.peek().isAtStartOfLine { return false } - if self.currentToken.trailingTriviaByteLength != 0 || self.peek().leadingTriviaByteLength != 0 { + switch self.peek().rawTokenKind { + case .identifier: + return true + case .keyword: + // allow keywords right after '#' so we can diagnose it when parsing. + return (self.currentToken.trailingTriviaByteLength == 0 && self.peek().leadingTriviaByteLength == 0) + default: return false } - return true } mutating func atStartOfDeclaration( @@ -47,7 +53,7 @@ extension TokenConsumer { allowInitDecl: Bool = true, allowRecovery: Bool = false ) -> Bool { - if self.at(anyIn: PoundDeclarationStart.self) != nil { + if self.at(.poundIfKeyword) { return true } @@ -188,13 +194,9 @@ extension Parser { /// If `inMemberDeclList` is `true`, we know that the next item must be a /// declaration and thus start with a keyword. This allows futher recovery. mutating func parseDeclaration(inMemberDeclList: Bool = false) -> RawDeclSyntax { - switch self.at(anyIn: PoundDeclarationStart.self) { - case (.poundIfKeyword, _)?: - if self.withLookahead({ $0.consumeIfConfigOfAttributes() }) { - // If we are at a `#if` of attributes, the `#if` directive should be - // parsed when we're parsing the attributes. - break - } + // If we are at a `#if` of attributes, the `#if` directive should be + // parsed when we're parsing the attributes. + if self.at(.poundIfKeyword) && !self.withLookahead({ $0.consumeIfConfigOfAttributes() }) { let directive = self.parsePoundIfDirective { (parser, _) in let parsedDecl = parser.parseDeclaration() let semicolon = parser.consume(if: .semicolon) @@ -220,8 +222,6 @@ extension Parser { return .decls(RawMemberDeclListSyntax(elements: elements, arena: parser.arena)) } return RawDeclSyntax(directive) - case nil: - break } let attrs = DeclAttributes( @@ -2096,25 +2096,24 @@ extension Parser { _ handle: RecoveryConsumptionHandle ) -> RawMacroExpansionDeclSyntax { - let (unexpectedBeforePound, poundKeyword) = self.eat(handle) - // Don't allow space between '#' and the macro name. - if poundKeyword.trailingTriviaByteLength != 0 || self.currentToken.leadingTriviaByteLength != 0 { - return RawMacroExpansionDeclSyntax( - attributes: attrs.attributes, - modifiers: attrs.modifiers, - unexpectedBeforePound, - poundToken: poundKeyword, - macro: self.missingToken(.identifier), - genericArguments: nil, - leftParen: nil, - argumentList: .init(elements: [], arena: self.arena), - rightParen: nil, - trailingClosure: nil, - additionalTrailingClosures: nil, - arena: self.arena - ) + 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) + } + var unexpectedBeforeMacro: RawUnexpectedNodesSyntax? + var macro: RawTokenSyntax + if !self.currentToken.isAtStartOfLine { + (unexpectedBeforeMacro, macro) = self.expectIdentifier(keywordRecovery: 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) } - let (unexpectedBeforeMacro, macro) = self.expectIdentifier(keywordRecovery: true) // Parse the optional generic argument list. let generics: RawGenericArgumentClauseSyntax? @@ -2155,7 +2154,7 @@ extension Parser { attributes: attrs.attributes, modifiers: attrs.modifiers, unexpectedBeforePound, - poundToken: poundKeyword, + poundToken: pound, unexpectedBeforeMacro, macro: macro, genericArguments: generics, diff --git a/Sources/SwiftParser/Expressions.swift b/Sources/SwiftParser/Expressions.swift index 889317662d5..a70be39e750 100644 --- a/Sources/SwiftParser/Expressions.swift +++ b/Sources/SwiftParser/Expressions.swift @@ -1411,21 +1411,25 @@ extension Parser { pattern: PatternContext, flavor: ExprFlavor ) -> RawMacroExpansionExprSyntax { - if !atStartOfFreestandingMacroExpansion() { - return RawMacroExpansionExprSyntax( - poundToken: self.consumeAnyToken(), - macro: self.missingToken(.identifier), - genericArguments: nil, - leftParen: nil, - argumentList: .init(elements: [], arena: self.arena), - rightParen: nil, - trailingClosure: nil, - additionalTrailingClosures: nil, - arena: self.arena - ) + var (unexpectedBeforePound, pound) = self.expect(.pound) + if pound.trailingTriviaByteLength != 0 { + // If there are whitespaces after '#' diagnose. + unexpectedBeforePound = RawUnexpectedNodesSyntax(combining: unexpectedBeforePound, pound, arena: self.arena) + pound = self.missingToken(.pound) + } + var unexpectedBeforeMacro: RawUnexpectedNodesSyntax? + var macro: RawTokenSyntax + if !self.currentToken.isAtStartOfLine { + (unexpectedBeforeMacro, macro) = self.expectIdentifier(keywordRecovery: true) + if macro.leadingTriviaByteLength != 0 { + // If there're whitespaces after '#' diagnose. + unexpectedBeforeMacro = RawUnexpectedNodesSyntax(combining: unexpectedBeforeMacro, macro, arena: self.arena) + pound = self.missingToken(.identifier, text: macro.tokenText) + } + } else { + unexpectedBeforeMacro = nil + macro = self.missingToken(.identifier) } - let poundKeyword = self.consumeAnyToken() - let (unexpectedBeforeMacro, macro) = self.expectIdentifier(keywordRecovery: true) // Parse the optional generic argument list. let generics: RawGenericArgumentClauseSyntax? @@ -1460,7 +1464,8 @@ extension Parser { } return RawMacroExpansionExprSyntax( - poundToken: poundKeyword, + unexpectedBeforePound, + poundToken: pound, unexpectedBeforeMacro, macro: macro, genericArguments: generics, diff --git a/Sources/SwiftParser/TokenSpecSet.swift b/Sources/SwiftParser/TokenSpecSet.swift index 4004fdd6c80..72f541a4b38 100644 --- a/Sources/SwiftParser/TokenSpecSet.swift +++ b/Sources/SwiftParser/TokenSpecSet.swift @@ -426,23 +426,6 @@ enum OperatorLike: TokenSpecSet { } } -enum PoundDeclarationStart: TokenSpecSet { - case poundIfKeyword - - init?(lexeme: Lexer.Lexeme) { - switch lexeme.rawTokenKind { - case .poundIfKeyword: self = .poundIfKeyword - default: return nil - } - } - - var spec: TokenSpec { - switch self { - case .poundIfKeyword: return .poundIfKeyword - } - } -} - enum SwitchCaseStart: TokenSpecSet { case `case` case `default` diff --git a/Sources/SwiftParserDiagnostics/DiagnosticExtensions.swift b/Sources/SwiftParserDiagnostics/DiagnosticExtensions.swift index 0e196e514d8..fb622f7cd25 100644 --- a/Sources/SwiftParserDiagnostics/DiagnosticExtensions.swift +++ b/Sources/SwiftParserDiagnostics/DiagnosticExtensions.swift @@ -59,7 +59,7 @@ extension FixIt.MultiNodeChange { /// removed node will be transferred to the trailing trivia of the previous token. static func makeMissing(_ tokens: [TokenSyntax], transferTrivia: Bool = true) -> Self { precondition(!tokens.isEmpty) - precondition(tokens.allSatisfy({ $0.presence == .present })) + precondition(tokens.allSatisfy({ $0.isPresent })) var changes = tokens.map { FixIt.Change.replace( oldNode: Syntax($0), @@ -111,7 +111,7 @@ extension FixIt.MultiNodeChange { class MissingNodesBasicFormatter: BasicFormat { override func isMutable(_ token: TokenSyntax) -> Bool { // Assume that all missing nodes will be made present by the Fix-It. - return token.presence == .missing + return token.isMissing } } @@ -153,7 +153,7 @@ extension FixIt.MultiNodeChange { } if let previousToken = node.previousToken(viewMode: .fixedUp), - previousToken.presence == .present, + previousToken.isPresent, let firstToken = node.firstToken(viewMode: .all), previousToken.trailingTrivia.allSatisfy({ $0.isWhitespace }), !BasicFormat().requiresWhitespace(between: previousToken, and: firstToken), diff --git a/Sources/SwiftParserDiagnostics/MissingNodesError.swift b/Sources/SwiftParserDiagnostics/MissingNodesError.swift index 5e0845a892b..5f801850225 100644 --- a/Sources/SwiftParserDiagnostics/MissingNodesError.swift +++ b/Sources/SwiftParserDiagnostics/MissingNodesError.swift @@ -76,7 +76,7 @@ fileprivate enum NodesDescriptionPart { for missingNode in nodes { if let token = missingNode.as(TokenSyntax.self) { let newPart: NodesDescriptionPart - if token.presence == .present { + if token.isPresent { newPart = .tokensWithDefaultText([token]) } else { let (rawKind, text) = token.tokenKind.decomposeToRaw() @@ -371,7 +371,7 @@ extension ParseDiagnosticsGenerator { let siblings = parentWithTokens.children(viewMode: .all) let siblingsAfter = siblings[siblings.index(after: index)...] for sibling in siblingsAfter { - if sibling.as(TokenSyntax.self)?.presence == .missing { + if sibling.as(TokenSyntax.self)?.isMissing ?? false { // Handle missing sibling tokens missingNodes += [sibling] } else if sibling.raw.kind.isMissing { diff --git a/Sources/SwiftParserDiagnostics/MissingTokenError.swift b/Sources/SwiftParserDiagnostics/MissingTokenError.swift index 1a5ac85fe61..e0d1530355b 100644 --- a/Sources/SwiftParserDiagnostics/MissingTokenError.swift +++ b/Sources/SwiftParserDiagnostics/MissingTokenError.swift @@ -108,7 +108,7 @@ extension ParseDiagnosticsGenerator { if let identifier = missingToken.nextToken(viewMode: .all), identifier.tokenView.rawKind == .identifier, - identifier.presence == .missing + identifier.isMissing { // The extraneous whitespace caused a missing identifier, output a // diagnostic inserting it instead of a diagnostic to fix the trivia @@ -126,7 +126,7 @@ extension ParseDiagnosticsGenerator { ) } else { let fixIt = FixIt(message: .removeExtraneousWhitespace, changes: changes) - addDiagnostic(invalidToken, .invalidWhitespaceAfterPeriod, fixIts: [fixIt], handledNodes: [invalidTokenContainer.id]) + addDiagnostic(invalidToken, ExtraneousWhitespace(tokenWithWhitespace: invalidToken), fixIts: [fixIt], handledNodes: [invalidTokenContainer.id]) } return true } diff --git a/Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift b/Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift index f6a1155d00c..42198837ff9 100644 --- a/Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift +++ b/Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift @@ -178,11 +178,11 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { let correctTokens = correctTokens.compactMap({ $0 }) // Ignore `correctTokens` that are already present. - let correctAndMissingTokens = correctTokens.filter({ $0.presence == .missing }) + let correctAndMissingTokens = correctTokens.filter({ $0.isMissing }) var changes: [FixIt.MultiNodeChange] = [] if let misplacedToken = misplacedTokens.only, let correctToken = correctTokens.only, misplacedToken.nextToken(viewMode: .all) == correctToken || misplacedToken.previousToken(viewMode: .all) == correctToken, - correctToken.presence == .missing + correctToken.isMissing { // We are exchanging two adjacent tokens, transfer the trivia from the incorrect token to the corrected token. changes += misplacedTokens.map { FixIt.MultiNodeChange.makeMissing($0, transferTrivia: false) } @@ -297,7 +297,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { guard let specifier = specifier else { continue } - if specifier.presence == .present { + if specifier.isPresent { for case .some(let unexpected) in unexpectedNodes { for duplicateSpecifier in unexpected.presentTokens(satisfying: isOfSameKind) { addDiagnostic( @@ -314,6 +314,47 @@ 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 diagnsotic 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 { @@ -410,7 +451,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { return .skipChildren } - if token.presence == .missing { + if token.isMissing { handleMissingToken(token) } else { if let tokenDiagnostic = token.tokenDiagnostic { @@ -469,7 +510,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { return .skipChildren } - if node.leftSquareBracket.presence == .missing && node.rightSquareBracket.presence == .present { + if node.leftSquareBracket.isMissing && node.rightSquareBracket.isPresent { addDiagnostic( node.rightSquareBracket, .extraRightBracket, @@ -580,7 +621,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { if let versionTuple = node.versionInfo?.versionTuple, let unexpectedVersionTuple = node.unexpectedBetweenVersionInfoAndRightParen { - if versionTuple.major.presence == .missing { + if versionTuple.major.isMissing { addDiagnostic( versionTuple, CannotParseVersionTuple(versionTuple: unexpectedVersionTuple), @@ -603,7 +644,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { return .skipChildren } - if node.label.presence == .missing { + if node.label.isMissing { addDiagnostic( node.label, .canImportWrongSecondParameterLabel, @@ -656,7 +697,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { if shouldSkip(node) { return .skipChildren } - if let semicolon = node.semicolon, semicolon.presence == .missing { + if let semicolon = node.semicolon, semicolon.isMissing { if !node.item.hasError { // Only diagnose the missing semicolon if the item doesn't contain any errors. // If the item contains errors, the root cause is most likely something different and not the missing semicolon. @@ -674,7 +715,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { handledNodes.append(semicolon.id) } } - if let semicolon = node.semicolon, semicolon.presence == .present, node.item.isMissingAllTokens { + if let semicolon = node.semicolon, semicolon.isPresent, node.item.isMissingAllTokens { addDiagnostic( node, .standaloneSemicolonStatement, @@ -707,7 +748,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { if let unexpected = node.unexpectedBetweenBodyAndTrailingComma, let token = unexpected.presentTokens(satisfying: { $0.tokenKind == .binaryOperator("&&") }).first, let trailingComma = node.trailingComma, - trailingComma.presence == .missing, + trailingComma.isMissing, let previous = node.unexpectedBetweenBodyAndTrailingComma?.previousToken(viewMode: .sourceAccurate) { @@ -767,7 +808,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { if shouldSkip(node) { return .skipChildren } - if node.floatingDigits.presence == .missing, + if node.floatingDigits.isMissing, let (period, integerLiteral) = node.unexpectedAfterFloatingDigits?.twoPresentTokens( firstSatisfying: { $0.tokenKind == .period }, secondSatisfying: { $0.tokenKind.isIntegerLiteral } @@ -875,7 +916,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { return .skipChildren } // Emit a custom diagnostic for an unexpected '...' after the type name. - if node.each?.presence == .present { + if node.each?.isPresent ?? false { removeToken( node.unexpectedBetweenNameAndColon, where: { $0.tokenKind == .ellipsis }, @@ -916,7 +957,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { if shouldSkip(node) { return .skipChildren } - if node.identifier.presence == .missing, let unexpected = node.unexpectedBeforeIdentifier { + if node.identifier.isMissing, let unexpected = node.unexpectedBeforeIdentifier { if unexpected.first?.as(TokenSyntax.self)?.tokenKind == .pound { addDiagnostic(unexpected, UnknownDirectiveError(unexpected: unexpected), handledNodes: [unexpected.id, node.identifier.id]) } else if let availability = unexpected.first?.as(AvailabilityConditionSyntax.self) { @@ -957,7 +998,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { for clause in node.clauses where clause.hasError { if let unexpectedBeforePoundKeyword = clause.unexpectedBeforePoundKeyword, clause.poundKeyword.tokenKind == .poundElseifKeyword, - clause.poundKeyword.presence == .missing + clause.poundKeyword.isMissing { let unexpectedTokens = unexpectedBeforePoundKeyword @@ -1004,7 +1045,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { addDiagnostic(node.conditions, MissingConditionInStatement(node: node), handledNodes: [node.conditions.id]) } - if let leftBrace = node.elseBody?.as(CodeBlockSyntax.self)?.leftBrace, leftBrace.presence == .missing { + if let leftBrace = node.elseBody?.as(CodeBlockSyntax.self)?.leftBrace, leftBrace.isMissing { addDiagnostic(leftBrace, .expectedLeftBraceOrIfAfterElse, handledNodes: [leftBrace.id]) } @@ -1032,7 +1073,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { ) } - if node.equal.presence == .missing { + if node.equal.isMissing { exchangeTokens( unexpected: node.unexpectedBeforeEqual, unexpectedTokenCondition: { $0.tokenKind == .colon }, @@ -1079,11 +1120,37 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { return .visitChildren } + public override func visit(_ node: MacroExpansionDeclSyntax) -> SyntaxVisitorContinueKind { + if shouldSkip(node) { + return .skipChildren + } + + handleExtraneousWhitespaceError( + unexpectedBefore: node.unexpectedBetweenModifiersAndPoundToken, + token: node.poundToken + ) + + return .visitChildren + } + + public override func visit(_ node: MacroExpansionExprSyntax) -> SyntaxVisitorContinueKind { + if shouldSkip(node) { + return .skipChildren + } + + handleExtraneousWhitespaceError( + unexpectedBefore: node.unexpectedBeforePoundToken, + token: node.poundToken + ) + + return .visitChildren + } + public override func visit(_ node: MemberDeclListItemSyntax) -> SyntaxVisitorContinueKind { if shouldSkip(node) { return .skipChildren } - if let semicolon = node.semicolon, semicolon.presence == .missing { + if let semicolon = node.semicolon, semicolon.isMissing { if !node.decl.hasError { // Only diagnose the missing semicolon if the decl doesn't contain any errors. // If the decl contains errors, the root cause is most likely something different and not the missing semicolon. @@ -1133,7 +1200,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { return .skipChildren } if let token = node.unexpectedBetweenModuleLabelAndColon?.onlyPresentToken(where: { $0.tokenKind.isIdentifier }), - node.moduleLabel.presence == .missing + node.moduleLabel.isMissing { addDiagnostic( node, @@ -1208,7 +1275,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { } if let message { let fixIts: [FixIt] - if node.identifier.presence == .present { + if node.identifier.isPresent { fixIts = [FixIt(message: RemoveNodesFixIt(unexpected), changes: .makeMissing(unexpected))] } else { fixIts = [] @@ -1227,7 +1294,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { if shouldSkip(node) { return .skipChildren } - if let unexpected = node.unexpectedBetweenColonAndFlag ?? node.unexpectedAfterFlag, node.flag.presence == .missing { + if let unexpected = node.unexpectedBetweenColonAndFlag ?? node.unexpectedAfterFlag, node.flag.isMissing { addDiagnostic(unexpected, .invalidFlagAfterPrecedenceGroupAssignment, handledNodes: [unexpected.id, node.flag.id]) } return .visitChildren @@ -1237,7 +1304,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { if shouldSkip(node) { return .skipChildren } - if node.value.presence == .missing { + if node.value.isMissing { addDiagnostic( Syntax(node.unexpectedBetweenColonAndValue) ?? Syntax(node.value), .invalidPrecedenceGroupAssociativity, @@ -1267,7 +1334,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { if shouldSkip(node) { return .skipChildren } - if node.equalityToken.presence == .missing && node.rightTypeIdentifier.isMissingAllTokens { + if node.equalityToken.isMissing && node.rightTypeIdentifier.isMissingAllTokens { addDiagnostic( node.equalityToken, .missingConformanceRequirement, @@ -1541,7 +1608,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { if shouldSkip(node) { return .skipChildren } - if node.equal.presence == .missing { + if node.equal.isMissing { exchangeTokens( unexpected: node.unexpectedBeforeEqual, unexpectedTokenCondition: { $0.tokenKind == .colon }, @@ -1559,8 +1626,8 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { } if let token = node.unexpectedBetweenMessageLabelAndColon?.onlyPresentToken(where: { $0.tokenKind.isIdentifier }), - token.presence == .present, - node.messageLabel.presence == .missing + token.isPresent, + node.messageLabel.isMissing { addDiagnostic( node, @@ -1588,7 +1655,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor { if shouldSkip(node) { return .skipChildren } - if node.colonMark.presence == .missing { + if node.colonMark.isMissing { if let siblings = node.parent?.children(viewMode: .all), let nextSibling = siblings[siblings.index(after: node.index)...].first, nextSibling.is(MissingExprSyntax.self) diff --git a/Sources/SwiftParserDiagnostics/ParserDiagnosticMessages.swift b/Sources/SwiftParserDiagnostics/ParserDiagnosticMessages.swift index dc25ddc4839..60e8dd06126 100644 --- a/Sources/SwiftParserDiagnostics/ParserDiagnosticMessages.swift +++ b/Sources/SwiftParserDiagnostics/ParserDiagnosticMessages.swift @@ -168,9 +168,6 @@ extension DiagnosticMessage where Self == StaticParserError { public static var invalidPrecedenceGroupAssociativity: Self { .init("Expected 'none', 'left', or 'right' after 'associativity'") } - public static var invalidWhitespaceAfterPeriod: Self { - .init("extraneous whitespace after '.' is not permitted") - } public static var joinConditionsUsingComma: Self { .init("expected ',' joining parts of a multi-clause condition") } @@ -316,6 +313,14 @@ public struct ExtaneousCodeAtTopLevel: ParserError { } } +public struct ExtraneousWhitespace: ParserError { + public let tokenWithWhitespace: TokenSyntax + + public var message: String { + return "extraneous whitespace after '\(tokenWithWhitespace.text)' is not permitted" + } +} + public struct IdentifierNotAllowedInOperatorName: ParserError { public let identifier: TokenSyntax diff --git a/Sources/SwiftParserDiagnostics/PresenceUtils.swift b/Sources/SwiftParserDiagnostics/PresenceUtils.swift index 71c083cbb63..f52a24de5fb 100644 --- a/Sources/SwiftParserDiagnostics/PresenceUtils.swift +++ b/Sources/SwiftParserDiagnostics/PresenceUtils.swift @@ -27,7 +27,7 @@ class PresentNodeChecker: SyntaxAnyVisitor { } override func visit(_ node: TokenSyntax) -> SyntaxVisitorContinueKind { - if node.presence == .present { + if node.isPresent { hasPresentToken = true } return .visitChildren @@ -46,7 +46,7 @@ extension SyntaxProtocol { /// Transforms a syntax tree by making all missing tokens present. class PresentMaker: SyntaxRewriter { override func visit(_ token: TokenSyntax) -> TokenSyntax { - if token.presence == .missing { + if token.isMissing { let presentToken: TokenSyntax let (rawKind, text) = token.tokenKind.decomposeToRaw() if let text = text, (!text.isEmpty || rawKind == .stringSegment) { // string segments can have empty text @@ -64,7 +64,7 @@ class PresentMaker: SyntaxRewriter { class MissingMaker: SyntaxRewriter { override func visit(_ node: TokenSyntax) -> TokenSyntax { - guard node.presence == .present else { + guard node.isPresent else { return node } return TokenSyntax(node.tokenKind, presence: .missing) diff --git a/Sources/SwiftParserDiagnostics/SyntaxExtensions.swift b/Sources/SwiftParserDiagnostics/SyntaxExtensions.swift index 2c9dbcb2309..2095e32832a 100644 --- a/Sources/SwiftParserDiagnostics/SyntaxExtensions.swift +++ b/Sources/SwiftParserDiagnostics/SyntaxExtensions.swift @@ -173,3 +173,13 @@ public extension TriviaPiece { } } } + +extension TokenSyntax { + var isMissing: Bool { + return presence == .missing + } + + var isPresent: Bool { + return presence == .present + } +} diff --git a/Tests/SwiftParserTest/DeclarationTests.swift b/Tests/SwiftParserTest/DeclarationTests.swift index c8ca6b3e3e1..e4a93b5a5a0 100644 --- a/Tests/SwiftParserTest/DeclarationTests.swift +++ b/Tests/SwiftParserTest/DeclarationTests.swift @@ -1464,44 +1464,38 @@ final class DeclarationTests: XCTestCase { assertParse( """ @attribute #topLevelWithAttr + """, + substructure: Syntax( + MacroExpansionDeclSyntax( + attributes: [.attribute(AttributeSyntax(attributeName: TypeSyntax("attribute")))], + macro: "topLevelWithAttr", + argumentList: [] + ) + ) + ) + + assertParse( + """ public #topLevelWithModifier + """, + substructure: Syntax( + MacroExpansionDeclSyntax( + modifiers: [DeclModifierSyntax(name: .keyword(.public))], + macro: "topLevelWithModifier", + argumentList: [] + ) + ) + ) + + assertParse( + """ #topLevelBare """, substructure: Syntax( - CodeBlockItemListSyntax([ - CodeBlockItemSyntax( - item: .decl( - DeclSyntax( - MacroExpansionDeclSyntax( - attributes: [.attribute(AttributeSyntax(attributeName: TypeSyntax("attribute")))], - macro: "topLevelWithAttr", - argumentList: [] - ) - ) - ) - ), - CodeBlockItemSyntax( - item: .decl( - DeclSyntax( - MacroExpansionDeclSyntax( - modifiers: [DeclModifierSyntax(name: .keyword(.public))], - macro: "topLevelWithModifier", - argumentList: [] - ) - ) - ) - ), - CodeBlockItemSyntax( - item: .expr( - ExprSyntax( - MacroExpansionExprSyntax( - macro: "topLevelBare", - argumentList: [] - ) - ) - ) - ), - ]) + MacroExpansionExprSyntax( + macro: "topLevelBare", + argumentList: [] + ) ) ) @@ -1509,33 +1503,43 @@ final class DeclarationTests: XCTestCase { """ struct S { @attribute #memberWithAttr + } + """, + substructure: Syntax( + MacroExpansionDeclSyntax( + attributes: [.attribute(AttributeSyntax(attributeName: TypeSyntax("attribute")))], + macro: "memberWithAttr", + argumentList: [] + ) + ) + ) + + assertParse( + """ + struct S { public #memberWithModifier + } + """, + substructure: Syntax( + MacroExpansionDeclSyntax( + modifiers: [DeclModifierSyntax(name: .keyword(.public))], + macro: "memberWithModifier", + argumentList: [] + ) + ) + ) + + assertParse( + """ + struct S { #memberBare } """, substructure: Syntax( - MemberDeclListSyntax([ - MemberDeclListItemSyntax( - decl: MacroExpansionDeclSyntax( - attributes: [.attribute(AttributeSyntax(attributeName: TypeSyntax("attribute")))], - macro: "memberWithAttr", - argumentList: [] - ) - ), - MemberDeclListItemSyntax( - decl: MacroExpansionDeclSyntax( - modifiers: [DeclModifierSyntax(name: .keyword(.public))], - macro: "memberWithModifier", - argumentList: [] - ) - ), - MemberDeclListItemSyntax( - decl: MacroExpansionDeclSyntax( - macro: "memberBare", - argumentList: [] - ) - ), - ]) + MacroExpansionDeclSyntax( + macro: "memberBare", + argumentList: [] + ) ) ) @@ -1543,38 +1547,44 @@ final class DeclarationTests: XCTestCase { """ func test() { @attribute #bodyWithAttr + } + """, + substructure: Syntax( + MacroExpansionDeclSyntax( + attributes: [.attribute(AttributeSyntax(attributeName: TypeSyntax("attribute")))], + macro: "bodyWithAttr", + argumentList: [] + ) + ) + ) + + assertParse( + """ + func test() { public #bodyWithModifier + } + """, + substructure: Syntax( + MacroExpansionDeclSyntax( + modifiers: [DeclModifierSyntax(name: .keyword(.public))], + macro: "bodyWithModifier", + argumentList: [] + ) + + ) + ) + + assertParse( + """ + func test() { #bodyBare } """, substructure: Syntax( - FunctionDeclSyntax( - identifier: .identifier("test"), - signature: FunctionSignatureSyntax( - input: ParameterClauseSyntax(parameterList: []) - ) - ) { - DeclSyntax( - MacroExpansionDeclSyntax( - attributes: [.attribute(AttributeSyntax(attributeName: TypeSyntax("attribute")))], - macro: "bodyWithAttr", - argumentList: [] - ) - ) - DeclSyntax( - MacroExpansionDeclSyntax( - modifiers: [DeclModifierSyntax(name: .keyword(.public))], - macro: "bodyWithModifier", - argumentList: [] - ) - ) - ExprSyntax( - MacroExpansionExprSyntax( - macro: "bodyBare", - argumentList: [] - ) - ) - } + MacroExpansionExprSyntax( + macro: "bodyBare", + argumentList: [] + ) ) ) @@ -1588,24 +1598,15 @@ final class DeclarationTests: XCTestCase { } """, substructure: Syntax( - FunctionDeclSyntax( - identifier: .identifier("test"), - signature: FunctionSignatureSyntax( - input: ParameterClauseSyntax(parameterList: []) - ) - ) { - DeclSyntax( - MacroExpansionDeclSyntax( - attributes: [ - .attribute(AttributeSyntax(attributeName: TypeSyntax("attrib1"))), - .attribute(AttributeSyntax(attributeName: TypeSyntax("attrib2"))), - ], - modifiers: [DeclModifierSyntax(name: .keyword(.public))], - macro: "declMacro", - argumentList: [] - ) - ) - } + MacroExpansionDeclSyntax( + attributes: [ + .attribute(AttributeSyntax(attributeName: TypeSyntax("attrib1"))), + .attribute(AttributeSyntax(attributeName: TypeSyntax("attrib2"))), + ], + modifiers: [DeclModifierSyntax(name: .keyword(.public))], + macro: "declMacro", + argumentList: [] + ) ) ) @@ -1613,55 +1614,139 @@ final class DeclarationTests: XCTestCase { """ struct S { @attrib #1️⃣class - #2️⃣struct } """, substructure: Syntax( - MemberDeclListSyntax([ - MemberDeclListItemSyntax( - decl: MacroExpansionDeclSyntax( - attributes: [.attribute(AttributeSyntax(attributeName: TypeSyntax("attrib")))], - poundToken: .poundToken(), - /*unexpectedBetweenPoundTokenAndMacro:*/ [ - TokenSyntax.keyword(.class) - ], - macro: .identifier("", presence: .missing), - argumentList: [] - ) - ), - MemberDeclListItemSyntax( - decl: MacroExpansionDeclSyntax( - poundToken: .poundToken(), - /*unexpectedBetweenPoundTokenAndMacro:*/ [ - TokenSyntax.keyword(.struct) - ], - macro: .identifier("", presence: .missing), - argumentList: [] - ) - ), - ]) + MacroExpansionDeclSyntax( + attributes: [.attribute(AttributeSyntax(attributeName: TypeSyntax("attrib")))], + poundToken: .poundToken(), + /*unexpectedBetweenPoundTokenAndMacro:*/ [ + TokenSyntax.keyword(.class) + ], + macro: .identifier("", presence: .missing), + argumentList: [] + ) ), diagnostics: [ DiagnosticSpec( - locationMarker: "1️⃣", message: "keyword 'class' cannot be used as an identifier here", fixIts: ["if this name is unavoidable, use backticks to escape it"] - ), + ) + ], + fixedSource: """ + struct S { + @attrib #`class` + } + """ + ) + + assertParse( + """ + struct S { + #1️⃣struct + } + """, + substructure: Syntax( + MacroExpansionDeclSyntax( + poundToken: .poundToken(), + /*unexpectedBetweenPoundTokenAndMacro:*/ [ + TokenSyntax.keyword(.struct) + ], + macro: .identifier("", presence: .missing), + argumentList: [] + ) + ), + diagnostics: [ DiagnosticSpec( - locationMarker: "2️⃣", message: "keyword 'struct' cannot be used as an identifier here", fixIts: ["if this name is unavoidable, use backticks to escape it"] - ), + ) ], fixedSource: """ struct S { - @attrib #`class` #`struct` } """ ) } + func testWhitespaceBetweenPoundAndMacroName() { + assertParse( + """ + #1️⃣ myMacroName + """, + diagnostics: [ + DiagnosticSpec(message: "extraneous whitespace after '#' is not permitted", fixIts: ["remove whitespace"]) + ], + fixedSource: """ + #myMacroName + """ + ) + + assertParse( + """ + #1️⃣ /*comment*/ myMacroName + """, + diagnostics: [ + DiagnosticSpec(message: "extraneous whitespace after '#' is not permitted", fixIts: ["remove whitespace"]) + ], + fixedSource: """ + #myMacroName + """ + ) + + assertParse( + """ + #1️⃣ + myMacroName + """, + diagnostics: [ + DiagnosticSpec(message: "expected identifier in macro expansion", fixIts: ["insert identifier"]) + ], + fixedSource: """ + #<#identifier#> + myMacroName + """ + ) + + assertParse( + """ + struct Foo { + #1️⃣ myMacroName + } + """, + diagnostics: [ + DiagnosticSpec(message: "extraneous whitespace after '#' is not permitted", fixIts: ["remove whitespace"]) + ], + fixedSource: """ + struct Foo { + #myMacroName + } + """ + ) + + assertParse( + """ + struct Foo { + #1️⃣ + 2️⃣myMacroName3️⃣ + } + """, + diagnostics: [ + DiagnosticSpec(locationMarker: "1️⃣", message: "expected identifier in macro expansion", fixIts: ["insert identifier"]), + DiagnosticSpec(locationMarker: "2️⃣", message: "expected 'func' in function", fixIts: ["insert 'func'"]), + DiagnosticSpec(locationMarker: "3️⃣", message: "expected parameter clause in function signature", fixIts: ["insert parameter clause"]), + ], + fixedSource: """ + struct Foo { + #<#identifier#> + func + myMacroName() + } + """ + ) + } + func testVariableDeclWithGetSetButNoBrace() { assertParse( """ diff --git a/Tests/SwiftParserTest/RegexLiteralTests.swift b/Tests/SwiftParserTest/RegexLiteralTests.swift index 7804b678b4a..14c28ca1fae 100644 --- a/Tests/SwiftParserTest/RegexLiteralTests.swift +++ b/Tests/SwiftParserTest/RegexLiteralTests.swift @@ -751,10 +751,9 @@ final class RegexLiteralTests: XCTestCase { } func testBinOpDisambiguation26() { - // FIXME: The diagnostic should be one character back assertParse( """ - # 1️⃣/^ x/ + #1️⃣ /^ x/ """, diagnostics: [ DiagnosticSpec(message: "expected identifier in macro expansion", fixIts: ["insert identifier"]) diff --git a/Tests/SwiftParserTest/translated/IfconfigExprTests.swift b/Tests/SwiftParserTest/translated/IfconfigExprTests.swift index 3a1a01d58db..76e8f733c3b 100644 --- a/Tests/SwiftParserTest/translated/IfconfigExprTests.swift +++ b/Tests/SwiftParserTest/translated/IfconfigExprTests.swift @@ -524,23 +524,22 @@ final class IfconfigExprTests: XCTestCase { ) } - // FIXME: Diagnostics could be better. func testIfConfigExpr35() { assertParse( """ #if MY_FLAG - # 1️⃣elif + #1️⃣ elif #endif """, diagnostics: [ DiagnosticSpec( - message: "expected identifier in macro expansion", - fixIts: ["insert identifier"] + message: "extraneous whitespace after '#' is not permitted", + fixIts: ["remove whitespace"] ) ], fixedSource: """ #if MY_FLAG - #<#identifier#>elif + #elif #endif """ )