Skip to content

Commit 27a800f

Browse files
authored
Merge pull request swiftlang#147 from dylansturg/better_bad_code_errors
Use a location specific diagnostic message when the input is invalid.
2 parents d2e3fa2 + a1be70d commit 27a800f

File tree

6 files changed

+52
-33
lines changed

6 files changed

+52
-33
lines changed

Sources/SwiftFormat/SwiftFormatError.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13+
import SwiftSyntax
14+
1315
/// Errors that can be thrown by the `SwiftFormatter` and `SwiftLinter` APIs.
1416
public enum SwiftFormatError: Error {
1517

@@ -20,5 +22,5 @@ public enum SwiftFormatError: Error {
2022
case isDirectory
2123

2224
/// The file contains invalid or unrecognized Swift syntax and cannot be handled safely.
23-
case fileContainsInvalidSyntax
25+
case fileContainsInvalidSyntax(position: AbsolutePosition)
2426
}

Sources/SwiftFormat/SwiftFormatter.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,8 +91,8 @@ public final class SwiftFormatter {
9191
public func format<Output: TextOutputStream>(
9292
syntax: SourceFileSyntax, assumingFileURL url: URL?, to outputStream: inout Output
9393
) throws {
94-
guard isSyntaxValidForProcessing(Syntax(syntax)) else {
95-
throw SwiftFormatError.fileContainsInvalidSyntax
94+
if let position = firstInvalidSyntaxPosition(in: Syntax(syntax)) {
95+
throw SwiftFormatError.fileContainsInvalidSyntax(position: position)
9696
}
9797

9898
let assumedURL = url ?? URL(fileURLWithPath: "source")

Sources/SwiftFormat/SwiftLinter.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,8 @@ public final class SwiftLinter {
7575
/// - url: A file URL denoting the filename/path that should be assumed for this syntax tree.
7676
/// - Throws: If an unrecoverable error occurs when formatting the code.
7777
public func lint(syntax: SourceFileSyntax, assumingFileURL url: URL) throws {
78-
guard isSyntaxValidForProcessing(Syntax(syntax)) else {
79-
throw SwiftFormatError.fileContainsInvalidSyntax
78+
if let position = firstInvalidSyntaxPosition(in: Syntax(syntax)) {
79+
throw SwiftFormatError.fileContainsInvalidSyntax(position: position)
8080
}
8181

8282
let context = Context(

Sources/SwiftFormat/SyntaxValidatingVisitor.swift

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,61 +14,62 @@ import SwiftSyntax
1414

1515
/// A SyntaxVisitor that searches for nodes that cannot be handled safely.
1616
fileprivate class SyntaxValidatingVisitor: SyntaxVisitor {
17-
/// Tracks whether an invalid node has been encountered.
18-
var isValidSyntax = true
17+
/// Stores the start position of the first node that contains invalid syntax.
18+
var invalidSyntaxStartPosition: AbsolutePosition?
1919

2020
override func visit(_ node: UnknownSyntax) -> SyntaxVisitorContinueKind {
21-
isValidSyntax = false
21+
invalidSyntaxStartPosition = node.positionAfterSkippingLeadingTrivia
2222
return .skipChildren
2323
}
2424

2525
override func visit(_ node: UnknownDeclSyntax) -> SyntaxVisitorContinueKind {
26-
isValidSyntax = false
26+
invalidSyntaxStartPosition = node.positionAfterSkippingLeadingTrivia
2727
return .skipChildren
2828
}
2929

3030
override func visit(_ node: UnknownExprSyntax) -> SyntaxVisitorContinueKind {
31-
isValidSyntax = false
31+
invalidSyntaxStartPosition = node.positionAfterSkippingLeadingTrivia
3232
return .skipChildren
3333
}
3434

3535
override func visit(_ node: UnknownStmtSyntax) -> SyntaxVisitorContinueKind {
36-
isValidSyntax = false
36+
invalidSyntaxStartPosition = node.positionAfterSkippingLeadingTrivia
3737
return .skipChildren
3838
}
3939

4040
override func visit(_ node: UnknownTypeSyntax) -> SyntaxVisitorContinueKind {
41-
isValidSyntax = false
41+
invalidSyntaxStartPosition = node.positionAfterSkippingLeadingTrivia
4242
return .skipChildren
4343
}
4444

4545
override func visit(_ node: UnknownPatternSyntax) -> SyntaxVisitorContinueKind {
46-
isValidSyntax = false
46+
invalidSyntaxStartPosition = node.positionAfterSkippingLeadingTrivia
4747
return .skipChildren
4848
}
4949

5050
override func visit(_ node: NonEmptyTokenListSyntax) -> SyntaxVisitorContinueKind {
51-
isValidSyntax = false
51+
invalidSyntaxStartPosition = node.positionAfterSkippingLeadingTrivia
5252
return .skipChildren
5353
}
5454

5555
override func visit(_ node: AttributeSyntax) -> SyntaxVisitorContinueKind {
5656
// The token list is used to collect any unexpected tokens. When it's missing or empty, then
5757
// there were no unexpected tokens. Otherwise, the attribute is invalid.
5858
guard node.tokenList?.isEmpty ?? true else {
59-
isValidSyntax = false
59+
invalidSyntaxStartPosition = node.positionAfterSkippingLeadingTrivia
6060
return .skipChildren
6161
}
6262
return .visitChildren
6363
}
6464
}
6565

66-
/// Returns whether the given syntax contains any nodes which are invalid or unrecognized and
67-
/// cannot be handled safely.
66+
/// Determines whether the given syntax has any nodes which are invalid or unrecognized, and, if
67+
/// so, returns the starting position of the first such node. Otherwise, returns nil indicating the
68+
/// syntax is valid.
6869
///
6970
/// - Parameter syntax: The root of a tree of syntax nodes to check for compatibility.
70-
func isSyntaxValidForProcessing(_ syntax: Syntax) -> Bool {
71+
func firstInvalidSyntaxPosition(in syntax: Syntax) -> AbsolutePosition? {
7172
let visitor = SyntaxValidatingVisitor()
7273
visitor.walk(syntax)
73-
return visitor.isValidSyntax
74+
return visitor.invalidSyntaxStartPosition
7475
}

Sources/swift-format/Run.swift

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,14 @@ func lintMain(
4848
diagnosticEngine.diagnose(
4949
Diagnostic.Message(.error, "Unable to lint \(path): file is not readable or does not exist."))
5050
return 1
51-
} catch SwiftFormatError.fileContainsInvalidSyntax {
52-
let path = assumingFileURL.path
53-
diagnosticEngine.diagnose(
54-
Diagnostic.Message(
55-
.error, "Unable to line \(path): file contains invalid or unrecognized Swift syntax."))
56-
return 1
57-
} catch {
51+
} catch SwiftFormatError.fileContainsInvalidSyntax(let position) {
52+
let path = assumingFileURL.path
53+
let location = SourceLocationConverter(file: path, source: source).location(for: position)
54+
diagnosticEngine.diagnose(
55+
Diagnostic.Message(.error, "file contains invalid or unrecognized Swift syntax."),
56+
location: location)
57+
return 1
58+
} catch {
5859
let path = assumingFileURL.path
5960
diagnosticEngine.diagnose(Diagnostic.Message(.error, "Unable to lint \(path): \(error)"))
6061
exit(1)
@@ -107,11 +108,12 @@ func formatMain(
107108
Diagnostic.Message(
108109
.error, "Unable to format \(path): file is not readable or does not exist."))
109110
return 1
110-
} catch SwiftFormatError.fileContainsInvalidSyntax {
111+
} catch SwiftFormatError.fileContainsInvalidSyntax(let position) {
111112
let path = assumingFileURL.path
113+
let location = SourceLocationConverter(file: path, source: source).location(for: position)
112114
diagnosticEngine.diagnose(
113-
Diagnostic.Message(
114-
.error, "Unable to format \(path): file contains invalid or unrecognized Swift syntax."))
115+
Diagnostic.Message(.error, "file contains invalid or unrecognized Swift syntax."),
116+
location: location)
115117
return 1
116118
} catch {
117119
let path = assumingFileURL.path

Tests/SwiftFormatTests/SyntaxValidatingVisitorTests.swift

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ final class SyntaxValidatingVisitorTests: XCTestCase {
1818
@unknown default: break
1919
}
2020
"""
21-
XCTAssertTrue(isSyntaxValidForProcessing(createSyntax(from: input)))
21+
XCTAssertNil(firstInvalidSyntaxPosition(in: createSyntax(from: input)))
2222
}
2323

2424
func testInvalidSyntax() {
@@ -28,7 +28,7 @@ final class SyntaxValidatingVisitorTests: XCTestCase {
2828
var bar = 0
2929
}
3030
"""
31-
XCTAssertFalse(isSyntaxValidForProcessing(createSyntax(from: input)))
31+
assertInvalidSyntax(in: input, atLine: 1, column: 1)
3232

3333
input =
3434
"""
@@ -37,17 +37,31 @@ final class SyntaxValidatingVisitorTests: XCTestCase {
3737
@unknown what_is_this default: break
3838
}
3939
"""
40-
XCTAssertFalse(isSyntaxValidForProcessing(createSyntax(from: input)))
40+
assertInvalidSyntax(in: input, atLine: 3, column: 1)
4141

4242
input =
4343
"""
4444
@unknown c class Foo {}
4545
"""
46-
XCTAssertFalse(isSyntaxValidForProcessing(createSyntax(from: input)))
46+
assertInvalidSyntax(in: input, atLine: 1, column: 1)
4747
}
4848

4949
/// Parses the given source into a syntax tree.
5050
private func createSyntax(from source: String) -> Syntax {
5151
return Syntax(try! SyntaxParser.parse(source: source))
5252
}
53+
54+
/// Asserts that `SyntaxValidatingVisitor` finds invalid syntax in the given source code at the
55+
/// given line and column.
56+
private func assertInvalidSyntax(
57+
in source: String, atLine: Int, column: Int, file: StaticString = #file, line: UInt = #line
58+
) {
59+
guard let position = firstInvalidSyntaxPosition(in: createSyntax(from: source)) else {
60+
XCTFail("No invalid syntax was found", file: file, line: line)
61+
return
62+
}
63+
let location = SourceLocationConverter(file: "", source: source).location(for: position)
64+
XCTAssertEqual(location.line, atLine, file: file, line: line)
65+
XCTAssertEqual(location.column, column, file: file, line: line)
66+
}
5367
}

0 commit comments

Comments
 (0)