Skip to content

Commit da6cedd

Browse files
authored
Add Slice to replace Snippet Chunks (#50)
SE-0356 latest changes include parsing "slices" out of snippets. These are not quite the same as the current "chunks", which were a speculation that eventually formed into "slices". Deprecate chunks and add the slice data model. Move snippet presentation code to a line-based format: slices are represented simply as a name and index range to pull their code lines. Add the `isVirtual` property to modules, for modules that are created implicitly to hold relationships. When snippet symbol graphs are created, a fake module is created to hold the snippets, since snippets do not come from any one module. This property simplifies what is basically just a heuristic in DocC to a simple factual check. Added round-trip decoding tests for `SymbolGraph.Module`. rdar://95220716
1 parent 35cecb5 commit da6cedd

File tree

7 files changed

+177
-24
lines changed

7 files changed

+177
-24
lines changed

Sources/SymbolKit/SymbolGraph/Module.swift

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import Foundation
1212

1313
extension SymbolGraph {
1414
/// A ``Module-swift.struct`` describes the module from which the symbols were extracted..
15-
public struct Module: Codable {
15+
public struct Module: Codable, Equatable {
1616
/// The name of the module.
1717
public var name: String
1818

@@ -25,11 +25,25 @@ extension SymbolGraph {
2525
/// The [semantic version](https://semver.org) of the module, if availble.
2626
public var version: SemanticVersion?
2727

28-
public init(name: String, platform: Platform, version: SemanticVersion? = nil, bystanders: [String]? = nil) {
28+
/// `true` if the module represents a virtual module, not created from source,
29+
/// but one created implicitly to hold relationships.
30+
public var isVirtual: Bool = false
31+
32+
public init(name: String, platform: Platform, version: SemanticVersion? = nil, bystanders: [String]? = nil, isVirtual: Bool = false) {
2933
self.name = name
3034
self.platform = platform
3135
self.version = version
3236
self.bystanders = bystanders
37+
self.isVirtual = isVirtual
38+
}
39+
40+
public init(from decoder: Decoder) throws {
41+
let container = try decoder.container(keyedBy: CodingKeys.self)
42+
self.name = try container.decode(String.self, forKey: .name)
43+
self.bystanders = try container.decodeIfPresent([String].self, forKey: .bystanders)
44+
self.platform = try container.decode(Platform.self, forKey: .platform)
45+
self.version = try container.decodeIfPresent(SemanticVersion.self, forKey: .version)
46+
self.isVirtual = try container.decodeIfPresent(Bool.self, forKey: .isVirtual) ?? false
3347
}
3448
}
3549
}

Sources/SymbolKit/SymbolGraph/OperatingSystem.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ extension SymbolGraph {
1212
/**
1313
The operating system intended for a ``Module-swift.struct``'s deployment.
1414
*/
15-
public struct OperatingSystem: Codable {
15+
public struct OperatingSystem: Codable, Equatable {
1616
/**
1717
The name of the operating system, such as `macOS` or `Linux`.
1818
*/

Sources/SymbolKit/SymbolGraph/Platform.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
extension SymbolGraph {
1212
/// A ``Platform`` describes the deployment environment for a ``Module-swift.struct``.
13-
public struct Platform: Codable {
13+
public struct Platform: Codable, Equatable {
1414
/**
1515
The name of the architecture that this module targets, such as `x86_64` or `arm64`. If the module doesn't have a specific architecture, this may be undefined.
1616
*/

Sources/SymbolKit/SymbolGraph/Symbol/Snippet.swift

Lines changed: 63 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -10,38 +10,82 @@
1010

1111
extension SymbolGraph.Symbol {
1212
public struct Snippet: Mixin, Codable {
13-
public struct Chunk: Codable {
14-
public var name: String?
15-
public var language: String?
16-
public var code: String
17-
public init(name: String?, language: String?, code: String) {
18-
self.name = name
19-
self.language = language
20-
self.code = code
21-
}
13+
enum CodingKeys: String, CodingKey {
14+
// TODO: Remove after obsoleting Chunks.
15+
case chunks
16+
case language
17+
case slices
18+
case lines
2219
}
2320

2421
public static let mixinKey = "snippet"
25-
26-
public var chunks: [Chunk]
27-
28-
enum CodingKeys: String, CodingKey {
29-
case chunks
22+
23+
/// The language of the snippet if known.
24+
public var language: String?
25+
26+
/// The visible lines of code of the snippet to display.
27+
public var lines: [String]
28+
29+
/// Named spans of lines in the snippet.
30+
public var slices: [String: Range<Int>]
31+
32+
// TODO: Remove after obsoleting Chunks.
33+
private var _chunks = [Chunk]()
34+
35+
public init(language: String?, lines: [String], slices: [String: Range<Int>]) {
36+
self.language = language
37+
self.lines = lines
38+
self.slices = slices
3039
}
3140

3241
public init(from decoder: Decoder) throws {
3342
let container = try decoder.container(keyedBy: CodingKeys.self)
34-
let chunks = try container.decode([Chunk].self, forKey: .chunks)
35-
self.init(chunks: chunks)
43+
let language = try container.decodeIfPresent(String.self, forKey: .language)
44+
let lines = try container.decode([String].self, forKey: .lines)
45+
let slices = try container.decodeIfPresent([String: Range<Int>].self, forKey: .slices) ?? [:]
46+
self.init(language: language, lines: lines, slices: slices)
47+
48+
// TODO: Remove after obsoleting Chunks.
49+
self._chunks = try container.decodeIfPresent([Chunk].self, forKey: .chunks) ?? []
3650
}
3751

3852
public func encode(to encoder: Encoder) throws {
3953
var container = encoder.container(keyedBy: CodingKeys.self)
40-
try container.encode(chunks, forKey: .chunks)
54+
try container.encodeIfPresent(language, forKey: .language)
55+
try container.encode(lines, forKey: .lines)
56+
if !slices.isEmpty {
57+
try container.encode(slices, forKey: .slices)
58+
}
59+
if !_chunks.isEmpty {
60+
try container.encode(_chunks, forKey: .chunks)
61+
}
4162
}
63+
}
64+
}
4265

43-
public init(chunks: [Chunk]) {
44-
self.chunks = chunks
66+
extension SymbolGraph.Symbol.Snippet {
67+
public struct Chunk: Codable {
68+
public var name: String?
69+
public var language: String?
70+
public var code: String
71+
@available(*, deprecated, message: "Chunks are no longer supported. Use `Slice` instead.")
72+
public init(name: String?, language: String?, code: String) {
73+
self.name = name
74+
self.language = language
75+
self.code = code
4576
}
4677
}
78+
79+
@available(*, deprecated, message: "Chunks are no longer supported. Use `slices` instead.")
80+
public var chunks: [Chunk] {
81+
return _chunks
82+
}
83+
84+
@available(*, deprecated, renamed: "init(slices:)")
85+
public init(chunks: [Chunk]) {
86+
self._chunks = chunks
87+
self.language = chunks.first?.language
88+
self.slices = [:]
89+
self.lines = []
90+
}
4791
}

Sources/SymbolKit/SymbolGraph/Symbol/Symbol.swift

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ extension SymbolGraph {
5454
/// The in-source documentation comment attached to a symbol.
5555
public var docComment: LineList?
5656

57+
/// If true, the symbol was created implicitly and not from source.
58+
public var isVirtual: Bool
59+
5760
/// If the symbol has a documentation comment, whether the documentation comment is from
5861
/// the same module as the symbol or not.
5962
///
@@ -102,13 +105,14 @@ extension SymbolGraph {
102105
/// Information about a symbol that is not necessarily common to all symbols.
103106
public var mixins: [String: Mixin] = [:]
104107

105-
public init(identifier: Identifier, names: Names, pathComponents: [String], docComment: LineList?, accessLevel: AccessControl, kind: Kind, mixins: [String: Mixin]) {
108+
public init(identifier: Identifier, names: Names, pathComponents: [String], docComment: LineList?, accessLevel: AccessControl, kind: Kind, mixins: [String: Mixin], isVirtual: Bool = false) {
106109
self.identifier = identifier
107110
self.names = names
108111
self.pathComponents = pathComponents
109112
self.docComment = docComment
110113
self.accessLevel = accessLevel
111114
self.kind = kind
115+
self.isVirtual = isVirtual
112116
self.mixins = mixins
113117
}
114118

@@ -121,6 +125,7 @@ extension SymbolGraph {
121125
case names
122126
case docComment
123127
case accessLevel
128+
case isVirtual
124129

125130
// Mixins
126131
case availability
@@ -157,6 +162,7 @@ extension SymbolGraph {
157162
names = try container.decode(Names.self, forKey: .names)
158163
docComment = try container.decodeIfPresent(LineList.self, forKey: .docComment)
159164
accessLevel = try container.decode(AccessControl.self, forKey: .accessLevel)
165+
isVirtual = try container.decodeIfPresent(Bool.self, forKey: .isVirtual) ?? false
160166
let leftoverMetadataKeys = Set(container.allKeys).intersection(CodingKeys.mixinKeys)
161167
for key in leftoverMetadataKeys {
162168
if let decoded = try decodeMetadataItemForKey(key, from: container) {
@@ -178,6 +184,9 @@ extension SymbolGraph {
178184
try container.encode(names, forKey: .names)
179185
try container.encodeIfPresent(docComment, forKey: .docComment)
180186
try container.encode(accessLevel, forKey: .accessLevel)
187+
if isVirtual {
188+
try container.encode(isVirtual, forKey: .isVirtual)
189+
}
181190

182191
// Mixins
183192

Sources/SymbolKit/UnifiedSymbolGraph/UnifiedSymbol.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ extension UnifiedSymbolGraph {
4444
/// The access level of the symbol.
4545
public var accessLevel: [Selector: SymbolGraph.Symbol.AccessControl]
4646

47+
/// If true, the symbol was created implicitly and not from source.
48+
public var isVirtual: [Selector: Bool]
49+
4750
/// Information about a symbol that is not necessarily common to all symbols.
4851
public var mixins: [Selector: [String: Mixin]]
4952

@@ -67,6 +70,7 @@ extension UnifiedSymbolGraph {
6770
self.docComment[selector] = docComment
6871
}
6972
self.accessLevel = [selector: sym.accessLevel]
73+
self.isVirtual = [selector: sym.isVirtual]
7074
self.mixins = [selector: sym.mixins]
7175
}
7276

@@ -94,6 +98,7 @@ extension UnifiedSymbolGraph {
9498
self.names[selector] = symbol.names
9599
self.docComment[selector] = symbol.docComment
96100
self.accessLevel[selector] = symbol.accessLevel
101+
self.isVirtual[selector] = symbol.isVirtual
97102
self.mixins[selector] = symbol.mixins
98103
}
99104
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2022 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See https://swift.org/LICENSE.txt for license information
8+
See https://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
import XCTest
12+
import SymbolKit
13+
14+
extension SymbolGraph.Module {
15+
func roundTripDecode() throws -> Self {
16+
let encoder = JSONEncoder()
17+
let encoded = try encoder.encode(self)
18+
let decoder = JSONDecoder()
19+
return try decoder.decode(SymbolGraph.Module.self, from: encoded)
20+
}
21+
}
22+
23+
class ModuleTests: XCTestCase {
24+
static let os = SymbolGraph.OperatingSystem(name: "macOS", minimumVersion: .init(major: 10, minor: 9, patch: 0))
25+
static let platform = SymbolGraph.Platform(architecture: "arm64", vendor: "Apple", operatingSystem: os)
26+
27+
func testFullRoundTripCoding() throws {
28+
let module = SymbolGraph.Module(name: "Test", platform: ModuleTests.platform, bystanders: ["A"], isVirtual: true)
29+
let decodedModule = try module.roundTripDecode()
30+
XCTAssertEqual(module, decodedModule)
31+
}
32+
33+
func testOptionalBystanders() throws {
34+
do {
35+
// bystanders = nil
36+
let module = SymbolGraph.Module(name: "Test", platform: ModuleTests.platform)
37+
let decodedModule = try module.roundTripDecode()
38+
XCTAssertNil(decodedModule.bystanders)
39+
}
40+
41+
do {
42+
// bystanders = ["A"]
43+
let module = SymbolGraph.Module(name: "Test", platform: ModuleTests.platform, bystanders: ["A"])
44+
let decodedModule = try module.roundTripDecode()
45+
XCTAssertEqual(["A"], decodedModule.bystanders)
46+
}
47+
}
48+
49+
func testOptionalIsVirtual() throws {
50+
do {
51+
// isVirtual = false
52+
let module = SymbolGraph.Module(name: "Test", platform: ModuleTests.platform)
53+
let decodedModule = try module.roundTripDecode()
54+
XCTAssertFalse(decodedModule.isVirtual)
55+
}
56+
57+
do {
58+
// isVirtual = true
59+
let module = SymbolGraph.Module(name: "Test", platform: ModuleTests.platform, isVirtual: true)
60+
let decodedModule = try module.roundTripDecode()
61+
XCTAssertTrue(decodedModule.isVirtual)
62+
}
63+
}
64+
65+
func testOptionalVersion() throws {
66+
do {
67+
// version = nil
68+
let module = SymbolGraph.Module(name: "Test", platform: ModuleTests.platform)
69+
let decodedModule = try module.roundTripDecode()
70+
XCTAssertNil(decodedModule.version)
71+
}
72+
73+
do {
74+
// version = 1.0.0
75+
let version = SymbolGraph.SemanticVersion(major: 1, minor: 0, patch: 0)
76+
let module = SymbolGraph.Module(name: "Test", platform: ModuleTests.platform, version: version)
77+
let decodedModule = try module.roundTripDecode()
78+
XCTAssertEqual(version, decodedModule.version)
79+
}
80+
}
81+
}

0 commit comments

Comments
 (0)