Skip to content

Improve diagnostic in testMissingArgumentToAttribute #738

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 1 commit into from
Sep 27, 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
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ let package = Package(
),
.target(
name: "SwiftParser",
dependencies: ["SwiftDiagnostics", "SwiftSyntax"],
dependencies: ["SwiftBasicFormat", "SwiftDiagnostics", "SwiftSyntax"],
exclude: [
"CMakeLists.txt",
"README.md",
Expand Down
1 change: 1 addition & 0 deletions Sources/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
# See http://swift.org/LICENSE.txt for license information
# See http://swift.org/CONTRIBUTORS.txt for Swift project authors

add_subdirectory(SwiftBasicFormat)
add_subdirectory(SwiftSyntax)
add_subdirectory(SwiftDiagnostics)
add_subdirectory(SwiftParser)
Expand Down
27 changes: 27 additions & 0 deletions Sources/SwiftBasicFormat/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# This source file is part of the Swift.org open source project
#
# Copyright (c) 2014 - 2022 Apple Inc. and the Swift project authors
# Licensed under Apache License v2.0 with Runtime Library Exception
#
# See http://swift.org/LICENSE.txt for license information
# See http://swift.org/CONTRIBUTORS.txt for Swift project authors

add_library(SwiftBasicFormat STATIC
Trivia+Extension.swift
generated/Format.swift
)

target_link_libraries(SwiftBasicFormat PUBLIC
SwiftSyntax)

set_property(GLOBAL APPEND PROPERTY SWIFTSYNTAX_EXPORTS SwiftBasicFormat)

# NOTE: workaround for CMake not setting up include flags yet
set_target_properties(SwiftBasicFormat PROPERTIES
INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY})

install(TARGETS SwiftBasicFormat
EXPORT SwiftSyntaxTargets
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
RUNTIME DESTINATION bin)
15 changes: 11 additions & 4 deletions Sources/SwiftParser/Attributes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -685,11 +685,18 @@ extension Parser {
let (unexpectedBeforeAtSign, atSign) = self.expect(.atSign)
let (unexpectedBeforeDynamicReplacementToken, dynamicReplacementToken) = self.expectContextualKeyword("_dynamicReplacement")
let (unexpectedBeforeLeftParen, leftParen) = self.expect(.leftParen)
let (unexpectedBeforeLabel, label) = self.expect(.forKeyword)
let (unexpectedBeforeLabel, label) = self.expect(.forKeyword, remapping: .identifier)
let (unexpectedBeforeColon, colon) = self.expect(.colon)
let (base, args) = self.parseDeclNameRef([
.zeroArgCompoundNames, .keywordsUsingSpecialNames, .operators,
])
let base: RawTokenSyntax
let args: RawDeclNameArgumentsSyntax?
if label.isMissing && colon.isMissing && self.currentToken.isAtStartOfLine {
base = RawTokenSyntax(missing: .identifier, arena: self.arena)
args = nil
} else {
(base, args) = self.parseDeclNameRef([
.zeroArgCompoundNames, .keywordsUsingSpecialNames, .operators,
])
}
let method = RawDeclNameSyntax(declBaseName: RawSyntax(base), declNameArguments: args, arena: self.arena)
let (unexpectedBeforeRightParen, rightParen) = self.expect(.rightParen)
return RawAttributeSyntax(
Expand Down
2 changes: 2 additions & 0 deletions Sources/SwiftParser/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,14 @@ add_library(SwiftParser STATIC

Diagnostics/ParserDiagnosticMessages.swift
Diagnostics/ParseDiagnosticsGenerator.swift
Diagnostics/PresenceUtils.swift

gyb_generated/DeclarationAttribute.swift
gyb_generated/DeclarationModifier.swift
gyb_generated/TypeAttribute.swift)

target_link_libraries(SwiftParser PUBLIC
SwiftBasicFormat
SwiftSyntax
SwiftDiagnostics)

Expand Down
38 changes: 31 additions & 7 deletions Sources/SwiftParser/Diagnostics/ParseDiagnosticsGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -46,15 +46,20 @@ fileprivate extension FixIt.Change {
)
}

static func makePresent(node: TokenSyntax, leadingTrivia: Trivia = [], trailingTrivia: Trivia = []) -> FixIt.Change {
assert(node.presence == .missing)
static func makePresent<T: SyntaxProtocol>(node: T) -> FixIt.Change {
return .replace(
oldNode: Syntax(node),
newNode: Syntax(TokenSyntax(node.tokenKind, leadingTrivia: leadingTrivia, trailingTrivia: trailingTrivia, presence: .present))
newNode: PresentMaker().visit(Syntax(node))
)
}
}

fileprivate extension FixIt {
init(message: StaticParserFixIt, changes: [Change]) {
self.init(message: message as FixItMessage, changes: changes)
}
}

public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
private var diagnostics: [Diagnostic] = []

Expand Down Expand Up @@ -138,7 +143,11 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
let previousParent = invalidIdentifier.parent?.as(UnexpectedNodesSyntax.self) {
addDiagnostic(invalidIdentifier, InvalidIdentifierError(invalidIdentifier: invalidIdentifier), handledNodes: [previousParent.id])
} else {
addDiagnostic(node, MissingTokenError(missingToken: node))
addDiagnostic(node, MissingTokenError(missingToken: node), fixIts: [
FixIt(message: InsertTokenFixIt(missingToken: node), changes: [
.makePresent(node: node)
])
])
}
}
return .skipChildren
Expand Down Expand Up @@ -178,6 +187,21 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
return handleMissingSyntax(node)
}

public override func visit(_ node: AttributeSyntax) -> SyntaxVisitorContinueKind {
if shouldSkip(node) {
return .skipChildren
}
if let argument = node.argument, argument.isMissingAllTokens {
addDiagnostic(argument, MissingAttributeArgument(attributeName: node.attributeName), fixIts: [
FixIt(message: .insertAttributeArguments, changes: [
.makePresent(node: argument)
])
], handledNodes: [argument.id])
return .visitChildren
}
return .visitChildren
}

public override func visit(_ node: ForInStmtSyntax) -> SyntaxVisitorContinueKind {
if shouldSkip(node) {
return .skipChildren
Expand Down Expand Up @@ -219,17 +243,17 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
let unexpectedBeforeReturnType = output.unexpectedBetweenArrowAndReturnType,
let throwsInReturnPosition = unexpectedBeforeReturnType.tokens(withKind: .throwsKeyword).first {
addDiagnostic(throwsInReturnPosition, .throwsInReturnPosition, fixIts: [
FixIt(message: StaticParserFixIt.moveThrowBeforeArrow, changes: [
FixIt(message: .moveThrowBeforeArrow, changes: [
.makeMissing(node: throwsInReturnPosition),
.makePresent(node: missingThrowsKeyword, trailingTrivia: .space),
.makePresent(node: missingThrowsKeyword),
])
], handledNodes: [unexpectedBeforeReturnType.id, missingThrowsKeyword.id, throwsInReturnPosition.id])
return .visitChildren
}
return .visitChildren
}

override public func visit(_ node: ParameterClauseSyntax) -> SyntaxVisitorContinueKind {
public override func visit(_ node: ParameterClauseSyntax) -> SyntaxVisitorContinueKind {
if shouldSkip(node) {
return .skipChildren
}
Expand Down
42 changes: 30 additions & 12 deletions Sources/SwiftParser/Diagnostics/ParserDiagnosticMessages.swift
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ public extension ParserFixIt {
}
}

// MARK: - Static diagnostics
// MARK: - Errors (please sort alphabetically)

/// Please order the cases in this enum alphabetically by case name.
public enum StaticParserError: String, DiagnosticMessage {
Expand All @@ -104,16 +104,6 @@ public enum StaticParserError: String, DiagnosticMessage {
public var severity: DiagnosticSeverity { .error }
}

public enum StaticParserFixIt: String, FixItMessage {
case moveThrowBeforeArrow = "Move 'throws' before '->'"

public var message: String { self.rawValue }

public var fixItID: MessageID {
MessageID(domain: diagnosticDomain, id: "\(type(of: self)).\(self)")
}
}

// MARK: - Diagnostics (please sort alphabetically)

public struct ExtaneousCodeAtTopLevel: ParserError {
Expand Down Expand Up @@ -148,7 +138,7 @@ public struct MissingNodeError: ParserError {
var message: String
var hasNamedParent = false
if let parent = missingNode.parent,
let childName = parent.childNameForDiagnostics(missingNode.index) {
let childName = parent.childNameForDiagnostics(missingNode.index) {
message = "Expected \(childName)"
if let parentTypeName = parent.nodeTypeNameForDiagnostics(inherit: false) {
message += " of \(parentTypeName)"
Expand All @@ -173,6 +163,15 @@ public struct MissingNodeError: ParserError {
}
}

public struct MissingAttributeArgument: ParserError {
/// The name of the attribute that's missing the argument, without `@`.
public let attributeName: TokenSyntax

public var message: String {
return "Expected argument for '@\(attributeName)' attribute"
Copy link
Contributor

Choose a reason for hiding this comment

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

argument(s) IMO, same with the fix-it message.

}
}

public struct MissingTokenError: ParserError {
public let missingToken: TokenSyntax

Expand Down Expand Up @@ -226,3 +225,22 @@ public struct UnexpectedNodesError: ParserError {
return message
}
}

// MARK: - Fix-Its (please sort alphabetically)

public enum StaticParserFixIt: String, FixItMessage {
case insertAttributeArguments = "Insert attribute argument"
case moveThrowBeforeArrow = "Move 'throws' before '->'"

public var message: String { self.rawValue }

public var fixItID: MessageID {
MessageID(domain: diagnosticDomain, id: "\(type(of: self)).\(self)")
}
}

public struct InsertTokenFixIt: ParserFixIt {
let missingToken: TokenSyntax

public var message: String { "Insert '\(missingToken.text)'" }
}
63 changes: 63 additions & 0 deletions Sources/SwiftParser/Diagnostics/PresenceUtils.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
//===--- PresenceUtils.swift ----------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2022 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
//
//===----------------------------------------------------------------------===//

@_spi(RawSyntax) import SwiftSyntax
import SwiftBasicFormat

/// Walks a tree and checks whether the tree contained any present tokens.
class PresentNodeChecker: SyntaxAnyVisitor {
Copy link
Contributor

Choose a reason for hiding this comment

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

We should really introduce a .stop sooner rather than later 😅

var hasPresentToken: Bool = false

override func visitAny(_ node: Syntax) -> SyntaxVisitorContinueKind {
if hasPresentToken {
// If we already saw a present token, we don't need to continue.
return .skipChildren
} else {
return .visitChildren
}
}

override func visit(_ node: TokenSyntax) -> SyntaxVisitorContinueKind {
if node.presence == .present {
hasPresentToken = true
}
return .visitChildren
}
}

extension SyntaxProtocol {
/// Returns `true` if all tokens nodes in this tree are missing.
var isMissingAllTokens: Bool {
let checker = PresentNodeChecker(viewMode: .all)
checker.walk(Syntax(self))
return !checker.hasPresentToken
}
}

/// Transforms a syntax tree by making all missing tokens present.
class PresentMaker: SyntaxRewriter {
override func visit(_ token: TokenSyntax) -> Syntax {
if token.presence == .missing {
let presentToken: TokenSyntax
let (rawKind, text) = token.tokenKind.decomposeToRaw()
if let text = text, !text.isEmpty {
presentToken = TokenSyntax(token.tokenKind, presence: .present)
} else {
let newKind = TokenKind.fromRaw(kind: rawKind, text: rawKind.defaultText.map(String.init) ?? "<#\(rawKind.nameForDiagnostics)#>")
presentToken = TokenSyntax(newKind, presence: .present)
}
return Syntax(Format().format(syntax: presentToken))
Copy link
Contributor

Choose a reason for hiding this comment

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

Same comment about Format().format as the other PR, ie. it would be nice if this was an extension on the token instead. Maybe we can change that when fixing up Doug's comment in the original Format extraction PR though, so fine for it to be a follow up.

} else {
return Syntax(token)
}
}
}
13 changes: 10 additions & 3 deletions Sources/SwiftParser/Parser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -323,12 +323,19 @@ extension Parser {
/// a missing token of the requested kind.
@_spi(RawSyntax)
public mutating func expect(
_ kind: RawTokenKind
_ kind: RawTokenKind,
remapping: RawTokenKind? = nil
) -> (unexpected: RawUnexpectedNodesSyntax?, token: RawTokenSyntax) {
return expectImpl(
consume: { $0.consume(if: kind) },
consume: { $0.consume(if: kind, remapping: remapping) },
canRecoverTo: { $0.canRecoverTo([kind]) },
makeMissing: { $0.missingToken(kind, text: nil) }
makeMissing: {
if let remapping = remapping {
return $0.missingToken(remapping, text: kind.defaultText)
} else {
return $0.missingToken(kind, text: nil)
}
}
)
}

Expand Down
7 changes: 6 additions & 1 deletion Sources/SwiftParser/TokenConsumer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -139,10 +139,15 @@ extension TokenConsumer {
@_spi(RawSyntax)
public mutating func consume(
if kind: RawTokenKind,
remapping: RawTokenKind? = nil,
where condition: (Lexer.Lexeme) -> Bool = { _ in true}
) -> Token? {
if self.at(kind, where: condition) {
return self.consumeAnyToken()
if let remapping = remapping {
return self.consumeAnyToken(remapping: remapping)
} else {
return self.consumeAnyToken()
}
}
return nil
}
Expand Down
3 changes: 2 additions & 1 deletion Sources/SwiftSyntax/TokenKind.swift.gyb
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,8 @@ public enum RawTokenKind: Equatable, Hashable {
case ${token.swift_kind()}
% end

var defaultText: SyntaxText? {
@_spi(RawSyntax)
Copy link
Contributor

Choose a reason for hiding this comment

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

I was going to mention this in the other PR, but since this seems to extract this part... IMO we shouldn't be using RawSyntax in the diagnostic generator. It should be a general client, which I wouldn't expect to need it. In saying that, RawTokenKind seems somewhat useful to me, so perhaps it shouldn't be RawSyntax?

public var defaultText: SyntaxText? {
switch self {
case .eof: return ""
% for token in SYNTAX_TOKENS:
Expand Down
3 changes: 2 additions & 1 deletion Sources/SwiftSyntax/gyb_generated/TokenKind.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1259,7 +1259,8 @@ public enum RawTokenKind: Equatable, Hashable {
case stringInterpolationAnchor
case yield

var defaultText: SyntaxText? {
@_spi(RawSyntax)
public var defaultText: SyntaxText? {
switch self {
case .eof: return ""
case .associatedtypeKeyword: return "associatedtype"
Expand Down
15 changes: 9 additions & 6 deletions Tests/SwiftParserTest/Attributes.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,19 @@ final class AttributeTests: XCTestCase {
func testMissingArgumentToAttribute() {
AssertParse(
"""
@_dynamicReplacement(#^DIAG_1^#
@_dynamicReplacement(#^DIAG^#
func #^DIAG_2^#test_dynamic_replacement_for2() {
}
""",
diagnostics: [
DiagnosticSpec(locationMarker: "DIAG_1", message: "Expected 'for' in attribute argument"),
DiagnosticSpec(locationMarker: "DIAG_1", message: "Expected ':' in attribute argument"),
DiagnosticSpec(locationMarker: "DIAG_2", message: "Expected ')' to end attribute"),
DiagnosticSpec(locationMarker: "DIAG_2", message: "Expected declaration after attribute"),
]
DiagnosticSpec(message: "Expected argument for '@_dynamicReplacement' attribute", fixIts: ["Insert attribute argument"]),
DiagnosticSpec(message: "Expected ')' to end attribute", fixIts: ["Insert ')'"]),
],
fixedSource: """
@_dynamicReplacement(for: <#identifier#>)
func test_dynamic_replacement_for2() {
}
"""
)
}

Expand Down