Skip to content

enforce overload declaration ordering when computing the highlighted differences #1250

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

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,17 @@ struct DeclarationsSectionTranslator: RenderSectionTranslator {
/// Fetch the common fragments for the given references, or compute it if necessary.
func commonFragments(
for mainDeclaration: OverloadDeclaration,
overloadDeclarations: [OverloadDeclaration]
overloadDeclarations: [OverloadDeclaration],
mainDeclarationIndex: Int
) -> [SymbolGraph.Symbol.DeclarationFragments.Fragment] {
if let fragments = commonFragments(for: mainDeclaration.reference) {
return fragments
}

let preProcessedDeclarations = [mainDeclaration.declaration] + overloadDeclarations.map(\.declaration)
var preProcessedDeclarations = overloadDeclarations.map(\.declaration)
// Insert the main declaration according to the display index so the ordering is consistent
// between overloaded symbols
preProcessedDeclarations.insert(mainDeclaration.declaration, at: mainDeclarationIndex)

// Collect the "common fragments" so we can highlight the ones that are different
// in each declaration
Expand Down Expand Up @@ -216,7 +220,9 @@ struct DeclarationsSectionTranslator: RenderSectionTranslator {
// in each declaration
let commonFragments = commonFragments(
for: (mainDeclaration, renderNode.identifier, nil),
overloadDeclarations: processedOverloadDeclarations)
overloadDeclarations: processedOverloadDeclarations,
mainDeclarationIndex: overloads.displayIndex
)

renderedTokens = translateDeclaration(
mainDeclaration,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import Foundation
import XCTest
@testable import SwiftDocC
import SwiftDocCTestUtilities
import SymbolKit

class DeclarationsRenderSectionTests: XCTestCase {
func testDecodingTokens() throws {
Expand Down Expand Up @@ -322,6 +323,91 @@ class DeclarationsRenderSectionTests: XCTestCase {
}
}

func testInconsistentHighlightDiff() throws {
enableFeatureFlag(\.isExperimentalOverloadedSymbolPresentationEnabled)

// Generate a symbol graph with many overload groups that share declarations.
// The overloaded declarations have two legitimate solutions for their longest common subsequence:
// one that ends in a close-parenthesis, and one that ends in a space.
// By alternating the order in which these declarations appear,
// the computed difference highlighting can differ
// unless the declarations are sorted prior to the calculation.
// Ensure that the overload difference highlighting is consistent for these declarations.

// init(_ content: MyClass) throws
let declaration1: SymbolGraph.Symbol.DeclarationFragments = .init(declarationFragments: [
.init(kind: .keyword, spelling: "init", preciseIdentifier: nil),
.init(kind: .text, spelling: "(", preciseIdentifier: nil),
.init(kind: .externalParameter, spelling: "_", preciseIdentifier: nil),
.init(kind: .text, spelling: " ", preciseIdentifier: nil),
.init(kind: .internalParameter, spelling: "content", preciseIdentifier: nil),
.init(kind: .text, spelling: ": ", preciseIdentifier: nil),
.init(kind: .typeIdentifier, spelling: "MyClass", preciseIdentifier: "s:MyClass"),
.init(kind: .text, spelling: ") ", preciseIdentifier: nil),
.init(kind: .keyword, spelling: "throws", preciseIdentifier: nil),
])

// init(_ content: some ConvertibleToMyClass)
let declaration2: SymbolGraph.Symbol.DeclarationFragments = .init(declarationFragments: [
.init(kind: .keyword, spelling: "init", preciseIdentifier: nil),
.init(kind: .text, spelling: "(", preciseIdentifier: nil),
.init(kind: .externalParameter, spelling: "_", preciseIdentifier: nil),
.init(kind: .text, spelling: " ", preciseIdentifier: nil),
.init(kind: .internalParameter, spelling: "content", preciseIdentifier: nil),
.init(kind: .text, spelling: ": ", preciseIdentifier: nil),
.init(kind: .keyword, spelling: "some", preciseIdentifier: nil),
.init(kind: .text, spelling: " ", preciseIdentifier: nil),
.init(kind: .typeIdentifier, spelling: "ConvertibleToMyClass", preciseIdentifier: "s:ConvertibleToMyClass"),
.init(kind: .text, spelling: ")", preciseIdentifier: nil),
])
let overloadsCount = 10
let symbols = (0...overloadsCount).flatMap({ index in
let reverseDeclarations = index % 2 != 0
return [
makeSymbol(
id: "overload-\(index)-1",
kind: .func,
pathComponents: ["overload-\(index)"],
otherMixins: [reverseDeclarations ? declaration2 : declaration1]),
makeSymbol(
id: "overload-\(index)-2",
kind: .func,
pathComponents: ["overload-\(index)"],
otherMixins: [reverseDeclarations ? declaration1 : declaration2]),
]
})
let symbolGraph = makeSymbolGraph(moduleName: "FancierOverloads", symbols: symbols)

let catalog = Folder(name: "unit-test.docc", content: [
InfoPlist(displayName: "FancierOverloads", identifier: "com.test.example"),
JSONFile(name: "FancierOverloads.symbols.json", content: symbolGraph),
])

let (bundle, context) = try loadBundle(catalog: catalog)

func assertDeclarations(for USR: String, file: StaticString = #filePath, line: UInt = #line) throws {
let reference = try XCTUnwrap(context.documentationCache.reference(symbolID: USR), file: file, line: line)
let symbol = try XCTUnwrap(context.entity(with: reference).semantic as? Symbol, file: file, line: line)
var translator = RenderNodeTranslator(context: context, bundle: bundle, identifier: reference)
let renderNode = try XCTUnwrap(translator.visitSymbol(symbol) as? RenderNode, file: file, line: line)
let declarationsSection = try XCTUnwrap(renderNode.primaryContentSections.compactMap({ $0 as? DeclarationsRenderSection }).first, file: file, line: line)
XCTAssertEqual(declarationsSection.declarations.count, 1, file: file, line: line)
let declarations = try XCTUnwrap(declarationsSection.declarations.first, file: file, line: line)

XCTAssertEqual(declarationsAndHighlights(for: declarations), [
"init(_ content: MyClass) throws",
" ~~~~~~~~ ~~~~~~",
"init(_ content: some ConvertibleToMyClass)",
" ~~~~ ~~~~~~~~~~~~~~~~~~~~~",
], file: file, line: line)
}

for i in 0...overloadsCount {
try assertDeclarations(for: "overload-\(i)-1")
try assertDeclarations(for: "overload-\(i)-2")
}
}

func testDontHighlightWhenOverloadsAreDisabled() throws {
let symbolGraphFile = Bundle.module.url(
forResource: "FancyOverloads",
Expand Down Expand Up @@ -403,3 +489,12 @@ func declarationAndHighlights(for tokens: [DeclarationRenderSection.Token]) -> [
tokens.map({ String(repeating: $0.highlight == .changed ? "~" : " ", count: $0.text.count) }).joined()
]
}

func declarationsAndHighlights(for section: DeclarationRenderSection) -> [String] {
guard let otherDeclarations = section.otherDeclarations else {
return []
}
var declarations = otherDeclarations.declarations.map(\.tokens)
declarations.insert(section.tokens, at: otherDeclarations.displayIndex)
return declarations.flatMap(declarationAndHighlights(for:))
}