diff --git a/Sources/SwiftParser/Lexer/RegexLiteralLexer.swift b/Sources/SwiftParser/Lexer/RegexLiteralLexer.swift index e6ffe512ef9..86678983929 100644 --- a/Sources/SwiftParser/Lexer/RegexLiteralLexer.swift +++ b/Sources/SwiftParser/Lexer/RegexLiteralLexer.swift @@ -207,9 +207,6 @@ fileprivate struct RegexLiteralLexer { lastUnespacedSpaceOrTab.position.advanced(by: 1).pointer == slashBegin.position.pointer { if mustBeRegex { - // TODO: We ought to have a fix-it that suggests #/.../#. We could - // suggest escaping, but that would be wrong if the user has written (?x). - // TODO: Should we suggest #/.../# for space-as-first character too? builder.recordPatternError(.spaceAtEndOfRegexLiteral, at: lastUnespacedSpaceOrTab) } else { return .notARegex @@ -256,7 +253,6 @@ fileprivate struct RegexLiteralLexer { // } // if mustBeRegex { - // TODO: We ought to have a fix-it that inserts a backslash to escape. builder.recordPatternError(.spaceAtStartOfRegexLiteral, at: cursor) } else { return .notARegex diff --git a/Sources/SwiftParserDiagnostics/LexerDiagnosticMessages.swift b/Sources/SwiftParserDiagnostics/LexerDiagnosticMessages.swift index 20fcc3a4d12..e25d883da49 100644 --- a/Sources/SwiftParserDiagnostics/LexerDiagnosticMessages.swift +++ b/Sources/SwiftParserDiagnostics/LexerDiagnosticMessages.swift @@ -325,8 +325,93 @@ extension SwiftSyntax.TokenDiagnostic { changes.append(.replaceLeadingTrivia(token: nextToken, newTrivia: [])) } return [FixIt(message: .removeExtraneousWhitespace, changes: changes)] + case .spaceAtEndOfRegexLiteral: + if let regexLiteral = token.regexParent { + // wouldn't suggest `insertBackslash` because the potential presence of (?x) somewhere preceding the trailing + // space could mean the trailing space has no semantic meaning. Escaping the space could change the semantics. + return [regexLiteral.convertToExtendedRegexLiteralFixIt] + } else { + return [] + } + case .spaceAtStartOfRegexLiteral: + guard let regexLiteral = token.regexParent else { + return [] + } + + let regexText = regexLiteral.regex.text + let lastIndex = regexText.index(before: regexText.endIndex) + if regexText.startIndex != lastIndex && regexText[lastIndex].isWhitespace { + // if the regex has a distinct trailing space, same as the handling at `case .spaceAtEndOfRegexLiteral` + return [regexLiteral.convertToExtendedRegexLiteralFixIt] + } else { + let escapedRegexText = #"\\#(regexText)"# + return [ + regexLiteral.convertToExtendedRegexLiteralFixIt, + FixIt( + message: .insertBackslash, + changes: [ + .replace( + oldNode: Syntax(regexLiteral), + newNode: Syntax( + regexLiteral + .with(\.regex, .regexLiteralPattern(escapedRegexText)) + ) + ) + ] + ), + ] + } default: return [] } } } + +private extension TokenSyntax { + var regexParent: RegexLiteralExprSyntax? { + var parent = Syntax(self) + while parent.kind != .regexLiteralExpr, let upper = parent.parent { + parent = upper + } + return parent.as(RegexLiteralExprSyntax.self) + } +} + +private extension RegexLiteralExprSyntax { + /// Creates a Fix-it that suggests converting to extended regex literal + /// + /// Covers the following cases: + /// ```swift + /// let leadingSpaceRegex = / ,/ + /// // converts to + /// let leadingSpaceExtendedRegex = #/ ,/# + /// + /// let leadingAndTrailingSpaceRegex = / , / + /// // converts to + /// let leadingAndTrailingSpaceExtendedRegex = #/ , /# + /// + /// let trailingSpaceRegex = /, / + /// // converts to + /// let trailingSpaceExtendedRegex = #/ ,/# + /// + /// let trailingSpaceMissingClosingSlashRegex = /, + /// // converts to + /// let trailingSpaceExtendedRegex = #/, /# + /// ``` + var convertToExtendedRegexLiteralFixIt: FixIt { + FixIt( + message: .convertToExtendedRegexLiteral, + changes: [ + .replace( + oldNode: Syntax(self), + newNode: Syntax( + with(\.openingSlash, .regexSlashToken()) + .with(\.openingPounds, .regexPoundDelimiter("#", leadingTrivia: leadingTrivia)) + .with(\.closingPounds, .regexPoundDelimiter("#", trailingTrivia: trailingTrivia)) + .with(\.closingSlash, .regexSlashToken()) + ) + ) + ] + ) + } +} diff --git a/Sources/SwiftParserDiagnostics/ParserDiagnosticMessages.swift b/Sources/SwiftParserDiagnostics/ParserDiagnosticMessages.swift index f6f8a3b6a59..2a1b2a02c40 100644 --- a/Sources/SwiftParserDiagnostics/ParserDiagnosticMessages.swift +++ b/Sources/SwiftParserDiagnostics/ParserDiagnosticMessages.swift @@ -655,6 +655,9 @@ extension FixItMessage where Self == StaticParserFixIt { public static var insertAttributeArguments: Self { .init("insert attribute argument") } + public static var insertBackslash: Self { + .init(#"insert '\'"#) + } public static var insertNewline: Self { .init("insert newline") } @@ -691,6 +694,9 @@ extension FixItMessage where Self == StaticParserFixIt { public static var wrapInBackticks: Self { .init("if this name is unavoidable, use backticks to escape it") } + public static var convertToExtendedRegexLiteral: Self { + .init("convert to extended regex literal with '#'") + } } public struct InsertFixIt: ParserFixIt { diff --git a/Tests/SwiftParserTest/Assertions.swift b/Tests/SwiftParserTest/Assertions.swift index 8cce493526a..0d637934486 100644 --- a/Tests/SwiftParserTest/Assertions.swift +++ b/Tests/SwiftParserTest/Assertions.swift @@ -289,9 +289,9 @@ struct DiagnosticSpec { } /// Assert that `location` is the same as that of `locationMarker` in `tree`. -func assertLocation( +func assertLocation( _ location: SourceLocation, - in tree: T, + in tree: some SyntaxProtocol, markerLocations: [String: Int], expectedLocationMarker locationMarker: String, file: StaticString = #filePath, @@ -315,9 +315,9 @@ func assertLocation( /// Assert that the diagnostic `note` produced in `tree` matches `spec`, /// using `markerLocations` to translate markers to actual source locations. -func assertNote( +func assertNote( _ note: Note, - in tree: T, + in tree: some SyntaxProtocol, markerLocations: [String: Int], expected spec: NoteSpec ) { @@ -335,9 +335,9 @@ func assertNote( /// Assert that the diagnostic `diag` produced in `tree` matches `spec`, /// using `markerLocations` to translate markers to actual source locations. -func assertDiagnostic( +func assertDiagnostic( _ diag: Diagnostic, - in tree: T, + in tree: some SyntaxProtocol, markerLocations: [String: Int], expected spec: DiagnosticSpec ) { @@ -491,9 +491,9 @@ public struct AssertParseOptions: OptionSet, Sendable { extension ParserTestCase { /// After a test case has been mutated, assert that the mutated source /// round-trips and doesn’t hit any assertion failures in the parser. - fileprivate static func assertMutationRoundTrip( + fileprivate static func assertMutationRoundTrip( source: [UInt8], - _ parse: (inout Parser) -> S, + _ parse: (inout Parser) -> some SyntaxProtocol, swiftVersion: Parser.SwiftVersion?, experimentalFeatures: Parser.ExperimentalFeatures, file: StaticString, @@ -524,6 +524,76 @@ extension ParserTestCase { } } + enum FixItsApplication { + /// Apply only the fix-its whose messages are included in `applyFixIts` to generate `fixedSource`. + case optIn(applyFixIts: [String], fixedSource: String) + /// Apply all fix-its to generate `fixedSource`. + case all(fixedSource: String) + + init?(applyFixIts: [String]?, expectedFixedSource: String?) { + if let applyFixIts { + self = .optIn(applyFixIts: applyFixIts, fixedSource: expectedFixedSource ?? "") + } else if let expectedFixedSource { + self = .all(fixedSource: expectedFixedSource) + } else { + return nil + } + } + + var applyFixIts: [String]? { + switch self { + case .optIn(let applyFixIts, _): + return applyFixIts + case .all: + return nil + } + } + + var expectedFixedSource: String { + switch self { + case .optIn(_, let fixedSource), .all(let fixedSource): + return fixedSource + } + } + } + + /// Convenient version of `assertParse` that allows checking a single configuration of applied Fix-Its. + /// + /// - Parameters: + /// - applyFixIts: Applies only the fix-its with these messages. Nil means applying all fix-its. + /// - expectedFixedSource: Asserts that the source after applying fix-its matches + /// this string. + func assertParse( + _ markedSource: String, + _ parse: @Sendable (inout Parser) -> some SyntaxProtocol = { SourceFileSyntax.parse(from: &$0) }, + substructure expectedSubstructure: (some SyntaxProtocol)? = Optional.none, + substructureAfterMarker: String = "START", + diagnostics expectedDiagnostics: [DiagnosticSpec] = [], + applyFixIts: [String]? = nil, + fixedSource expectedFixedSource: String? = nil, + options: AssertParseOptions = [], + swiftVersion: Parser.SwiftVersion? = nil, + experimentalFeatures: Parser.ExperimentalFeatures? = nil, + file: StaticString = #filePath, + line: UInt = #line + ) { + assertParse( + markedSource, + parse, + substructure: expectedSubstructure, + substructureAfterMarker: substructureAfterMarker, + diagnostics: expectedDiagnostics, + fixItsApplications: FixItsApplication(applyFixIts: applyFixIts, expectedFixedSource: expectedFixedSource).map { + [$0] + } ?? [], + options: options, + swiftVersion: swiftVersion, + experimentalFeatures: experimentalFeatures, + file: file, + line: line + ) + } + /// Verifies that parsing of `markedSource` produces expected results using a /// combination of various testing techniques: /// @@ -531,7 +601,7 @@ extension ParserTestCase { /// parsed tree is the same as the input. /// 2. Checks that parsing produces the expected list of diagnostics. If no /// diagnostics are passed, asserts that the input parses without any errors. - /// 3. Checks that applying all Fix-Its of the source code results in the + /// 3. Checks that applying fix-its of the source code results in the /// expected fixed source, effectively testing the Fix-Its. /// 4. If a substructure is passed, asserts that the parsed tree contains a /// subtree of that structure. @@ -558,21 +628,19 @@ extension ParserTestCase { /// - expectedDiagnostics: Asserts the given diagnostics were output, by default it /// asserts the parse was successful (ie. it has no diagnostics). Note /// that `DiagnosticsSpec` uses the location marked by `1️⃣` by default. - /// - applyFixIts: Applies only the fix-its with these messages. - /// - expectedFixedSource: Asserts that the source after applying fix-its matches - /// this string. + /// - fixItsApplications: A list of `FixItsApplication` to check for whether applying certain fix-its to the source + /// will generate a certain expected fixed source. An empty list means no fix-its are expected. /// - swiftVersion: The version of Swift using which the file should be parsed. /// Defaults to the latest version. /// - experimentalFeatures: A list of experimental features to enable, or /// `nil` to enable the default set of features provided by the test case. - func assertParse( + func assertParse( _ markedSource: String, - _ parse: @Sendable (inout Parser) -> S = { SourceFileSyntax.parse(from: &$0) }, + _ parse: @Sendable (inout Parser) -> some SyntaxProtocol = { SourceFileSyntax.parse(from: &$0) }, substructure expectedSubstructure: (some SyntaxProtocol)? = Optional.none, substructureAfterMarker: String = "START", diagnostics expectedDiagnostics: [DiagnosticSpec] = [], - applyFixIts: [String]? = nil, - fixedSource expectedFixedSource: String? = nil, + fixItsApplications: [FixItsApplication] = [], options: AssertParseOptions = [], swiftVersion: Parser.SwiftVersion? = nil, experimentalFeatures: Parser.ExperimentalFeatures? = nil, @@ -591,7 +659,7 @@ extension ParserTestCase { parser.enableAlternativeTokenChoices() } #endif - let tree: S = parse(&parser) + let tree = parse(&parser) // Round-trip assertStringsEqualWithDiff( @@ -640,27 +708,30 @@ extension ParserTestCase { } // Applying Fix-Its - if expectedDiagnostics.contains(where: { !$0.fixIts.isEmpty }) && expectedFixedSource == nil { + if expectedDiagnostics.contains(where: { !$0.fixIts.isEmpty }) && fixItsApplications.isEmpty { XCTFail("Expected a fixed source if the test case produces diagnostics with Fix-Its", file: file, line: line) - } else if let expectedFixedSource = expectedFixedSource { - let fixedTree = FixItApplier.applyFixes(from: diags, filterByMessages: applyFixIts, to: tree) - var fixedTreeDescription = fixedTree.description - if options.contains(.normalizeNewlinesInFixedSource) { - fixedTreeDescription = - fixedTreeDescription - .replacingOccurrences(of: "\r\n", with: "\n") - .replacingOccurrences(of: "\r", with: "\n") + } else { + for fixItsApplication in fixItsApplications { + let applyFixIts = fixItsApplication.applyFixIts + let fixedTree = FixItApplier.applyFixes(from: diags, filterByMessages: applyFixIts, to: tree) + var fixedTreeDescription = fixedTree.description + if options.contains(.normalizeNewlinesInFixedSource) { + fixedTreeDescription = + fixedTreeDescription + .replacingOccurrences(of: "\r\n", with: "\n") + .replacingOccurrences(of: "\r", with: "\n") + } + assertStringsEqualWithDiff( + fixedTreeDescription.trimmingTrailingWhitespace(), + fixItsApplication.expectedFixedSource.trimmingTrailingWhitespace(), + "Applying \(applyFixIts?.description ?? "all Fix-Its") didn’t produce the expected fixed source", + file: file, + line: line + ) } - assertStringsEqualWithDiff( - fixedTreeDescription.trimmingTrailingWhitespace(), - expectedFixedSource.trimmingTrailingWhitespace(), - "Applying all Fix-Its didn’t produce the expected fixed source", - file: file, - line: line - ) } - if expectedDiagnostics.allSatisfy({ $0.fixIts.isEmpty }) && expectedFixedSource != nil { + if expectedDiagnostics.allSatisfy({ $0.fixIts.isEmpty }) && !fixItsApplications.isEmpty { XCTFail( "Fixed source was provided but the test case produces no diagnostics with Fix-Its", file: file, @@ -747,9 +818,9 @@ class TriviaRemover: SyntaxRewriter { } } -func assertBasicFormat( +func assertBasicFormat( source: String, - parse: (inout Parser) -> S, + parse: (inout Parser) -> some SyntaxProtocol, swiftVersion: Parser.SwiftVersion?, experimentalFeatures: Parser.ExperimentalFeatures, file: StaticString = #filePath, diff --git a/Tests/SwiftParserTest/DeclarationTests.swift b/Tests/SwiftParserTest/DeclarationTests.swift index 9985df5a7b4..35a680d090b 100644 --- a/Tests/SwiftParserTest/DeclarationTests.swift +++ b/Tests/SwiftParserTest/DeclarationTests.swift @@ -1268,25 +1268,34 @@ final class DeclarationTests: ParserTestCase { ), DiagnosticSpec( locationMarker: "4️⃣", - message: "bare slash regex literal may not start with space" + message: "bare slash regex literal may not start with space", + fixIts: [ + "convert to extended regex literal with '#'", + #"insert '\'"#, + ] ), DiagnosticSpec( locationMarker: "5️⃣", message: "expected '/' to end regex literal", notes: [NoteSpec(locationMarker: "3️⃣", message: "to match this opening '/'")], - fixIts: ["insert '/\'"] + fixIts: ["insert '/'"] ), DiagnosticSpec( locationMarker: "6️⃣", message: "extraneous brace at top level" ), ], - fixedSource: """ + applyFixIts: [ + "insert '}'", + #"insert '\'"#, + "insert '/'", + ], + fixedSource: #""" struct S { } - / ###line 25 "line-directive.swift"/ + /\ ###line 25 "line-directive.swift"/ } - """ + """# ) } diff --git a/Tests/SwiftParserTest/RegexLiteralTests.swift b/Tests/SwiftParserTest/RegexLiteralTests.swift index 6796f0bffa3..56cf4d85dd7 100644 --- a/Tests/SwiftParserTest/RegexLiteralTests.swift +++ b/Tests/SwiftParserTest/RegexLiteralTests.swift @@ -429,7 +429,27 @@ final class RegexLiteralTests: ParserTestCase { /1️⃣ a/ """, diagnostics: [ - DiagnosticSpec(message: "bare slash regex literal may not start with space") + DiagnosticSpec( + message: "bare slash regex literal may not start with space", + fixIts: [ + "convert to extended regex literal with '#'", + #"insert '\'"#, + ] + ) + ], + fixItsApplications: [ + .optIn( + applyFixIts: ["convert to extended regex literal with '#'"], + fixedSource: """ + #/ a/# + """ + ), + .optIn( + applyFixIts: [#"insert '\'"#], + fixedSource: #""" + /\ a/ + """# + ), ] ) } @@ -440,7 +460,27 @@ final class RegexLiteralTests: ParserTestCase { let x = /1️⃣ a/ """, diagnostics: [ - DiagnosticSpec(message: "bare slash regex literal may not start with space") + DiagnosticSpec( + message: "bare slash regex literal may not start with space", + fixIts: [ + "convert to extended regex literal with '#'", + #"insert '\'"#, + ] + ) + ], + fixItsApplications: [ + .optIn( + applyFixIts: ["convert to extended regex literal with '#'"], + fixedSource: """ + let x = #/ a/# + """ + ), + .optIn( + applyFixIts: [#"insert '\'"#], + fixedSource: #""" + let x = /\ a/ + """# + ), ] ) } @@ -489,25 +529,71 @@ final class RegexLiteralTests: ParserTestCase { ) } + func testClosingSpace4() { + assertParse( + """ + /,1️⃣ / + """, + diagnostics: [ + DiagnosticSpec( + message: "bare slash regex literal may not end with space", + fixIts: ["convert to extended regex literal with '#'"] + ) + ], + fixedSource: """ + #/, /# + """ + ) + } + + func testClosingSpace5() { + assertParse( + """ + let x = /,1️⃣ / + """, + diagnostics: [ + DiagnosticSpec( + message: "bare slash regex literal may not end with space", + fixIts: ["convert to extended regex literal with '#'"] + ) + ], + fixedSource: """ + let x = #/, /# + """ + ) + } + func testOpeningAndClosingSpace1() { assertParse( """ - /1️⃣ / + /1️⃣ / """, diagnostics: [ - DiagnosticSpec(message: "bare slash regex literal may not start with space") - ] + DiagnosticSpec( + message: "bare slash regex literal may not start with space", + fixIts: ["convert to extended regex literal with '#'"] + ) + ], + fixedSource: """ + #/ /# + """ ) } func testOpeningAndClosingSpace2() { assertParse( """ - x += /1️⃣ / + x += /1️⃣ / """, diagnostics: [ - DiagnosticSpec(message: "bare slash regex literal may not start with space") - ] + DiagnosticSpec( + message: "bare slash regex literal may not start with space", + fixIts: ["convert to extended regex literal with '#'"] + ) + ], + fixedSource: """ + x += #/ /# + """ ) } @@ -525,7 +611,27 @@ final class RegexLiteralTests: ParserTestCase { /1️⃣ / """, diagnostics: [ - DiagnosticSpec(message: "bare slash regex literal may not start with space") + DiagnosticSpec( + message: "bare slash regex literal may not start with space", + fixIts: [ + "convert to extended regex literal with '#'", + #"insert '\'"#, + ] + ) + ], + fixItsApplications: [ + .optIn( + applyFixIts: ["convert to extended regex literal with '#'"], + fixedSource: """ + #/ /# + """ + ), + .optIn( + applyFixIts: [#"insert '\'"#], + fixedSource: #""" + /\ / + """# + ), ] ) } @@ -536,7 +642,27 @@ final class RegexLiteralTests: ParserTestCase { let x = /1️⃣ / """, diagnostics: [ - DiagnosticSpec(message: "bare slash regex literal may not start with space") + DiagnosticSpec( + message: "bare slash regex literal may not start with space", + fixIts: [ + "convert to extended regex literal with '#'", + #"insert '\'"#, + ] + ) + ], + fixItsApplications: [ + .optIn( + applyFixIts: ["convert to extended regex literal with '#'"], + fixedSource: """ + let x = #/ /# + """ + ), + .optIn( + applyFixIts: [#"insert '\'"#], + fixedSource: #""" + let x = /\ / + """# + ), ] ) } diff --git a/Tests/SwiftParserTest/translated/ForwardSlashRegexSkippingAllowedTests.swift b/Tests/SwiftParserTest/translated/ForwardSlashRegexSkippingAllowedTests.swift index 00ee9285cf7..6e786f3cc5c 100644 --- a/Tests/SwiftParserTest/translated/ForwardSlashRegexSkippingAllowedTests.swift +++ b/Tests/SwiftParserTest/translated/ForwardSlashRegexSkippingAllowedTests.swift @@ -37,7 +37,27 @@ final class ForwardSlashRegexSkippingAllowedTests: ParserTestCase { func a() { /1️⃣ {}/ } """, diagnostics: [ - DiagnosticSpec(message: "bare slash regex literal may not start with space") + DiagnosticSpec( + message: "bare slash regex literal may not start with space", + fixIts: [ + "convert to extended regex literal with '#'", + #"insert '\'"#, + ] + ) + ], + fixItsApplications: [ + .optIn( + applyFixIts: ["convert to extended regex literal with '#'"], + fixedSource: """ + func a() { #/ {}/# } + """ + ), + .optIn( + applyFixIts: [#"insert '\'"#], + fixedSource: #""" + func a() { /\ {}/ } + """# + ), ] ) } @@ -48,7 +68,27 @@ final class ForwardSlashRegexSkippingAllowedTests: ParserTestCase { func b() { /1️⃣ \{}/ } """#, diagnostics: [ - DiagnosticSpec(message: "bare slash regex literal may not start with space") + DiagnosticSpec( + message: "bare slash regex literal may not start with space", + fixIts: [ + "convert to extended regex literal with '#'", + #"insert '\'"#, + ] + ) + ], + fixItsApplications: [ + .optIn( + applyFixIts: ["convert to extended regex literal with '#'"], + fixedSource: #""" + func b() { #/ \{}/# } + """# + ), + .optIn( + applyFixIts: [#"insert '\'"#], + fixedSource: #""" + func b() { /\ \{}/ } + """# + ), ] ) } @@ -59,7 +99,27 @@ final class ForwardSlashRegexSkippingAllowedTests: ParserTestCase { func c() { /1️⃣ {"{"}/ } """#, diagnostics: [ - DiagnosticSpec(message: "bare slash regex literal may not start with space") + DiagnosticSpec( + message: "bare slash regex literal may not start with space", + fixIts: [ + "convert to extended regex literal with '#'", + #"insert '\'"#, + ] + ) + ], + fixItsApplications: [ + .optIn( + applyFixIts: ["convert to extended regex literal with '#'"], + fixedSource: #""" + func c() { #/ {"{"}/# } + """# + ), + .optIn( + applyFixIts: [#"insert '\'"#], + fixedSource: #""" + func c() { /\ {"{"}/ } + """# + ), ] ) } diff --git a/Tests/SwiftParserTest/translated/ForwardSlashRegexSkippingInvalidTests.swift b/Tests/SwiftParserTest/translated/ForwardSlashRegexSkippingInvalidTests.swift index 3b1d4903788..3959dc9d2bf 100644 --- a/Tests/SwiftParserTest/translated/ForwardSlashRegexSkippingInvalidTests.swift +++ b/Tests/SwiftParserTest/translated/ForwardSlashRegexSkippingInvalidTests.swift @@ -23,8 +23,18 @@ final class ForwardSlashRegexSkippingInvalidTests: ParserTestCase { func a() { _ = /1️⃣ x*/ } """, diagnostics: [ - DiagnosticSpec(message: "bare slash regex literal may not start with space") - ] + DiagnosticSpec( + message: "bare slash regex literal may not start with space", + fixIts: [ + "convert to extended regex literal with '#'", + #"insert '\'"#, + ] + ) + ], + applyFixIts: [], + fixedSource: """ + func a() { _ = / x*/ } + """ ) } @@ -48,9 +58,28 @@ final class ForwardSlashRegexSkippingInvalidTests: ParserTestCase { func d() { _ = /2️⃣ x{*/ } """, diagnostics: [ - DiagnosticSpec(locationMarker: "1️⃣", message: "bare slash regex literal may not start with space"), - DiagnosticSpec(locationMarker: "2️⃣", message: "bare slash regex literal may not start with space"), - ] + DiagnosticSpec( + locationMarker: "1️⃣", + message: "bare slash regex literal may not start with space", + fixIts: [ + "convert to extended regex literal with '#'", + #"insert '\'"#, + ] + ), + DiagnosticSpec( + locationMarker: "2️⃣", + message: "bare slash regex literal may not start with space", + fixIts: [ + "convert to extended regex literal with '#'", + #"insert '\'"#, + ] + ), + ], + applyFixIts: [], + fixedSource: """ + func c() { _ = / x}*/ } + func d() { _ = / x{*/ } + """ ) } @@ -65,7 +94,11 @@ final class ForwardSlashRegexSkippingInvalidTests: ParserTestCase { diagnostics: [ DiagnosticSpec( locationMarker: "1️⃣", - message: "bare slash regex literal may not start with space" + message: "bare slash regex literal may not start with space", + fixIts: [ + "convert to extended regex literal with '#'", + #"insert '\'"#, + ] ), DiagnosticSpec( locationMarker: "2️⃣", @@ -74,6 +107,7 @@ final class ForwardSlashRegexSkippingInvalidTests: ParserTestCase { fixIts: ["insert '/'"] ), ], + applyFixIts: ["insert '/'"], fixedSource: """ func e() { _ = / }/ @@ -92,7 +126,11 @@ final class ForwardSlashRegexSkippingInvalidTests: ParserTestCase { diagnostics: [ DiagnosticSpec( locationMarker: "1️⃣", - message: "bare slash regex literal may not start with space" + message: "bare slash regex literal may not start with space", + fixIts: [ + "convert to extended regex literal with '#'", + #"insert '\'"#, + ] ), DiagnosticSpec( locationMarker: "2️⃣", @@ -101,6 +139,7 @@ final class ForwardSlashRegexSkippingInvalidTests: ParserTestCase { fixIts: ["insert '/'"] ), ], + applyFixIts: ["insert '/'"], fixedSource: """ func f() { _ = / {/ @@ -269,7 +308,11 @@ final class ForwardSlashRegexSkippingInvalidTests: ParserTestCase { diagnostics: [ DiagnosticSpec( locationMarker: "1️⃣", - message: "bare slash regex literal may not start with space" + message: "bare slash regex literal may not start with space", + fixIts: [ + "convert to extended regex literal with '#'", + #"insert '\'"#, + ] ), DiagnosticSpec( locationMarker: "2️⃣", @@ -282,6 +325,7 @@ final class ForwardSlashRegexSkippingInvalidTests: ParserTestCase { message: "extraneous brace at top level" ), ], + applyFixIts: ["insert '/'"], fixedSource: #""" func m() { _ = / "/ @@ -298,8 +342,18 @@ final class ForwardSlashRegexSkippingInvalidTests: ParserTestCase { func n() { /1️⃣ "{"}/ } """#, diagnostics: [ - DiagnosticSpec(message: "bare slash regex literal may not start with space") - ] + DiagnosticSpec( + message: "bare slash regex literal may not start with space", + fixIts: [ + "convert to extended regex literal with '#'", + #"insert '\'"#, + ] + ) + ], + applyFixIts: [], + fixedSource: #""" + func n() { / "{"}/ } + """# ) } @@ -341,8 +395,18 @@ final class ForwardSlashRegexSkippingInvalidTests: ParserTestCase { func err1() { _ = /1️⃣ 0xG}/ } """, diagnostics: [ - DiagnosticSpec(message: "bare slash regex literal may not start with space") - ] + DiagnosticSpec( + message: "bare slash regex literal may not start with space", + fixIts: [ + "convert to extended regex literal with '#'", + #"insert '\'"#, + ] + ) + ], + applyFixIts: [], + fixedSource: """ + func err1() { _ = / 0xG}/ } + """ ) } @@ -352,8 +416,18 @@ final class ForwardSlashRegexSkippingInvalidTests: ParserTestCase { func err2() { _ = /1️⃣ 0oG}/ } """, diagnostics: [ - DiagnosticSpec(message: "bare slash regex literal may not start with space") - ] + DiagnosticSpec( + message: "bare slash regex literal may not start with space", + fixIts: [ + "convert to extended regex literal with '#'", + #"insert '\'"#, + ] + ) + ], + applyFixIts: [], + fixedSource: """ + func err2() { _ = / 0oG}/ } + """ ) } @@ -363,8 +437,18 @@ final class ForwardSlashRegexSkippingInvalidTests: ParserTestCase { func err3() { _ = /1️⃣ {"/ } """, diagnostics: [ - DiagnosticSpec(message: "bare slash regex literal may not start with space") - ] + DiagnosticSpec( + message: "bare slash regex literal may not start with space", + fixIts: [ + "convert to extended regex literal with '#'", + #"insert '\'"#, + ] + ) + ], + applyFixIts: [], + fixedSource: """ + func err3() { _ = / {"/ } + """ ) } @@ -374,8 +458,18 @@ final class ForwardSlashRegexSkippingInvalidTests: ParserTestCase { func err4() { _ = /1️⃣ {'/ } """, diagnostics: [ - DiagnosticSpec(message: "bare slash regex literal may not start with space") - ] + DiagnosticSpec( + message: "bare slash regex literal may not start with space", + fixIts: [ + "convert to extended regex literal with '#'", + #"insert '\'"#, + ] + ) + ], + applyFixIts: [], + fixedSource: """ + func err4() { _ = / {'/ } + """ ) } @@ -385,8 +479,18 @@ final class ForwardSlashRegexSkippingInvalidTests: ParserTestCase { func err5() { _ = /1️⃣ {<#placeholder#>/ } """, diagnostics: [ - DiagnosticSpec(message: "bare slash regex literal may not start with space") - ] + DiagnosticSpec( + message: "bare slash regex literal may not start with space", + fixIts: [ + "convert to extended regex literal with '#'", + #"insert '\'"#, + ] + ) + ], + applyFixIts: [], + fixedSource: """ + func err5() { _ = / {<#placeholder#>/ } + """ ) } } diff --git a/Tests/SwiftParserTest/translated/ForwardSlashRegexSkippingTests.swift b/Tests/SwiftParserTest/translated/ForwardSlashRegexSkippingTests.swift index a2a0ddee2e4..7635d9086ad 100644 --- a/Tests/SwiftParserTest/translated/ForwardSlashRegexSkippingTests.swift +++ b/Tests/SwiftParserTest/translated/ForwardSlashRegexSkippingTests.swift @@ -329,8 +329,18 @@ final class ForwardSlashRegexSkippingTests: ParserTestCase { """, diagnostics: [ // TODO: Old parser had a fix-it to add backslash to escape - DiagnosticSpec(message: "bare slash regex literal may not start with space") - ] + DiagnosticSpec( + message: "bare slash regex literal may not start with space", + fixIts: [ + "convert to extended regex literal with '#'", + #"insert '\'"#, + ] + ) + ], + applyFixIts: [], + fixedSource: """ + func a4() { _ = / / } + """ ) } diff --git a/Tests/SwiftParserTest/translated/ForwardSlashRegexTests.swift b/Tests/SwiftParserTest/translated/ForwardSlashRegexTests.swift index 403cca06ecf..25e94d39f4a 100644 --- a/Tests/SwiftParserTest/translated/ForwardSlashRegexTests.swift +++ b/Tests/SwiftParserTest/translated/ForwardSlashRegexTests.swift @@ -167,8 +167,18 @@ final class ForwardSlashRegexTests: ParserTestCase { _ = !/1️⃣ / """, diagnostics: [ - DiagnosticSpec(message: "bare slash regex literal may not start with space") - ] + DiagnosticSpec( + message: "bare slash regex literal may not start with space", + fixIts: [ + "convert to extended regex literal with '#'", + #"insert '\'"#, + ] + ) + ], + applyFixIts: [], + fixedSource: """ + _ = !/ / + """ ) } @@ -178,8 +188,18 @@ final class ForwardSlashRegexTests: ParserTestCase { _ = !!/1️⃣ / """, diagnostics: [ - DiagnosticSpec(message: "bare slash regex literal may not start with space") - ] + DiagnosticSpec( + message: "bare slash regex literal may not start with space", + fixIts: [ + "convert to extended regex literal with '#'", + #"insert '\'"#, + ] + ) + ], + applyFixIts: [], + fixedSource: """ + _ = !!/ / + """ ) } @@ -1634,8 +1654,18 @@ final class ForwardSlashRegexTests: ParserTestCase { _ = /1️⃣ / """, diagnostics: [ - DiagnosticSpec(message: "bare slash regex literal may not start with space") - ] + DiagnosticSpec( + message: "bare slash regex literal may not start with space", + fixIts: [ + "convert to extended regex literal with '#'", + #"insert '\'"#, + ] + ) + ], + applyFixIts: [], + fixedSource: """ + _ = / / + """ ) } @@ -1645,8 +1675,14 @@ final class ForwardSlashRegexTests: ParserTestCase { _ = /1️⃣ / """, diagnostics: [ - DiagnosticSpec(message: "bare slash regex literal may not start with space") - ] + DiagnosticSpec( + message: "bare slash regex literal may not start with space", + fixIts: ["convert to extended regex literal with '#'"] + ) + ], + fixedSource: """ + _ = #/ /# + """ ) } @@ -1689,7 +1725,11 @@ final class ForwardSlashRegexTests: ParserTestCase { _ = ℹ️/1️⃣ 2️⃣ """, diagnostics: [ - DiagnosticSpec(locationMarker: "1️⃣", message: "bare slash regex literal may not start with space"), + DiagnosticSpec( + locationMarker: "1️⃣", + message: "bare slash regex literal may not start with space", + fixIts: ["convert to extended regex literal with '#'"] + ), DiagnosticSpec( locationMarker: "2️⃣", message: "expected '/' to end regex literal", @@ -1697,9 +1737,20 @@ final class ForwardSlashRegexTests: ParserTestCase { fixIts: ["insert '/'"] ), ], - fixedSource: """ - _ = / / - """ + fixItsApplications: [ + .optIn( + applyFixIts: ["convert to extended regex literal with '#'"], + fixedSource: """ + _ = #/ /# + """ + ), + .optIn( + applyFixIts: ["insert '/'"], + fixedSource: """ + _ = / / + """ + ), + ] ) } diff --git a/Tests/SwiftParserTest/translated/InvalidTests.swift b/Tests/SwiftParserTest/translated/InvalidTests.swift index f971ad7dc15..df72cbc7bdb 100644 --- a/Tests/SwiftParserTest/translated/InvalidTests.swift +++ b/Tests/SwiftParserTest/translated/InvalidTests.swift @@ -538,84 +538,84 @@ final class InvalidTests: ParserTestCase { } func testInvalid23() { - let testCases: [UInt: (fixIt: String, fixedSource: String)] = [ - #line: ("join the identifiers together", "func dogcow() {}"), - #line: ("join the identifiers together with camel-case", "func dogCow() {}"), - ] - - for (line, testCase) in testCases { - assertParse( - """ - func dog 1️⃣cow() {} - """, - diagnostics: [ - DiagnosticSpec( - message: "found an unexpected second identifier in function; is there an accidental break?", - fixIts: [ - "join the identifiers together", - "join the identifiers together with camel-case", - ] - ) - ], - applyFixIts: [testCase.fixIt], - fixedSource: testCase.fixedSource, - line: line - ) - } + assertParse( + """ + func dog 1️⃣cow() {} + """, + diagnostics: [ + DiagnosticSpec( + message: "found an unexpected second identifier in function; is there an accidental break?", + fixIts: [ + "join the identifiers together", + "join the identifiers together with camel-case", + ] + ) + ], + fixItsApplications: [ + .optIn( + applyFixIts: ["join the identifiers together"], + fixedSource: "func dogcow() {}" + ), + .optIn( + applyFixIts: ["join the identifiers together with camel-case"], + fixedSource: "func dogCow() {}" + ), + ] + ) } func testThreeIdentifersForFunctionName() { - let testCases: [UInt: (fixIt: String, fixedSource: String)] = [ - #line: ("join the identifiers together", "func dogcowsheep() {}"), - #line: ("join the identifiers together with camel-case", "func dogCowSheep() {}"), - ] - - for (line, testCase) in testCases { - assertParse( - """ - func dog 1️⃣cow sheep() {} - """, - diagnostics: [ - DiagnosticSpec( - message: "found an unexpected second identifier in function; is there an accidental break?", - fixIts: [ - "join the identifiers together", - "join the identifiers together with camel-case", - ] - ) - ], - applyFixIts: [testCase.fixIt], - fixedSource: testCase.fixedSource, - line: line - ) - } + assertParse( + """ + func dog 1️⃣cow sheep() {} + """, + diagnostics: [ + DiagnosticSpec( + message: "found an unexpected second identifier in function; is there an accidental break?", + fixIts: [ + "join the identifiers together", + "join the identifiers together with camel-case", + ] + ) + ], + fixItsApplications: [ + .optIn( + applyFixIts: ["join the identifiers together"], + fixedSource: "func dogcowsheep() {}" + ), + .optIn( + applyFixIts: ["join the identifiers together with camel-case"], + fixedSource: "func dogCowSheep() {}" + ), + ] + ) } func testInvalid25() { - let testCases: [UInt: (fixIt: String, fixedSource: String)] = [ - #line: ("join the identifiers together", "func friendship(x: T) {}"), - #line: ("join the identifiers together with camel-case", "func friendShip(x: T) {}"), - ] - - for (line, testCase) in testCases { - assertParse( - """ - func friend 1️⃣ship(x: T) {} - """, - diagnostics: [ - DiagnosticSpec( - message: "found an unexpected second identifier in function; is there an accidental break?", - fixIts: [ - "join the identifiers together", - "join the identifiers together with camel-case", - ] - ) - ], - applyFixIts: [testCase.fixIt], - fixedSource: testCase.fixedSource, - line: line - ) - } + assertParse( + """ + func friend 1️⃣ship(x: T) {} + """, + diagnostics: [ + DiagnosticSpec( + message: "found an unexpected second identifier in function; is there an accidental break?", + fixIts: [ + "join the identifiers together", + "join the identifiers together with camel-case", + ] + ) + ], + fixItsApplications: [ + .optIn( + applyFixIts: ["join the identifiers together"], + fixedSource: "func friendship(x: T) {}" + ), + .optIn( + applyFixIts: ["join the identifiers together with camel-case"], + fixedSource: "func friendShip(x: T) {}" + ), + ] + ) } func testInvalid26() { diff --git a/Tests/SwiftParserTest/translated/RecoveryTests.swift b/Tests/SwiftParserTest/translated/RecoveryTests.swift index 93ce16e2c43..9a04d223636 100644 --- a/Tests/SwiftParserTest/translated/RecoveryTests.swift +++ b/Tests/SwiftParserTest/translated/RecoveryTests.swift @@ -1066,29 +1066,28 @@ final class RecoveryTests: ParserTestCase { // MARK: - Recovery for multiple identifiers in decls func testRecovery58() { - let testCases: [UInt: (fixIt: String, fixedSource: String)] = [ - #line: ("join the identifiers together", "protocol Multiident {}"), - #line: ("join the identifiers together with camel-case", "protocol MultiIdent {}"), - ] - - for (line, testCase) in testCases { - assertParse( - """ - protocol Multi 1️⃣ident {} - """, - diagnostics: [ - DiagnosticSpec( - message: "found an unexpected second identifier in protocol; is there an accidental break?", - highlight: "ident", - fixIts: ["join the identifiers together", "join the identifiers together with camel-case"], - line: line - ) - ], - applyFixIts: [testCase.fixIt], - fixedSource: testCase.fixedSource, - line: line - ) - } + assertParse( + """ + protocol Multi 1️⃣ident {} + """, + diagnostics: [ + DiagnosticSpec( + message: "found an unexpected second identifier in protocol; is there an accidental break?", + highlight: "ident", + fixIts: ["join the identifiers together", "join the identifiers together with camel-case"] + ) + ], + fixItsApplications: [ + .optIn( + applyFixIts: ["join the identifiers together"], + fixedSource: "protocol Multiident {}" + ), + .optIn( + applyFixIts: ["join the identifiers together with camel-case"], + fixedSource: "protocol MultiIdent {}" + ), + ] + ) } func testRecovery60() {