Skip to content

Commit e1ec258

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 469be07 commit e1ec258

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",
@@ -1678,7 +1685,8 @@ public let DECL_NODES: [Node] = [
16781685
),
16791686
Child(
16801687
name: "name",
1681-
kind: .token(choices: [.token(.binaryOperator), .token(.prefixOperator), .token(.postfixOperator)])
1688+
kind: .token(choices: [.token(.binaryOperator), .token(.prefixOperator), .token(.postfixOperator)]),
1689+
nameForDiagnostics: "custom operator"
16821690
),
16831691
Child(
16841692
name: "operatorPrecedenceAndTypes",
@@ -1971,7 +1979,7 @@ public let DECL_NODES: [Node] = [
19711979
Node(
19721980
kind: .precedenceGroupDecl,
19731981
base: .decl,
1974-
nameForDiagnostics: "precedencegroup",
1982+
nameForDiagnostics: "precedence group",
19751983
documentation: "A Swift `precedencegroup` declaration.",
19761984
traits: [
19771985
"Braced",
@@ -1999,6 +2007,7 @@ public let DECL_NODES: [Node] = [
19992007
Child(
20002008
name: "name",
20012009
kind: .token(choices: [.token(.identifier)]),
2010+
nameForDiagnostics: "name",
20022011
documentation: "The name of this precedence group."
20032012
),
20042013
Child(
@@ -2119,6 +2128,7 @@ public let DECL_NODES: [Node] = [
21192128
Child(
21202129
name: "name",
21212130
kind: .token(choices: [.token(.identifier)]),
2131+
nameForDiagnostics: "name",
21222132
documentation: "The name of the protocol."
21232133
),
21242134
Child(
@@ -2299,6 +2309,7 @@ public let DECL_NODES: [Node] = [
22992309
Child(
23002310
name: "name",
23012311
kind: .token(choices: [.token(.identifier)]),
2312+
nameForDiagnostics: "name",
23022313
documentation:
23032314
"Declares the name of this struct. If the name matches a reserved keyword use backticks to escape it."
23042315
),
@@ -2465,7 +2476,8 @@ public let DECL_NODES: [Node] = [
24652476
),
24662477
Child(
24672478
name: "name",
2468-
kind: .token(choices: [.token(.identifier)])
2479+
kind: .token(choices: [.token(.identifier)]),
2480+
nameForDiagnostics: "name"
24692481
),
24702482
Child(
24712483
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)