Skip to content

Commit 53aa5dd

Browse files
authored
Merge pull request #392 from tayloraswift/doclink-equivalence
support disambiguation filters in doclinks
2 parents d211cd3 + 2f9580a commit 53aa5dd

17 files changed

+155
-40
lines changed

Sources/SymbolGraphLinker/Resolution/SSGC.OutlineDiagnostic.swift

+11-2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ extension SSGC
88
{
99
case annealedIncorrectHash(in:UCF.Selector, to:FNV24)
1010
case unresolvedAbsolute(Doclink)
11+
case unresolvedRelative(Doclink)
1112
case suggestReformat(Doclink, to:UCF.Selector)
1213
}
1314
}
@@ -26,13 +27,16 @@ extension SSGC.OutlineDiagnostic:Diagnostic
2627
"""
2728

2829
case .unresolvedAbsolute(let doclink):
30+
fallthrough
31+
32+
case .unresolvedRelative(let doclink):
2933
output[.warning] = """
30-
doclink '\(doclink)' does not resolve to any article (or tutorial) in this package
34+
doclink '\(doclink.value)' does not resolve to any article (or tutorial) in this package
3135
"""
3236

3337
case .suggestReformat(let doclink, to: _):
3438
output[.warning] = """
35-
doclink '\(doclink)' referencing symbol documentation could be written as \
39+
doclink '\(doclink.value)' referencing symbol documentation could be written as \
3640
a backtick-delimited codelink
3741
"""
3842
}
@@ -52,6 +56,11 @@ extension SSGC.OutlineDiagnostic:Diagnostic
5256
documentation
5357
"""
5458

59+
case .unresolvedRelative(let doclink):
60+
output[.note] = """
61+
could not convert relative doclink '\(doclink.page)' to a UCF selector
62+
"""
63+
5564
case .suggestReformat(_, to: let codelink):
5665
output[.note] = """
5766
reformat the link as ``\(codelink)`` to suppress this warning

Sources/SymbolGraphLinker/Resolution/SSGC.OutlineResolver.swift

+7-2
Original file line numberDiff line numberDiff line change
@@ -222,12 +222,17 @@ extension SSGC.OutlineResolver
222222
}
223223
else
224224
{
225+
if doclink.absolute
226+
{
227+
self.diagnostics[source] = SSGC.OutlineDiagnostic.unresolvedAbsolute(doclink)
228+
return nil
229+
}
225230
// Resolution might still succeed by reinterpreting the doclink as a codelink.
226231
guard
227-
let codelink:UCF.Selector = .equivalent(to: doclink)
232+
let codelink:UCF.Selector = .init(doclink.page)
228233
else
229234
{
230-
self.diagnostics[source] = SSGC.OutlineDiagnostic.unresolvedAbsolute(doclink)
235+
self.diagnostics[source] = SSGC.OutlineDiagnostic.unresolvedRelative(doclink)
231236
return nil
232237
}
233238
guard

Sources/UCF/Codelinks/Grammar/UCF.ArrowRule.swift

+3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import Grammar
22

33
extension UCF
44
{
5+
/// Arrow ::= \s * '->' \s *
56
enum ArrowRule:ParsingRule
67
{
78
typealias Location = String.Index
@@ -11,8 +12,10 @@ extension UCF
1112
_ input:inout ParsingInput<some ParsingDiagnostics<Source>>) throws
1213
where Source:Collection<Terminal>, Source.Index == Location
1314
{
15+
input.parse(as: UnicodeEncoding<Location, Terminal>.Space.self, in: Void.self)
1416
try input.parse(as: UnicodeEncoding<Location, Terminal>.Hyphen.self)
1517
try input.parse(as: UnicodeEncoding<Location, Terminal>.AngleRight.self)
18+
input.parse(as: UnicodeEncoding<Location, Terminal>.Space.self, in: Void.self)
1619
}
1720
}
1821
}

Sources/UCF/Codelinks/Grammar/UCF.BracketPatternRule.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Grammar
22

33
extension UCF
44
{
5-
/// BracketPattern ::= '[' TypePattern ( ':' TypePattern ) ? ']'
5+
/// BracketPattern ::= '[' TypePattern ( \s * ':' \s * TypePattern ) ? ']'
66
enum BracketPatternRule:ParsingRule
77
{
88
typealias Location = String.Index
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import Grammar
2+
3+
extension UCF
4+
{
5+
/// DisambiguationSuffix ::= SignatureSuffix Clauses ? | Clauses
6+
///
7+
/// Note that the leading whitespace is considered part of the disambiguator.
8+
enum DisambiguationSuffixRule:ParsingRule
9+
{
10+
typealias Location = String.Index
11+
typealias Terminal = Unicode.Scalar
12+
typealias Construction = (SignaturePattern?, [(String, String?)])
13+
14+
static func parse<Diagnostics>(
15+
_ input:inout ParsingInput<Diagnostics>) throws -> Construction where
16+
Diagnostics:ParsingDiagnostics,
17+
Diagnostics.Source.Element == Terminal,
18+
Diagnostics.Source.Index == Location
19+
{
20+
if let clauses:[(String, String?)] = input.parse(as: DisambiguatorRule.Clauses?.self)
21+
{
22+
return (nil, clauses)
23+
}
24+
25+
let signature:SignaturePattern = try input.parse(as: SignatureSuffixRule.self)
26+
return (signature, input.parse(as: DisambiguatorRule.Clauses?.self) ?? [])
27+
}
28+
}
29+
}

Sources/UCF/Codelinks/Grammar/UCF.DisambiguatorRule.Clause.AlphanumericWord.swift

+3-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Grammar
22

33
extension UCF.DisambiguatorRule.Clause
44
{
5-
/// AlphanumericWord ::= ' ' * [0-9A-Za-z] + ' ' *
5+
/// AlphanumericWord ::= Space ? [0-9A-Za-z] + Space ?
66
enum AlphanumericWord:ParsingRule
77
{
88
typealias Location = String.Index
@@ -14,14 +14,14 @@ extension UCF.DisambiguatorRule.Clause
1414
Diagnostics.Source.Element == Terminal,
1515
Diagnostics.Source.Index == Location
1616
{
17-
input.parse(as: UnicodeEncoding<Location, Terminal>.Space.self, in: Void.self)
17+
input.parse(as: UCF.SpaceRule?.self)
1818

1919
let start:Location = input.index
2020
try input.parse(as: AlphanumericCodepoint.self)
2121
input.parse(as: AlphanumericCodepoint.self, in: Void.self)
2222
let end:Location = input.index
2323

24-
input.parse(as: UnicodeEncoding<Location, Terminal>.Space.self, in: Void.self)
24+
input.parse(as: UCF.SpaceRule?.self)
2525

2626
return start ..< end
2727
}

Sources/UCF/Codelinks/Grammar/UCF.DisambiguatorRule.Clauses.swift

+2-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Grammar
22

33
extension UCF.DisambiguatorRule
44
{
5-
/// Clauses ::= ' ' + '[' Clause ( ',' Clause ) * ']'
5+
/// Clauses ::= Space '[' Clause ( ',' Clause ) * ']'
66
///
77
/// Note that the leading whitespace is considered part of the filter.
88
enum Clauses:ParsingRule
@@ -16,8 +16,7 @@ extension UCF.DisambiguatorRule
1616
Diagnostics.Source.Element == Terminal,
1717
Diagnostics.Source.Index == Location
1818
{
19-
try input.parse(as: UnicodeEncoding<Location, Terminal>.Space.self)
20-
input.parse(as: UnicodeEncoding<Location, Terminal>.Space.self, in: Void.self)
19+
try input.parse(as: UCF.SpaceRule.self)
2120

2221
// No padding around structural characters; ``DisambiguationClauseRule`` already
2322
// trims whitespace.

Sources/UCF/Codelinks/Grammar/UCF.DisambiguatorRule.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Grammar
22

33
extension UCF
44
{
5-
/// Disambiguator ::= ' ' + SignaturePattern Clauses ? | Clauses
5+
/// Disambiguator ::= \s + SignaturePattern Clauses ? | Clauses
66
///
77
/// Note that the leading whitespace is considered part of the disambiguator.
88
enum DisambiguatorRule:ParsingRule

Sources/UCF/Codelinks/Grammar/UCF.FunctionPatternRule.swift

+2-3
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Grammar
22

33
extension UCF
44
{
5-
/// FunctionPattern ::= TuplePattern ( '->' TypePattern ) ?
5+
/// FunctionPattern ::= TuplePattern ( Arrow TypePattern ) ?
66
enum FunctionPatternRule:ParsingRule
77
{
88
typealias Location = String.Index
@@ -18,8 +18,7 @@ extension UCF
1818
{
1919
let tuple:[TypePattern] = try input.parse(as: TuplePatternRule.self)
2020

21-
if case ()? = input.parse(
22-
as: Pattern.Pad<ArrowRule, UnicodeEncoding<Location, Terminal>.Space>?.self)
21+
if case ()? = input.parse(as: ArrowRule?.self)
2322
{
2423
return (tuple, try input.parse(as: TypePatternRule.self))
2524
}

Sources/UCF/Codelinks/Grammar/UCF.NominalPatternRule.GenericArguments.swift

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Grammar
22

33
extension UCF.NominalPatternRule
44
{
5-
/// GenericArguments ::= '<' TypePattern ( ',' TypePattern ) * '>'
5+
/// GenericArguments ::= '<' \s * TypePattern ( \s * ',' \s * TypePattern ) * \s * '>'
66
enum GenericArguments:ParsingRule
77
{
88
typealias Location = String.Index
@@ -15,11 +15,13 @@ extension UCF.NominalPatternRule
1515
Diagnostics.Source.Index == Location
1616
{
1717
try input.parse(as: UnicodeEncoding<Location, Terminal>.AngleLeft.self)
18+
input.parse(as: UnicodeEncoding<Location, Terminal>.Space.self, in: Void.self)
1819
let types:[UCF.TypePattern] = try input.parse(as: Pattern.Join<UCF.TypePatternRule,
1920
Pattern.Pad<
2021
UnicodeEncoding<Location, Terminal>.Comma,
2122
UnicodeEncoding<Location, Terminal>.Space>,
2223
[UCF.TypePattern]>.self)
24+
input.parse(as: UnicodeEncoding<Location, Terminal>.Space.self, in: Void.self)
2325
try input.parse(as: UnicodeEncoding<Location, Terminal>.AngleRight.self)
2426
return types
2527
}

Sources/UCF/Codelinks/Grammar/UCF.SignaturePatternRule.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Grammar
22

33
extension UCF
44
{
5-
/// SignaturePattern ::= FunctionPattern | '->' ' ' * TypePattern
5+
/// SignaturePattern ::= FunctionPattern | Arrow TypePattern
66
enum SignaturePatternRule:ParsingRule
77
{
88
typealias Location = String.Index
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import Grammar
2+
3+
extension UCF
4+
{
5+
/// Space ::= \s + | '-'
6+
enum SpaceRule:ParsingRule
7+
{
8+
typealias Location = String.Index
9+
typealias Terminal = Unicode.Scalar
10+
11+
static func parse<Diagnostics>(
12+
_ input:inout ParsingInput<Diagnostics>) throws -> Void where
13+
Diagnostics:ParsingDiagnostics,
14+
Diagnostics.Source.Element == Terminal,
15+
Diagnostics.Source.Index == Location
16+
{
17+
if case ()? = input.parse(as: UnicodeEncoding<Location, Terminal>.Space?.self)
18+
{
19+
input.parse(as: UnicodeEncoding<Location, Terminal>.Space.self, in: Void.self)
20+
}
21+
else
22+
{
23+
try input.parse(as: UnicodeEncoding<Location, Terminal>.Hyphen.self)
24+
}
25+
}
26+
}
27+
}

Sources/UCF/Codelinks/Grammar/UCF.TuplePatternRule.swift

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Grammar
22

33
extension UCF
44
{
5-
/// TuplePattern ::= '(' ( TypePattern ( ',' TypePattern ) * ) ? ')'
5+
/// TuplePattern ::= '(' \s * ( TypePattern ( \s * ',' TypePattern ) * ) ? \s * ')'
66
enum TuplePatternRule:ParsingRule
77
{
88
typealias Location = String.Index
@@ -15,6 +15,7 @@ extension UCF
1515
Diagnostics.Source.Index == Location
1616
{
1717
try input.parse(as: UnicodeEncoding<Location, Terminal>.ParenthesisLeft.self)
18+
input.parse(as: UnicodeEncoding<Location, Terminal>.Space.self, in: Void.self)
1819

1920
/// This is not a Join, as it is legal for there to be no elements in the tuple.
2021
var types:[TypePattern] = []
@@ -32,6 +33,7 @@ extension UCF
3233
}
3334
}
3435

36+
input.parse(as: UnicodeEncoding<Location, Terminal>.Space.self, in: Void.self)
3537
try input.parse(as: UnicodeEncoding<Location, Terminal>.ParenthesisRight.self)
3638

3739
return types

Sources/UCF/Codelinks/Grammar/UCF.TypePatternRule.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Grammar
22

33
extension UCF
44
{
5-
/// TypePattern ::= TypeElement ( '&' TypeElement ) *
5+
/// TypePattern ::= TypeElement ( \s * '&' \s * TypeElement ) *
66
enum TypePatternRule:ParsingRule
77
{
88
typealias Location = String.Index

Sources/UCF/Codelinks/UCF.Selector.Suffix.swift

+14-4
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,21 @@ extension UCF.Selector.Suffix
5050
/// The `string` must start with a hyphen (`-`)!
5151
static func parse(legacy string:Substring) -> Self?
5252
{
53-
if let pattern:UCF.SignaturePattern = try? UCF.SignatureSuffixRule.parse(
54-
string.unicodeScalars)
53+
let (signature, clauses):(UCF.SignaturePattern?, [(String, String?)])
54+
do
55+
{
56+
(signature, clauses) = try UCF.DisambiguationSuffixRule.parse(string.unicodeScalars)
57+
58+
if let disambiguator:UCF.Disambiguator = .init(
59+
signature: signature,
60+
clauses: clauses,
61+
source: string)
62+
{
63+
return .unidoc(disambiguator)
64+
}
65+
}
66+
catch
5567
{
56-
return .unidoc(.init(conditions: [],
57-
signature: .init(parsed: pattern, source: string)))
5868
}
5969

6070
assert(string.startIndex < string.endIndex)

Sources/UCF/Codelinks/UCF.Selector.swift

-13
Original file line numberDiff line numberDiff line change
@@ -232,16 +232,3 @@ extension UCF.Selector
232232
return nil
233233
}
234234
}
235-
extension UCF.Selector
236-
{
237-
@inlinable public
238-
static func equivalent(to doclink:Doclink) -> Self?
239-
{
240-
if doclink.absolute
241-
{
242-
return nil
243-
}
244-
245-
return .init(doclink.path.joined(separator: "/"))
246-
}
247-
}

Sources/UCF/Doclinks/Doclink.swift

+47-4
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,40 @@ extension Doclink
2525
{
2626
self.absolute ? self.path.first : nil
2727
}
28+
29+
@inlinable public
30+
var page:String
31+
{
32+
var first:Bool = true
33+
var text:String = self.absolute ? "//" : ""
34+
for component:String in self.path
35+
{
36+
if first
37+
{
38+
first = false
39+
}
40+
else
41+
{
42+
text.append("/")
43+
}
44+
45+
text.append(component)
46+
}
47+
return text
48+
}
49+
50+
@inlinable public
51+
var value:String
52+
{
53+
var text:String = self.page
54+
if let fragment:String = self.fragment
55+
{
56+
text.append("#")
57+
text.append(fragment)
58+
}
59+
return text
60+
}
61+
2862
/// Returns the string value of the doclink, without the `doc:` prefix, percent-encoding any
2963
/// special characters as needed.
3064
@inlinable public
@@ -53,6 +87,7 @@ extension Doclink
5387
return text
5488
}
5589
}
90+
@available(*, deprecated)
5691
extension Doclink:CustomStringConvertible
5792
{
5893
@inlinable public
@@ -113,11 +148,19 @@ extension Doclink
113148
end = uri.endIndex
114149
}
115150

116-
if let path:URI.Path = .init(relative: uri[start ..< end])
151+
/// The URI path parser doesn’t know how to handle optionals due to the
152+
/// question character so we need to manually split it off and append
153+
/// it to the last path component.
154+
let question:String.Index? = uri[start ..< end].firstIndex(of: "?")
155+
if let path:URI.Path = .init(relative: uri[start ..< (question ?? end)])
117156
{
118-
self.init(absolute: slashes >= 2,
119-
path: path.normalized(),
120-
fragment: fragment?.decoded)
157+
var path:[String] = path.normalized()
158+
if let question:String.Index,
159+
let i:Int = path.indices.last
160+
{
161+
path[i] += uri[question...]
162+
}
163+
self.init(absolute: slashes >= 2, path: path, fragment: fragment?.decoded)
121164
}
122165
else
123166
{

0 commit comments

Comments
 (0)