Skip to content

Commit

Permalink
Add "Add documentation" code action to stub out documentation for a f…
Browse files Browse the repository at this point in the history
…unction

This code action takes an undocumented function declaration like

    func refactor(syntax: DeclSyntax, in context: Void) -> DeclSyntax?

and adds stub documentation for the parameters / result / etc., like this:

    /// A description
    /// - Parameters:
    ///   - syntax:
    ///   - context:
    ///
    /// - Returns:
  • Loading branch information
DougGregor committed May 4, 2024
1 parent ab32186 commit b628738
Show file tree
Hide file tree
Showing 5 changed files with 470 additions and 1 deletion.
1 change: 1 addition & 0 deletions Sources/SourceKitLSP/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ target_sources(SourceKitLSP PRIVATE
Clang/ClangLanguageService.swift)
target_sources(SourceKitLSP PRIVATE
Swift/AdjustPositionToStartOfIdentifier.swift
Swift/CodeActions/AddDocumentation.swift
Swift/CodeActions/ConvertIntegerLiteral.swift
Swift/CodeActions/PackageManifestEdits.swift
Swift/CodeActions/SyntaxCodeActionProvider.swift
Expand Down
156 changes: 156 additions & 0 deletions Sources/SourceKitLSP/Swift/CodeActions/AddDocumentation.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
//===----------------------------------------------------------------------===//
//
// 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 SwiftParser
import SwiftRefactor
import SwiftSyntax

/// Insert a documentation template associated with a function or macro.
///
/// ## Before
///
/// ```swift
/// static func refactor(syntax: DeclSyntax, in context: Void) -> DeclSyntax? {}
/// ```
///
/// ## After
///
/// ```swift
/// ///
/// /// - Parameters:
/// /// - syntax:
/// /// - context:
/// /// - Returns:
/// static func refactor(syntax: DeclSyntax, in context: Void) -> DeclSyntax? {}
/// ```
@_spi(Testing)
public struct AddDocumentation: EditRefactoringProvider {
@_spi(Testing)
public static func textRefactor(syntax: DeclSyntax, in context: Void) -> [SourceEdit] {
let hasDocumentation = syntax.leadingTrivia.contains(where: { trivia in
switch trivia {
case .blockComment(_), .docBlockComment(_), .lineComment(_), .docLineComment(_):
return true
default:
return false
}
})

guard !hasDocumentation else {
return []
}

let indentation = [.newlines(1)] + syntax.leadingTrivia.lastLineIndentation()
var content: [TriviaPiece] = []
content.append(contentsOf: indentation)
content.append(.docLineComment("/// A description"))

if let parameters = syntax.parameters?.parameters {
if let onlyParam = parameters.only {
let paramToken = onlyParam.secondName?.text ?? onlyParam.firstName.text
content.append(contentsOf: indentation)
content.append(.docLineComment("/// - Parameter \(paramToken):"))
} else {
content.append(contentsOf: indentation)
content.append(.docLineComment("/// - Parameters:"))
content.append(
contentsOf: parameters.flatMap({ param in
indentation + [
.docLineComment("/// - \(param.secondName?.text ?? param.firstName.text):")
]
})
)
content.append(contentsOf: indentation)
content.append(.docLineComment("///"))
}
}

if syntax.throwsKeyword != nil {
content.append(contentsOf: indentation)
content.append(.docLineComment("/// - Throws:"))
}

if syntax.returnType != nil {
content.append(contentsOf: indentation)
content.append(.docLineComment("/// - Returns:"))
}

let insertPos = syntax.position
return [
SourceEdit(
range: insertPos..<insertPos,
replacement: Trivia(pieces: content).description
)
]
}
}

extension AddDocumentation: SyntaxRefactoringCodeActionProvider {
static var title: String { "Add documentation" }
}

extension DeclSyntax {
fileprivate var parameters: FunctionParameterClauseSyntax? {
switch self.syntaxNodeType {
case is FunctionDeclSyntax.Type:
return self.as(FunctionDeclSyntax.self)!.signature.parameterClause
case is SubscriptDeclSyntax.Type:
return self.as(SubscriptDeclSyntax.self)!.parameterClause
case is InitializerDeclSyntax.Type:
return self.as(InitializerDeclSyntax.self)!.signature.parameterClause
case is MacroDeclSyntax.Type:
return self.as(MacroDeclSyntax.self)!.signature.parameterClause
default:
return nil
}
}

fileprivate var throwsKeyword: TokenSyntax? {
switch self.syntaxNodeType {
case is FunctionDeclSyntax.Type:
return self.as(FunctionDeclSyntax.self)!.signature.effectSpecifiers?
.throwsClause?.throwsSpecifier
case is InitializerDeclSyntax.Type:
return self.as(InitializerDeclSyntax.self)!.signature.effectSpecifiers?
.throwsClause?.throwsSpecifier
default:
return nil
}
}

fileprivate var returnType: TypeSyntax? {
switch self.syntaxNodeType {
case is FunctionDeclSyntax.Type:
return self.as(FunctionDeclSyntax.self)!.signature.returnClause?.type
case is SubscriptDeclSyntax.Type:
return self.as(SubscriptDeclSyntax.self)!.returnClause.type
case is InitializerDeclSyntax.Type:
return self.as(InitializerDeclSyntax.self)!.signature.returnClause?.type
case is MacroDeclSyntax.Type:
return self.as(MacroDeclSyntax.self)!.signature.returnClause?.type
default:
return nil
}
}
}

extension Trivia {
/// Produce trivia from the last newline to the end, dropping anything
/// prior to that.
fileprivate func lastLineIndentation() -> Trivia {
guard let lastNewline = pieces.lastIndex(where: { $0.isNewline }) else {
return self
}

return Trivia(pieces: pieces[(lastNewline + 1)...])
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import SwiftRefactor
/// List of all of the syntactic code action providers, which can be used
/// to produce code actions using only the swift-syntax tree of a file.
let allSyntaxCodeActions: [SyntaxCodeActionProvider.Type] = [
AddDocumentation.self,
AddSeparatorsToIntegerLiteral.self,
ConvertIntegerLiteral.self,
FormatRawStringLiteral.self,
Expand Down
48 changes: 47 additions & 1 deletion Tests/SourceKitLSPTests/CodeActionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,23 @@ final class CodeActionTests: XCTestCase {
command: expectedCommand
)

XCTAssertEqual(result, .codeActions([expectedCodeAction]))
guard case .codeActions(var resultActions) = result else {
XCTFail("Result doesn't have code actions: \(String(describing: result))")
return
}

// Filter out "Add documentation"; we test it elsewhere
if let addDocIndex = resultActions.firstIndex(where: {
$0.title == "Add documentation"
}
) {
resultActions.remove(at: addDocIndex)
} else {
XCTFail("Missing 'Add documentation'.")
return
}

XCTAssertEqual(resultActions, [expectedCodeAction])
}

func testCodeActionsRemovePlaceholders() async throws {
Expand Down Expand Up @@ -441,6 +457,36 @@ final class CodeActionTests: XCTestCase {
try await fulfillmentOfOrThrow([editReceived])
}

func testAddDocumentationCodeActionResult() async throws {
let testClient = try await TestSourceKitLSPClient(capabilities: clientCapabilitiesWithCodeActionSupport())
let uri = DocumentURI.for(.swift)
let positions = testClient.openDocument(
"""
2️⃣func refacto1️⃣r(syntax: DeclSyntax, in context: Void) -> DeclSyntax? { }3️⃣
""",
uri: uri
)

let testPosition = positions["1️⃣"]
let request = CodeActionRequest(
range: Range(testPosition),
context: .init(),
textDocument: TextDocumentIdentifier(uri)
)
let result = try await testClient.send(request)

guard case .codeActions(let codeActions) = result else {
XCTFail("Expected code actions")
return
}

// Make sure we get an add-documentation action.
let addDocAction = codeActions.first { action in
return action.title == "Add documentation"
}
XCTAssertNotNil(addDocAction)
}

func testCodeActionForFixItsProducedBySwiftSyntax() async throws {
let project = try await MultiFileTestProject(files: [
"test.swift": "protocol 1️⃣Multi 2️⃣ident 3️⃣{}",
Expand Down

0 comments on commit b628738

Please sign in to comment.