diff --git a/Sources/SwiftParser/Diagnostics/ParserDiagnosticMessages.swift b/Sources/SwiftParser/Diagnostics/ParserDiagnosticMessages.swift index e1cccde67d0..c3945037110 100644 --- a/Sources/SwiftParser/Diagnostics/ParserDiagnosticMessages.swift +++ b/Sources/SwiftParser/Diagnostics/ParserDiagnosticMessages.swift @@ -90,7 +90,7 @@ public enum StaticParserError: String, DiagnosticMessage { } public enum StaticParserFixIt: String, FixItMessage { - case moveThrowBeforeArrow = "Move 'throws' in before of '->'" + case moveThrowBeforeArrow = "Move 'throws' before '->'" public var message: String { self.rawValue } diff --git a/Sources/SwiftParser/SwiftParser.docc/FixingBugs.md b/Sources/SwiftParser/SwiftParser.docc/FixingBugs.md index 71dde081d9d..98a3df79b38 100644 --- a/Sources/SwiftParser/SwiftParser.docc/FixingBugs.md +++ b/Sources/SwiftParser/SwiftParser.docc/FixingBugs.md @@ -10,14 +10,13 @@ Once you’ve written a test case (see below), set a breakpoint in `Parser.parse 1. Add a new test case in `SwiftParserTest` that looks like the following ```swift - try AssertParse({ $0.parseSourceFile() }) { + AssertParse( """ <#your code that does not round trip#> """ - } + ) ``` 2. Run the test case, read the error message to figure out which part of the source file does not round-trip -3. Optional: Reduce the test case even further by deleting more source code and calling into a specific production of the parser instead of `Parser.parseSourceFile` ## Parse of Valid Source Failed @@ -28,20 +27,14 @@ Diagnostics are produced when the parsed syntax tree contains missing or unexpec 1. Add a test case in `SwiftParserTest` that looks like the following ```swift - let source = """ - <#your code that produces an invalid syntax tree#> - """ - - let tree = withParser(source: source) { - Syntax(raw: $0.parseSourceFile().raw) - } - XCTAssertHasSubstructure( - tree, - <#create a syntax node that you expect the tree to have#> + AssertParse( + """ + <#your code that produces an invalid syntax tree#> + """, + substructure: <#create a syntax node that you expect the tree to have#> ) ``` -2. Optional: Reduce the test case even further by deleting more source code and calling into a specific production of the parser instead of `Parser.parseSourceFile` -3. Run the test case and navigate the debugger to the place that produced the invalid syntax node. +2. Run the test case and navigate the debugger to the place that produced the invalid syntax node. ## Unhelpful Diagnostic Produced @@ -51,31 +44,29 @@ Unhelpful diagnostics can result from two reasons: To distinguish these cases run the following command and look at the dumped syntax tree. Use your own judgment to decide whether this models the intended meaning of the source code reasonably well. ``` -swift-parser-test print-tree /path/to/file/with/bad/diagnostic +swift-parser-test print-tree /path/to/file/with/unhelpful/diagnostic.swift ``` Fixing the first case where the parser does not recover according to the user’s intent is similar to [Parse of Valid Source Code Produced an Invalid Syntax Tree](#Parse-of-Valid-Source-Code-Produced-an-Invalid-Syntax-Tree). See for documentation how parser recovery works and determine how to recover better from the invalid source code. To add a new, more contextual diagnostic, perform the following steps. -1. Add a test case to `DiagnosticTests.swift` like the following: +1. Add a test case in `SwiftParserTest` that looks like the following ```swift - let source = """ - <#your code that produces a bad diagnostic#> - } - """ - let loop = withParser(source: source) { - Syntax(raw: $0.parserSourceFile().raw) - } + AssertParse( + """ + <#your code that produced the unhelpful diagnostic#> + """, + diagnostics: [ + DiagnosticSpec(message: "<#expected diagnostic message#>") + ] + ) ``` -2. Optional: Call a more specific production than `parseSourceFile` in the test case. +2. Mark the location at which you expect the diagnostic to be produced with `#^DIAG^#`. If you expect multiple diagnostics to be produced, you can use multiple of these markers with different names and use these markers by passing a `locationMarker` to `DiagnosticSpec`. 3. Determine which node encompasses all information that is necessary to produce the improved diagnostic – for example `FunctionSignatureSyntax` contains all information to diagnose if the `throws` keyword was written after the `->` instead of in front of it. 4. If the diagnostic message you want to emit does not exist yet, add a case to for the new diagnostic. 5. If the function does not already exist, write a new visit method on . 6. In that visitation method, detect the pattern for which the improved diagnostic should be emitted and emit it using `diagnostics.append`. 7. Mark the missing or garbage nodes that are covered by the new diagnostic as handled by adding their `SyntaxIdentifier`s to `handledNodes`. -8. Assert that the new diagnostic is emitted by addding the following to your test case: - ```swift - XCTAssertSingleDiagnostic(in: tree, line: <#expected line#>, column: <#expected column#>, expectedKind: .<#expected diagnostic kind#>) - ``` +8. If the diagnostic produces Fix-Its assert that they are generated by adding the Fix-It's message to the `DiagnosticSpec` with the `fixIt` parameter and asserting that applying the Fix-Its produces the correct source code by adding the `fixedSource` parameter to `AssertParse`. diff --git a/Sources/_SwiftSyntaxTestSupport/LocationMarkers.swift b/Sources/_SwiftSyntaxTestSupport/LocationMarkers.swift new file mode 100644 index 00000000000..29f53fdb27f --- /dev/null +++ b/Sources/_SwiftSyntaxTestSupport/LocationMarkers.swift @@ -0,0 +1,64 @@ +//===--- LocationMarkers.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 +// +//===----------------------------------------------------------------------===// + +/// Finds all marked ranges in the given text, see `Marker`. +fileprivate func findMarkedRanges(text: String) -> [Marker] { + var markers = [Marker]() + while let marker = nextMarkedRange(text: text, from: markers.last?.range.upperBound ?? text.startIndex) { + markers.append(marker) + } + return markers +} + +fileprivate func nextMarkedRange(text: String, from: String.Index) -> Marker? { + guard let start = text.range(of: "#^", range: from ..< text.endIndex), + let end = text.range(of: "^#", range: start.upperBound ..< text.endIndex) else { + return nil + } + + let markerRange = start.lowerBound ..< end.upperBound + let name = text[start.upperBound ..< end.lowerBound] + + // Expand to the whole line if the line only contains the marker + let lineRange = text.lineRange(for: start) + if text[lineRange].trimmingCharacters(in: .whitespacesAndNewlines) == text[markerRange] { + return Marker(name: name, range: lineRange) + } + return Marker(name: name, range: markerRange) +} + +fileprivate struct Marker { + /// The name of the marker without the `#^` and `^#` markup. + let name: Substring + /// The range of the marker. + /// + /// If the marker contains all the the non-whitepace characters on the line, + /// this is the range of the entire line. Otherwise it's the range of the + /// marker itself, including the `#^` and `^#` markup. + let range: Range +} + +public func extractMarkers(_ markedText: String) -> (markers: [String: Int], textWithoutMarkers: String) { + var text = "" + var markers = [String: Int]() + var lastIndex = markedText.startIndex + for marker in findMarkedRanges(text: markedText) { + text += markedText[lastIndex ..< marker.range.lowerBound] + lastIndex = marker.range.upperBound + + assert(markers[String(marker.name)] == nil, "Marker names must be unique") + markers[String(marker.name)] = text.utf8.count + } + text += markedText[lastIndex ..< markedText.endIndex] + + return (markers, text) +} diff --git a/Sources/_SwiftSyntaxTestSupport/Syntax+Assertions.swift b/Sources/_SwiftSyntaxTestSupport/Syntax+Assertions.swift index bf21d1f97b4..90d7c42d0c7 100644 --- a/Sources/_SwiftSyntaxTestSupport/Syntax+Assertions.swift +++ b/Sources/_SwiftSyntaxTestSupport/Syntax+Assertions.swift @@ -33,60 +33,6 @@ public func XCTAssertNextIsNil(_ iterator: inout Ite XCTAssertNil(iterator.next()) } -/// Verifies that the tree parsed from `actual` has the same structure as -/// `expected` when parsed with `parse`, ie. it has the same structure and -/// optionally the same trivia (if `includeTrivia` is set). -public func XCTAssertSameStructure( - _ actual: String, - parse: (String) throws -> Syntax, - _ expected: Syntax, - includeTrivia: Bool = false, - file: StaticString = #filePath, line: UInt = #line -) throws { - let actualTree = try parse(actual) - XCTAssertSameStructure(actualTree, expected, includeTrivia: includeTrivia, file: file, line: line) -} - -/// Verifies that two trees are equivalent, ie. they have the same structure -/// and optionally the same trivia if `includeTrivia` is set. -public func XCTAssertSameStructure( - _ actual: ActualTree, - _ expected: ExpectedTree, - includeTrivia: Bool = false, - file: StaticString = #filePath, line: UInt = #line -) - where ActualTree: SyntaxProtocol, ExpectedTree: SyntaxProtocol -{ - let diff = actual.findFirstDifference(baseline: expected, includeTrivia: includeTrivia) - XCTAssertNil(diff, diff!.debugDescription, file: file, line: line) -} - -/// See `SubtreeMatcher.assertSameStructure`. -public func XCTAssertHasSubstructure( - _ markedText: String, - parse: (String) throws -> Syntax, - afterMarker: String? = nil, - _ expected: ExpectedTree, - includeTrivia: Bool = false, - file: StaticString = #filePath, line: UInt = #line -) throws { - let subtreeMatcher = try SubtreeMatcher(markedText, parse: parse) - try subtreeMatcher.assertSameStructure(afterMarker: afterMarker, Syntax(expected), file: file, line: line) -} - -/// See `SubtreeMatcher.assertSameStructure`. -public func XCTAssertHasSubstructure( - _ actualTree: ActualTree, - _ expected: ExpectedTree, - includeTrivia: Bool = false, - file: StaticString = #filePath, line: UInt = #line -) throws - where ActualTree: SyntaxProtocol, ExpectedTree: SyntaxProtocol -{ - let subtreeMatcher = SubtreeMatcher(Syntax(actualTree)) - try subtreeMatcher.assertSameStructure(Syntax(expected), file: file, line: line) -} - /// Allows matching a subtrees of the given `markedText` against /// `baseline`/`expected` trees, where a combination of markers and the type /// of the `expected` tree is used to first find the subtree to match. Note @@ -135,24 +81,14 @@ public struct SubtreeMatcher { private var actualTree: Syntax public init(_ markedText: String, parse: (String) throws -> Syntax) throws { - var text = "" - var markers = [String: Int]() - var lastIndex = markedText.startIndex - for marker in findMarkedRanges(text: markedText) { - text += markedText[lastIndex ..< marker.range.lowerBound] - lastIndex = marker.range.upperBound - - assert(markers[String(marker.name)] == nil, "Marker names must be unique") - markers[String(marker.name)] = text.utf8.count - } - text += markedText[lastIndex ..< markedText.endIndex] + let (markers, text) = extractMarkers(markedText) self.markers = markers.isEmpty ? ["DEFAULT": 0] : markers self.actualTree = try parse(text) } - public init(_ actualTree: Syntax) { - self.markers = ["DEFAULT": 0] + public init(_ actualTree: Syntax, markers: [String: Int]) { + self.markers = markers.isEmpty ? ["DEFAULT": 0] : markers self.actualTree = actualTree } @@ -172,8 +108,8 @@ public struct SubtreeMatcher { return subtree.findFirstDifference(baseline: baseline, includeTrivia: includeTrivia) } - /// Same as `XCTAssertSameStructure`, but uses the subtree found from parsing - /// the text passed into `init(markedText:)` as the `actual` tree. + /// Verifies that the the subtree found from parsing the text passed into + /// `init(markedText:)` has the same structure as `expected`. public func assertSameStructure(afterMarker: String? = nil, _ expected: Syntax, includeTrivia: Bool = false, file: StaticString = #filePath, line: UInt = #line) throws { let diff = try findFirstDifference(afterMarker: afterMarker, baseline: expected, includeTrivia: includeTrivia) @@ -195,43 +131,6 @@ public enum SubtreeError: Error, CustomStringConvertible { } } -/// Finds all marked ranges in the given text, see `Marker`. -fileprivate func findMarkedRanges(text: String) -> [Marker] { - var markers = [Marker]() - while let marker = nextMarkedRange(text: text, from: markers.last?.range.upperBound ?? text.startIndex) { - markers.append(marker) - } - return markers -} - -fileprivate func nextMarkedRange(text: String, from: String.Index) -> Marker? { - guard let start = text.range(of: "#^", range: from ..< text.endIndex), - let end = text.range(of: "^#", range: start.upperBound ..< text.endIndex) else { - return nil - } - - let markerRange = start.lowerBound ..< end.upperBound - let name = text[start.upperBound ..< end.lowerBound] - - // Expand to the whole line if the line only contains the marker - let lineRange = text.lineRange(for: start) - if text[lineRange].trimmingCharacters(in: .whitespacesAndNewlines) == text[markerRange] { - return Marker(name: name, range: lineRange) - } - return Marker(name: name, range: markerRange) -} - -fileprivate struct Marker { - /// The name of the marker without the `#^` and `^#` markup. - let name: Substring - /// The range of the marker. - /// - /// If the marker contains all the the non-whitepace characters on the line, - /// this is the range of the entire line. Otherwise it's the range of the - /// marker itself, including the `#^` and `^#` markup. - let range: Range -} - fileprivate class SyntaxTypeFinder: SyntaxAnyVisitor { private let offset: Int private let type: SyntaxProtocol.Type diff --git a/Tests/SwiftParserTest/Assertions.swift b/Tests/SwiftParserTest/Assertions.swift index 42ea04bac8f..b1f756eec65 100644 --- a/Tests/SwiftParserTest/Assertions.swift +++ b/Tests/SwiftParserTest/Assertions.swift @@ -1,6 +1,8 @@ import XCTest @_spi(RawSyntax) import SwiftSyntax @_spi(Testing) @_spi(RawSyntax) import SwiftParser +import SwiftDiagnostics +import _SwiftSyntaxTestSupport // MARK: Lexing Assertions @@ -49,33 +51,165 @@ func AssertEqualTokens(_ actual: [Lexer.Lexeme], _ expected: [Lexer.Lexeme], fil // MARK: Parsing Assertions +/// An abstract data structure to describe how a diagnostic produced by the parser should look like. +struct DiagnosticSpec { + /// The name of a maker (of the form `#^DIAG^#`) in the source code that marks the location where the diagnostis should be produced. + let locationMarker: String + /// If not `nil`, assert that the diagnostic has the given ID. + let id: MessageID? + /// If not `nil`, assert that the diagnostic has the given message. + let message: String? + /// If not `nil`, assert that the highlighted range has this content. + let highlight: String? + /// If not `nil`, assert that the diagnostic contains fix-its with these messages. + /// Use the `fixedSource` parameter on `AssertParse` to check that applying the Fix-It yields the expected result. + let fixIts: [String]? + + init(locationMarker: String = "DIAG", id: MessageID? = nil, message: String?, highlight: String? = nil, fixIts: [String]? = nil) { + self.locationMarker = locationMarker + self.id = id + self.message = message + self.highlight = highlight + self.fixIts = fixIts + } +} + +class FixItApplier: SyntaxRewriter { + var changes: [FixIt.Change] + + init(diagnostics: [Diagnostic]) { + self.changes = diagnostics.flatMap({ $0.fixIts }).flatMap({ $0.changes }) + } + + public override func visitAny(_ node: Syntax) -> Syntax? { + for change in changes { + switch change { + case .replace(oldNode: let oldNode, newNode: let newNode) where oldNode.id == node.id: + return newNode + default: + break + } + } + return nil + } + + /// Applies all Fix-Its in `diagnostics` to `tree` and returns the fixed syntax tree. + public static func applyFixes(in diagnostics: [Diagnostic], to tree: T) -> Syntax { + let applier = FixItApplier(diagnostics: diagnostics) + return applier.visit(Syntax(tree)) + } +} + +/// Assert that the diagnostic `diag` produced in `tree` matches `spec`, +/// using `markerLocations` to translate markers to actual source locations. +func AssertDiagnostic( + _ diag: Diagnostic, + in tree: T, + markerLocations: [String: Int], + expected spec: DiagnosticSpec, + file: StaticString = #filePath, + line: UInt = #line +) { + if let markerLoc = markerLocations[spec.locationMarker] { + let locationConverter = SourceLocationConverter(file: "/test.swift", source: tree.description) + let actualLocation = diag.location(converter: locationConverter) + let expectedLocation = locationConverter.location(for: AbsolutePosition(utf8Offset: markerLoc)) + if let actualLine = actualLocation.line, + let actualColumn = actualLocation.column, + let expectedLine = expectedLocation.line, + let expectedColumn = expectedLocation.column { + XCTAssertEqual( + actualLine, expectedLine, + "Expected diagnostic on line \(expectedLine) but got \(actualLine)", + file: file, line: line + ) + XCTAssertEqual( + actualColumn, expectedColumn, + "Expected diagnostic on column \(expectedColumn) but got \(actualColumn)", + file: file, line: line + ) + } else { + XCTFail("Failed to resolve diagnostic location to line/column", file: file, line: line) + } + } else { + XCTFail("Did not find marker #^\(spec.locationMarker)^# in the source code", file: file, line: line) + } + if let id = spec.id { + XCTAssertEqual(diag.diagnosticID, id, file: file, line: line) + } + if let message = spec.message { + XCTAssertEqual(diag.message, message, file: file, line: line) + } + if let highlight = spec.highlight { + AssertStringsEqualWithDiff(diag.highlights.map(\.description).joined(), highlight, file: file, line: line) + } + if let fixIts = spec.fixIts { + XCTAssertEqual( + fixIts, diag.fixIts.map(\.message.message), + "Fix-Its for diagnostic did not match expected Fix-Its", + file: file, line: line + ) + } +} + +/// Parse `markedSource` and perform the following assertions: +/// - The parsed syntax tree should be printable back to the original source code (round-tripping) +/// - Parsing produced the given `diagnostics` (`diagnostics = []` asserts that the parse was successful) +/// - If `fixedSource` is not `nil`, assert that applying all fixes from the diagnostics produces `fixedSource` +/// The source file can be marked with markers of the form `#^DIAG^#` to mark source locations that can be referred to by `diagnostics`. +/// These markers are removed before parsing the source file. +/// By default, `DiagnosticSpec` asserts that the diagnostics is produced at a location marked by `#^DIAG^#`. +/// `parseSyntax` can be used to adjust the production that should be used as the entry point to parse the source code. +/// If `substructure` is not `nil`, asserts that the parsed syntax tree contains this substructure. func AssertParse( - _ parseSyntax: (inout Parser) -> Node, - allowErrors: Bool = false, + _ markedSource: String, + _ parseSyntax: (inout Parser) -> Node = { $0.parseSourceFile() }, + substructure expectedSubstructure: Syntax? = nil, + diagnostics expectedDiagnostics: [DiagnosticSpec] = [], + fixedSource expectedFixedSource: String? = nil, file: StaticString = #file, - line: UInt = #line, - _ source: () -> String -) throws { + line: UInt = #line +) { // Verify the parser can round-trip the source - let src = source() - var source = src - source.withUTF8 { buf in + let (markerLocations, source) = extractMarkers(markedSource) + var src = source + src.withUTF8 { buf in var parser = Parser(buf) withExtendedLifetime(parser) { - let parse = Syntax(raw: parseSyntax(&parser).raw) - AssertStringsEqualWithDiff("\(parse)", src, additionalInfo: """ + let tree = Syntax(raw: parseSyntax(&parser).raw) + + // Round-trip + AssertStringsEqualWithDiff("\(tree)", source, additionalInfo: """ + Source failed to round-trip. + Actual syntax tree: - \(parse.recursiveDescription) + \(tree.recursiveDescription) """, file: file, line: line) - if !allowErrors { - let diagnostics = ParseDiagnosticsGenerator.diagnostics(for: Syntax(raw: parse.raw)) - XCTAssertEqual( - diagnostics.count, 0, - """ - Received the following diagnostics while parsing the source code: - \(diagnostics) - """, - file: file, line: line) + + // Substructure + if let expectedSubstructure = expectedSubstructure { + let subtreeMatcher = SubtreeMatcher(Syntax(tree), markers: [:]) + do { + try subtreeMatcher.assertSameStructure(Syntax(expectedSubstructure), file: file, line: line) + } catch { + XCTFail("Matching for a subtree failed with error: \(error)", file: file, line: line) + } + } + + // Diagnostics + let diags = ParseDiagnosticsGenerator.diagnostics(for: tree) + XCTAssertEqual(diags.count, expectedDiagnostics.count, """ + Expected \(expectedDiagnostics.count) diagnostics but received \(diags.count): + \(diags.map(\.debugDescription).joined(separator: "\n")) + """, file: file, line: line) + for (diag, expectedDiag) in zip(diags, expectedDiagnostics) { + AssertDiagnostic(diag, in: tree, markerLocations: markerLocations, expected: expectedDiag, file: file, line: line) + } + + // Applying Fix-Its + if let expectedFixedSource = expectedFixedSource { + let fixedSource = FixItApplier.applyFixes(in: diags, to: tree).description + AssertStringsEqualWithDiff(fixedSource, expectedFixedSource, file: file, line: line) } } } diff --git a/Tests/SwiftParserTest/Attributes.swift b/Tests/SwiftParserTest/Attributes.swift new file mode 100644 index 00000000000..ddee6ef0fe6 --- /dev/null +++ b/Tests/SwiftParserTest/Attributes.swift @@ -0,0 +1,21 @@ +@_spi(RawSyntax) import SwiftSyntax +@_spi(RawSyntax) import SwiftParser +import XCTest + +final class AttributeTests: XCTestCase { + func testMissingArgumentToAttribute() { + AssertParse( + """ + @_dynamicReplacement(#^DIAG_1^# + func #^DIAG_2^#test_dynamic_replacement_for2() { + } + """, + diagnostics: [ + // FIXME: We should be complaining about the missing ')' for the attribute + DiagnosticSpec(locationMarker: "DIAG_1", message: "Expected 'for'"), + DiagnosticSpec(locationMarker: "DIAG_1", message: "Expected ':'"), + DiagnosticSpec(locationMarker: "DIAG_2", message: "Expected ')'"), + ] + ) + } +} diff --git a/Tests/SwiftParserTest/Availability.swift b/Tests/SwiftParserTest/Availability.swift index 534253e62b1..d0fbb88d3b9 100644 --- a/Tests/SwiftParserTest/Availability.swift +++ b/Tests/SwiftParserTest/Availability.swift @@ -4,7 +4,7 @@ import XCTest final class AvailabilityTests: XCTestCase { func testAvailableMember() throws { - try AssertParse({ $0.parseSourceFile() }) { + AssertParse( """ @available(OSX 10.0, introduced: 10.12) // expected-error@-1 {{'introduced' can't be combined with shorthand specification 'OSX 10.0'}} @@ -14,18 +14,19 @@ final class AvailabilityTests: XCTestCase { @available(iOS 6.0, OSX 10.8, *) func availableOnMultiplePlatforms() {} """ - } + ) - try AssertParse({ $0.parseClassDeclaration(.empty) }) { + AssertParse( """ class IncrementalParseTransition { @available(*, deprecated, message: "Use the initializer taking 'ConcurrentEdits' instead") public convenience init() {} } - """ - } + """, + { $0.parseClassDeclaration(.empty) } + ) - try AssertParse({ $0.parseSourceFile() }) { + AssertParse( """ extension String { @available(macOS 10.15.4, iOS 13.4, watchOS 6.2, tvOS 13.4, *) @@ -35,9 +36,9 @@ final class AvailabilityTests: XCTestCase { public func fiddle() { } } """ - } + ) - try AssertParse({ $0.parseSourceFile() }) { + AssertParse( """ @available( iOSApplicationExtension, @@ -47,6 +48,6 @@ final class AvailabilityTests: XCTestCase { "Use something else because this is definitely deprecated.") func f2() {} """ - } + ) } } diff --git a/Tests/SwiftParserTest/Declarations.swift b/Tests/SwiftParserTest/Declarations.swift index 038985aafbe..96e2f5a0675 100644 --- a/Tests/SwiftParserTest/Declarations.swift +++ b/Tests/SwiftParserTest/Declarations.swift @@ -3,71 +3,41 @@ import XCTest final class DeclarationTests: XCTestCase { - func testImports() throws { - try AssertParse({ $0.parseImportDeclaration(.empty) }) { - "import Foundation" - } + func testImports() { + AssertParse("import Foundation") - try AssertParse({ $0.parseDeclaration() }) { - "@_spi(Private) import SwiftUI" - } + AssertParse("@_spi(Private) import SwiftUI") - try AssertParse({ $0.parseDeclaration() }) { - "@_exported import class Foundation.Thread" - } + AssertParse("@_exported import class Foundation.Thread") - try AssertParse({ $0.parseDeclaration() }) { - """ - @_private(sourceFile: "YetAnotherFile.swift") import Foundation - """ - } + AssertParse(#"@_private(sourceFile: "YetAnotherFile.swift") import Foundation"#) } - func testStructParsing() throws { - try AssertParse({ $0.parseStructDeclaration(.empty) }) { - """ - struct Foo { - } - """ - } + func testStructParsing() { + AssertParse("struct Foo {}") } - func testFuncParsing() throws { - try AssertParse({ $0.parseFuncDeclaration(.empty) }) { - """ - func foo() { - } - """ - } + func testFuncParsing() { + AssertParse("func foo() {}") - try AssertParse({ $0.parseFuncDeclaration(.empty) }) { - """ - func foo() -> Slice> { - } - """ - } + AssertParse("func foo() -> Slice> {}") - try AssertParse({ $0.parseSourceFile() }) { + AssertParse( """ func onEscapingAutoclosure(_ fn: @Sendable @autoclosure @escaping () -> Int) { } func onEscapingAutoclosure2(_ fn: @escaping @autoclosure @Sendable () -> Int) { } - func bar(_ : String) async throws -> [[String]: Array] {} + func bar(_ : String) async -> [[String]: Array] {} func tupleMembersFunc() -> (Type.Inner, Type2.Inner2) {} func myFun(var1: S) { // do stuff } """ - } + ) } - func testClassParsing() throws { - try AssertParse({ $0.parseClassDeclaration(.empty) }) { - """ - class Foo { - } - """ - } + func testClassParsing() { + AssertParse("class Foo {}") - try AssertParse({ $0.parseSourceFile() }) { + AssertParse( """ @dynamicMemberLookup @available(swift 4.0) public class MyClass { @@ -75,25 +45,21 @@ final class DeclarationTests: XCTestCase { let B: Double } """ - } + ) - try AssertParse({ $0.parseGenericParameters() }) { - "<@NSApplicationMain T: AnyObject>" - } + AssertParse( + "<@NSApplicationMain T: AnyObject>", + { $0.parseGenericParameters() } + ) } - func testActorParsing() throws { - try AssertParse({ $0.parseActorDeclaration(.empty) }) { - """ - actor Foo { - } - """ - } + func testActorParsing() { + AssertParse("actor Foo {}") - try AssertParse({ $0.parseActorDeclaration(.empty) }) { + AssertParse( """ actor Foo { - nonisolated init?() throws { + nonisolated init?() { for (x, y, z) in self.triples { assert(isSafe) } @@ -103,24 +69,15 @@ final class DeclarationTests: XCTestCase { } } """ - } + ) } - func testProtocolParsing() throws { - try AssertParse({ $0.parseProtocolDeclaration(.empty) }) { - """ - protocol Foo { - } - """ - } + func testProtocolParsing() { + AssertParse("protocol Foo {}") - try AssertParse({ $0.parseProtocolDeclaration(.empty) }) { - """ - protocol P { init() } - """ - } + AssertParse("protocol P { init() }") - try AssertParse({ $0.parseProtocolDeclaration(.empty) }) { + AssertParse( """ protocol P { associatedtype Foo: Bar where X.Y == Z.W.W.Self @@ -129,40 +86,37 @@ final class DeclarationTests: XCTestCase { subscript(index: Int) -> R } """ - } + ) } - func testVariableDeclarations() throws { - try AssertParse({ $0.parseDeclaration() }) { - """ - private unowned(unsafe) var foo: Int - """ - } + func testVariableDeclarations() { + AssertParse("private unowned(unsafe) var foo: Int") + + AssertParse("_ = foo?.description") - try AssertParse({ $0.parseSourceFile() }, allowErrors: true) { - "_ = foo/* */?.description" - } + AssertParse( + "_ = foo/* */?.description#^DIAG^#", + diagnostics: [ + DiagnosticSpec(message: "Expected ':' after '? ...' in ternary expression") + ] + ) - try AssertParse({ $0.parseLetOrVarDeclaration(.empty) }) { - "var a = Array?(from: decoder)" - } + AssertParse("var a = Array?(from: decoder)") - try AssertParse({ $0.parseSourceFile() }) { - "@Wrapper var café = 42" - } + AssertParse("@Wrapper var café = 42") - try AssertParse({ $0.parseLetOrVarDeclaration(.empty) }) { + AssertParse( """ var x: T { - get async throws { + get async { foo() bar() } } """ - } + ) - try AssertParse({ $0.parseLetOrVarDeclaration(.empty) }) { + AssertParse( """ var foo: Int { _read { @@ -174,40 +128,28 @@ final class DeclarationTests: XCTestCase { } } """ - } + ) - try AssertParse({ $0.parseSourceFile() }) { + AssertParse( """ async let a = fetch("1.jpg") async let b: Image = fetch("2.jpg") async let secondPhotoToFetch = fetch("3.jpg") async let theVeryLastPhotoWeWant = fetch("4.jpg") """ - } + ) } - func testTypealias() throws { - try AssertParse({ $0.parseTypealiasDeclaration(.empty) }) { - """ - typealias Foo = Int - """ - } + func testTypealias() { + AssertParse("typealias Foo = Int") - try AssertParse({ $0.parseTypealiasDeclaration(.empty) }) { - """ - typealias MyAlias = (_ a: Int, _ b: Double, _ c: Bool, _ d: String) -> Bool - """ - } + AssertParse("typealias MyAlias = (_ a: Int, _ b: Double, _ c: Bool, _ d: String) -> Bool") - try AssertParse({ $0.parseSourceFile() }) { - """ - typealias A = @attr1 @attr2(hello) (Int) -> Void - """ - } + AssertParse("typealias A = @attr1 @attr2(hello) (Int) -> Void") } - func testPrecedenceGroup() throws { - try AssertParse({ $0.parsePrecedenceGroupDeclaration(.empty) }) { + func testPrecedenceGroup() { + AssertParse( """ precedencegroup FooGroup { higherThan: Group1, Group2 @@ -216,28 +158,24 @@ final class DeclarationTests: XCTestCase { assignment: false } """ - } + ) - try AssertParse({ $0.parsePrecedenceGroupDeclaration(.empty) }) { + AssertParse( """ precedencegroup FunnyPrecedence { associativity: left higherThan: MultiplicationPrecedence } """ - } + ) } - func testOperators() throws { - try AssertParse({ $0.parseDeclaration() }) { - """ - infix operator *-* : FunnyPrecedence - """ - } + func testOperators() { + AssertParse("infix operator *-* : FunnyPrecedence") } - func testObjCAttribute() throws { - try AssertParse({ $0.parseSourceFile() }) { + func testObjCAttribute() { + AssertParse( """ @objc( thisMethodHasAVeryLongName: @@ -246,11 +184,11 @@ final class DeclarationTests: XCTestCase { ) func f() {} """ - } + ) } - func testDifferentiableAttribute() throws { - try AssertParse({ $0.parseSourceFile() }) { + func testDifferentiableAttribute() { + AssertParse( """ @differentiable(wrt: x where T: D) func foo(_ x: T) -> T {} @@ -264,23 +202,19 @@ final class DeclarationTests: XCTestCase { @differentiable(wrt: (x, y)) func foo(_ x: T) -> T {} """ - } + ) } - func testParsePoundError() throws { - try AssertParse({ $0.parsePoundDiagnosticDeclaration() }) { - #"#error("Unsupported platform")"# - } + func testParsePoundError() { + AssertParse(#"#error("Unsupported platform")"#) } - func testParsePoundWarning() throws { - try AssertParse({ $0.parsePoundDiagnosticDeclaration() }) { - #"#warning("Unsupported platform")"# - } + func testParsePoundWarning() { + AssertParse(#"#warning("Unsupported platform")"#) } - func testParseSpecializeAttribute() throws { - try AssertParse({ $0.parseSourceFile() }) { + func testParseSpecializeAttribute() { + AssertParse( #""" @_specialize(where T == Int, U == Float) mutating func exchangeSecond(_ u: U, _ t: T) -> (U, T) { @@ -337,9 +271,9 @@ final class DeclarationTests: XCTestCase { __consuming func __specialize__copyContents(initializing: Swift.UnsafeMutableBufferPointer) -> (Iterator, Int) { Builtin.unreachable() } } """# - } + ) - try AssertParse({ $0.parseSourceFile() }) { + AssertParse( """ @_specialize(where T: _Trivial(32), T: _Trivial(64), T: _Trivial, T: _RefCountedObject) @_specialize(where T: _Trivial, T: _Trivial(64)) @@ -350,20 +284,20 @@ final class DeclarationTests: XCTestCase { return 55555 } """ - } + ) } - func testParseDynamicReplacement() throws { - try AssertParse({ $0.parseDeclaration() }) { + func testParseDynamicReplacement() { + AssertParse( """ @_dynamicReplacement(for: dynamic_replaceable()) func replacement() { dynamic_replaceable() } """ - } + ) - try AssertParse({ $0.parseDeclaration() }) { + AssertParse( """ @_dynamicReplacement(for: subscript(_:)) subscript(x y: Int) -> Int { @@ -375,29 +309,29 @@ final class DeclarationTests: XCTestCase { } } """ - } + ) - try AssertParse({ $0.parseDeclaration() }) { + AssertParse( """ @_dynamicReplacement(for: dynamic_replaceable_var) var r : Int { return 0 } """ - } + ) - try AssertParse({ $0.parseDeclaration() }) { + AssertParse( """ @_dynamicReplacement(for: init(x:)) init(y: Int) { self.init(x: y + 1) } """ - } + ) } - func testEnumParsing() throws { - try AssertParse({ $0.parseEnumDeclaration(.empty) }) { + func testEnumParsing() { + AssertParse( """ enum Content { case keyPath(KeyPath) @@ -405,17 +339,341 @@ final class DeclarationTests: XCTestCase { case value(Value?) } """ - } + ) } - func testStandaloneModifier() throws { - try AssertParse({ $0.parseSourceFile() }) { + func testStandaloneModifier() { + AssertParse( """ struct a { public } """ - } + ) + } + + func testMissingColonInFunctionSignature() { + AssertParse( + "(first second #^DIAG^#Int)", + { $0.parseFunctionSignature() }, + diagnostics: [ + DiagnosticSpec(message: "Expected ':' in function parameter") + ] + ) + } + + func testExtraArgumentLabelsInFunctionSignature() { + AssertParse( + "(first second #^DIAG^#third fourth: Int)", + { $0.parseFunctionSignature() }, + diagnostics: [ + DiagnosticSpec(message: "Unexpected text 'third fourth' found in function parameter") + ] + ) + } + + func testMissingClosingParenInFunctionSignature() { + AssertParse( + "(first second: Int#^DIAG^#", + { $0.parseFunctionSignature() }, + diagnostics: [ + DiagnosticSpec(message: "Expected ')' to end parameter clause") + ] + ) + } + + func testMissingOpeningParenInFunctionSignature() { + AssertParse( + "#^DIAG^#first second: Int)", + { $0.parseFunctionSignature() }, + diagnostics: [ + DiagnosticSpec(message: "Expected '(' to start parameter clause") + ] + ) + } + + func testNoParamsForFunction() { + AssertParse( + """ + class MyClass { + func withoutParameters#^DIAG^# + + func withParameters() {} + } + """, + substructure: Syntax(FunctionDeclSyntax( + attributes: nil, + modifiers: nil, + funcKeyword: .funcKeyword(), + identifier: .identifier("withoutParameters"), + genericParameterClause: nil, + signature: FunctionSignatureSyntax( + input: ParameterClauseSyntax( + leftParen: .leftParenToken(presence: .missing), + parameterList: FunctionParameterListSyntax([]), + rightParen: .rightParenToken(presence: .missing) + ), + asyncOrReasyncKeyword: nil, + throwsOrRethrowsKeyword: nil, + output: nil + ), + genericWhereClause: nil, + body: nil + )), + diagnostics: [ + DiagnosticSpec(message: "Expected argument list in function declaration") + ] + ) + } + + func testThrowsInWrongLocation() { + AssertParse( + "() -> #^DIAG^#throws Int", + { $0.parseFunctionSignature() }, + diagnostics: [ + DiagnosticSpec(message: "'throws' may only occur before '->'", fixIts: ["Move 'throws' before '->'"]) + ], + fixedSource: "() throws -> Int" + ) + } + + func testExtraneousRightBraceRecovery() { + // FIXME: This test case should produce a diagnostics + AssertParse("class ABC { let def = ghi(jkl: mno) } }") + } + + func testMissingSubscriptReturnClause() { + AssertParse( + """ + struct Foo { + subscript(x: String) #^DIAG^#{} + } + """, + diagnostics: [ + // FIXME: This diagnostic should be more contextual + DiagnosticSpec(message: "Expected '->'") + ] + ) + } + + func testClassWithLeadingNumber() { + AssertParse( + """ + class #^DIAG^#23class { + // expected-error@-1 {{class name can only start with a letter or underscore, not a number}} + // expected-error@-2 {{'c' is not a valid digit in integer literal}} + func 24method() {} + // expected-error@-1 {{function name can only start with a letter or underscore, not a number}} + // expected-error@-2 {{'m' is not a valid digit in integer literal}} + } + """, + // FIXME: These are simply bad diagnostics. We should be complaining that identifiers cannot start with digits. + diagnostics: [ + DiagnosticSpec(message: "Expected '' in declaration"), + DiagnosticSpec(message: "Expected '{'"), + DiagnosticSpec(message: "Expected '}'"), + ] + ) + } + + func testAccessors() { + AssertParse( + """ + var bad1 : Int { + _read async { 0 } + } + """ + ) + + AssertParse( + """ + var bad2 : Int { + get reasync { 0 } + } + """ + ) + } + + func testExpressionMember() { + AssertParse( + """ + struct S { + #^DIAG^#/ ###line 25 "line-directive.swift" + } + """, + diagnostics: [ + // FIXME: The diagnostic should not contain a newline. + DiagnosticSpec( + message: """ + Unexpected text ' + / ###line 25 "line-directive.swift"' + """ + ) + ] + ) + } + + func testBogusProtocolRequirements() { + // FIXME: This test case should produce a diagnostics + AssertParse( + """ + protocol P { + var prop : Int { get bogus rethrows set } + } + """ + ) + } + + func testTextRecovery() { + AssertParse( + """ + Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do #^DIAG_1^#eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.#^DIAG_2^# + """, + diagnostics: [ + DiagnosticSpec(locationMarker: "DIAG_1", message: "Expected '{'"), + DiagnosticSpec(locationMarker: "DIAG_2", message: "Expected '}'"), + ] + ) + } + + func testRecoverOneExtraLabel() { + AssertParse( + "(first second #^DIAG^#third: Int)", + { $0.parseFunctionSignature() }, + substructure: Syntax(FunctionParameterSyntax( + attributes: nil, + firstName: TokenSyntax.identifier("first"), + secondName: TokenSyntax.identifier("second"), + UnexpectedNodesSyntax([Syntax(TokenSyntax.identifier("third"))]), + colon: TokenSyntax.colonToken(), + type: TypeSyntax(SimpleTypeIdentifierSyntax(name: TokenSyntax.identifier("Int"), genericArgumentClause: nil)), + ellipsis: nil, + defaultArgument: nil, + trailingComma: nil + )), + diagnostics: [ + DiagnosticSpec(message: "Unexpected text 'third' found in function parameter") + ] + ) + } + + func testRecoverTwoExtraLabels() { + AssertParse( + "(first second #^DIAG^#third fourth: Int)", + { $0.parseFunctionSignature() }, + substructure: Syntax(FunctionParameterSyntax( + attributes: nil, + firstName: TokenSyntax.identifier("first"), + secondName: TokenSyntax.identifier("second"), + UnexpectedNodesSyntax([Syntax(TokenSyntax.identifier("third")), Syntax(TokenSyntax.identifier("fourth"))]), + colon: TokenSyntax.colonToken(), + type: TypeSyntax(SimpleTypeIdentifierSyntax(name: TokenSyntax.identifier("Int"), genericArgumentClause: nil)), + ellipsis: nil, + defaultArgument: nil, + trailingComma: nil + )), + diagnostics: [ + DiagnosticSpec(message: "Unexpected text 'third fourth' found in function parameter") + ] + ) + } + + func testDontRecoverFromDeclKeyword() { + AssertParse( + "func foo(first second #^MISSING_COLON^#third #^MISSING_RPAREN^#struct#^MISSING_IDENTIFIER^##^BRACES^#: Int) {}", + substructure: Syntax(FunctionParameterSyntax( + attributes: nil, + firstName: .identifier("first"), + secondName: .identifier("second"), + colon: .colonToken(presence: .missing), + type: TypeSyntax(SimpleTypeIdentifierSyntax(name: .identifier("third"), genericArgumentClause: nil)), + ellipsis: nil, + defaultArgument: nil, + trailingComma: nil + )), + diagnostics: [ + DiagnosticSpec(locationMarker: "MISSING_COLON", message: "Expected ':' in function parameter"), + DiagnosticSpec(locationMarker: "MISSING_RPAREN", message: "Expected ')' to end parameter clause"), + // FIXME: We should issues something like "Expected identifier in declaration" + DiagnosticSpec(locationMarker: "MISSING_IDENTIFIER", message: "Expected '' in declaration"), + DiagnosticSpec(locationMarker: "BRACES", message: "Expected '{'"), + DiagnosticSpec(locationMarker: "BRACES", message: "Expected '}'"), + ] + ) + } + + func testRecoverFromParens() { + AssertParse( + "(first second #^DIAG^#[third fourth]: Int)", + { $0.parseFunctionSignature() }, + substructure: Syntax(FunctionParameterSyntax( + attributes: nil, + firstName: TokenSyntax.identifier("first"), + secondName: TokenSyntax.identifier("second"), + UnexpectedNodesSyntax([ + Syntax(TokenSyntax.leftSquareBracketToken()), + Syntax(TokenSyntax.identifier("third")), + Syntax(TokenSyntax.identifier("fourth")), + Syntax(TokenSyntax.rightSquareBracketToken()) + ]), + colon: TokenSyntax.colonToken(), + type: TypeSyntax(SimpleTypeIdentifierSyntax(name: TokenSyntax.identifier("Int"), genericArgumentClause: nil)), + ellipsis: nil, + defaultArgument: nil, + trailingComma: nil + )), + diagnostics: [ + DiagnosticSpec(message: "Unexpected text '[third fourth]' found in function parameter") + ] + ) + } + + func testDontRecoverFromUnbalancedParens() { + AssertParse( + "func foo(first second #^COLON^#[third #^RSQUARE_COLON^#fourth: Int) {}", + substructure: Syntax(FunctionParameterSyntax( + attributes: nil, + firstName: TokenSyntax.identifier("first"), + secondName: TokenSyntax.identifier("second"), + colon: TokenSyntax(.colon, presence: .missing), + type: TypeSyntax(ArrayTypeSyntax( + leftSquareBracket: TokenSyntax.leftSquareBracketToken(), + elementType: TypeSyntax(SimpleTypeIdentifierSyntax(name: TokenSyntax.identifier("third"), genericArgumentClause: nil)), + rightSquareBracket: TokenSyntax(.rightSquareBracket, presence: .missing) + )), + ellipsis: nil, + defaultArgument: nil, + trailingComma: nil + )), + diagnostics: [ + DiagnosticSpec(locationMarker: "COLON", message: "Expected ':' in function parameter"), + DiagnosticSpec(locationMarker: "RSQUARE_COLON" , message: "Expected ']' to end type"), + DiagnosticSpec(locationMarker: "RSQUARE_COLON", message: "Expected ')' to end parameter clause"), + ] + ) + } + + func testDontRecoverIfNewlineIsBeforeColon() { + AssertParse( + """ + func foo(first second #^COLON^#third#^RPAREN^# + : Int) {} + """, + substructure: Syntax(FunctionParameterSyntax( + attributes: nil, + firstName: TokenSyntax.identifier("first"), + secondName: TokenSyntax.identifier("second"), + colon: TokenSyntax(.colon, presence: .missing), + type: TypeSyntax(SimpleTypeIdentifierSyntax(name: TokenSyntax.identifier("third"), genericArgumentClause: nil)), + ellipsis: nil, + defaultArgument: nil, + trailingComma: nil + )), + diagnostics: [ + DiagnosticSpec(locationMarker: "COLON", message: "Expected ':' in function parameter"), + DiagnosticSpec(locationMarker: "RPAREN", message: "Expected ')' to end parameter clause"), + ] + ) } } diff --git a/Tests/SwiftParserTest/DiagnosticAssertions.swift b/Tests/SwiftParserTest/DiagnosticAssertions.swift deleted file mode 100644 index 3e0cf9b55a2..00000000000 --- a/Tests/SwiftParserTest/DiagnosticAssertions.swift +++ /dev/null @@ -1,90 +0,0 @@ -import SwiftDiagnostics -import SwiftSyntax -import SwiftParser -import XCTest -import _SwiftSyntaxTestSupport - -public class FixItApplier: SyntaxRewriter { - var changes: [FixIt.Change] - - init(diagnostics: [Diagnostic]) { - self.changes = diagnostics.flatMap({ $0.fixIts }).flatMap({ $0.changes }) - } - - public override func visitAny(_ node: Syntax) -> Syntax? { - for change in changes { - switch change { - case .replace(oldNode: let oldNode, newNode: let newNode) where oldNode.id == node.id: - return newNode - default: - break - } - } - return nil - } - - /// Applies all Fix-Its in `diagnostics` to `tree` and returns the fixed syntax tree. - public static func applyFixes(in diagnostics: [Diagnostic], to tree: T) -> Syntax { - let applier = FixItApplier(diagnostics: diagnostics) - return applier.visit(Syntax(tree)) - } -} - -/// Asserts that the diagnostics `diag` inside `tree` occurs at `line` and -/// `column`. -/// If `id` is not `nil`, assert that the diagnostic has the given message. -/// If `message` is not `nil`, assert that the diagnostic has the given message. -/// If `highlight` is not `nil`, assert that the highlighted range has this content. -func XCTAssertDiagnostic( - _ diag: Diagnostic, - in tree: T, - line: Int, - column: Int, - id: MessageID? = nil, - message: String? = nil, - highlight: String? = nil, - testFile: StaticString = #filePath, - testLine: UInt = #line -) { - let locationConverter = SourceLocationConverter(file: "/test.swift", source: tree.description) - let location = diag.location(converter: locationConverter) - XCTAssertEqual(location.line, line, "Expected diagnostic on line \(line) but got \(location.line ?? -1)", file: testFile, line: testLine) - XCTAssertEqual(location.column, column, "Expected diagnostic on column \(column) but got \(location.column ?? -1)", file: testFile, line: testLine) - if let id = id { - XCTAssertEqual(diag.diagnosticID, id, file: testFile, line: testLine) - } - if let message = message { - XCTAssertEqual(diag.message, message, file: testFile, line: testLine) - } - if let highlight = highlight { - AssertStringsEqualWithDiff(diag.highlights.map(\.description).joined(), highlight, file: testFile, line: testLine) - } -} - -/// Assert that producing diagnostics for `tree` generates a single diagnostic -/// at `line` and `column`. -/// If `message` is not `nil`, assert that the diagnostic has the given message. -/// If `id` is not `nil`, assert that the diagnostic has the given message. -/// If `expectedFixedSource` is not `nil`, assert that the source code that it is source code that result from applying all Fix-Its. -func XCTAssertSingleDiagnostic( - in tree: T, - line: Int, - column: Int, - id: MessageID? = nil, - message: String? = nil, - highlight: String? = nil, - expectedFixedSource: String? = nil, - testFile: StaticString = #filePath, - testLine: UInt = #line -) { - let diags = ParseDiagnosticsGenerator.diagnostics(for: tree) - guard diags.count == 1 else { - XCTFail("Received \(diags.count) diagnostics but expected excatly one: \(diags)", file: testFile, line: testLine) - return - } - XCTAssertDiagnostic(diags.first!, in: tree, line: line, column: column, id: id, message: message, highlight: highlight, testFile: testFile, testLine: testLine) - if let expectedFixedSource = expectedFixedSource { - let fixedSource = FixItApplier.applyFixes(in: diags, to: tree).description - AssertStringsEqualWithDiff(fixedSource, expectedFixedSource, file: testFile, line: testLine) - } -} diff --git a/Tests/SwiftParserTest/DiagnosticTests.swift b/Tests/SwiftParserTest/DiagnosticTests.swift deleted file mode 100644 index cdb6b57107f..00000000000 --- a/Tests/SwiftParserTest/DiagnosticTests.swift +++ /dev/null @@ -1,108 +0,0 @@ -import XCTest -@_spi(RawSyntax) import SwiftSyntax -@_spi(Testing) @_spi(RawSyntax) import SwiftParser -import _SwiftSyntaxTestSupport - -public class DiagnosticTests: XCTestCase { - public func testMissingTokenDiags() throws { - let source = """ - (first second Int) - """ - let signature = withParser(source: source) { Syntax(raw: $0.parseFunctionSignature().raw) } - - XCTAssertSingleDiagnostic(in: signature, line: 1, column: 15, id: MissingTokenError.diagnosticID, message: "Expected ':' in function parameter") - } - - public func testUnexpectedDiags() throws { - let source = """ - (first second third fourth: Int) - """ - let signature = withParser(source: source) { Syntax(raw: $0.parseFunctionSignature().raw) } - - XCTAssertSingleDiagnostic(in: signature, line: 1, column: 15, message: "Unexpected text 'third fourth' found in function parameter") - } - - public func testCStyleForLoop() throws { - let source = """ - for let x = 0; x < 10; x += 1, y += 1 { - } - """ - let loop = withParser(source: source) { - Syntax(raw: $0.parseForEachStatement().raw).as(ForInStmtSyntax.self)! - } - - XCTAssertSingleDiagnostic( - in: loop, - line: 1, column: 1, - message: "C-style for statement has been removed in Swift 3", - highlight: "let x = 0; x < 10; x += 1, y += 1 " - ) - } - - public func testMissingClosingParen() throws { - let source = """ - (first second: Int - """ - let signature = withParser(source: source) { - Syntax(raw: $0.parseFunctionSignature().raw).as(FunctionSignatureSyntax.self)! - } - - XCTAssertSingleDiagnostic(in: signature, line: 1, column: 19, message: "Expected ')' to end parameter clause") - } - - public func testMissingOpeningParen() throws { - let source = """ - first second: Int) - """ - let signature = withParser(source: source) { - Syntax(raw: $0.parseFunctionSignature().raw).as(FunctionSignatureSyntax.self)! - } - - XCTAssertSingleDiagnostic(in: signature, line: 1, column: 1, message: "Expected '(' to start parameter clause") - } - - public func testThrowsInWrongLocation() throws { - let source = """ - () -> throws Int - """ - - let signature = withParser(source: source) { - Syntax(raw: $0.parseFunctionSignature().raw).as(FunctionSignatureSyntax.self)! - } - - XCTAssertSingleDiagnostic( - in: signature, - line: 1, column: 7, - message: "'throws' may only occur before '->'", - expectedFixedSource: "() throws -> Int" - ) - } - - public func testNoParamsForFunction() throws { - let source = """ - class MyClass { - func withoutParameters - - func withParameters() {} - } - """ - - let classDecl = withParser(source: source) { - Syntax(raw: $0.parseDeclaration().raw).as(ClassDeclSyntax.self)! - } - - XCTAssertSingleDiagnostic(in: classDecl, line: 2, column: 25, message: "Expected argument list in function declaration") - } - - func testMissingColonInTernary() throws { - let source = """ - foo ? 1 - """ - - let node = withParser(source: source) { - Syntax(raw: $0.parseExpression().raw) - } - - XCTAssertSingleDiagnostic(in: node, line: 1, column: 8, message: "Expected ':' after '? ...' in ternary expression") - } -} diff --git a/Tests/SwiftParserTest/Directives.swift b/Tests/SwiftParserTest/Directives.swift index ce7356c589b..0aed319b310 100644 --- a/Tests/SwiftParserTest/Directives.swift +++ b/Tests/SwiftParserTest/Directives.swift @@ -3,8 +3,8 @@ import XCTest final class DirectiveTests: XCTestCase { - func testSwitchIfConfig() throws { - try AssertParse({ $0.parseStatement() }) { + func testSwitchIfConfig() { + AssertParse( """ switch x { case 1: fallthrough @@ -25,11 +25,11 @@ final class DirectiveTests: XCTestCase { case 10: print(10) } """ - } + ) } - func testPostfixIfConfigExpression() throws { - try AssertParse({ $0.parseExpression() }) { + func testPostfixIfConfigExpression() { + AssertParse( """ foo .bar() @@ -54,20 +54,47 @@ final class DirectiveTests: XCTestCase { #endif #endif """ - } + ) } - func testSourceLocation() throws { - try AssertParse({ $0.parsePoundSourceLocationDirective() }) { + func testSourceLocation() { + AssertParse( """ #sourceLocation() """ - } + ) - try AssertParse({ $0.parsePoundSourceLocationDirective() }) { + AssertParse( """ #sourceLocation(file: "foo", line: 42) """ - } + ) } + + public func testUnterminatedPoundIf() { + AssertParse( + "#if test#^DIAG^#", + diagnostics: [ + DiagnosticSpec(message: "Expected '#endif' in declaration") + ] + ) + } + + func testExtraSyntaxInDirective() { + // FIXME: This test case should produce a diagnostics + + AssertParse( + """ + #if os(iOS) + func foo() {} + } // expected-error{{unexpected '}' in conditional compilation block}} + #else + func bar() {} + func baz() {} + } // expected-error{{unexpected '}' in conditional compilation block}} + #endif + """ + ) + } + } diff --git a/Tests/SwiftParserTest/Expressions.swift b/Tests/SwiftParserTest/Expressions.swift index 62c97bd29b1..410e5e0a4f2 100644 --- a/Tests/SwiftParserTest/Expressions.swift +++ b/Tests/SwiftParserTest/Expressions.swift @@ -3,49 +3,37 @@ import XCTest final class ExpressionTests: XCTestCase { - func testTernary() throws { - try AssertParse({ $0.parseSourceFile() }) { - "let a =" - } - - try AssertParse({ $0.parseExpression() }, allowErrors: false) { - """ - a ? b : c ? d : e - """ - } - try AssertParse({ $0.parseExpression() }) { - """ - a ? b : - """ - } + func testTernary() { + AssertParse("let a =") + + AssertParse("a ? b : c ? d : e") + AssertParse("a ? b :") } - func testSequence() throws { - try AssertParse({ $0.parseExpression() }, allowErrors: false) { - """ - A as? B + C -> D is E as! F ? G = 42 : H - """ - } + func testSequence() { + AssertParse( + "A as? B + C -> D is E as! F ? G = 42 : H" + ) } - func testClosureLiterals() throws { - try AssertParse({ $0.parseClosureExpression() }) { + func testClosureLiterals() { + AssertParse( #""" { @MainActor (a: Int) async -> Int in print("hi") } """# - } + ) - try AssertParse({ $0.parseClosureExpression() }) { + AssertParse( """ { [weak self, weak weakB = b] foo in return 0 } """ - } + ) } - func testTrailingClosures() throws { - try AssertParse({ $0.parseSourceFile() }) { + func testTrailingClosures() { + AssertParse( """ var button = View.Button[5, 4, 3 ] { @@ -53,61 +41,49 @@ final class ExpressionTests: XCTestCase { Text("ABC") } """ - } + ) - try AssertParse({ $0.parseExpression() }) { - """ - compactMap { (parserDiag) in } - """ - } + AssertParse("compactMap { (parserDiag) in }") } - func testSequenceExpressions() throws { - try AssertParse({ $0.parseSequenceExpressionElement(.basic) }) { - """ - await a() - """ - } + func testSequenceExpressions() { + AssertParse("await a()") } - func testNestedTypeSpecialization() throws { - try AssertParse({ $0.parseExpression() }) { - """ - Swift.Array>() - """ - } + func testNestedTypeSpecialization() { + AssertParse("Swift.Array>()") } - func testObjectLiterals() throws { - try AssertParse({ $0.parseSourceFile() }) { + func testObjectLiterals() { + AssertParse( """ #colorLiteral() #colorLiteral(red: 1.0) #colorLiteral(red: 1.0, green: 1.0) #colorLiteral(red: 1.0, green: 1.0, blue: 1.0, alpha: 1.0) """ - } + ) - try AssertParse({ $0.parseSourceFile() }) { + AssertParse( """ #imageLiteral() #imageLiteral(resourceName: "foo.png") #imageLiteral(resourceName: "foo/bar/baz/qux.png") #imageLiteral(resourceName: "foo/bar/baz/quux.png") """ - } + ) } - func testKeypathExpression() throws { - try AssertParse({ $0.parseExpression() }) { + func testKeypathExpression() { + AssertParse( #""" children.filter(\.type.defaultInitialization.isEmpty) """# - } + ) } - func testBasicLiterals() throws { - try AssertParse({ $0.parseSourceFile() }) { + func testBasicLiterals() { + AssertParse( """ #file (#line) @@ -120,88 +96,72 @@ final class ExpressionTests: XCTestCase { __FUNCTION__ __DSO_HANDLE__ """ - } + ) } - func testRegexLiteral() throws { - try AssertParse({ $0.parseExpression() }) { - #""" - /(?[[:alpha:]]\w*) = (?[0-9A-F]+)/ - """# - } + func testRegexLiteral() { + AssertParse( + #""" + /(?[[:alpha:]]\w*) = (?[0-9A-F]+)/ + """# + ) } - func testInitializerExpression() throws { - try AssertParse({ $0.parseExpression() }) { - """ - Lexer.Cursor(input: input, previous: 0) - """ - } + func testInitializerExpression() { + AssertParse("Lexer.Cursor(input: input, previous: 0)") } - func testCollectionLiterals() throws { - try AssertParse({ $0.parseExpression() }) { - "[Dictionary: Int]()" - } - - try AssertParse({ $0.parseExpression() }) { - "[(Int, Double) -> Bool]()" - } - - try AssertParse({ $0.parseExpression() }) { - "[(Int, Double) throws -> Bool]()" - } - - try AssertParse({ $0.parseExpression() }) { - "_ = [@convention(block) () -> Int]().count" - } - - try AssertParse({ $0.parseExpression() }) { - "A<@convention(c) () -> Int32>.c()" - } - - try AssertParse({ $0.parseExpression() }) { - "A<(@autoclosure @escaping () -> Int, Int) -> Void>.c()" - } + func testCollectionLiterals() { + AssertParse("[Dictionary: Int]()") + AssertParse("[(Int, Double) -> Bool]()") + AssertParse("[(Int, Double) -> Bool]()") + AssertParse("_ = [@convention(block) () -> Int]().count") + AssertParse("A<@convention(c) () -> Int32>.c()") + AssertParse("A<(@autoclosure @escaping () -> Int, Int) -> Void>.c()") + AssertParse("_ = [String: (@escaping (A) -> Int) -> Void]().keys") - try AssertParse({ $0.parseExpression() }) { - "_ = [String: (@escaping (A) -> Int) -> Void]().keys" - } - - try AssertParse({ $0.parseExpression() }) { + AssertParse( """ [ condition ? firstOption : secondOption, bar(), ] """ - } + ) - try AssertParse({ $0.parseExpression() }, allowErrors: true) { - "[," - } + AssertParse( + "[,#^DIAG^#", + diagnostics: [ + DiagnosticSpec(message: "Expected ']' to end expression") + ] + ) - try AssertParse({ $0.parseExpression() }, allowErrors: true) { + AssertParse( """ - ([1:) - """ - } + ([1:#^DIAG^#) + """, + diagnostics: [ + // FIXME: Why is this diagnostic produced? + DiagnosticSpec(message: "Expected ':'"), + DiagnosticSpec(message: "Expected ']' to end expression"), + ] + ) } - func testInterpolatedStringLiterals() throws { - try AssertParse({ $0.parseSourceFile() }) { + func testInterpolatedStringLiterals() { + AssertParse( #""" return "Fixit: \(range.debugDescription) Text: \"\(text)\"" """# - } + ) - try AssertParse({ $0.parseExpression() }) { + AssertParse( #""" "text \(array.map({ "\($0)" }).joined(separator: ",")) text" """# - } + ) - try AssertParse({ $0.parseExpression() }) { + AssertParse( #""" """ \(gen(xx) { (x) in @@ -211,28 +171,33 @@ final class ExpressionTests: XCTestCase { }) """ """# - } + ) } - func testStringLiterals() throws { - try AssertParse({ $0.parseExpression() }) { + func testStringLiterals() { + AssertParse( #""" "" """# - } + ) - try AssertParse({ $0.parseExpression() }) { + AssertParse( #""" """ """ """# - } + ) - try AssertParse({ $0.parseExpression() }, allowErrors: true) { - #"" >> \( abc } ) << ""# - } + AssertParse( + #""" + " >> \( abc #^DIAG^#} ) << " + """#, + diagnostics: [ + DiagnosticSpec(message: "Unexpected text '} '") + ] + ) - try AssertParse({ $0.parseSourceFile() }) { + AssertParse( ##""" @@ -242,21 +207,27 @@ final class ExpressionTests: XCTestCase { """## - } + ) - try AssertParse({ $0.parseExpression() }, allowErrors: true) { - #""\","# - } + AssertParse( + #""" + "\",#^DIAG^# + """#, + diagnostics: [ + // FIXME: Should be Expected '"' in string literal + DiagnosticSpec(message: #"Expected '"' in expression"#) + ] + ) - try AssertParse({ $0.parseExpression() }) { + AssertParse( #""" "(?i)\\b((?:[a-z][\\w-]+:(?:/{1,3}|[a-z0-9%])|www\\d{0,3}[.]|[a-z0-9.\\-]+[.][a-z]{2,4}/)" + "(?:[^\\s()<>]+|\\(([^\\s()<>]+|(\\([^\\s()<>]+\\)))*\\))+(?:\\(([^\\s()<>]+|(\\([^\\s()<>]+\\)))*" + "\\)|[^\\s`!()\\[\\]{};:'\".,<>?«»“”‘’]))" """# - } + ) - try AssertParse({ $0.parseExpression() }) { + AssertParse( #""" """ Custom(custom: \(interval),\ @@ -265,53 +236,146 @@ final class ExpressionTests: XCTestCase { Plain: \(units))" """ """# - } + ) - try AssertParse({ $0.parseExpression() }) { + AssertParse( #""" "Founded: \(Date.appleFounding, format: 📆)" """# - } + ) - try AssertParse({ $0.parseExpression()}) { + AssertParse( """ "" """ - } + ) - try AssertParse({ $0.parseExpression() }) { + AssertParse( ##""" #"""# """## - } + ) - try AssertParse({ $0.parseExpression() }) { + AssertParse( ##""" #"""""# """## - } + ) - try AssertParse({ $0.parseExpression() }) { + AssertParse( ##""" #""" multiline raw """# """## - } + ) - try AssertParse({ $0.parseExpression() }) { + AssertParse( #""" "\(x)" """# - } + ) } - func testRangeSubscript() throws { - try AssertParse({ $0.parseExpression() }) { + func testSingleQuoteStringLiteral() { + // FIXME: This test case should produce a diagnostics + AssertParse( + #""" + 'red' + """# + ) + } + + func testStringBogusClosingDelimiters() { + AssertParse( + #"\\(#^DIAG^#"#, + diagnostics: [ + DiagnosticSpec(message: "Expected ')' to end expression") + ] + ) + + AssertParse( + ##""" + #"\\("# + """## + ) + + AssertParse( + #""" + "#^DIAG^# + """#, + diagnostics: [ + DiagnosticSpec(message: #"Expected '"' in expression"#) + ] + ) + + AssertParse( + #""" + "'#^DIAG^# + """#, + diagnostics: [ + DiagnosticSpec(message: #"Expected '"' in expression"#) + ] + ) + } + + func testRangeSubscript() { + AssertParse( """ text[...] """ - } + ) + } + + func testMissingColonInTernary() { + AssertParse( + "foo ? 1#^DIAG^#", + diagnostics: [ + DiagnosticSpec(message: "Expected ':' after '? ...' in ternary expression") + ] + ) + } + + func testBogusKeypathBaseRecovery() { + AssertParse( + #""" + func nestThoseIfs() {\n if false != true {\n print "\(i)\"\n#^DIAG^# + """#, + diagnostics: [ + DiagnosticSpec(message: #"Expected '"' in expression"#), + DiagnosticSpec(message: "Expected '}'"), + DiagnosticSpec(message: "Expected '}'"), + ] + ) + } + + func testMissingArrowInArrowExpr() { + AssertParse( + "[(Int) -> #^DIAG^#throws Int]()", + diagnostics: [ + // FIXME: We should suggest to move 'throws' in front of '->' + DiagnosticSpec(message: "Unexpected text 'throws Int' found in expression") + ] + ) + + AssertParse( + "let _ = [Int throws #^DIAG^#Int]()", + diagnostics: [ + DiagnosticSpec(message: "Expected '->' in expression") + ] + ) + } + + func testBogusThrowingTernary() { + // FIXME: This test case should produce a diagnostics + AssertParse( + """ + do { + true ? () : throw opaque_error() + } catch _ { + } + """ + ) } } diff --git a/Tests/SwiftParserTest/ParserTests.swift b/Tests/SwiftParserTest/ParserTests.swift index f83d315d890..bcb2022f876 100644 --- a/Tests/SwiftParserTest/ParserTests.swift +++ b/Tests/SwiftParserTest/ParserTests.swift @@ -4,6 +4,9 @@ import SwiftParser public class ParserTests: XCTestCase { func testSelfParse() throws { + // Allow skipping the self parse test in local development environments + // because it takes very long compared to all the other tests. + try XCTSkipIf(ProcessInfo.processInfo.environment["SKIP_SELF_PARSE"] == "1") let currentDir = URL(fileURLWithPath: #file) .deletingLastPathComponent() .deletingLastPathComponent() diff --git a/Tests/SwiftParserTest/RecoveryTests.swift b/Tests/SwiftParserTest/RecoveryTests.swift deleted file mode 100644 index 293f06ec5ca..00000000000 --- a/Tests/SwiftParserTest/RecoveryTests.swift +++ /dev/null @@ -1,390 +0,0 @@ -import XCTest -@_spi(RawSyntax) import SwiftSyntax -@_spi(Testing) @_spi(RawSyntax) import SwiftParser -import _SwiftSyntaxTestSupport - -public class RecoveryTests: XCTestCase { - func testTopLevelCaseRecovery() throws { - try AssertParse({ $0.parseSourceFile() }) { - "/*#-editable-code Swift Platground editable area*/default/*#-end-editable-code*/" - } - - try AssertParse({ $0.parseSourceFile() }) { - "case:" - } - - try AssertParse({ $0.parseSourceFile() }) { - #"case: { ("Hello World") }"# - } - } - - func testBogusKeypathBaseRecovery() throws { - try AssertParse({ $0.parseSourceFile() }, allowErrors: true) { - #"func nestThoseIfs() {\n if false != true {\n print "\(i)\"\n"# - } - } - - func testExtraneousRightBraceRecovery() throws { - try AssertParse({ $0.parseSourceFile() }) { - "class ABC { let def = ghi(jkl: mno) } }" - } - } - - func testMissingIfClauseIntroducer() throws { - try AssertParse({ $0.parseSourceFile() }) { - "if _ = 42 {}" - } - } - - func testMissingSubscriptReturnClause() throws { - try AssertParse({ $0.parseSourceFile() }, allowErrors: true) { - """ - struct Foo { - subscript(x: String) {} - } - """ - } - } - - func testSingleQuoteStringLiteral() throws { - try AssertParse({ $0.parseExpression() }) { - #""" - 'red' - """# - } - } - - func testClassWithLeadingNumber() throws { - try AssertParse({ $0.parseSourceFile() }, allowErrors: true) { - """ - class 23class { - // expected-error@-1 {{class name can only start with a letter or underscore, not a number}} - // expected-error@-2 {{'c' is not a valid digit in integer literal}} - func 24method() {} - // expected-error@-1 {{function name can only start with a letter or underscore, not a number}} - // expected-error@-2 {{'m' is not a valid digit in integer literal}} - } - """ - } - } - - func testAttributesOnStatements() throws { - try AssertParse({ $0.parseSourceFile() }) { - """ - func test1() { - @s return - } - func test2() { - @unknown return - } - """ - } - } - - func testMissingArrowInArrowExpr() throws { - try AssertParse({ $0.parseSourceFile() }, allowErrors: true) { - """ - [(Int) -> throws Int]() - let _ = [Int throws Int]() - """ - } - } - - func testBogusSwitchStatement() throws { - try AssertParse({ $0.parseStatement() }) { - """ - switch x { - print() - #if true - print() - #endif - case .A, .B: - break - } - """ - } - - try AssertParse({ $0.parseStatement() }) { - """ - switch x { - print() - #if ENABLE_C - case .NOT_EXIST: - break - case .C: - break - #endif - case .A, .B: - break - } - """ - } - } - - func testBogusLineLabel() throws { - try AssertParse({ $0.parseSourceFile() }) { - """ - LABEL: - """ - } - } - - func testStringBogusClosingDelimiters() throws { - try AssertParse({ $0.parseSourceFile() }, allowErrors: true) { - #"\\("# - } - - try AssertParse({ $0.parseExpression() }) { - ##""" - #"\\("# - """## - } - - try AssertParse({ $0.parseStringLiteral() }, allowErrors: true) { - #""" - " - """# - } - - try AssertParse({ $0.parseStringLiteral() }, allowErrors: true) { - #""" - "' - """# - } - } - - func testMissingArgumentToAttribute() throws { - try AssertParse({ $0.parseSourceFile() }, allowErrors: true) { - """ - @_dynamicReplacement( - func test_dynamic_replacement_for2() { - } - """ - } - } - - func testBogusThrowingTernary() throws { - try AssertParse({ $0.parseStatement() }) { - """ - do { - true ? () : throw opaque_error() - } catch _ { - } - """ - } - } - - func testAccessors() throws { - try AssertParse({ $0.parseDeclaration() }) { - """ - var bad1 : Int { - _read async { 0 } - } - """ - } - - try AssertParse({ $0.parseDeclaration() }) { - """ - var bad2 : Int { - get reasync { 0 } - } - """ - } - } - - func testExpressionMember() throws { - try AssertParse({ $0.parseSourceFile() }, allowErrors: true) { - """ - struct S { - / ###line 25 "line-directive.swift" - } - """ - } - } - - func testBogusProtocolRequirements() throws { - try AssertParse({ $0.parseDeclaration() }) { - """ - protocol P { - var prop : Int { get bogus rethrows set } - } - """ - } - } - - func testExtraSyntaxInDirective() throws { - try AssertParse({ $0.parseDeclaration() }, allowErrors: true) { - """ - #if os(iOS) - func foo() {} - } // expected-error{{unexpected '}' in conditional compilation block}} - #else - func bar() {} - func baz() {} - } // expected-error{{unexpected '}' in conditional compilation block}} - #endif - """ - } - } - - func testRecoverOneExtraLabel() throws { - try XCTAssertHasSubstructure( - "(first second third: Int)", - parse: { withParser(source: $0) { Syntax(raw: $0.parseFunctionSignature().raw) } }, - FunctionParameterSyntax( - attributes: nil, - firstName: TokenSyntax.identifier("first"), - secondName: TokenSyntax.identifier("second"), - UnexpectedNodesSyntax([Syntax(TokenSyntax.identifier("third"))]), - colon: TokenSyntax.colonToken(), - type: TypeSyntax(SimpleTypeIdentifierSyntax(name: TokenSyntax.identifier("Int"), genericArgumentClause: nil)), - ellipsis: nil, - defaultArgument: nil, - trailingComma: nil - ) - ) - } - - func testRecoverTwoExtraLabels() throws { - try XCTAssertHasSubstructure( - "(first second third fourth: Int)", - parse: { withParser(source: $0) { Syntax(raw: $0.parseFunctionSignature().raw) } }, - FunctionParameterSyntax( - attributes: nil, - firstName: TokenSyntax.identifier("first"), - secondName: TokenSyntax.identifier("second"), - UnexpectedNodesSyntax([Syntax(TokenSyntax.identifier("third")), Syntax(TokenSyntax.identifier("fourth"))]), - colon: TokenSyntax.colonToken(), - type: TypeSyntax(SimpleTypeIdentifierSyntax(name: TokenSyntax.identifier("Int"), genericArgumentClause: nil)), - ellipsis: nil, - defaultArgument: nil, - trailingComma: nil - ) - ) - } - - func testDontRecoverFromDeclKeyword() throws { - var source = """ - (first second third struct: Int) - """ - let (_, currentToken): (RawFunctionSignatureSyntax, Lexer.Lexeme) = - source.withUTF8 { buffer in - var parser = Parser(buffer) - return (parser.parseFunctionSignature(), parser.currentToken) - } - - // The 'struct' keyword should be taken as an indicator that a new decl - // starts here, so `parseFunctionSignature` shouldn't eat it. - XCTAssertEqual(currentToken.tokenKind, .structKeyword) - } - - func testRecoverFromParens() throws { - try XCTAssertHasSubstructure( - "(first second [third fourth]: Int)", - parse: { withParser(source: $0) { Syntax(raw: $0.parseFunctionSignature().raw) } }, - FunctionParameterSyntax( - attributes: nil, - firstName: TokenSyntax.identifier("first"), - secondName: TokenSyntax.identifier("second"), - UnexpectedNodesSyntax([ - Syntax(TokenSyntax.leftSquareBracketToken()), - Syntax(TokenSyntax.identifier("third")), - Syntax(TokenSyntax.identifier("fourth")), - Syntax(TokenSyntax.rightSquareBracketToken()) - ]), - colon: TokenSyntax.colonToken(), - type: TypeSyntax(SimpleTypeIdentifierSyntax(name: TokenSyntax.identifier("Int"), genericArgumentClause: nil)), - ellipsis: nil, - defaultArgument: nil, - trailingComma: nil - ) - ) - } - - func testDontRecoverFromUnbalancedParens() throws { - let source = """ - (first second [third fourth: Int) - """ - try withParser(source: source) { parser in - let signature = Syntax(raw: parser.parseFunctionSignature().raw) - let currentToken = parser.currentToken - XCTAssertEqual(currentToken.tokenKind, .identifier) - XCTAssertEqual(currentToken.tokenText, "fourth") - try XCTAssertHasSubstructure( - signature, - FunctionParameterSyntax( - attributes: nil, - firstName: TokenSyntax.identifier("first"), - secondName: TokenSyntax.identifier("second"), - colon: TokenSyntax(.colon, presence: .missing), - type: TypeSyntax(ArrayTypeSyntax( - leftSquareBracket: TokenSyntax.leftSquareBracketToken(), - elementType: TypeSyntax(SimpleTypeIdentifierSyntax(name: TokenSyntax.identifier("third"), genericArgumentClause: nil)), - rightSquareBracket: TokenSyntax(.rightSquareBracket, presence: .missing) - )), - ellipsis: nil, - defaultArgument: nil, - trailingComma: nil - ) - ) - } - } - - func testDontRecoverIfNewlineIsBeforeColon() throws { - var source = """ - (first second third - : Int) - """ - let (_, currentToken): (RawFunctionSignatureSyntax, Lexer.Lexeme) = - source.withUTF8 { buffer in - var parser = Parser(buffer) - return (parser.parseFunctionSignature(), parser.currentToken) - } - - XCTAssertEqual(currentToken.tokenKind, .colon) - } - - func testTextRecovery() throws { - try AssertParse({ $0.parseSourceFile() }, allowErrors: true) { - """ - Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. - """ - } - } - - public func testNoParamsForFunction() throws { - let source = """ - class MyClass { - func withoutParameters - - func withParameters() {} - } - """ - - let classDecl = withParser(source: source) { - Syntax(raw: $0.parseDeclaration().raw) - } - try XCTAssertHasSubstructure( - classDecl, - FunctionDeclSyntax( - attributes: nil, - modifiers: nil, - funcKeyword: .funcKeyword(), - identifier: .identifier("withoutParameters"), - genericParameterClause: nil, - signature: FunctionSignatureSyntax( - input: ParameterClauseSyntax( - leftParen: .leftParenToken(presence: .missing), - parameterList: FunctionParameterListSyntax([]), - rightParen: .rightParenToken(presence: .missing) - ), - asyncOrReasyncKeyword: nil, - throwsOrRethrowsKeyword: nil, - output: nil - ), - genericWhereClause: nil, - body: nil - ) - ) - } -} diff --git a/Tests/SwiftParserTest/Statements.swift b/Tests/SwiftParserTest/Statements.swift index 766f623325c..96e8f9fadba 100644 --- a/Tests/SwiftParserTest/Statements.swift +++ b/Tests/SwiftParserTest/Statements.swift @@ -3,53 +3,51 @@ import XCTest final class StatementTests: XCTestCase { - func testIf() throws { - try AssertParse({ $0.parseIfStatement() }) { - """ - if let x { } - """ - } + func testIf() { + AssertParse("if let x { }") - try AssertParse({ $0.parseIfStatement() }, allowErrors: true) { + AssertParse( """ - if case* ! = x { + if case#^DIAG^#* ! = x { bar() } - """ - } + """, + diagnostics: [ + DiagnosticSpec(message: "Expected '='"), + DiagnosticSpec(message: "Unexpected text '* ! = x '"), + ] + ) } - func testNestedIfs() throws { - try AssertParse({ $0.parseDeclaration() }) { - let nest = 22 - var example = "func nestThoseIfs() {\n" - for index in (0...nest) { - let indent = String(repeating: " ", count: index + 1) - example += indent + "if false != true {\n" - example += indent + " print \"\\(i)\"\n" - } + func testNestedIfs() { + let nest = 22 + var source = "func nestThoseIfs() {\n" + for index in (0...nest) { + let indent = String(repeating: " ", count: index + 1) + source += indent + "if false != true {\n" + source += indent + " print \"\\(i)\"\n" + } - for index in (0...nest).reversed() { - let indent = String(repeating: " ", count: index + 1) - example += indent + "}\n" - } - example += "}" - return example + for index in (0...nest).reversed() { + let indent = String(repeating: " ", count: index + 1) + source += indent + "}\n" } + source += "}" + AssertParse(source) } - func testDo() throws { - try AssertParse({ $0.parseDoStatement() }) { + func testDo() { + AssertParse( """ do { } """ - } + ) } - func testDoCatch() throws { - try AssertParse({ $0.parseDoStatement() }) { + func testDoCatch() { + AssertParse( """ do { @@ -57,45 +55,41 @@ final class StatementTests: XCTestCase { } """ - } + ) } - func testReturn() throws { - try AssertParse({ $0.parseReturnStatement() }) { - "return" - } + func testReturn() { + AssertParse("return") - try AssertParse({ $0.parseReturnStatement() }) { + AssertParse( #""" return "assert(\(assertChoices.joined(separator: " || ")))" """# - } + ) - try AssertParse({ $0.parseReturnStatement() }) { - "return true ? nil : nil" - } + AssertParse("return true ? nil : nil") } - func testSwitch() throws { - try AssertParse({ $0.parseStatement() }) { + func testSwitch() { + AssertParse( """ switch x { case .A, .B: break } """ - } + ) - try AssertParse({ $0.parseStatement() }) { + AssertParse( """ switch 0 { @$dollar case _: break } """ - } + ) - try AssertParse({ $0.parseStatement() }) { + AssertParse( """ switch x { case .A: @@ -109,6 +103,93 @@ final class StatementTests: XCTestCase { #endif } """ - } + ) + } + + func testCStyleForLoop() { + AssertParse( + """ + #^DIAG^#for let x = 0; x < 10; x += 1, y += 1 { + } + """, + diagnostics: [ + DiagnosticSpec(message: "C-style for statement has been removed in Swift 3", highlight: "let x = 0; x < 10; x += 1, y += 1 ") + ] + ) + } + + func testTopLevelCaseRecovery() { + // FIXME: These test cases should produce diagnostics + AssertParse( + "/*#-editable-code Swift Platground editable area*/default/*#-end-editable-code*/" + ) + + AssertParse("case:") + + AssertParse( + #""" + case: { ("Hello World") } + """# + ) + } + + func testMissingIfClauseIntroducer() { + // FIXME: This test case should produce a diagnostics + AssertParse("if _ = 42 {}") + } + + func testAttributesOnStatements() { + // FIXME: This test case should produce a diagnostics + AssertParse( + """ + func test1() { + @s return + } + func test2() { + @unknown return + } + """ + ) + } + + func testBogusSwitchStatement() { + // FIXME: This test case should produce a diagnostics + AssertParse( + """ + switch x { + print() + #if true + print() + #endif + case .A, .B: + break + } + """ + ) + + AssertParse( + """ + switch x { + print() + #if ENABLE_C + case .NOT_EXIST: + break + case .C: + break + #endif + case .A, .B: + break + } + """ + ) + } + + // FIXME: This test case should produce a diagnostic + func testBogusLineLabel() { + AssertParse( + """ + LABEL: + """ + ) } } diff --git a/Tests/SwiftParserTest/Types.swift b/Tests/SwiftParserTest/Types.swift index 815d11e7bbe..11b18cd8e8d 100644 --- a/Tests/SwiftParserTest/Types.swift +++ b/Tests/SwiftParserTest/Types.swift @@ -4,23 +4,26 @@ import XCTest final class TypeTests: XCTestCase { func testClosureParsing() throws { - try AssertParse({ $0.parseType() }) { - "(a, b) -> c" - } + AssertParse( + "(a, b) -> c", + { $0.parseType() } + ) - try AssertParse({ $0.parseType() }) { - "@MainActor (a, b) async throws -> c" - } + AssertParse( + "@MainActor (a, b) async throws -> c", + { $0.parseType() } + ) } func testGenericTypeWithTrivia() throws { // N.B. Whitespace is significant here. - try AssertParse({ $0.parseType() }) { + AssertParse( """ Foo >> - """ - } + """, + { $0.parseType() } + ) } }