diff --git a/CMakeLists.txt b/CMakeLists.txt index eeca45548..a2424622b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,6 +26,8 @@ find_package(SwiftCollections QUIET) find_package(SwiftSyntax CONFIG REQUIRED) find_package(SwiftASN1 CONFIG REQUIRED) find_package(SwiftCrypto CONFIG REQUIRED) +find_package(SwiftMarkdown CONFIG REQUIRED) +find_package(cmark-gfm CONFIG REQUIRED) include(SwiftSupport) diff --git a/Package.swift b/Package.swift index 563d6c4e1..a9afee1ce 100644 --- a/Package.swift +++ b/Package.swift @@ -613,6 +613,7 @@ var targets: [Target] = [ "ToolchainRegistry", "TSCExtensions", .product(name: "IndexStoreDB", package: "indexstore-db"), + .product(name: "Markdown", package: "swift-markdown"), .product(name: "Crypto", package: "swift-crypto"), .product(name: "SwiftToolsSupport-auto", package: "swift-tools-support-core"), ] diff --git a/Sources/SwiftLanguageService/CMakeLists.txt b/Sources/SwiftLanguageService/CMakeLists.txt index 5d8f66ba1..171b67fb9 100644 --- a/Sources/SwiftLanguageService/CMakeLists.txt +++ b/Sources/SwiftLanguageService/CMakeLists.txt @@ -46,6 +46,7 @@ add_library(SwiftLanguageService STATIC GeneratedInterfaceManager.swift SignatureHelp.swift AdjustPositionToStartOfArgument.swift + ExtractParametersDocumentation.swift ) set_target_properties(SwiftLanguageService PROPERTIES INTERFACE_INCLUDE_DIRECTORIES ${CMAKE_Swift_MODULE_DIRECTORY}) @@ -59,6 +60,9 @@ target_link_libraries(SwiftLanguageService PUBLIC IndexStoreDB SwiftSyntax::SwiftBasicFormat SwiftSyntax::SwiftSyntax + SwiftMarkdown::Markdown + libcmark-gfm + libcmark-gfm-extensions ) target_link_libraries(SourceKitLSP PRIVATE diff --git a/Sources/SwiftLanguageService/ExtractParametersDocumentation.swift b/Sources/SwiftLanguageService/ExtractParametersDocumentation.swift new file mode 100644 index 000000000..91a4c8b47 --- /dev/null +++ b/Sources/SwiftLanguageService/ExtractParametersDocumentation.swift @@ -0,0 +1,259 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 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 Foundation +import Markdown + +private struct Parameter { + let name: String + let documentation: String +} + +/// Extracts parameter documentation from a Doxygen parameter command. +private func extractParameter(from doxygenParameter: DoxygenParameter) -> Parameter { + return Parameter( + name: doxygenParameter.name, + documentation: Document(doxygenParameter.blockChildren).format(), + ) +} + +/// Extracts parameter documentation from an unordered list. +/// +/// - Returns: A new ``UnorderedList`` with the items that were not added to the parameters if any. +private func extractParameters( + from unorderedList: UnorderedList +) -> (remaining: UnorderedList?, parameters: [Parameter]) { + var parameters: [Parameter] = [] + var newItems: [ListItem] = [] + + for item in unorderedList.listItems { + if let param = extractSingleParameter(from: item) { + parameters.append(param) + } else if let params = extractParametersOutline(from: item) { + parameters.append(contentsOf: params) + } else { + newItems.append(item) + } + } + + if newItems.isEmpty { + return (remaining: nil, parameters: parameters) + } + + return (remaining: UnorderedList(newItems), parameters: parameters) +} + +/// Parameter documentation from a `Parameters:` outline. +/// +/// Example: +/// ```markdown +/// - Parameters: +/// - param: description +/// ``` +/// +/// - Returns: The extracted parameters if any, nil otherwise. +private func extractParametersOutline(from listItem: ListItem) -> [Parameter]? { + guard + let firstChild = listItem.child(at: 0) as? Paragraph, + let headingText = firstChild.child(at: 0) as? Text, + headingText.string.trimmingCharacters(in: .whitespaces).lowercased().hasPrefix("parameters:") + else { + return nil + } + + return listItem.children.flatMap { child in + guard let nestedList = child as? UnorderedList else { + return [] as [Parameter] + } + + return nestedList.listItems.compactMap { extractParameter(listItem: $0) } + } +} + +/// Extracts parameter documentation from a single parameter. +/// +/// Example: +/// ```markdown +/// - Parameter param: description +/// ``` +/// +/// - Returns: The extracted parameter if any, nil otherwise. +private func extractSingleParameter(from listItem: ListItem) -> Parameter? { + guard let paragraph = listItem.child(at: 0) as? Paragraph, + let paragraphText = paragraph.child(at: 0) as? Text + else { + return nil + } + + let parameterPrefix = "parameter " + let paragraphContent = paragraphText.string + + guard + let prefixEnd = paragraphContent.index( + paragraphContent.startIndex, + offsetBy: parameterPrefix.count, + limitedBy: paragraphContent.endIndex + ) + else { + return nil + } + + let potentialMatch = paragraphContent[.. Parameter? { + guard let paragraph = listItem.child(at: 0) as? Paragraph else { + return nil + } + + guard let firstText = paragraph.child(at: 0) as? Text else { + return extractParameterWithRawIdentifier(from: listItem) + } + + let components = firstText.string.split(separator: ":", maxSplits: 1, omittingEmptySubsequences: false) + + guard components.count == 2 else { + return extractParameterWithRawIdentifier(from: listItem) + } + + let name = String(components[0]).trimmingCharacters(in: .whitespaces) + guard !name.isEmpty else { + return nil + } + + let remainingFirstTextContent = String(components[1]).trimmingCharacters(in: .whitespaces) + let remainingParagraphChildren = [Text(remainingFirstTextContent)] + paragraph.inlineChildren.dropFirst() + let remainingChildren = [Paragraph(remainingParagraphChildren)] + listItem.blockChildren.dropFirst() + let documentation = Document(remainingChildren).format() + + return Parameter(name: name, documentation: documentation) +} + +/// Extracts a parameter with its name as a raw identifier. +/// +/// Example: +/// ```markdown +/// - Parameter `foo bar`: documentation +/// - Parameters: +/// - `foo bar`: documentation +/// ``` +/// +/// - Parameters: +/// - listItem: The list item to extract the parameter from +private func extractParameterWithRawIdentifier(from listItem: ListItem) -> Parameter? { + guard let paragraph = listItem.child(at: 0) as? Paragraph, + let rawIdentifier = paragraph.child(at: 0) as? InlineCode, + let text = paragraph.child(at: 1) as? Text + else { + return nil + } + + let textContent = text.string.trimmingCharacters(in: .whitespaces) + + guard textContent.hasPrefix(":") else { + return nil + } + + let remainingTextContent = String(textContent.dropFirst()).trimmingCharacters(in: .whitespaces) + let remainingParagraphChildren = + [Text(remainingTextContent)] + paragraph.inlineChildren.dropFirst(2) + let remainingChildren = [Paragraph(remainingParagraphChildren)] + listItem.blockChildren.dropFirst(1) + let documentation = Document(remainingChildren).format() + + return Parameter(name: rawIdentifier.code, documentation: documentation) +} + +/// Extracts parameter documentation from markdown text. +/// +/// The parameter extraction implementation is almost ported from the implementation in the Swift compiler codebase. +/// +/// The problem with doing that in the Swift compiler codebase is that once you parse a the comment as markdown into +/// a `Document` you cannot easily convert it back into markdown (we'd need to write our own markdown formatter). +/// Besides, `cmark` doesn't handle Doxygen commands. +/// +/// We considered using `swift-docc` but we faced some problems with it: +/// +/// 1. We would need to refactor existing use of `swift-docc` in SourceKit-LSP to reuse some of that logic here besides +/// providing the required arguments. +/// 2. The result returned by DocC can't be directly converted to markdown, we'd need to provide our own DocC markdown renderer. +/// +/// Implementing this using `swift-markdown` allows us to easily parse the comment, process it, convert it back to markdown. +/// It also provides minimal parsing for Doxygen commands (we're only interested in `\param`) allowing us to use the same +/// implementation for Clang-based declarations. +/// +/// Although this approach involves code duplication, it's simple enough for the initial implementation. We should consider +/// `swift-docc` in the future. +/// +/// - Parameter markdown: The markdown text to extract parameters from +/// - Returns: A tuple containing the extracted parameters dictionary and the remaining markdown text +package func extractParametersDocumentation( + from markdown: String +) -> (parameters: [String: String], remaining: String) { + let document = Document(parsing: markdown, options: [.parseBlockDirectives, .parseMinimalDoxygen]) + + var parameters: [String: String] = [:] + var remainingBlocks: [any BlockMarkup] = [] + + for block in document.blockChildren { + switch block { + case let unorderedList as UnorderedList: + let (newUnorderedList, params) = extractParameters(from: unorderedList) + if let newUnorderedList { + remainingBlocks.append(newUnorderedList) + } + + for param in params { + // If duplicate parameter documentation is found, keep the first one following swift-docc's behavior + parameters[param.name] = parameters[param.name] ?? param.documentation + } + + case let doxygenParameter as DoxygenParameter: + let param = extractParameter(from: doxygenParameter) + // If duplicate parameter documentation is found, keep the first one following swift-docc's behavior + parameters[param.name] = parameters[param.name] ?? param.documentation + + default: + remainingBlocks.append(block) + } + } + + let remaining = Document(remainingBlocks).format() + + return (parameters: parameters, remaining: remaining) +} diff --git a/Sources/SwiftLanguageService/SignatureHelp.swift b/Sources/SwiftLanguageService/SignatureHelp.swift index 1c74da66f..8939c628f 100644 --- a/Sources/SwiftLanguageService/SignatureHelp.swift +++ b/Sources/SwiftLanguageService/SignatureHelp.swift @@ -36,7 +36,13 @@ fileprivate extension String { } fileprivate extension ParameterInformation { - init?(_ parameter: SKDResponseDictionary, _ signatureLabel: String, _ keys: sourcekitd_api_keys) { + /// - Parameter parameterDocumentation: A dictionary with the parameter name as the key and its documentation as the value. + init?( + _ parameter: SKDResponseDictionary, + _ signatureLabel: String, + _ keys: sourcekitd_api_keys, + _ parameterDocumentation: [String: String] + ) { guard let nameOffset = parameter[keys.nameOffset] as Int?, let nameLength = parameter[keys.nameLength] as Int? else { @@ -44,8 +50,10 @@ fileprivate extension ParameterInformation { } let documentation: StringOrMarkupContent? = - if let docComment: String = parameter[keys.docComment] { - .markupContent(MarkupContent(kind: .markdown, value: docComment)) + if let name = parameter[keys.name] as String?, + let documentation = parameterDocumentation[name] + { + .markupContent(MarkupContent(kind: .markdown, value: documentation)) } else { nil } @@ -68,7 +76,20 @@ fileprivate extension SignatureInformation { return nil } - let parameters = skParameters.compactMap { ParameterInformation($0, label, keys) } + let documentation: StringOrMarkupContent? + let parameterDocumentation: [String: String] + + if let docComment: String = signature[keys.docComment] { + let (parameterComments, signatureComment) = extractParametersDocumentation(from: docComment) + + documentation = .markupContent(MarkupContent(kind: .markdown, value: signatureComment)) + parameterDocumentation = parameterComments + } else { + documentation = nil + parameterDocumentation = [:] + } + + let parameters = skParameters.compactMap { ParameterInformation($0, label, keys, parameterDocumentation) } let activeParameter: Int? = if let activeParam: Int = signature[keys.activeParameter] { @@ -88,13 +109,6 @@ fileprivate extension SignatureInformation { nil } - let documentation: StringOrMarkupContent? = - if let docComment: String = signature[keys.docComment] { - .markupContent(MarkupContent(kind: .markdown, value: docComment)) - } else { - nil - } - self.init( label: label, documentation: documentation, diff --git a/Tests/SourceKitLSPTests/ExtractParametersDocumentationTests.swift b/Tests/SourceKitLSPTests/ExtractParametersDocumentationTests.swift new file mode 100644 index 000000000..93bd664bd --- /dev/null +++ b/Tests/SourceKitLSPTests/ExtractParametersDocumentationTests.swift @@ -0,0 +1,350 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift.org open source project +// +// Copyright (c) 2025 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 Foundation +import SKTestSupport +@_spi(Testing) import SwiftLanguageService +import XCTest + +final class ExtractParametersDocumentationTests: XCTestCase { + func testParameterOutlineBasic() { + let comment = """ + This is a function that does something. + + - Parameters: + - name: The name parameter + - age: The age parameter + + This is additional documentation. + """ + + let (parameters, remaining) = extractParametersDocumentation(from: comment) + + XCTAssertEqual( + parameters, + [ + "name": "The name parameter", + "age": "The age parameter", + ] + ) + + XCTAssertEqual( + remaining, + """ + This is a function that does something. + + This is additional documentation. + """ + ) + } + + func testParameterOutlineWithComplexDescriptions() { + let comment = """ + Function documentation. + + - Parameters: + - callback: A closure that is called when the operation completes. + This can be `nil` if no callback is needed. + - timeout: The maximum time to wait in seconds. + Must be greater than 0. + + ```swift + let value = 5 + ``` + """ + + let (parameters, remaining) = extractParametersDocumentation(from: comment) + + XCTAssertEqual( + parameters, + [ + "callback": """ + A closure that is called when the operation completes. + This can be `nil` if no callback is needed. + """, + "timeout": """ + The maximum time to wait in seconds. + Must be greater than 0. + + ```swift + let value = 5 + ``` + """, + ] + ) + + XCTAssertEqual(remaining, "Function documentation.") + } + + func testParameterOutlineCaseInsensitive() { + let comment = """ + - PArAmEtERs: + - value: A test value + """ + + let (parameters, remaining) = extractParametersDocumentation(from: comment) + + XCTAssertEqual(parameters, ["value": "A test value"]) + XCTAssertTrue(remaining.isEmpty) + } + + func testSeparatedParametersBasic() { + let comment = """ + This function does something. + + - Parameter name: The name of the person + - Parameter age: The age of the person + + Additional documentation. + """ + + let (parameters, remaining) = extractParametersDocumentation(from: comment) + + XCTAssertEqual( + parameters, + [ + "name": "The name of the person", + "age": "The age of the person", + ] + ) + + XCTAssertEqual( + remaining, + """ + This function does something. + + Additional documentation. + """ + ) + } + + func testSeparatedParametersCaseInsensitive() { + let comment = """ + - parameter value: A test value + - PARAMETER count: A test count + """ + + let (parameters, remaining) = extractParametersDocumentation(from: comment) + + XCTAssertEqual( + parameters, + [ + "value": "A test value", + "count": "A test count", + ] + ) + + XCTAssertTrue(remaining.isEmpty) + } + + func testSeparatedParameterWithComplexDescription() { + let comment = """ + - Parameter completion: A completion handler that is called when the request finishes. + The handler receives a `Result` containing either the response data or an error. + This parameter can be `nil` if no completion handling is needed. + """ + + let (parameters, remaining) = extractParametersDocumentation(from: comment) + + XCTAssertEqual( + parameters, + [ + "completion": """ + A completion handler that is called when the request finishes. + The handler receives a `Result` containing either the response data or an error. + This parameter can be `nil` if no completion handling is needed. + """ + ] + ) + + XCTAssertTrue(remaining.isEmpty) + } + + func testDoxygenParameterBasic() { + let comment = #""" + This function processes data. + + \param input The input data to process + \param options Configuration options + + \returns The processed result + """# + + let (parameters, remaining) = extractParametersDocumentation(from: comment) + + XCTAssertEqual( + parameters, + [ + "input": "The input data to process", + "options": "Configuration options", + ] + ) + + XCTAssertEqual( + remaining, + #""" + This function processes data. + + \returns The processed result + """# + ) + } + + func testMarkdownWithoutParameters() { + let comment = """ + This is a function that takes no parameters. + + It does something useful and returns a value. + + - Returns: The processed result. + """ + + let (parameters, remaining) = extractParametersDocumentation(from: comment) + + XCTAssertEqual(parameters, [:]) + XCTAssertEqual( + remaining, + """ + This is a function that takes no parameters. + + It does something useful and returns a value. + + - Returns: The processed result. + """ + ) + } + + func testParameterExtractionDoesNotAffectOtherLists() { + let comment = """ + This function has various lists: + + - Parameters: + - name: The user name + - Returns: The processed result. + - Throws: An error if the function fails. + - Precondition: the user is logged in. + """ + + let (parameters, remaining) = extractParametersDocumentation(from: comment) + + XCTAssertEqual(parameters, ["name": "The user name"]) + + XCTAssertEqual( + remaining, + """ + This function has various lists: + + - Returns: The processed result. + - Throws: An error if the function fails. + - Precondition: the user is logged in. + """ + ) + } + + /// Tests that we drop non-parameter items in the parameter outline. Aligns with swift-docc. + func testDropsNonParameterItemsInParameterOutline() { + let comment = """ + - Parameters: + - number: The number to do stuff with + - TODO Improve this documentation + """ + + let (parameters, remaining) = extractParametersDocumentation(from: comment) + + XCTAssertEqual(parameters, ["number": "The number to do stuff with"]) + XCTAssertTrue(remaining.isEmpty) + } + + /// Tests that we drop duplicate parameter documentation and keep the first one. Aligns with swift-docc. + func testDropsDuplicateParameterDocumentation() { + let comment = """ + - Parameters: + - number: The number to do stuff with + - number: The number to do amazing stuff with + """ + + let (parameters, remaining) = extractParametersDocumentation(from: comment) + + XCTAssertEqual(parameters, ["number": "The number to do stuff with"]) + XCTAssertTrue(remaining.isEmpty) + } + + /// Tests that we drop text after the colon in the parameter outline. Aligns with swift-docc. + func testDropsTextAfterColonInParameterOutline() { + let comment = """ + - Parameters: listing parameter documentation below + - number: The number to do stuff + """ + + let (parameters, remaining) = extractParametersDocumentation(from: comment) + + XCTAssertEqual(parameters, ["number": "The number to do stuff"]) + XCTAssertTrue(remaining.isEmpty) + } + + /// Tests that we support mixed parameter documentation styles in a single comment. Aligns with swift-docc. + func testMixedParameterDocumentationStyles() { + let comment = #""" + Function documentation. + + - Parameters: + - first: First parameter from Parameters section + - Parameter second: Second parameter from separate Parameter + \param third Third parameter from Doxygen style + + Additional documentation. + """# + + let (parameters, remaining) = extractParametersDocumentation(from: comment) + + XCTAssertEqual( + parameters, + [ + "first": "First parameter from Parameters section", + "second": "Second parameter from separate Parameter", + "third": "Third parameter from Doxygen style", + ] + ) + + XCTAssertEqual( + remaining, + """ + Function documentation. + + Additional documentation. + """ + ) + } + + func testSeparatedParameterWithRawIdentifier() { + let comment = """ + - Parameter `foo: bar :) `: hello + """ + + let (parameters, remaining) = extractParametersDocumentation(from: comment) + + XCTAssertEqual(parameters, ["foo: bar :) ": "hello"]) + XCTAssertTrue(remaining.isEmpty) + } + + func testParameterOutlineWithRawIdentifier() { + let comment = """ + - Parameters: + - `foo: bar :) `: hello + """ + + let (parameters, remaining) = extractParametersDocumentation(from: comment) + + XCTAssertEqual(parameters, ["foo: bar :) ": "hello"]) + XCTAssertTrue(remaining.isEmpty) + } +} diff --git a/Tests/SourceKitLSPTests/SwiftSignatureHelpTests.swift b/Tests/SourceKitLSPTests/SwiftSignatureHelpTests.swift index bcfd40395..ed77b26b5 100644 --- a/Tests/SourceKitLSPTests/SwiftSignatureHelpTests.swift +++ b/Tests/SourceKitLSPTests/SwiftSignatureHelpTests.swift @@ -59,9 +59,7 @@ final class SwiftSignatureHelpTests: XCTestCase { kind: .markdown, value: """ This is a test function - - Parameters: - - a: The first parameter - - b: The second parameter + - Returns: The result of the test """ ) @@ -70,8 +68,14 @@ final class SwiftSignatureHelpTests: XCTestCase { XCTAssertEqual( signature.parameters, [ - ParameterInformation(label: .offsets(start: 5, end: 11)), - ParameterInformation(label: .offsets(start: 13, end: 22)), + ParameterInformation( + label: .offsets(start: 5, end: 11), + documentation: .markupContent(MarkupContent(kind: .markdown, value: "The first parameter")), + ), + ParameterInformation( + label: .offsets(start: 13, end: 22), + documentation: .markupContent(MarkupContent(kind: .markdown, value: "The second parameter")), + ), ] ) } @@ -120,9 +124,7 @@ final class SwiftSignatureHelpTests: XCTestCase { kind: .markdown, value: """ Returns the element at the given row and column - - Parameters: - - row: The row index - - column: The column index + - Returns: The element at the given row and column """ ) @@ -131,8 +133,14 @@ final class SwiftSignatureHelpTests: XCTestCase { XCTAssertEqual( signature.parameters, [ - ParameterInformation(label: .offsets(start: 10, end: 18)), - ParameterInformation(label: .offsets(start: 20, end: 31)), + ParameterInformation( + label: .offsets(start: 10, end: 18), + documentation: .markupContent(MarkupContent(kind: .markdown, value: "The row index")), + ), + ParameterInformation( + label: .offsets(start: 20, end: 31), + documentation: .markupContent(MarkupContent(kind: .markdown, value: "The column index")), + ), ] ) } @@ -235,20 +243,21 @@ final class SwiftSignatureHelpTests: XCTestCase { .markupContent( MarkupContent( kind: .markdown, - value: """ - The label as an offset within the signature label - - Parameters: - - start: The start offset - - end: The end offset - """ + value: "The label as an offset within the signature label" ) ) ) XCTAssertEqual( signature.parameters, [ - ParameterInformation(label: .offsets(start: 7, end: 17)), - ParameterInformation(label: .offsets(start: 19, end: 27)), + ParameterInformation( + label: .offsets(start: 7, end: 17), + documentation: .markupContent(MarkupContent(kind: .markdown, value: "The start offset")) + ), + ParameterInformation( + label: .offsets(start: 19, end: 27), + documentation: .markupContent(MarkupContent(kind: .markdown, value: "The end offset")) + ), ] ) } @@ -639,9 +648,7 @@ final class SwiftSignatureHelpTests: XCTestCase { kind: .markdown, value: """ A utility function that combines values - - Parameters: - - first: The first value - - second: The second value + - Returns: The combined result """ ) @@ -650,9 +657,88 @@ final class SwiftSignatureHelpTests: XCTestCase { XCTAssertEqual( signature.parameters, [ - ParameterInformation(label: .offsets(start: 8, end: 21)), - ParameterInformation(label: .offsets(start: 23, end: 34)), + ParameterInformation( + label: .offsets(start: 8, end: 21), + documentation: .markupContent(MarkupContent(kind: .markdown, value: "The first value")) + ), + ParameterInformation( + label: .offsets(start: 23, end: 34), + documentation: .markupContent(MarkupContent(kind: .markdown, value: "The second value")) + ), ] ) } + + func testSignatureHelpMatchesParametersWithInternalNames() async throws { + try await SkipUnless.sourcekitdSupportsSignatureHelp() + + let testClient = try await TestSourceKitLSPClient() + let uri = DocumentURI(for: .swift) + + let positions = testClient.openDocument( + """ + /// - Parameter number: The number to add 1 to + func addOne(to number: Int) -> Int { number + 1 } + + func main() { + addOne(1️⃣) + } + """, + uri: uri + ) + + let result = try await testClient.send( + SignatureHelpRequest( + textDocument: TextDocumentIdentifier(uri), + position: positions["1️⃣"] + ) + ) + + let signatureHelp = try XCTUnwrap(result) + let signature = try XCTUnwrap(signatureHelp.signatures.only) + let signatureDocumentation = try XCTUnwrap(signature.documentation) + let parameter = try XCTUnwrap(signature.parameters?.only) + + XCTAssertEqual(signatureDocumentation, .markupContent(MarkupContent(kind: .markdown, value: ""))) + XCTAssertEqual( + parameter.documentation, + .markupContent(MarkupContent(kind: .markdown, value: "The number to add 1 to")) + ) + } + + /// Tests that we drop parameter documentation for parameters that don't exist aligning with swift-docc. + func testSignatureHelpDropsNonExistentParameterDocumentation() async throws { + try await SkipUnless.sourcekitdSupportsSignatureHelp() + + let testClient = try await TestSourceKitLSPClient() + let uri = DocumentURI(for: .swift) + + let positions = testClient.openDocument( + """ + /// - Parameters: + /// - numberWithTypo: The number to do stuff with + func compute(number: Int) {} + + func main() { + compute(1️⃣) + } + """, + uri: uri + ) + + let result = try await testClient.send( + SignatureHelpRequest( + textDocument: TextDocumentIdentifier(uri), + position: positions["1️⃣"] + ) + ) + + let signatureHelp = try XCTUnwrap(result) + let signature = try XCTUnwrap(signatureHelp.signatures.only) + let parameter = try XCTUnwrap(signature.parameters?.only) + let signatureDocumentation = try XCTUnwrap(signature.documentation) + + XCTAssertEqual(signatureDocumentation, .markupContent(MarkupContent(kind: .markdown, value: ""))) + XCTAssertNil(parameter.documentation) + } }