-
Notifications
You must be signed in to change notification settings - Fork 280
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Convert implicitly unwrapped optionals to proper optionals #1539
base: main
Are you sure you want to change the base?
Convert implicitly unwrapped optionals to proper optionals #1539
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very nice.
import SwiftSyntax | ||
|
||
/// Convert implicitly unwrapped optionals (IUOs) to proper optionals | ||
@_spi(Testing) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As far as I can tell, we don’t use this type in tests, so ConvertIUOToProperOptional
doesn’t need to be @_spi
or public
but can be internal
instead.
|
||
/// Convert implicitly unwrapped optionals (IUOs) to proper optionals | ||
@_spi(Testing) | ||
public struct ConvertIUOToProperOptional: SyntaxRefactoringProvider { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I’d suggest that we’re a little more verbose here and spell out IUO
as ImplicitlyUnwrappedOptional
. It’s a little more verbose but makes the code action easier to newcomers to the code base.
Also, let’s just call the ?
syntax Optional instead of Proper Optional.
public struct ConvertIUOToProperOptional: SyntaxRefactoringProvider { | |
public struct ConvertImplicitlyUnwrappedOptionalToOptional: SyntaxRefactoringProvider { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree, I was under a false impression there was a restriction on the maximum length of any file name, as I had never come across a file name longer than this!
} | ||
|
||
extension ConvertIUOToProperOptional: SyntaxRefactoringCodeActionProvider { | ||
static let title: String = "Convert Implicitly Unwrapped Optional to Proper Optional" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
static let title: String = "Convert Implicitly Unwrapped Optional to Proper Optional" | |
static let title: String = "Convert Implicitly Unwrapped Optional to Optional" |
public typealias Input = ImplicitlyUnwrappedOptionalTypeSyntax | ||
public typealias Context = Void | ||
public typealias Output = OptionalTypeSyntax | ||
|
||
@_spi(Testing) | ||
public static func refactor(syntax: Input, in context: Context) -> Output? { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can save all the type aliases if we spell out the types in the function signature. I think it also makes the code easier to read because you don’t always need to cross-reference the type aliases when reading the function signature of refactor
.
public typealias Input = ImplicitlyUnwrappedOptionalTypeSyntax | |
public typealias Context = Void | |
public typealias Output = OptionalTypeSyntax | |
@_spi(Testing) | |
public static func refactor(syntax: Input, in context: Context) -> Output? { | |
@_spi(Testing) | |
public static func refactor(syntax: ImplicitlyUnwrappedOptionalTypeSyntax, in context: Void) -> OptionalTypeSyntax? { |
guard let token = scope.innermostNodeContainingRange else { | ||
return nil | ||
} | ||
|
||
return if let iuoType = token.as(Input.self) ?? token.parent?.as(Input.self) { | ||
iuoType | ||
} else if token.is(TokenSyntax.self), | ||
let wrappedType = token.parent?.as(TypeSyntax.self), | ||
let iuoType = wrappedType.parent?.as(Input.self), | ||
iuoType.wrappedType == wrappedType | ||
{ | ||
iuoType | ||
} else { | ||
nil | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is there any reason why you didn’t use findParentOrSelf(ofType:stoppingIf:)
like the other refactoring actions?
I think the benefit of that would be that the refactoring applies in more situations. Eg. in your test case, you would also get the code action when placing the cursor in Int
, which seems reasonable to me.
Something like
guard let token = scope.innermostNodeContainingRange else { | |
return nil | |
} | |
return if let iuoType = token.as(Input.self) ?? token.parent?.as(Input.self) { | |
iuoType | |
} else if token.is(TokenSyntax.self), | |
let wrappedType = token.parent?.as(TypeSyntax.self), | |
let iuoType = wrappedType.parent?.as(Input.self), | |
iuoType.wrappedType == wrappedType | |
{ | |
iuoType | |
} else { | |
nil | |
} | |
return scope.innermostNodeContainingRange?.findParentOfSelf( | |
ofType: ImplicitlyUnwrappedOptionalTypeSyntax.self, | |
stoppingIf: { $0.is(CodeBlockSyntax.self) || $0.is(MemberBlockSyntax.self) } | |
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't use findParentOrSelf(ofType:stoppingIf:)
like other refactoring actions because I wanted to restrict the scope of this code action to the nearest type syntax code that might make local reasoning easier and the namespace of code actions cleaner?
Say, if this code action is triggered by pointing to a type situated deeply inside a heavily nested IUO, it may confuse me a bit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it’s fair to assume that usually types are not nested too deeply and in the past we’ve had more issues with being to restrictive with the scopes in which refactorings are shown than being to widely applicable.
In my original plan, I wanted to insert optional chaining for any code affected by the refactoring in the same source file, do you think it's a worthwhile and feasible effort? |
…1320 - created ConvertImplicitlyUnwrappedOptionalToOptional that will suggest a conversion when the token in concern is - an IUO; - a direct child of an IUO; or - a direct child of the wrappedType of an IUO. - registered in SyntaxCodeActions.allSyntaxCodeActions - added a test in CodeActionTests - added ConvertImplicitlyUnwrappedOptionalToOptional.swift to CMakeLists of SourceKitLSP
c8ce60b
to
e772363
Compare
The question is: What exactly should we do here? I can think of essentially three options:
I would be leaning towards option (1) even though I don’t like introducing force unwraps to the user’s code base.
I think the biggest win with such an implementation is to have a blueprint of a refactoring that uses information from sourcekitd and the index to then perform syntactic refactorings. This refactoring in itself isn’t all that interesting, I think but it could clear the pathway for quite a few interesting refactorings. Some that come to my mind would be:
|
Having looked into the spec of LSP for a while, I think we need to add a new field public struct CodeAction: ResponseType, Hashable {
...
/// A data entry field that is preserved between a `CodeActionRequest` and a `CodeActionResolveRequest`.
public var data: LSPAny?
...
public init(
title: String,
kind: CodeActionKind? = nil,
diagnostics: [Diagnostic]? = nil,
edit: WorkspaceEdit? = nil,
command: Command? = nil,
data: LSPAny? = nil
) {
self.title = title
self.kind = kind
self.diagnostics = diagnostics
self.edit = edit
self.command = command
self.data = data
}
} To play it safe, I think we need to keep empty both
Our most pessimistic expectation may be any well-behaving and non-assuming sourcekit-LSP client should issue a |
Oh yes, the
It would be good to double-check how a few editors behave but I assume that you’re right here. We should, however, populate |
I can envision the necessity of using index for the resolution of this code action, it then implies if the index wasn't available the resolution couldn't proceed. Should we introduce a mechanism to disable code actions when certain conditions aren't met? |
I think it would be fine to offer the code action and then return an error response from |
fixed #1320