Skip to content

Commit

Permalink
Merge pull request #1206 from DougGregor/add-documentation-code-action
Browse files Browse the repository at this point in the history
Add "Add documentation" code action to stub out documentation for a function
  • Loading branch information
DougGregor authored May 5, 2024
2 parents ee6d696 + c298626 commit ccea54f
Show file tree
Hide file tree
Showing 7 changed files with 491 additions and 18 deletions.
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
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import SwiftSyntax

/// Protocol that adapts a SyntaxRefactoringProvider (that comes from
/// swift-syntax) into a SyntaxCodeActionProvider.
protocol SyntaxRefactoringCodeActionProvider: SyntaxCodeActionProvider, SyntaxRefactoringProvider {
protocol SyntaxRefactoringCodeActionProvider: SyntaxCodeActionProvider, EditRefactoringProvider {
static var title: String { get }
}

Expand All @@ -31,20 +31,23 @@ extension SyntaxRefactoringCodeActionProvider where Self.Context == Void {
return []
}

guard let refactored = Self.refactor(syntax: node) else {
let sourceEdits = Self.textRefactor(syntax: node)
if sourceEdits.isEmpty {
return []
}

let edit = TextEdit(
range: scope.snapshot.range(of: node),
newText: refactored.description
)
let textEdits = sourceEdits.map { edit in
TextEdit(
range: scope.snapshot.range(of: edit.range),
newText: edit.replacement
)
}

return [
CodeAction(
title: Self.title,
kind: .refactorInline,
edit: WorkspaceEdit(changes: [scope.snapshot.uri: [edit]])
edit: WorkspaceEdit(changes: [scope.snapshot.uri: textEdits])
)
]
}
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
21 changes: 11 additions & 10 deletions Tests/SourceKitLSPTests/PullDiagnosticsTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,17 +93,18 @@ final class PullDiagnosticsTests: XCTestCase {
return
}

XCTAssertEqual(actions.count, 1)
let action = try XCTUnwrap(actions.first)
// Allow the action message to be the one before or after
// https://github.com/apple/swift/pull/67909, ensuring this test passes with
// a sourcekitd that contains the change from that PR as well as older
// toolchains that don't contain the change yet.
XCTAssertEqual(actions.count, 2)
XCTAssert(
[
"Add stubs for conformance",
"Do you want to add protocol stubs?",
].contains(action.title)
actions.contains { action in
// Allow the action message to be the one before or after
// https://github.com/apple/swift/pull/67909, ensuring this test passes with
// a sourcekitd that contains the change from that PR as well as older
// toolchains that don't contain the change yet.
[
"Add stubs for conformance",
"Do you want to add protocol stubs?",
].contains(action.title)
}
)
}

Expand Down
Loading

0 comments on commit ccea54f

Please sign in to comment.