Skip to content

Commit e1c3cf2

Browse files
committed
Improve module selector (and other) diagnostics
Add tailored diagnostics for unexpected module selectors which offer either one or two fix-its: • Remove the module selector • Convert `Foo::bar` to `bar = Foo::bar` (in certain declaration syntaxes) Making these messages clear required adding `nameForDiagnostics` properties to a bunch of children, which also impacted other existing diagnostics (for the better, IMHO). If we don’t like those changes or think they need more work, this commit can be severed from the rest of the PR.
1 parent b4ac41a commit e1c3cf2

15 files changed

+754
-297
lines changed

CodeGeneration/Sources/SyntaxSupport/DeclNodes.swift

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ public let DECL_NODES: [Node] = [
157157
Node(
158158
kind: .accessorParameters,
159159
base: .syntax,
160-
nameForDiagnostics: nil,
160+
nameForDiagnostics: "accessor parameter",
161161
traits: [
162162
"Parenthesized"
163163
],
@@ -209,6 +209,7 @@ public let DECL_NODES: [Node] = [
209209
Child(
210210
name: "name",
211211
kind: .token(choices: [.token(.identifier)]),
212+
nameForDiagnostics: "name",
212213
documentation: "The name of the actor. If the name matches a reserved keyword use backticks to escape it."
213214
),
214215
Child(
@@ -301,6 +302,7 @@ public let DECL_NODES: [Node] = [
301302
Child(
302303
name: "name",
303304
kind: .token(choices: [.token(.identifier)]),
305+
nameForDiagnostics: "name",
304306
documentation: "The name of this associated type."
305307
),
306308
Child(
@@ -389,6 +391,7 @@ public let DECL_NODES: [Node] = [
389391
Child(
390392
name: "name",
391393
kind: .token(choices: [.token(.identifier)]),
394+
nameForDiagnostics: "name",
392395
documentation: "The name of the class."
393396
),
394397
Child(
@@ -770,6 +773,7 @@ public let DECL_NODES: [Node] = [
770773
Child(
771774
name: "name",
772775
kind: .token(choices: [.token(.identifier)]),
776+
nameForDiagnostics: "name",
773777
documentation: "The name of this case."
774778
),
775779
Child(
@@ -833,6 +837,7 @@ public let DECL_NODES: [Node] = [
833837
Child(
834838
name: "name",
835839
kind: .token(choices: [.token(.identifier)]),
840+
nameForDiagnostics: "name",
836841
documentation:
837842
"Declares the name of this enum. If the name matches a reserved keyword use backticks to escape it."
838843
),
@@ -1009,6 +1014,7 @@ public let DECL_NODES: [Node] = [
10091014
.token(.prefixOperator),
10101015
.token(.postfixOperator),
10111016
]),
1017+
nameForDiagnostics: "name",
10121018
documentation: "The name of the function. If the name matches a reserved keyword use backticks to escape it."
10131019
),
10141020
Child(
@@ -1492,7 +1498,8 @@ public let DECL_NODES: [Node] = [
14921498
),
14931499
Child(
14941500
name: "name",
1495-
kind: .token(choices: [.token(.identifier)])
1501+
kind: .token(choices: [.token(.identifier)]),
1502+
nameForDiagnostics: "name"
14961503
),
14971504
Child(
14981505
name: "genericParameterClause",
@@ -1684,7 +1691,8 @@ public let DECL_NODES: [Node] = [
16841691
),
16851692
Child(
16861693
name: "name",
1687-
kind: .token(choices: [.token(.binaryOperator), .token(.prefixOperator), .token(.postfixOperator)])
1694+
kind: .token(choices: [.token(.binaryOperator), .token(.prefixOperator), .token(.postfixOperator)]),
1695+
nameForDiagnostics: "custom operator"
16881696
),
16891697
Child(
16901698
name: "operatorPrecedenceAndTypes",
@@ -1977,7 +1985,7 @@ public let DECL_NODES: [Node] = [
19771985
Node(
19781986
kind: .precedenceGroupDecl,
19791987
base: .decl,
1980-
nameForDiagnostics: "precedencegroup",
1988+
nameForDiagnostics: "precedence group",
19811989
documentation: "A Swift `precedencegroup` declaration.",
19821990
traits: [
19831991
"Braced",
@@ -2005,6 +2013,7 @@ public let DECL_NODES: [Node] = [
20052013
Child(
20062014
name: "name",
20072015
kind: .token(choices: [.token(.identifier)]),
2016+
nameForDiagnostics: "name",
20082017
documentation: "The name of this precedence group."
20092018
),
20102019
Child(
@@ -2125,6 +2134,7 @@ public let DECL_NODES: [Node] = [
21252134
Child(
21262135
name: "name",
21272136
kind: .token(choices: [.token(.identifier)]),
2137+
nameForDiagnostics: "name",
21282138
documentation: "The name of the protocol."
21292139
),
21302140
Child(
@@ -2305,6 +2315,7 @@ public let DECL_NODES: [Node] = [
23052315
Child(
23062316
name: "name",
23072317
kind: .token(choices: [.token(.identifier)]),
2318+
nameForDiagnostics: "name",
23082319
documentation:
23092320
"Declares the name of this struct. If the name matches a reserved keyword use backticks to escape it."
23102321
),
@@ -2471,7 +2482,8 @@ public let DECL_NODES: [Node] = [
24712482
),
24722483
Child(
24732484
name: "name",
2474-
kind: .token(choices: [.token(.identifier)])
2485+
kind: .token(choices: [.token(.identifier)]),
2486+
nameForDiagnostics: "name"
24752487
),
24762488
Child(
24772489
name: "genericParameterClause",

CodeGeneration/Sources/SyntaxSupport/ExprNodes.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -773,7 +773,7 @@ public let EXPR_NODES: [Node] = [
773773
Node(
774774
kind: .discardAssignmentExpr,
775775
base: .expr,
776-
nameForDiagnostics: nil,
776+
nameForDiagnostics: "wildcard expression",
777777
documentation: """
778778
A `_` that discards a value inside an assignment.
779779
@@ -1516,7 +1516,7 @@ public let EXPR_NODES: [Node] = [
15161516
Node(
15171517
kind: .nilLiteralExpr,
15181518
base: .expr,
1519-
nameForDiagnostics: nil,
1519+
nameForDiagnostics: "'nil' keyword",
15201520
children: [
15211521
Child(
15221522
name: "nilKeyword",
@@ -1886,7 +1886,7 @@ public let EXPR_NODES: [Node] = [
18861886
Node(
18871887
kind: .superExpr,
18881888
base: .expr,
1889-
nameForDiagnostics: nil,
1889+
nameForDiagnostics: "'super' keyword",
18901890
children: [
18911891
Child(
18921892
name: "superKeyword",

Sources/SwiftParserDiagnostics/ParseDiagnosticsGenerator.swift

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -497,6 +497,73 @@ public class ParseDiagnosticsGenerator: SyntaxAnyVisitor {
497497
],
498498
handledNodes: [node.id, otherNode.id]
499499
)
500+
} else if let (moduleName, colonColon) = node.twoPresentTokens(
501+
firstSatisfying: { $0.tokenKind.isIdentifier },
502+
secondSatisfying: { $0.tokenKind == .colonColon }
503+
) {
504+
// Looks like a module selector.
505+
var fixIts: [FixIt] = []
506+
507+
func canExplicitlyInitializeVariable() -> TokenSyntax? {
508+
if let capture = node.parent?.as(ClosureCaptureSyntax.self),
509+
capture.unexpectedBeforeSpecifier == node || capture.unexpectedBetweenSpecifierAndName == node,
510+
capture.initializer == nil
511+
{
512+
return capture.name
513+
}
514+
515+
if let pattern = node.parent?.as(IdentifierPatternSyntax.self),
516+
pattern.unexpectedBeforeIdentifier == node,
517+
pattern.parent?.is(OptionalBindingConditionSyntax.self) ?? false
518+
{
519+
return pattern.identifier
520+
}
521+
522+
return nil
523+
}
524+
525+
if let explicitVarName = canExplicitlyInitializeVariable() {
526+
// Suggest turning `Foo::bar` into `bar = Foo::bar`
527+
fixIts.append(
528+
FixIt(
529+
message: ExplicitlyInitializeFixIt(newVariableName: explicitVarName),
530+
changes: [
531+
FixIt.MultiNodeChange(
532+
.replace(
533+
oldNode: Syntax(node),
534+
newNode: Syntax(
535+
UnexpectedNodesSyntax(
536+
[
537+
Syntax(
538+
explicitVarName
539+
.with(\.leadingTrivia, moduleName.leadingTrivia)
540+
.with(\.trailingTrivia, [.spaces(1)])
541+
),
542+
Syntax(TokenSyntax.equalToken(trailingTrivia: [.spaces(1)])),
543+
Syntax(moduleName.with(\.leadingTrivia, [])),
544+
Syntax(colonColon),
545+
]
546+
)
547+
)
548+
)
549+
)
550+
]
551+
)
552+
)
553+
}
554+
// Suggest removing the module selector.
555+
fixIts.append(
556+
FixIt(
557+
message: RemoveNodesFixIt([Syntax(node)]),
558+
changes: [.makeMissing(node, transferTrivia: true)]
559+
)
560+
)
561+
addDiagnostic(
562+
node,
563+
CannotUseModuleSelectorError(unexpectedNodes: node),
564+
highlights: [Syntax(node)],
565+
fixIts: fixIts
566+
)
500567
} else {
501568
addDiagnostic(node, UnexpectedNodesError(unexpectedNodes: node), highlights: [Syntax(node)])
502569
}

Sources/SwiftParserDiagnostics/ParserDiagnosticMessages.swift

Lines changed: 67 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@
1313
#if compiler(>=6)
1414
public import SwiftDiagnostics
1515
@_spi(Diagnostics) internal import SwiftParser
16-
@_spi(RawSyntax) public import SwiftSyntax
16+
@_spi(ExperimentalLanguageFeatures) @_spi(RawSyntax) public import SwiftSyntax
1717
#else
1818
import SwiftDiagnostics
1919
@_spi(Diagnostics) import SwiftParser
20-
@_spi(RawSyntax) import SwiftSyntax
20+
@_spi(ExperimentalLanguageFeatures) @_spi(RawSyntax) import SwiftSyntax
2121
#endif
2222

2323
fileprivate let diagnosticDomain: String = "SwiftParser"
@@ -309,6 +309,63 @@ public struct CannotParseVersionTuple: ParserError {
309309
}
310310
}
311311

312+
public struct CannotUseModuleSelectorError: ParserError {
313+
public let unexpectedNodes: UnexpectedNodesSyntax
314+
315+
public var message: String {
316+
var qualifiedSyntaxDescription = "code"
317+
if let parent = unexpectedNodes.parent {
318+
if let parentDeclRef = parent.as(DeclReferenceExprSyntax.self),
319+
parentDeclRef.moduleSelector == nil,
320+
parentDeclRef.unexpectedBeforeModuleSelector == unexpectedNodes
321+
{
322+
// We only insert a module selector here if the `baseName` is some sort of exotic token that can't take a
323+
// module selector. Blame that token.
324+
qualifiedSyntaxDescription = self.exoticNameDescription(for: parentDeclRef.baseName)
325+
} else if let parentTypeName = parent.ancestorOrSelf(mapping: self.parentDescription(for:)) {
326+
qualifiedSyntaxDescription = parentTypeName
327+
// If not first child, and subsequent child has name, add it.
328+
let siblings = parent.children(viewMode: .sourceAccurate)
329+
if siblings.first(where: { $0.totalLength.utf8Length > 0 })?.id != unexpectedNodes.id,
330+
let nextSibling = siblings.suffix(from: siblings.index(of: self.unexpectedNodes)!).dropFirst().first,
331+
let nextSiblingChildName = nextSibling.childNameInParent
332+
{
333+
qualifiedSyntaxDescription += " \(nextSiblingChildName)"
334+
}
335+
} else if let parentTypeName = parent.ancestorOrSelf(mapping: self.parentDescription(for:)) {
336+
qualifiedSyntaxDescription = "code in \(parentTypeName)"
337+
}
338+
}
339+
340+
return "\(qualifiedSyntaxDescription) cannot be qualified with a module selector"
341+
}
342+
343+
private func parentDescription(for node: Syntax) -> String? {
344+
if let name = node.nodeTypeNameForDiagnostics(allowBlockNames: false) {
345+
return name
346+
}
347+
348+
// Special case for otherwise undistinguishable syntax inside an attribute's argument list.
349+
if node.keyPathInParent == \AttributeSyntax.arguments {
350+
return "attribute argument"
351+
}
352+
353+
return nil
354+
}
355+
356+
private func exoticNameDescription(for nameToken: TokenSyntax) -> String {
357+
switch nameToken.tokenKind {
358+
case .keyword(let keyword):
359+
return "'\(keyword.defaultText)' keyword"
360+
case .binaryOperator(let text), .dollarIdentifier(let text), .identifier(let text), .postfixOperator(let text),
361+
.prefixOperator(let text):
362+
return "\(nameToken.tokenKind.nameForDiagnostics) '\(text)'"
363+
default:
364+
return nameToken.tokenKind.nameForDiagnostics
365+
}
366+
}
367+
}
368+
312369
public struct DeclarationNotPermittedInContext: ParserError {
313370
public var missingDecl: MissingDeclSyntax
314371
public var invalidDecl: DeclSyntax
@@ -741,6 +798,14 @@ extension FixItMessage where Self == StaticParserFixIt {
741798
}
742799
}
743800

801+
public struct ExplicitlyInitializeFixIt: ParserFixIt {
802+
public let newVariableName: TokenSyntax
803+
804+
public var message: String {
805+
"explicitly initialize variable '\(newVariableName.text)'"
806+
}
807+
}
808+
744809
public struct InsertFixIt: ParserFixIt {
745810
public let tokenToBeInserted: TokenSyntax
746811

Sources/SwiftParserDiagnostics/SyntaxExtensions.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ extension SyntaxProtocol {
9090
if !allowBlockNames && (syntax.is(CodeBlockSyntax.self) || syntax.is(MemberBlockSyntax.self)) {
9191
return nil
9292
}
93+
if let relation = syntax.as(PrecedenceGroupRelationSyntax.self) {
94+
return "'\(relation.higherThanOrLowerThanLabel.text)' property of precedencegroup"
95+
}
9396
return syntax.kind.nameForDiagnostics
9497
}
9598

0 commit comments

Comments
 (0)