diff --git a/Sources/SourceKitLSP/CMakeLists.txt b/Sources/SourceKitLSP/CMakeLists.txt index 821167f9f..518137d4d 100644 --- a/Sources/SourceKitLSP/CMakeLists.txt +++ b/Sources/SourceKitLSP/CMakeLists.txt @@ -30,6 +30,7 @@ target_sources(SourceKitLSP PRIVATE Swift/AdjustPositionToStartOfIdentifier.swift Swift/CodeActions/AddDocumentation.swift Swift/CodeActions/ConvertIntegerLiteral.swift + Swift/CodeActions/ConvertImplicitlyUnwrappedOptionalToOptional.swift Swift/CodeActions/ConvertJSONToCodableStruct.swift Swift/CodeActions/PackageManifestEdits.swift Swift/CodeActions/SyntaxCodeActionProvider.swift diff --git a/Sources/SourceKitLSP/Swift/CodeActions/ConvertImplicitlyUnwrappedOptionalToOptional.swift b/Sources/SourceKitLSP/Swift/CodeActions/ConvertImplicitlyUnwrappedOptionalToOptional.swift new file mode 100644 index 000000000..45ce1abcd --- /dev/null +++ b/Sources/SourceKitLSP/Swift/CodeActions/ConvertImplicitlyUnwrappedOptionalToOptional.swift @@ -0,0 +1,56 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2014 - 2024 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 +// +//===----------------------------------------------------------------------===// + +import LanguageServerProtocol +import SwiftRefactor +import SwiftSyntax + +/// Convert implicitly unwrapped optionals to optionals +struct ConvertImplicitlyUnwrappedOptionalToOptional: SyntaxRefactoringProvider { + public static func refactor(syntax: ImplicitlyUnwrappedOptionalTypeSyntax, in context: Void) -> OptionalTypeSyntax? { + OptionalTypeSyntax( + leadingTrivia: syntax.leadingTrivia, + syntax.unexpectedBeforeWrappedType, + wrappedType: syntax.wrappedType, + syntax.unexpectedBetweenWrappedTypeAndExclamationMark, + questionMark: .postfixQuestionMarkToken( + leadingTrivia: syntax.exclamationMark.leadingTrivia, + trailingTrivia: syntax.exclamationMark.trailingTrivia + ) + ) + } +} + +extension ConvertImplicitlyUnwrappedOptionalToOptional: SyntaxRefactoringCodeActionProvider { + static let title: String = "Convert Implicitly Unwrapped Optional to Optional" + + static func nodeToRefactor(in scope: SyntaxCodeActionScope) -> ImplicitlyUnwrappedOptionalTypeSyntax? { + guard let token = scope.innermostNodeContainingRange else { + return nil + } + + return + if let iuoType = token.as(ImplicitlyUnwrappedOptionalTypeSyntax.self) + ?? token.parent?.as(ImplicitlyUnwrappedOptionalTypeSyntax.self) + { + iuoType + } else if token.is(TokenSyntax.self), + let wrappedType = token.parent?.as(TypeSyntax.self), + let iuoType = wrappedType.parent?.as(ImplicitlyUnwrappedOptionalTypeSyntax.self), + iuoType.wrappedType == wrappedType + { + iuoType + } else { + nil + } + } +} diff --git a/Sources/SourceKitLSP/Swift/CodeActions/SyntaxCodeActions.swift b/Sources/SourceKitLSP/Swift/CodeActions/SyntaxCodeActions.swift index acdea5dd8..740a278ec 100644 --- a/Sources/SourceKitLSP/Swift/CodeActions/SyntaxCodeActions.swift +++ b/Sources/SourceKitLSP/Swift/CodeActions/SyntaxCodeActions.swift @@ -18,6 +18,7 @@ let allSyntaxCodeActions: [SyntaxCodeActionProvider.Type] = [ AddDocumentation.self, AddSeparatorsToIntegerLiteral.self, ConvertIntegerLiteral.self, + ConvertImplicitlyUnwrappedOptionalToOptional.self, ConvertJSONToCodableStruct.self, FormatRawStringLiteral.self, MigrateToNewIfLetSyntax.self, diff --git a/Tests/SourceKitLSPTests/CodeActionTests.swift b/Tests/SourceKitLSPTests/CodeActionTests.swift index 9e9c26ae9..4989ea278 100644 --- a/Tests/SourceKitLSPTests/CodeActionTests.swift +++ b/Tests/SourceKitLSPTests/CodeActionTests.swift @@ -1006,6 +1006,34 @@ final class CodeActionTests: XCTestCase { } } + func testConvertIUOToProperOptional() async throws { + try await assertCodeActions( + """ + func (a: 1️⃣(String, /* tuple */ Int)!2️⃣/*tra3️⃣iling*/4️⃣) + """, + markers: ["1️⃣", "2️⃣", "3️⃣"], + ranges: [("1️⃣", "2️⃣"), ("1️⃣", "3️⃣")], + exhaustive: false + ) { uri, positions in + [ + CodeAction( + title: "Convert Implicitly Unwrapped Optional to Optional", + kind: .refactorInline, + edit: WorkspaceEdit( + changes: [ + uri: [ + TextEdit( + range: positions["1️⃣"]..