Skip to content

Highlight declaration differences in overloaded symbol groups #967

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

Merged
Merged
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

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,19 @@ extension DeclarationRenderSection.Token {
/// - Parameters:
/// - fragment: The symbol-graph declaration fragment to render.
/// - identifier: An optional reference to a symbol.
init(fragment: SymbolKit.SymbolGraph.Symbol.DeclarationFragments.Fragment, identifier: String?) {
init(
fragment: SymbolKit.SymbolGraph.Symbol.DeclarationFragments.Fragment,
identifier: String?,
highlight: Bool = false
) {
self.text = fragment.spelling
self.kind = Kind(rawValue: fragment.kind.rawValue) ?? .text
self.identifier = identifier
self.preciseIdentifier = fragment.preciseIdentifier
if highlight {
self.highlight = .changed
} else {
self.highlight = nil
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ public struct DeclarationRenderSection: Codable, Equatable {
/// For example, `123` is represented as a single token of kind "number".
public struct Token: Codable, Hashable, Equatable {
/// The token text content.
public let text: String
public var text: String
/// The token programming kind.
public let kind: Kind

Expand Down Expand Up @@ -114,24 +114,40 @@ public struct DeclarationRenderSection: Codable, Equatable {

/// If the token is a known symbol, its precise identifier as vended in the symbol graph.
public let preciseIdentifier: String?


/// The kind of highlight the token should be rendered with.
public var highlight: Highlight?

/// The kinds of highlights that can be applied to a token.
public enum Highlight: String, Codable, RawRepresentable {
/// A highlight representing generalized change, not specifically added or removed.
case changed
}

/// Creates a new declaration token with optional identifier and precise identifier.
/// - Parameters:
/// - text: The text content of the token.
/// - kind: The kind of the token.
/// - identifier: If the token refers to a known symbol, its identifier.
/// - preciseIdentifier: If the refers to a symbol, its precise identifier.
public init(text: String, kind: Kind, identifier: String? = nil, preciseIdentifier: String? = nil) {
public init(
text: String,
kind: Kind,
identifier: String? = nil,
preciseIdentifier: String? = nil,
highlight: Highlight? = nil
) {
self.text = text
self.kind = kind
self.identifier = identifier
self.preciseIdentifier = preciseIdentifier
self.highlight = highlight
}

// MARK: - Codable

private enum CodingKeys: CodingKey {
case text, kind, identifier, preciseIdentifier, otherDeclarations
case text, kind, identifier, preciseIdentifier, highlight, otherDeclarations
}

public func encode(to encoder: Encoder) throws {
Expand All @@ -141,6 +157,7 @@ public struct DeclarationRenderSection: Codable, Equatable {
try container.encode(kind, forKey: .kind)
try container.encodeIfPresent(identifier, forKey: .identifier)
try container.encodeIfPresent(preciseIdentifier, forKey: .preciseIdentifier)
try container.encodeIfPresent(highlight, forKey: .highlight)
}

public init(from decoder: Decoder) throws {
Expand All @@ -150,6 +167,7 @@ public struct DeclarationRenderSection: Codable, Equatable {
kind = try container.decode(Kind.self, forKey: .kind)
preciseIdentifier = try container.decodeIfPresent(String.self, forKey: .preciseIdentifier)
identifier = try container.decodeIfPresent(String.self, forKey: .identifier)
highlight = try container.decodeIfPresent(Highlight.self, forKey: .highlight)

if let reference = identifier {
decoder.registerReferences([reference])
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2616,6 +2616,12 @@
},
"preciseIdentifier": {
"type": "string"
},
"highlight": {
"type": "string",
"enum": [
"changed"
]
}
}
},
Expand Down
229 changes: 228 additions & 1 deletion Tests/SwiftDocCTests/Rendering/DeclarationsRenderSectionTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import Foundation
import XCTest
@testable import SwiftDocC
import SwiftDocCTestUtilities

class DeclarationsRenderSectionTests: XCTestCase {
func testDecodingTokens() throws {
Expand Down Expand Up @@ -76,7 +77,7 @@ class DeclarationsRenderSectionTests: XCTestCase {
)
}
}

func testDoNotEmitOtherDeclarationsIfEmpty() throws {

let encoder = RenderJSONEncoder.makeEncoder(prettyPrint: true)
Expand Down Expand Up @@ -151,4 +152,230 @@ class DeclarationsRenderSectionTests: XCTestCase {
XCTAssertEqual(declarationsSection.declarations.count, 2)
XCTAssert(declarationsSection.declarations.allSatisfy({ $0.platforms == [.iOS, .macOS] }))
}

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

let symbolGraphFile = Bundle.module.url(
forResource: "FancyOverloads",
withExtension: "symbols.json",
subdirectory: "Test Resources"
)!

let tempURL = try createTempFolder(content: [
Folder(name: "unit-test.docc", content: [
InfoPlist(displayName: "FancyOverloads", identifier: "com.test.example"),
CopyOfFile(original: symbolGraphFile),
])
])

let (_, bundle, context) = try loadBundle(from: tempURL)

// Make sure that type decorators like arrays, dictionaries, and optionals are correctly highlighted.
do {
// func overload1(param: Int) {} // <- overload group
// func overload1(param: Int?) {}
// func overload1(param: [Int]) {}
// func overload1(param: [Int]?) {}
// func overload1(param: Set<Int>) {}
// func overload1(param: [Int: Int]) {}
let reference = ResolvedTopicReference(
bundleIdentifier: bundle.identifier,
path: "/documentation/FancyOverloads/overload1(param:)-8nk5z",
sourceLanguage: .swift
)
let symbol = try XCTUnwrap(context.entity(with: reference).semantic as? Symbol)
var translator = RenderNodeTranslator(
context: context,
bundle: bundle,
identifier: reference,
source: nil
)
let renderNode = try XCTUnwrap(translator.visitSymbol(symbol) as? RenderNode)
let declarationsSection = try XCTUnwrap(renderNode.primaryContentSections.compactMap({ $0 as? DeclarationsRenderSection }).first)
XCTAssertEqual(declarationsSection.declarations.count, 1)
let declarations = try XCTUnwrap(declarationsSection.declarations.first)

XCTAssertEqual(
declarationAndHighlights(for: declarations.tokens),
[
"func overload1(param: Int)",
" ",
]
)

XCTAssertEqual(
declarations.otherDeclarations?.declarations.flatMap({ declarationAndHighlights(for: $0.tokens) }),
[
"func overload1(param: Int?)",
" ~ ",

"func overload1(param: Set<Int>)",
" ~~~~ ~ ",

"func overload1(param: [Int : Int])",
" ~ ~~~~~~ ",

"func overload1(param: [Int])",
" ~ ~ ",

"func overload1(param: [Int]?)",
" ~ ~~ ",
]
)
}

// Verify the behavior of the highlighter in the face of tuples and closures, which can
// confuse the differencing code with excess parentheses and commas.
do {
// func overload2(p1: Int, p2: Int) {}
// func overload2(p1: (Int, Int), p2: Int) {}
// func overload2(p1: Int, p2: (Int, Int)) {}
// func overload2(p1: (Int) -> (), p2: Int) {}
// func overload2(p1: (Int) -> Int, p2: Int) {}
// func overload2(p1: (Int) -> Int?, p2: Int) {}
// func overload2(p1: ((Int) -> Int)?, p2: Int) {} // <- overload group
let reference = ResolvedTopicReference(
bundleIdentifier: bundle.identifier,
path: "/documentation/FancyOverloads/overload2(p1:p2:)-4p1sq",
sourceLanguage: .swift
)
let symbol = try XCTUnwrap(context.entity(with: reference).semantic as? Symbol)
var translator = RenderNodeTranslator(
context: context,
bundle: bundle,
identifier: reference,
source: nil
)
let renderNode = try XCTUnwrap(translator.visitSymbol(symbol) as? RenderNode)
let declarationsSection = try XCTUnwrap(renderNode.primaryContentSections.compactMap({ $0 as? DeclarationsRenderSection }).first)
XCTAssertEqual(declarationsSection.declarations.count, 1)
let declarations = try XCTUnwrap(declarationsSection.declarations.first)

XCTAssertEqual(
declarationAndHighlights(for: declarations.tokens),
[
"func overload2(p1: ((Int) -> Int)?, p2: Int)",
" ~~ ~~~~~~~~~~ "
]
)

XCTAssertEqual(
declarations.otherDeclarations?.declarations.flatMap({ declarationAndHighlights(for: $0.tokens) }),
[
"func overload2(p1: (Int) -> (), p2: Int)",
" ~ ~~~~~~~ ",

"func overload2(p1: (Int) -> Int, p2: Int)",
" ~ ~~~~~~~~ ",

"func overload2(p1: (Int) -> Int?, p2: Int)",
" ~ ~~~~~~~~~ ",

// FIXME: adjust the token processing so that the comma inside the tuple isn't treated as common?
// (it breaks the declaration pretty-printer in Swift-DocC-Render and causes it to skip pretty-printing)
"func overload2(p1: (Int, Int), p2: Int)",
" ~ ~~~~~ ",

// FIXME: adjust the token processing so that the common parenthesis is always the final one
"func overload2(p1: Int, p2: (Int, Int))",
" ~ ~~~~~ ~",

"func overload2(p1: Int, p2: Int)",
" ",
]
)
}

// Verify that the presence of type parameters doesn't cause the opening parenthesis of an
// argument list to also be highlighted, since it is combined into the same token as the
// closing angle bracket in the symbol graph. Also ensure that the leading space of the
// rendered where clause is not highlighted.
do {
// func overload3(_ p: [Int: Int]) {} // <- overload group
// func overload3<T: Hashable>(_ p: [T: T]) {}
// func overload3<K: Hashable, V>(_ p: [K: V]) {}
let reference = ResolvedTopicReference(
bundleIdentifier: bundle.identifier,
path: "/documentation/FancyOverloads/overload3(_:)-xql2",
sourceLanguage: .swift
)
let symbol = try XCTUnwrap(context.entity(with: reference).semantic as? Symbol)
var translator = RenderNodeTranslator(
context: context,
bundle: bundle,
identifier: reference,
source: nil
)
let renderNode = try XCTUnwrap(translator.visitSymbol(symbol) as? RenderNode)
let declarationsSection = try XCTUnwrap(renderNode.primaryContentSections.compactMap({ $0 as? DeclarationsRenderSection }).first)
XCTAssertEqual(declarationsSection.declarations.count, 1)
let declarations = try XCTUnwrap(declarationsSection.declarations.first)

XCTAssertEqual(
declarationAndHighlights(for: declarations.tokens),
[
"func overload3(_ p: [Int : Int])",
" ~~~ ~~~ ",
]
)

XCTAssertEqual(
declarations.otherDeclarations?.declarations.flatMap({ declarationAndHighlights(for: $0.tokens) }),
[
"func overload3<K, V>(_ p: [K : V]) where K : Hashable",
" ~~~~~~ ~ ~ ~~~~~~~~~~~~~~~~~~",

"func overload3<T>(_ p: [T : T]) where T : Hashable",
" ~~~ ~ ~ ~~~~~~~~~~~~~~~~~~",
]
)
}
}

func testDontHighlightWhenOverloadsAreDisabled() throws {
let symbolGraphFile = Bundle.module.url(
forResource: "FancyOverloads",
withExtension: "symbols.json",
subdirectory: "Test Resources"
)!

let tempURL = try createTempFolder(content: [
Folder(name: "unit-test.docc", content: [
InfoPlist(displayName: "FancyOverloads", identifier: "com.test.example"),
CopyOfFile(original: symbolGraphFile),
])
])

let (_, bundle, context) = try loadBundle(from: tempURL)

for hash in ["7eht8", "8p1lo", "858ja"] {
let reference = ResolvedTopicReference(
bundleIdentifier: bundle.identifier,
path: "/documentation/FancyOverloads/overload3(_:)-\(hash)",
sourceLanguage: .swift
)
let symbol = try XCTUnwrap(context.entity(with: reference).semantic as? Symbol)
var translator = RenderNodeTranslator(
context: context,
bundle: bundle,
identifier: reference,
source: nil
)
let renderNode = try XCTUnwrap(translator.visitSymbol(symbol) as? RenderNode)
let declarationsSection = try XCTUnwrap(renderNode.primaryContentSections.compactMap({ $0 as? DeclarationsRenderSection }).first)
XCTAssertEqual(declarationsSection.declarations.count, 1)
let declarations = try XCTUnwrap(declarationsSection.declarations.first)

XCTAssert(declarations.tokens.allSatisfy({ $0.highlight == nil }))
}
}
}

/// Render a list of declaration tokens as a plain-text decoration and as a plain-text rendering of which characters are highlighted.
func declarationAndHighlights(for tokens: [DeclarationRenderSection.Token]) -> [String] {
[
tokens.map({ $0.text }).joined(),
tokens.map({ String(repeating: $0.highlight == .changed ? "~" : " ", count: $0.text.count) }).joined()
]
}
Loading