diff --git a/Sources/SwiftRefactor/CMakeLists.txt b/Sources/SwiftRefactor/CMakeLists.txt index 18a17a75802..1864c71f2ab 100644 --- a/Sources/SwiftRefactor/CMakeLists.txt +++ b/Sources/SwiftRefactor/CMakeLists.txt @@ -10,7 +10,9 @@ add_swift_syntax_library(SwiftRefactor AddSeparatorsToIntegerLiteral.swift CallToTrailingClosures.swift ConvertComputedPropertyToStored.swift + ConvertComputedPropertyToZeroParameterFunction.swift ConvertStoredPropertyToComputed.swift + ConvertZeroParameterFunctionToComputedProperty.swift ExpandEditorPlaceholder.swift FormatRawStringLiteral.swift IntegerLiteralUtilities.swift @@ -18,6 +20,7 @@ add_swift_syntax_library(SwiftRefactor OpaqueParameterToGeneric.swift RefactoringProvider.swift RemoveSeparatorsFromIntegerLiteral.swift + SyntaxUtils.swift ) target_link_swift_syntax_libraries(SwiftRefactor PUBLIC diff --git a/Sources/SwiftRefactor/CallToTrailingClosures.swift b/Sources/SwiftRefactor/CallToTrailingClosures.swift index f49c6465f3f..c096bca63d8 100644 --- a/Sources/SwiftRefactor/CallToTrailingClosures.swift +++ b/Sources/SwiftRefactor/CallToTrailingClosures.swift @@ -152,12 +152,6 @@ extension FunctionCallExprSyntax { } } -fileprivate extension Trivia { - var droppingLeadingWhitespace: Trivia { - return Trivia(pieces: self.drop(while: \.isWhitespace)) - } -} - fileprivate extension Sequence { func dropSuffix(while predicate: (Element) -> Bool) -> [Element] { self.reversed().drop(while: predicate).reversed() diff --git a/Sources/SwiftRefactor/ConvertComputedPropertyToStored.swift b/Sources/SwiftRefactor/ConvertComputedPropertyToStored.swift index 67965623296..64d73884a41 100644 --- a/Sources/SwiftRefactor/ConvertComputedPropertyToStored.swift +++ b/Sources/SwiftRefactor/ConvertComputedPropertyToStored.swift @@ -82,15 +82,3 @@ public struct ConvertComputedPropertyToStored: SyntaxRefactoringProvider { return nil } } - -fileprivate extension TokenSyntax { - var trivia: Trivia { - return leadingTrivia + trailingTrivia - } -} - -fileprivate extension Trivia { - var droppingTrailingWhitespace: Trivia { - return Trivia(pieces: self.reversed().drop(while: \.isWhitespace).reversed()) - } -} diff --git a/Sources/SwiftRefactor/ConvertComputedPropertyToZeroParameterFunction.swift b/Sources/SwiftRefactor/ConvertComputedPropertyToZeroParameterFunction.swift new file mode 100644 index 00000000000..bc89b5475ab --- /dev/null +++ b/Sources/SwiftRefactor/ConvertComputedPropertyToZeroParameterFunction.swift @@ -0,0 +1,105 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +#if swift(>=6) +public import SwiftSyntax +#else +import SwiftSyntax +#endif + +public struct ConvertComputedPropertyToZeroParameterFunction: SyntaxRefactoringProvider { + public static func refactor(syntax: VariableDeclSyntax, in context: Void) -> FunctionDeclSyntax? { + guard syntax.bindings.count == 1, + let binding = syntax.bindings.first, + let identifierPattern = binding.pattern.as(IdentifierPatternSyntax.self) + else { return nil } + + var statements: CodeBlockItemListSyntax + + guard let typeAnnotation = binding.typeAnnotation, + var accessorBlock = binding.accessorBlock + else { return nil } + + var effectSpecifiers: AccessorEffectSpecifiersSyntax? + + switch accessorBlock.accessors { + case .accessors(let accessors): + guard accessors.count == 1, let accessor = accessors.first, + accessor.accessorSpecifier.tokenKind == .keyword(.get), let codeBlock = accessor.body + else { return nil } + effectSpecifiers = accessor.effectSpecifiers + statements = codeBlock.statements + let accessorSpecifier = accessor.accessorSpecifier + statements.leadingTrivia = + accessorSpecifier.leadingTrivia + accessorSpecifier.trailingTrivia.droppingLeadingWhitespace + + codeBlock.leftBrace.leadingTrivia.droppingLeadingWhitespace + + codeBlock.leftBrace.trailingTrivia.droppingLeadingWhitespace + + statements.leadingTrivia + statements.trailingTrivia += codeBlock.rightBrace.trivia.droppingLeadingWhitespace + statements.trailingTrivia = statements.trailingTrivia.droppingTrailingWhitespace + case .getter(let codeBlock): + statements = codeBlock + } + + let returnType = typeAnnotation.type + + var returnClause: ReturnClauseSyntax? + let triviaAfterSignature: Trivia + + if !returnType.isVoid { + triviaAfterSignature = .space + returnClause = ReturnClauseSyntax( + arrow: .arrowToken( + leadingTrivia: typeAnnotation.colon.leadingTrivia, + trailingTrivia: typeAnnotation.colon.trailingTrivia + ), + type: returnType + ) + } else { + triviaAfterSignature = typeAnnotation.colon.leadingTrivia + typeAnnotation.colon.trailingTrivia + } + + accessorBlock.leftBrace.leadingTrivia = accessorBlock.leftBrace.leadingTrivia.droppingLeadingWhitespace + accessorBlock.rightBrace.trailingTrivia = accessorBlock.rightBrace.trailingTrivia.droppingTrailingWhitespace + + let body = CodeBlockSyntax( + leftBrace: accessorBlock.leftBrace, + statements: statements, + rightBrace: accessorBlock.rightBrace + ) + + var parameterClause = FunctionParameterClauseSyntax(parameters: []) + parameterClause.trailingTrivia = identifierPattern.identifier.trailingTrivia + triviaAfterSignature + + let functionEffectSpecifiers = FunctionEffectSpecifiersSyntax( + asyncSpecifier: effectSpecifiers?.asyncSpecifier, + throwsClause: effectSpecifiers?.throwsClause + ) + let functionSignature = FunctionSignatureSyntax( + parameterClause: parameterClause, + effectSpecifiers: functionEffectSpecifiers, + returnClause: returnClause + ) + + return FunctionDeclSyntax( + modifiers: syntax.modifiers, + funcKeyword: .keyword( + .func, + leadingTrivia: syntax.bindingSpecifier.leadingTrivia, + trailingTrivia: syntax.bindingSpecifier.trailingTrivia + ), + name: identifierPattern.identifier.with(\.trailingTrivia, []), + signature: functionSignature, + body: body + ) + } +} diff --git a/Sources/SwiftRefactor/ConvertZeroParameterFunctionToComputedProperty.swift b/Sources/SwiftRefactor/ConvertZeroParameterFunctionToComputedProperty.swift new file mode 100644 index 00000000000..2e8e336eeb6 --- /dev/null +++ b/Sources/SwiftRefactor/ConvertZeroParameterFunctionToComputedProperty.swift @@ -0,0 +1,70 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +#if swift(>=6) +public import SwiftSyntax +#else +import SwiftSyntax +#endif + +public struct ConvertZeroParameterFunctionToComputedProperty: SyntaxRefactoringProvider { + public static func refactor(syntax: FunctionDeclSyntax, in context: ()) -> VariableDeclSyntax? { + guard syntax.signature.parameterClause.parameters.isEmpty, + let body = syntax.body + else { return nil } + + let variableName = PatternSyntax( + IdentifierPatternSyntax( + leadingTrivia: syntax.funcKeyword.trailingTrivia, + identifier: syntax.name + ) + ) + + let triviaFromParameters = + (syntax.signature.parameterClause.leftParen.trivia + syntax.signature.parameterClause.rightParen.trivia) + .droppingTrailingWhitespace + + var variableType: TypeAnnotationSyntax? + + if let returnClause = syntax.signature.returnClause { + variableType = TypeAnnotationSyntax( + colon: .colonToken( + leadingTrivia: triviaFromParameters + returnClause.arrow.leadingTrivia, + trailingTrivia: returnClause.arrow.trailingTrivia + ), + type: returnClause.type + ) + } else { + variableType = TypeAnnotationSyntax( + colon: .colonToken( + leadingTrivia: triviaFromParameters, + trailingTrivia: .space + ), + type: TypeSyntax("Void").with(\.trailingTrivia, .space) + ) + } + + let accessorBlock = AccessorBlockSyntax( + leftBrace: body.leftBrace, + accessors: .getter(body.statements), + rightBrace: body.rightBrace + ) + + return VariableDeclSyntax( + modifiers: syntax.modifiers, + .var, + name: variableName, + type: variableType, + accessorBlock: accessorBlock + ) + } +} diff --git a/Sources/SwiftRefactor/SyntaxUtils.swift b/Sources/SwiftRefactor/SyntaxUtils.swift new file mode 100644 index 00000000000..4949a5bbff6 --- /dev/null +++ b/Sources/SwiftRefactor/SyntaxUtils.swift @@ -0,0 +1,43 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +#if swift(>=6) +public import SwiftSyntax +#else +import SwiftSyntax +#endif + +extension TokenSyntax { + var trivia: Trivia { + return leadingTrivia + trailingTrivia + } +} + +extension Trivia { + var droppingLeadingWhitespace: Trivia { + return Trivia(pieces: self.drop(while: \.isWhitespace)) + } + + var droppingTrailingWhitespace: Trivia { + return Trivia(pieces: self.reversed().drop(while: \.isWhitespace).reversed()) + } +} + +extension TypeSyntax { + var isVoid: Bool { + switch self.as(TypeSyntaxEnum.self) { + case .identifierType(let identifierType) where identifierType.name.text == "Void": return true + case .tupleType(let tupleType) where tupleType.elements.isEmpty: return true + default: return false + } + } +} diff --git a/Sources/SwiftSyntaxBuilder/ConvenienceInitializers.swift b/Sources/SwiftSyntaxBuilder/ConvenienceInitializers.swift index a4d306bfdb6..6342a4c5a86 100644 --- a/Sources/SwiftSyntaxBuilder/ConvenienceInitializers.swift +++ b/Sources/SwiftSyntaxBuilder/ConvenienceInitializers.swift @@ -403,7 +403,8 @@ extension VariableDeclSyntax { _ bindingSpecifier: Keyword, name: PatternSyntax, type: TypeAnnotationSyntax? = nil, - initializer: InitializerClauseSyntax? = nil + initializer: InitializerClauseSyntax? = nil, + accessorBlock: AccessorBlockSyntax? = nil ) { self.init( leadingTrivia: leadingTrivia, @@ -414,7 +415,8 @@ extension VariableDeclSyntax { PatternBindingSyntax( pattern: name, typeAnnotation: type, - initializer: initializer + initializer: initializer, + accessorBlock: accessorBlock ) } } diff --git a/Tests/SwiftRefactorTest/ConvertComputedPropertyToZeroParameterFunctionTests.swift b/Tests/SwiftRefactorTest/ConvertComputedPropertyToZeroParameterFunctionTests.swift new file mode 100644 index 00000000000..c75c4784b87 --- /dev/null +++ b/Tests/SwiftRefactorTest/ConvertComputedPropertyToZeroParameterFunctionTests.swift @@ -0,0 +1,318 @@ +//===----------------------------------------------------------------------===// +// +// 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 SwiftRefactor +import SwiftSyntax +import SwiftSyntaxBuilder +import XCTest +import _SwiftSyntaxTestSupport + +final class ConvertComputedPropertyToZeroParameterFunctionTests: XCTestCase { + func testRefactoringComputedPropertyToFunction() throws { + let baseline: DeclSyntax = """ + var asJSON: String { "" } + """ + + let expected: DeclSyntax = """ + func asJSON() -> String { "" } + """ + + try assertRefactorConvert(baseline, expected: expected) + } + + func testRefactoringComputedPropertyWithVoidToFunction() throws { + let baseline: DeclSyntax = """ + var asJSON: Void { () } + """ + + let expected: DeclSyntax = """ + func asJSON() { () } + """ + + try assertRefactorConvert(baseline, expected: expected) + } + + func testRefactoringComputedPropertyWithTupleToFunction() throws { + let baseline: DeclSyntax = """ + var asJSON: (String, String) { ("", "") } + """ + + let expected: DeclSyntax = """ + func asJSON() -> (String, String) { ("", "") } + """ + + try assertRefactorConvert(baseline, expected: expected) + } + + func testRefactoringComputedPropertyWithClosureToFunction() throws { + let baseline: DeclSyntax = """ + var asJSON: () -> Void { {} } + """ + + let expected: DeclSyntax = """ + func asJSON() -> () -> Void { {} } + """ + + try assertRefactorConvert(baseline, expected: expected) + } + + func testRefactoringComputedPropertyWithClosureToFunction2() throws { + let baseline: DeclSyntax = """ + var asJSON: () -> () { {} } + """ + + let expected: DeclSyntax = """ + func asJSON() -> () -> () { {} } + """ + + try assertRefactorConvert(baseline, expected: expected) + } + + func testRefactoringComputedPropertyWithVoidToFunctionWithSeparatingComment() throws { + let baseline: DeclSyntax = """ + var asJSON : /*comment*/ Void { () } + """ + + let expected: DeclSyntax = """ + func asJSON() /*comment*/ { () } + """ + + try assertRefactorConvert(baseline, expected: expected) + } + + func testRefactoringComputedPropertyToFunctionWithReturnStmt() throws { + let baseline: DeclSyntax = """ + var asJSON: String { return "" } + """ + + let expected: DeclSyntax = """ + func asJSON() -> String { return "" } + """ + + try assertRefactorConvert(baseline, expected: expected) + } + + func testRefactoringComputedPropertyToFunctionWithModifiers() throws { + let baseline: DeclSyntax = """ + static var asJSON: String { "" } + """ + + let expected: DeclSyntax = """ + static func asJSON() -> String { "" } + """ + + try assertRefactorConvert(baseline, expected: expected) + } + + func testRefactoringComputedPropertyToFunctionWithMultipleStms() throws { + let baseline: DeclSyntax = """ + var asJSON: String { + let builder = JSONBuilder() + return builder.convert() + } + """ + + let expected: DeclSyntax = """ + func asJSON() -> String { + let builder = JSONBuilder() + return builder.convert() + } + """ + + try assertRefactorConvert(baseline, expected: expected) + } + + func testRefactoringComputedPropertyToFunctionWithCommentsAndIndentations() throws { + let baseline: DeclSyntax = """ + static var asJSON : String { /*comment*/ "" /*comment*/ } + """ + + let expected: DeclSyntax = """ + static func asJSON() -> String { /*comment*/ "" /*comment*/ } + """ + + try assertRefactorConvert(baseline, expected: expected) + } + + func testRefactoringComputedPropertyToFunctionWithComments() throws { + let baseline: DeclSyntax = """ + // Comment + public static var asJSON : String { /*comment*/ + /*comment*/ return "String" + // Some documentation + } // Comment + """ + + let expected: DeclSyntax = """ + // Comment + public static func asJSON() -> String { /*comment*/ + /*comment*/ return "String" + // Some documentation + } // Comment + """ + + try assertRefactorConvert(baseline, expected: expected) + } + + func testRefactoringComputedPropertyToNothing() throws { + let baseline: DeclSyntax = """ + var x: Int { + get { 5 } + set { /*anything */ } + } + """ + try assertRefactorConvert(baseline, expected: nil) + } + + func testRefactoringComputedPropertyWithGetAccessorToFunction() throws { + let baseline: DeclSyntax = """ + var x: Int { + get { 5 } + } + """ + + let expected: DeclSyntax = """ + func x() -> Int { + 5 + } + """ + + try assertRefactorConvert(baseline, expected: expected) + } + + func testRefactoringComputedPropertyWithGetAccessorAndAsyncEffectSpecifierToFunction() throws { + let baseline: DeclSyntax = """ + var foo: Int { + get async { await someAsyncValue() } + } + + """ + + let expected: DeclSyntax = """ + func foo() async -> Int { + await someAsyncValue() + } + """ + + try assertRefactorConvert(baseline, expected: expected) + } + + func testRefactoringComputedPropertyWithGetAccessorAndThrowsEffectSpecifierToFunction() throws { + let baseline: DeclSyntax = """ + var foo: Int { + get throws { someAsyncValue() } + } + """ + + let expected: DeclSyntax = """ + func foo() throws -> Int { + someAsyncValue() + } + """ + + try assertRefactorConvert(baseline, expected: expected) + } + + func testRefactoringComputedPropertyWithGetAccessorAndAsyncThrowsEffectSpecifierToFunction() throws { + let baseline: DeclSyntax = """ + var foo: Int { + get async throws { someAsyncValue() } + } + """ + + let expected: DeclSyntax = """ + func foo() async throws -> Int { + someAsyncValue() + } + """ + + try assertRefactorConvert(baseline, expected: expected) + } + + func testRefactoringComputedPropertyWithLeadingTriviaInBindingToFunction() throws { + let baseline: DeclSyntax = """ + /// Documented behavior + var foo: Int { 0 } + """ + + let expected: DeclSyntax = """ + /// Documented behavior + func foo() -> Int { 0 } + """ + + try assertRefactorConvert(baseline, expected: expected) + } + + func testRefactoringComputedPropertyWithAccessorCommentsToFunction() throws { + let baseline: DeclSyntax = """ + var foo: Int { + get /*docs...*/ { 0 } + } + """ + + let expected: DeclSyntax = """ + func foo() -> Int { + /*docs...*/ 0 + } + """ + + try assertRefactorConvert(baseline, expected: expected) + } + + func testRefactoringComputedPropertyWithCommentsInsideAccessorToFunction() throws { + let baseline: DeclSyntax = """ + var foo: Int { + get { /*docs*/ 0 /*documented*/ } + } + """ + + let expected: DeclSyntax = """ + func foo() -> Int { + /*docs*/ 0 /*documented*/ + } + """ + + try assertRefactorConvert(baseline, expected: expected) + } + + func testRefactoringComputedPropertyWithAccessorMultipleCommentsToFunction() throws { + let baseline: DeclSyntax = """ + var foo: Int { // Leading comments + get /*docs...*/ { 0 } // docs + /*Trailing Comments*/ } + """ + + let expected: DeclSyntax = """ + func foo() -> Int { // Leading comments + /*docs...*/ 0 // docs + /*Trailing Comments*/ } + """ + + try assertRefactorConvert(baseline, expected: expected) + } +} + +fileprivate func assertRefactorConvert( + _ callDecl: DeclSyntax, + expected: DeclSyntax?, + file: StaticString = #filePath, + line: UInt = #line +) throws { + try assertRefactor( + callDecl, + context: (), + provider: ConvertComputedPropertyToZeroParameterFunction.self, + expected: expected, + file: file, + line: line + ) +} diff --git a/Tests/SwiftRefactorTest/ConvertZeroParameterFunctionToComputedPropertyTests.swift b/Tests/SwiftRefactorTest/ConvertZeroParameterFunctionToComputedPropertyTests.swift new file mode 100644 index 00000000000..a55e8610849 --- /dev/null +++ b/Tests/SwiftRefactorTest/ConvertZeroParameterFunctionToComputedPropertyTests.swift @@ -0,0 +1,157 @@ +//===----------------------------------------------------------------------===// +// +// 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 SwiftRefactor +import SwiftSyntax +import SwiftSyntaxBuilder +import XCTest +import _SwiftSyntaxTestSupport + +final class ConvertZeroParameterFunctionToComputedPropertyTests: XCTestCase { + func testRefactoringFunctionToComputedProperty() throws { + let baseline: DeclSyntax = """ + func asJSON() -> String { "" } + """ + + let expected: DeclSyntax = """ + var asJSON: String { "" } + """ + + try assertRefactorConvert(baseline, expected: expected) + } + + func testRefactoringFunctionToComputedPropertyWithVoidType() throws { + let baseline: DeclSyntax = """ + func asJSON() { () } + """ + + let expected: DeclSyntax = """ + var asJSON: Void { () } + """ + + try assertRefactorConvert(baseline, expected: expected) + } + + func testRefactoringFunctionToComputedPropertyWithTuple() throws { + let baseline: DeclSyntax = """ + func asJSON() -> (String, String) { ("", "") } + """ + + let expected: DeclSyntax = """ + var asJSON: (String, String) { ("", "") } + """ + + try assertRefactorConvert(baseline, expected: expected) + } + + func testRefactoringFunctionToComputedPropertyWithClosure() throws { + let baseline: DeclSyntax = """ + func asJSON() -> () -> Void { {} } + """ + + let expected: DeclSyntax = """ + var asJSON: () -> Void { {} } + """ + + try assertRefactorConvert(baseline, expected: expected) + } + + func testRefactoringFunctionToComputedPropertyWithModifiers() throws { + let baseline: DeclSyntax = """ + static func asJSON() -> String { "" } + """ + + let expected: DeclSyntax = """ + static var asJSON: String { "" } + """ + + try assertRefactorConvert(baseline, expected: expected) + } + + func testRefactoringFunctionToComputedPropertyWithModifiersAndIndentations() throws { + let baseline: DeclSyntax = """ + static func asJSON() -> String { "" } + """ + + let expected: DeclSyntax = """ + static var asJSON: String { "" } + """ + + try assertRefactorConvert(baseline, expected: expected) + } + + func testRefactoringFunctionToComputedPropertyWithModifiersAndComments() throws { + let baseline: DeclSyntax = """ + static func asJSON() -> String { // comment + /*comment*/ "" /*comment*/ + } // comment + """ + + let expected: DeclSyntax = """ + static var asJSON: String { // comment + /*comment*/ "" /*comment*/ + } // comment + """ + + try assertRefactorConvert(baseline, expected: expected) + } + + func testRefactoringFunctionToComputedPropertyWithReturnStms() throws { + let baseline: DeclSyntax = """ + static func asJSON() -> String { + return "" + } + """ + + let expected: DeclSyntax = """ + static var asJSON: String { + return "" + } + """ + + try assertRefactorConvert(baseline, expected: expected) + } + + func testRefactoringFunctionToComputedPropertyWithMultipleStms() throws { + let baseline: DeclSyntax = """ + static func asJSON() -> String { + let builder = JSONBuilder() + return builder.convert() + } + """ + + let expected: DeclSyntax = """ + static var asJSON: String { + let builder = JSONBuilder() + return builder.convert() + } + """ + + try assertRefactorConvert(baseline, expected: expected) + } +} + +fileprivate func assertRefactorConvert( + _ callDecl: DeclSyntax, + expected: DeclSyntax?, + file: StaticString = #filePath, + line: UInt = #line +) throws { + try assertRefactor( + callDecl, + context: (), + provider: ConvertZeroParameterFunctionToComputedProperty.self, + expected: expected, + file: file, + line: line + ) +}