Skip to content

Commit ca68c5b

Browse files
committed
count number of SPIs, and add some link resolver logic for translating external URLs
1 parent 1c63fac commit ca68c5b

File tree

11 files changed

+246
-71
lines changed

11 files changed

+246
-71
lines changed

Sources/CodelinkResolution/CodelinkResolver.Overloads.swift

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,29 @@ extension CodelinkResolver.Overloads
3434
}
3535
}
3636

37+
@inlinable mutating
38+
func overload(with overloads:Self)
39+
{
40+
switch (consume self, overloads)
41+
{
42+
case (.one(let one), .one(let other)):
43+
self = .some([one, other])
44+
45+
case (.one(let one), .some([])),
46+
(.some([]), .one(let one)):
47+
self = .one(one)
48+
49+
case (.some(var some), .one(let one)),
50+
(.one(let one), .some(var some)):
51+
some.append(one)
52+
self = .some(some)
53+
54+
case (.some(var some), .some(let others)):
55+
some += others
56+
self = .some(some)
57+
}
58+
}
59+
3760
@inlinable public mutating
3861
func overload(with overload:CodelinkResolver<Scalar>.Overload)
3962
{

Sources/CodelinkResolution/CodelinkResolver.Table.swift

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,23 @@ extension CodelinkResolver.Table:Sendable where Scalar:Sendable
2222
{
2323
}
2424
extension CodelinkResolver.Table
25+
{
26+
public
27+
func caseless() -> Self
28+
{
29+
var copy:Self = self
30+
copy.entries.removeAll(keepingCapacity: true)
31+
32+
for (path, overloads) in self.entries
33+
{
34+
copy.entries[.init(string: path.string.lowercased()), default: .some([])].overload(
35+
with: overloads)
36+
}
37+
38+
return copy
39+
}
40+
}
41+
extension CodelinkResolver.Table
2542
{
2643
@inlinable public
2744
subscript(namespace:Symbol.Module) -> CodelinkResolver<Scalar>.Overloads

Sources/SwiftinitPages/Contexts/Swiftinit.VertexPageContext (ext).swift

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,30 @@ extension Swiftinit.VertexPageContext
9090
switch repo.origin
9191
{
9292
case .github(let origin):
93-
return """
94-
https://raw.githubusercontent.com\
95-
/\(origin.owner)/\(origin.name)/\(refname)/\(file.symbol)
96-
"""
93+
// Files that lack a valid extension will not carry the correct `Content-Type`
94+
// header, and won’t display correctly in the browser. There is no simple way to
95+
// override this behavior, so files will just need to have the correct extension.
96+
guard
97+
let type:Substring = file.symbol.type
98+
else
99+
{
100+
return nil
101+
}
102+
103+
let prefix:String
104+
105+
switch type
106+
{
107+
case "gif": prefix = "https://raw.githubusercontent.com"
108+
case "jpg": prefix = "https://raw.githubusercontent.com"
109+
case "jpeg": prefix = "https://raw.githubusercontent.com"
110+
case "png": prefix = "https://raw.githubusercontent.com"
111+
case "svg": prefix = "https://raw.githubusercontent.com"
112+
case "webp": prefix = "https://media.githubusercontent.com/media"
113+
default: return nil
114+
}
115+
116+
return "\(prefix)/\(origin.owner)/\(origin.name)/\(refname)/\(file.symbol)"
97117
}
98118
}
99119
}

Sources/UnidocLinker/Modules/SymbolGraph.ModuleContext.swift

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,18 +19,32 @@ extension SymbolGraph
1919

2020
private(set)
2121
var codelinks:CodelinkResolver<Unidoc.Scalar>.Table
22+
/// This is needed to support URL translation from other package indexes.
23+
private(set)
24+
var caseless:CodelinkResolver<Unidoc.Scalar>.Table
2225
private(set)
2326
var imports:[Symbol.Module]
2427

28+
private
2529
init()
2630
{
2731
self.conformances = []
2832
self.codelinks = .init()
33+
self.caseless = .init()
2934
self.imports = []
3035
}
3136
}
3237
}
3338
extension SymbolGraph.ModuleContext
39+
{
40+
init(with build:(inout Self) throws -> ()) rethrows
41+
{
42+
self.init()
43+
try build(&self)
44+
self.caseless = self.codelinks.caseless()
45+
}
46+
}
47+
extension SymbolGraph.ModuleContext
3448
{
3549
private mutating
3650
func remember(conforms t:Unidoc.Scalar, to p:Unidoc.Scalar)

Sources/UnidocLinker/Resolver/Codelink (ext).swift

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,4 +35,54 @@ extension Codelink
3535
self.init(v3: unidocV3)
3636
}
3737
}
38+
39+
/// The `domain` must share indices with `link`.
40+
init?(translating link:__shared String, to domain:__shared Substring)
41+
{
42+
guard domain.endIndex < link.endIndex
43+
else
44+
{
45+
return nil
46+
}
47+
48+
let i:String.Index = link.index(after: domain.endIndex)
49+
// Does this look like a link to Swift documentation? If so, we probably already have a
50+
// local copy of it.
51+
switch domain
52+
{
53+
case "developer.apple.com":
54+
// https://developer.apple.com/documentation/swift/uint16
55+
let path:[Substring] = link[i...].split(separator: "/")
56+
if let c:Int = path.firstIndex(of: "documentation")
57+
{
58+
let d:Int = path.index(after: c)
59+
self.init(
60+
base: .qualified,
61+
path: .init(components: path[d...].map { $0.lowercased() }))
62+
}
63+
else
64+
{
65+
return nil
66+
}
67+
68+
case "swiftpackageindex.com":
69+
// https://swiftpackageindex.com/apple/swift-syntax/509.1.1/documentation/
70+
let path:[Substring] = link[i...].split(separator: "/")
71+
if let c:Int = path.firstIndex(of: "documentation")
72+
{
73+
let d:Int = path.index(after: c)
74+
self.init(
75+
base: .qualified,
76+
path: .init(components: path[d...].map { $0.lowercased() }))
77+
}
78+
else
79+
{
80+
return nil
81+
}
82+
83+
default:
84+
// We don't know how to translate other URLs yet.
85+
return nil
86+
}
87+
}
3888
}

Sources/UnidocLinker/Resolver/Unidoc.Resolver.swift

Lines changed: 67 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,18 @@ extension Unidoc
1414
{
1515
private
1616
let codelinks:CodelinkResolver<Scalar>
17+
private
18+
let caseless:CodelinkResolver<Scalar>
1719
private(set)
1820
var context:Linker
1921

20-
init(codelinks:CodelinkResolver<Scalar>, context:consuming Linker)
22+
init(
23+
codelinks:CodelinkResolver<Scalar>,
24+
caseless:CodelinkResolver<Scalar>,
25+
context:consuming Linker)
2126
{
2227
self.codelinks = codelinks
28+
self.caseless = caseless
2329
self.context = context
2430
}
2531
}
@@ -110,33 +116,74 @@ extension Unidoc.Resolver
110116
}
111117

112118
case .unresolved(let unresolved):
119+
let resolution:CodelinkResolver<Unidoc.Scalar>.Overload.Target?
120+
let codelink:Codelink
121+
122+
resolution:
113123
if case .web = unresolved.type
114124
{
115125
let domain:Substring = unresolved.link.prefix { $0 != "/" }
116-
// We will follow links to Apple, GitHub, and reputable open-source indexes.
117-
let safe:Bool = switch domain
126+
127+
if let link:Codelink = .init(translating: unresolved.link, to: domain)
118128
{
119-
case "developer.apple.com": true
120-
case "www.freebsd.org": true
121-
case "github.com": true
122-
case "tools.ietf.org": true
123-
case "man7.org": true
124-
case "developer.mozilla.org": true
125-
case "docs.scala-lang.org": true
126-
case "swiftinit.org": true
127-
case "forums.swift.org": true
128-
case "swift.org": true
129-
case "en.wikipedia.org": true
130-
default: false
129+
codelink = link
131130
}
131+
else
132+
{
133+
let root:Substring
134+
if let j:String.Index = domain.lastIndex(of: "."),
135+
let i:String.Index = domain[..<j].lastIndex(of: ".")
136+
{
137+
root = domain[domain.index(after: i)...]
138+
}
139+
else
140+
{
141+
root = domain
142+
}
143+
// We will follow links to GitHub and reputable open-source indexes.
144+
let safe:Bool = switch root
145+
{
146+
case "freebsd.org": true
147+
case "github.com": true
148+
case "ietf.org": true
149+
case "man7.org": true
150+
case "mozilla.org": true
151+
case "scala-lang.org": true
152+
case "swiftinit.org": true
153+
case "swift.org": true
154+
case "wikipedia.org": true
155+
default: false
156+
}
132157

133-
return .link(https: unresolved.link, safe: safe)
134-
}
158+
return .link(https: unresolved.link, safe: safe)
159+
}
160+
161+
// Translation always lowercases the URL, so we need to use the collated table.
162+
switch self.caseless.resolve(codelink)
163+
{
164+
case .some(let overloads):
165+
guard
166+
let overload:CodelinkResolver<Unidoc.Scalar>.Overload = overloads.first
167+
else
168+
{
169+
// Not an error, this was only speculative.
170+
return .link(https: unresolved.link, safe: false)
171+
}
172+
173+
resolution = overload.target
174+
175+
case .one(let overload):
176+
resolution = overload.target
177+
}
135178

136-
guard
137-
let (codelink, resolution):
138-
(Codelink, CodelinkResolver<Unidoc.Scalar>.Overload.Target?) =
179+
print("DEBUG: successful translation of '\(unresolved.link)'")
180+
}
181+
else if
182+
let resolved:(Codelink, CodelinkResolver<Unidoc.Scalar>.Overload.Target?) =
139183
self.resolve(unresolved)
184+
{
185+
(codelink, resolution) = resolved
186+
}
140187
else
141188
{
142189
return .text(unresolved.link)

Sources/UnidocLinker/Sema/WritableKeyPath (ext).swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@ import UnidocRecords
44

55
extension WritableKeyPath<Unidoc.Stats.Coverage, Int>
66
{
7+
// TODO: This is a temporary solution. This statistic should not depend on the snapshot.
8+
// Ideally, we should reformulate the scoring system to assign a “expected” amount of
9+
// documentation for each declaration, which does not depend on other declarations.
710
static
811
func classify(_ decl:SymbolGraph.Decl,
9-
from snapshot:Unidoc.Linker.Graph,
10-
at local:Int32) -> WritableKeyPath<Unidoc.Stats.Coverage, Int>
12+
_from snapshot:Unidoc.Linker.Graph,
13+
_at local:Int32) -> WritableKeyPath<Unidoc.Stats.Coverage, Int>
1114
{
1215
if case _? = decl.article
1316
{

Sources/UnidocLinker/Unidoc.Linker.Mesh.swift

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import BSON
12
import JSON
23
import SymbolGraphs
34
import Symbols
@@ -86,15 +87,18 @@ extension Unidoc.Linker.Mesh
8687
{
8788
if let decl:SymbolGraph.Decl = linker.current.decls.nodes[d].decl
8889
{
90+
let interface:BSON.Key = decl.signature.spis == nil ? "" : "__unknown__"
8991
let coverage:WritableKeyPath<Unidoc.Stats.Coverage, Int> = .classify(decl,
90-
from: linker.current,
91-
at: d)
92+
_from: linker.current,
93+
_at: d)
9294

9395
let decl:WritableKeyPath<Unidoc.Stats.Decl, Int> = .classify(decl)
9496

97+
cultures[c].census.interfaces[interface, default: 0] += 1
9598
cultures[c].census.unweighted.coverage[keyPath: coverage] += 1
9699
cultures[c].census.unweighted.decls[keyPath: decl] += 1
97100

101+
snapshot.census.interfaces[interface, default: 0] += 1
98102
snapshot.census.unweighted.coverage[keyPath: coverage] += 1
99103
snapshot.census.unweighted.decls[keyPath: decl] += 1
100104
}

Sources/UnidocLinker/Unidoc.Linker.swift

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -247,30 +247,31 @@ extension Unidoc.Linker
247247
{
248248
for (dependencies, cultures):([Symbol.Package: Set<String>], [Int]) in groups
249249
{
250-
var shared:SymbolGraph.ModuleContext = .init()
251-
252-
if let swift:Graph = self[dynamic: .swift]
253-
{
254-
shared.add(snapshot: swift, context: self, filter: nil)
255-
}
256-
for (package, products):(Symbol.Package, Set<String>) in
257-
dependencies.sorted(by: { $0.key < $1.key })
250+
let shared:SymbolGraph.ModuleContext = .init
258251
{
259-
guard
260-
let snapshot:Graph = self[dynamic: package]
261-
else
252+
if let swift:Graph = self[dynamic: .swift]
262253
{
263-
continue
254+
$0.add(snapshot: swift, context: self, filter: nil)
264255
}
265-
266-
var filter:Set<Int> = []
267-
for product:SymbolGraph.Product in snapshot.metadata.products
268-
where products.contains(product.name)
256+
for (package, products):(Symbol.Package, Set<String>) in
257+
dependencies.sorted(by: { $0.key < $1.key })
269258
{
270-
filter.formUnion(product.cultures)
271-
}
259+
guard
260+
let snapshot:Graph = self[dynamic: package]
261+
else
262+
{
263+
continue
264+
}
272265

273-
shared.add(snapshot: snapshot, context: self, filter: filter)
266+
var filter:Set<Int> = []
267+
for product:SymbolGraph.Product in snapshot.metadata.products
268+
where products.contains(product.name)
269+
{
270+
filter.formUnion(product.cultures)
271+
}
272+
273+
$0.add(snapshot: snapshot, context: self, filter: filter)
274+
}
274275
}
275276

276277
for c:Int in cultures
@@ -430,6 +431,10 @@ extension Unidoc.Linker
430431
namespace: namespace,
431432
imports: module.imports,
432433
path: scope)),
434+
caseless: .init(table: module.caseless, scope: .init(
435+
namespace: namespace,
436+
imports: module.imports,
437+
path: scope)),
433438
context: consume self)
434439

435440
do

0 commit comments

Comments
 (0)