Skip to content

Add diagnostic for misplaced specifiers #947

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

Merged
merged 2 commits into from
Oct 17, 2022
Merged
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
12 changes: 9 additions & 3 deletions Sources/SwiftParser/Declarations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1313,7 +1313,7 @@ extension Parser {
switch self.at(anyIn: ParameterModifier.self) {
case (._const, let handle)?:
elements.append(RawDeclModifierSyntax(name: self.eat(handle), detail: nil, arena: self.arena))
case (.isolated, let handle)? where !self.lookahead().startsParameterName(subject.isClosure):
case (.isolated, let handle)? where self.withLookahead({ !$0.startsParameterName(isClosure: subject.isClosure, allowMisplacedSpecifierRecovery: false) }):
elements.append(RawDeclModifierSyntax(name: self.eat(handle), detail: nil, arena: self.arena))
default:
break MODIFIER_LOOP
Expand Down Expand Up @@ -1351,13 +1351,18 @@ extension Parser {

let modifiers = parseParameterModifiers(for: subject)

var misplacedSpecifiers: [RawTokenSyntax] = []
while let specifier = self.consume(ifAnyIn: TypeSpecifier.self) {
misplacedSpecifiers.append(specifier)
}

let firstName: RawTokenSyntax?
let secondName: RawTokenSyntax?
let unexpectedBeforeColon: RawUnexpectedNodesSyntax?
let colon: RawTokenSyntax?
let shouldParseType: Bool

if self.lookahead().startsParameterName(subject.isClosure) {
if self.withLookahead({ $0.startsParameterName(isClosure: subject.isClosure, allowMisplacedSpecifierRecovery: false) }) {
if self.currentToken.canBeArgumentLabel {
firstName = self.parseArgumentLabel()
} else {
Expand Down Expand Up @@ -1387,7 +1392,7 @@ extension Parser {

let type: RawTypeSyntax?
if shouldParseType {
type = self.parseType()
type = self.parseType(misplacedSpecifiers: misplacedSpecifiers)
Copy link
Contributor

Choose a reason for hiding this comment

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

This name is somewhat confusing since function types can also have specifiers, but this is for the type specifiers specifically.

} else {
type = nil
}
Expand All @@ -1411,6 +1416,7 @@ extension Parser {
elements.append(RawFunctionParameterSyntax(
attributes: attrs,
modifiers: modifiers,
RawUnexpectedNodesSyntax(misplacedSpecifiers, arena: self.arena),
firstName: firstName,
secondName: secondName,
unexpectedBeforeColon,
Expand Down
40 changes: 34 additions & 6 deletions Sources/SwiftParser/Diagnostics/ParseDiagnosticsGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -179,9 +179,9 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
if node.presence == .missing {
// If there is an unexpected token in front of the identifier, we assume
// that this unexpected token was intended to be the identifier we are missing.
if case .identifier = node.tokenKind,
let invalidIdentifier = node.previousToken(viewMode: .all),
let previousParent = invalidIdentifier.parent?.as(UnexpectedNodesSyntax.self) {
if node.tokenKind.isIdentifier,
let invalidIdentifier = node.previousToken(viewMode: .all),
let previousParent = invalidIdentifier.parent?.as(UnexpectedNodesSyntax.self) {
let fixIts: [FixIt]
if invalidIdentifier.tokenKind.isKeyword {
fixIts = [FixIt(message: .wrapKeywordInBackticks, changes: [
Expand Down Expand Up @@ -273,9 +273,7 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
return
}
let message: DiagnosticMessage?
if let identifier = unexpected.onlyToken(where: {
if case .identifier = $0.tokenKind { return true } else { return false }
}) {
if let identifier = unexpected.onlyToken(where: { $0.tokenKind.isIdentifier }) {
message = IdentifierNotAllowedInOperatorName(identifier: identifier)
} else if let tokens = unexpected.onlyTokens(satisfying: { _ in true }) {
message = TokensNotAllowedInOperatorName(tokens: tokens)
Expand Down Expand Up @@ -459,6 +457,21 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
return .visitChildren
}

public override func visit(_ node: FunctionParameterSyntax) -> SyntaxVisitorContinueKind {
if shouldSkip(node) {
return .skipChildren
}
exchangeTokens(
unexpected: node.unexpectedBetweenModifiersAndFirstName,
unexpectedTokenCondition: { TypeSpecifier(token: $0) != nil },
correctTokens: [node.type?.as(AttributedTypeSyntax.self)?.specifier],
message: { SpecifierOnParameterName(misplacedSpecifiers: $0) },
moveFixIt: { MoveTokensInFrontOfTypeFixIt(movedTokens: $0) },
removeRedundantFixIt: { RemoveRedundantFixIt(removeTokens: $0) }
)
return .visitChildren
}

public override func visit(_ node: PrecedenceGroupAssignmentSyntax) -> SyntaxVisitorContinueKind {
if shouldSkip(node) {
return .skipChildren
Expand Down Expand Up @@ -537,6 +550,21 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
return .visitChildren
}

public override func visit(_ node: TupleTypeElementSyntax) -> SyntaxVisitorContinueKind {
if shouldSkip(node) {
return .skipChildren
}
exchangeTokens(
unexpected: node.unexpectedBetweenInOutAndName,
unexpectedTokenCondition: { TypeSpecifier(token: $0) != nil },
correctTokens: [node.type.as(AttributedTypeSyntax.self)?.specifier],
message: { SpecifierOnParameterName(misplacedSpecifiers: $0) },
moveFixIt: { MoveTokensInFrontOfTypeFixIt(movedTokens: $0) },
removeRedundantFixIt: { RemoveRedundantFixIt(removeTokens: $0) }
)
return .visitChildren
}

public override func visit(_ node: TypeInitializerClauseSyntax) -> SyntaxVisitorContinueKind {
if shouldSkip(node) {
return .skipChildren
Expand Down
17 changes: 17 additions & 0 deletions Sources/SwiftParser/Diagnostics/ParserDiagnosticMessages.swift
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,14 @@ public struct MissingAttributeArgument: ParserError {
}
}

public struct SpecifierOnParameterName: ParserError {
public let misplacedSpecifiers: [TokenSyntax]

public var message: String {
return "\(nodesDescription(misplacedSpecifiers, format: false)) before a parameter name is not allowed"
}
}

public struct TokensNotAllowedInOperatorName: ParserError {
public let tokens: [TokenSyntax]

Expand Down Expand Up @@ -230,6 +238,15 @@ public struct MoveTokensInFrontOfFixIt: ParserFixIt {
}
}

public struct MoveTokensInFrontOfTypeFixIt: ParserFixIt {
/// The token that should be moved
public let movedTokens: [TokenSyntax]

public var message: String {
"move \(nodesDescription(movedTokens, format: false)) in front of type"
}
}

public struct RemoveRedundantFixIt: ParserFixIt {
public let removeTokens: [TokenSyntax]

Expand Down
11 changes: 11 additions & 0 deletions Sources/SwiftParser/Diagnostics/SyntaxExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,14 @@ extension SyntaxProtocol {
return nil
}
}

extension TokenKind {
var isIdentifier: Bool {
switch self {
case .identifier:
return true
default:
return false
}
}
}
5 changes: 5 additions & 0 deletions Sources/SwiftParser/Lookahead.swift
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ extension Parser {
public func lookahead() -> Lookahead {
return Lookahead(cloning: self)
}

public func withLookahead<T>(_ body: (_: inout Lookahead) -> T) -> T {
var lookahead = lookahead()
return body(&lookahead)
}
}

extension Parser.Lookahead {
Expand Down
8 changes: 3 additions & 5 deletions Sources/SwiftParser/Names.swift
Original file line number Diff line number Diff line change
Expand Up @@ -237,15 +237,13 @@ extension Parser.Lookahead {

extension Lexer.Lexeme {
var canBeArgumentLabel: Bool {
switch self.tokenKind {
case .identifier where self.tokenText == "__shared" || self.tokenText == "__owned":
if TypeSpecifier(lexeme: self) != nil {
return false
}
switch self.tokenKind {
case .identifier, .wildcardKeyword:
// Identifiers, escaped identifiers, and '_' can be argument labels.
return true
case .inoutKeyword:
// inout cannot be used as an argument label.
return false
default:
// All other keywords can be argument labels.
return self.isKeyword
Expand Down
15 changes: 10 additions & 5 deletions Sources/SwiftParser/Patterns.swift
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,13 @@ extension Parser.Lookahead {

/// Determine whether we are at the start of a parameter name when
/// parsing a parameter.
func startsParameterName(_ isClosure: Bool) -> Bool {
/// If `allowMisplacedSpecifierRecovery` is `true`, then this will skip over any type
/// specifiers before checking whether this lookahead starts a parameter name.
mutating func startsParameterName(isClosure: Bool, allowMisplacedSpecifierRecovery: Bool) -> Bool {
if allowMisplacedSpecifierRecovery {
while self.consume(ifAnyIn: TypeSpecifier.self) != nil {}
}

// To have a parameter name here, we need a name.
guard self.currentToken.canBeArgumentLabel else {
return false
Expand All @@ -341,12 +347,11 @@ extension Parser.Lookahead {
// so look ahead one more token (two total) see if we have a ':' that would
// indicate that this is an argument label.
do {
var backtrack = self.lookahead()
if backtrack.at(.colon) {
if self.at(.colon) {
return true // isolated :
}
backtrack.consumeAnyToken()
return backtrack.currentToken.canBeArgumentLabel && backtrack.peek().tokenKind == .colon
self.consumeAnyToken()
return self.currentToken.canBeArgumentLabel && self.peek().tokenKind == .colon
}
}

Expand Down
40 changes: 40 additions & 0 deletions Sources/SwiftParser/RawTokenKindSubset.swift
Original file line number Diff line number Diff line change
Expand Up @@ -851,6 +851,46 @@ enum PrimaryExpressionStart: RawTokenKindSubset {
}
}

enum TypeSpecifier: RawTokenKindSubset {
case inoutKeyword
case owned
case shared

init?(lexeme: Lexer.Lexeme) {
switch (lexeme.tokenKind, lexeme.tokenText) {
case (.inoutKeyword, _): self = .inoutKeyword
case (.identifier, "__owned"): self = .owned
case (.identifier, "__shared"): self = .shared
default: return nil
}
}

init?(token: TokenSyntax) {
switch (token.tokenKind, token.text) {
case (.inoutKeyword, _): self = .inoutKeyword
case (.contextualKeyword, "__owned"): self = .owned
case (.contextualKeyword, "__shared"): self = .shared
default: return nil
}
}

var rawTokenKind: RawTokenKind {
switch self {
case .inoutKeyword: return .inoutKeyword
case .owned: return .identifier
case .shared: return .identifier
}
}

var contextualKeyword: SyntaxText? {
switch self {
case .inoutKeyword: return nil
case .owned: return "__owned"
case .shared: return "__shared"
}
}
}

/// Union of the following token kind subsets:
/// - `AwaitTry`
/// - `ExpressionPrefixOperator`
Expand Down
8 changes: 8 additions & 0 deletions Sources/SwiftParser/TokenConsumer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,14 @@ extension TokenConsumer {
return nil
}
}

mutating func consume<Subset: RawTokenKindSubset>(ifAnyIn subset: Subset.Type) -> Self.Token? {
if let (_, handle) = self.at(anyIn: subset) {
return self.eat(handle)
} else {
return nil
}
}
}

// MARK: Expecting Tokens (`expect`)
Expand Down
27 changes: 18 additions & 9 deletions Sources/SwiftParser/Types.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,8 @@ extension Parser {
/// type → self-type
/// type → '(' type ')'
@_spi(RawSyntax)
public mutating func parseType() -> RawTypeSyntax {
let type = self.parseTypeScalar()
public mutating func parseType(misplacedSpecifiers: [RawTokenSyntax] = []) -> RawTypeSyntax {
let type = self.parseTypeScalar(misplacedSpecifiers: misplacedSpecifiers)

// Parse pack expansion 'T...'.
if self.currentToken.isEllipsis {
Expand All @@ -47,8 +47,8 @@ extension Parser {
return type
}

mutating func parseTypeScalar() -> RawTypeSyntax {
let (specifier, attrList) = self.parseTypeAttributeList()
mutating func parseTypeScalar(misplacedSpecifiers: [RawTokenSyntax] = []) -> RawTypeSyntax {
let (specifier, attrList) = self.parseTypeAttributeList(misplacedSpecifiers: misplacedSpecifiers)
var base = RawTypeSyntax(self.parseSimpleOrCompositionType())
if self.lookahead().isAtFunctionTypeArrow() {
let firstEffect = self.parseEffectsSpecifier()
Expand Down Expand Up @@ -402,7 +402,11 @@ extension Parser {
let second: RawTokenSyntax?
let unexpectedBeforeColon: RawUnexpectedNodesSyntax?
let colon: RawTokenSyntax?
if self.lookahead().startsParameterName(false) {
var misplacedSpecifiers: [RawTokenSyntax] = []
if self.withLookahead({ $0.startsParameterName(isClosure: false, allowMisplacedSpecifierRecovery: true) }) {
while let specifier = self.consume(ifAnyIn: TypeSpecifier.self) {
misplacedSpecifiers.append(specifier)
}
first = self.parseArgumentLabel()
if let parsedColon = self.consume(if: .colon) {
second = nil
Expand All @@ -423,12 +427,13 @@ extension Parser {
colon = nil
}
// Parse the type annotation.
let type = self.parseType()
let type = self.parseType(misplacedSpecifiers: misplacedSpecifiers)
let ellipsis = self.currentToken.isEllipsis ? self.consumeAnyToken() : nil
let trailingComma = self.consume(if: .comma)
keepGoing = trailingComma != nil
elements.append(RawTupleTypeElementSyntax(
inOut: nil,
RawUnexpectedNodesSyntax(misplacedSpecifiers, arena: self.arena),
name: first,
secondName: second,
unexpectedBeforeColon,
Expand Down Expand Up @@ -593,7 +598,7 @@ extension Parser.Lookahead {

// If the tuple element starts with "ident :", then it is followed
// by a type annotation.
if self.startsParameterName(/*isClosure=*/false) {
if self.startsParameterName(isClosure: false, allowMisplacedSpecifierRecovery: false) {
self.consumeAnyToken()
if self.currentToken.canBeArgumentLabel {
self.consumeAnyToken()
Expand Down Expand Up @@ -783,8 +788,12 @@ extension Parser.Lookahead {

extension Parser {
@_spi(RawSyntax)
public mutating func parseTypeAttributeList() -> (RawTokenSyntax?, RawAttributeListSyntax?) {
let specifier = self.consume(ifAny: [.inoutKeyword], contextualKeywords: ["__shared", "__owned"])
public mutating func parseTypeAttributeList(misplacedSpecifiers: [RawTokenSyntax] = []) -> (RawTokenSyntax?, RawAttributeListSyntax?) {
var specifier: RawTokenSyntax? = self.consume(ifAnyIn: TypeSpecifier.self)
// We can only stick one specifier on this type. Let's pick the first one
Copy link
Contributor

Choose a reason for hiding this comment

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

I feel the same way about this PR as I do about mine. I'm not sure it's worth keeping around all the state in order to allow adding the "missing" specifier here.

Also note that specifiers are parsed into RawAttributeListSyntax below, without being marked as unexpected and without the eventual fix-it. The current diagnostic (in the actual compiler) isn't great about this either, we end up with a '<specifier>' may only be used on parameters diagnostic and only recover from a single specifier after the attribute list and after that lose the type.

Whatever we end up doing here, it would be nice to be consistent across All the Things (where "things" here are attributes/modifiers/specifiers). Ie. should we ever expect a single, or should we always allow a list? Do we diagnose moving them to their correct position, or just add them (parsed correctly) as unexpected nodes?

There are current clients (eg. SwiftLint) that check eg. the async keyword in a function declaration to determine if a function is async. Should that check pass if the async is in any position, or just the correct?

if specifier == nil, let misplacedSpecifier = misplacedSpecifiers.first {
specifier = missingToken(misplacedSpecifier.tokenKind, text: misplacedSpecifier.tokenText)
}

if self.at(any: [.atSign, .inoutKeyword]) {
return (specifier, self.parseTypeAttributeListPresent())
Copy link
Contributor

Choose a reason for hiding this comment

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

I can't add a comment where I want it - but TypeSpecifier could be used in the at below and in parseTypeAttributeListPresent (we change the name to modifiers there which is also interesting).

Expand Down
Loading