Skip to content

Commit

Permalink
Merge pull request #392 from tayloraswift/doclink-equivalence
Browse files Browse the repository at this point in the history
support disambiguation filters in doclinks
  • Loading branch information
tayloraswift authored Jan 7, 2025
2 parents d211cd3 + 2f9580a commit 53aa5dd
Show file tree
Hide file tree
Showing 17 changed files with 155 additions and 40 deletions.
13 changes: 11 additions & 2 deletions Sources/SymbolGraphLinker/Resolution/SSGC.OutlineDiagnostic.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ extension SSGC
{
case annealedIncorrectHash(in:UCF.Selector, to:FNV24)
case unresolvedAbsolute(Doclink)
case unresolvedRelative(Doclink)
case suggestReformat(Doclink, to:UCF.Selector)
}
}
Expand All @@ -26,13 +27,16 @@ extension SSGC.OutlineDiagnostic:Diagnostic
"""

case .unresolvedAbsolute(let doclink):
fallthrough

case .unresolvedRelative(let doclink):
output[.warning] = """
doclink '\(doclink)' does not resolve to any article (or tutorial) in this package
doclink '\(doclink.value)' does not resolve to any article (or tutorial) in this package
"""

case .suggestReformat(let doclink, to: _):
output[.warning] = """
doclink '\(doclink)' referencing symbol documentation could be written as \
doclink '\(doclink.value)' referencing symbol documentation could be written as \
a backtick-delimited codelink
"""
}
Expand All @@ -52,6 +56,11 @@ extension SSGC.OutlineDiagnostic:Diagnostic
documentation
"""

case .unresolvedRelative(let doclink):
output[.note] = """
could not convert relative doclink '\(doclink.page)' to a UCF selector
"""

case .suggestReformat(_, to: let codelink):
output[.note] = """
reformat the link as ``\(codelink)`` to suppress this warning
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -222,12 +222,17 @@ extension SSGC.OutlineResolver
}
else
{
if doclink.absolute
{
self.diagnostics[source] = SSGC.OutlineDiagnostic.unresolvedAbsolute(doclink)
return nil
}
// Resolution might still succeed by reinterpreting the doclink as a codelink.
guard
let codelink:UCF.Selector = .equivalent(to: doclink)
let codelink:UCF.Selector = .init(doclink.page)
else
{
self.diagnostics[source] = SSGC.OutlineDiagnostic.unresolvedAbsolute(doclink)
self.diagnostics[source] = SSGC.OutlineDiagnostic.unresolvedRelative(doclink)
return nil
}
guard
Expand Down
3 changes: 3 additions & 0 deletions Sources/UCF/Codelinks/Grammar/UCF.ArrowRule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Grammar

extension UCF
{
/// Arrow ::= \s * '->' \s *
enum ArrowRule:ParsingRule
{
typealias Location = String.Index
Expand All @@ -11,8 +12,10 @@ extension UCF
_ input:inout ParsingInput<some ParsingDiagnostics<Source>>) throws
where Source:Collection<Terminal>, Source.Index == Location
{
input.parse(as: UnicodeEncoding<Location, Terminal>.Space.self, in: Void.self)
try input.parse(as: UnicodeEncoding<Location, Terminal>.Hyphen.self)
try input.parse(as: UnicodeEncoding<Location, Terminal>.AngleRight.self)
input.parse(as: UnicodeEncoding<Location, Terminal>.Space.self, in: Void.self)
}
}
}
2 changes: 1 addition & 1 deletion Sources/UCF/Codelinks/Grammar/UCF.BracketPatternRule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Grammar

extension UCF
{
/// BracketPattern ::= '[' TypePattern ( ':' TypePattern ) ? ']'
/// BracketPattern ::= '[' TypePattern ( \s * ':' \s * TypePattern ) ? ']'
enum BracketPatternRule:ParsingRule
{
typealias Location = String.Index
Expand Down
29 changes: 29 additions & 0 deletions Sources/UCF/Codelinks/Grammar/UCF.DisambiguationSuffixRule.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import Grammar

extension UCF
{
/// DisambiguationSuffix ::= SignatureSuffix Clauses ? | Clauses
///
/// Note that the leading whitespace is considered part of the disambiguator.
enum DisambiguationSuffixRule:ParsingRule
{
typealias Location = String.Index
typealias Terminal = Unicode.Scalar
typealias Construction = (SignaturePattern?, [(String, String?)])

static func parse<Diagnostics>(
_ input:inout ParsingInput<Diagnostics>) throws -> Construction where
Diagnostics:ParsingDiagnostics,
Diagnostics.Source.Element == Terminal,
Diagnostics.Source.Index == Location
{
if let clauses:[(String, String?)] = input.parse(as: DisambiguatorRule.Clauses?.self)
{
return (nil, clauses)
}

let signature:SignaturePattern = try input.parse(as: SignatureSuffixRule.self)
return (signature, input.parse(as: DisambiguatorRule.Clauses?.self) ?? [])
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Grammar

extension UCF.DisambiguatorRule.Clause
{
/// AlphanumericWord ::= ' ' * [0-9A-Za-z] + ' ' *
/// AlphanumericWord ::= Space ? [0-9A-Za-z] + Space ?
enum AlphanumericWord:ParsingRule
{
typealias Location = String.Index
Expand All @@ -14,14 +14,14 @@ extension UCF.DisambiguatorRule.Clause
Diagnostics.Source.Element == Terminal,
Diagnostics.Source.Index == Location
{
input.parse(as: UnicodeEncoding<Location, Terminal>.Space.self, in: Void.self)
input.parse(as: UCF.SpaceRule?.self)

let start:Location = input.index
try input.parse(as: AlphanumericCodepoint.self)
input.parse(as: AlphanumericCodepoint.self, in: Void.self)
let end:Location = input.index

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

return start ..< end
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Grammar

extension UCF.DisambiguatorRule
{
/// Clauses ::= ' ' + '[' Clause ( ',' Clause ) * ']'
/// Clauses ::= Space '[' Clause ( ',' Clause ) * ']'
///
/// Note that the leading whitespace is considered part of the filter.
enum Clauses:ParsingRule
Expand All @@ -16,8 +16,7 @@ extension UCF.DisambiguatorRule
Diagnostics.Source.Element == Terminal,
Diagnostics.Source.Index == Location
{
try input.parse(as: UnicodeEncoding<Location, Terminal>.Space.self)
input.parse(as: UnicodeEncoding<Location, Terminal>.Space.self, in: Void.self)
try input.parse(as: UCF.SpaceRule.self)

// No padding around structural characters; ``DisambiguationClauseRule`` already
// trims whitespace.
Expand Down
2 changes: 1 addition & 1 deletion Sources/UCF/Codelinks/Grammar/UCF.DisambiguatorRule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Grammar

extension UCF
{
/// Disambiguator ::= ' ' + SignaturePattern Clauses ? | Clauses
/// Disambiguator ::= \s + SignaturePattern Clauses ? | Clauses
///
/// Note that the leading whitespace is considered part of the disambiguator.
enum DisambiguatorRule:ParsingRule
Expand Down
5 changes: 2 additions & 3 deletions Sources/UCF/Codelinks/Grammar/UCF.FunctionPatternRule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Grammar

extension UCF
{
/// FunctionPattern ::= TuplePattern ( '->' TypePattern ) ?
/// FunctionPattern ::= TuplePattern ( Arrow TypePattern ) ?
enum FunctionPatternRule:ParsingRule
{
typealias Location = String.Index
Expand All @@ -18,8 +18,7 @@ extension UCF
{
let tuple:[TypePattern] = try input.parse(as: TuplePatternRule.self)

if case ()? = input.parse(
as: Pattern.Pad<ArrowRule, UnicodeEncoding<Location, Terminal>.Space>?.self)
if case ()? = input.parse(as: ArrowRule?.self)
{
return (tuple, try input.parse(as: TypePatternRule.self))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Grammar

extension UCF.NominalPatternRule
{
/// GenericArguments ::= '<' TypePattern ( ',' TypePattern ) * '>'
/// GenericArguments ::= '<' \s * TypePattern ( \s * ',' \s * TypePattern ) * \s * '>'
enum GenericArguments:ParsingRule
{
typealias Location = String.Index
Expand All @@ -15,11 +15,13 @@ extension UCF.NominalPatternRule
Diagnostics.Source.Index == Location
{
try input.parse(as: UnicodeEncoding<Location, Terminal>.AngleLeft.self)
input.parse(as: UnicodeEncoding<Location, Terminal>.Space.self, in: Void.self)
let types:[UCF.TypePattern] = try input.parse(as: Pattern.Join<UCF.TypePatternRule,
Pattern.Pad<
UnicodeEncoding<Location, Terminal>.Comma,
UnicodeEncoding<Location, Terminal>.Space>,
[UCF.TypePattern]>.self)
input.parse(as: UnicodeEncoding<Location, Terminal>.Space.self, in: Void.self)
try input.parse(as: UnicodeEncoding<Location, Terminal>.AngleRight.self)
return types
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Grammar

extension UCF
{
/// SignaturePattern ::= FunctionPattern | '->' ' ' * TypePattern
/// SignaturePattern ::= FunctionPattern | Arrow TypePattern
enum SignaturePatternRule:ParsingRule
{
typealias Location = String.Index
Expand Down
27 changes: 27 additions & 0 deletions Sources/UCF/Codelinks/Grammar/UCF.SpaceRule.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Grammar

extension UCF
{
/// Space ::= \s + | '-'
enum SpaceRule:ParsingRule
{
typealias Location = String.Index
typealias Terminal = Unicode.Scalar

static func parse<Diagnostics>(
_ input:inout ParsingInput<Diagnostics>) throws -> Void where
Diagnostics:ParsingDiagnostics,
Diagnostics.Source.Element == Terminal,
Diagnostics.Source.Index == Location
{
if case ()? = input.parse(as: UnicodeEncoding<Location, Terminal>.Space?.self)
{
input.parse(as: UnicodeEncoding<Location, Terminal>.Space.self, in: Void.self)
}
else
{
try input.parse(as: UnicodeEncoding<Location, Terminal>.Hyphen.self)
}
}
}
}
4 changes: 3 additions & 1 deletion Sources/UCF/Codelinks/Grammar/UCF.TuplePatternRule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Grammar

extension UCF
{
/// TuplePattern ::= '(' ( TypePattern ( ',' TypePattern ) * ) ? ')'
/// TuplePattern ::= '(' \s * ( TypePattern ( \s * ',' TypePattern ) * ) ? \s * ')'
enum TuplePatternRule:ParsingRule
{
typealias Location = String.Index
Expand All @@ -15,6 +15,7 @@ extension UCF
Diagnostics.Source.Index == Location
{
try input.parse(as: UnicodeEncoding<Location, Terminal>.ParenthesisLeft.self)
input.parse(as: UnicodeEncoding<Location, Terminal>.Space.self, in: Void.self)

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

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

return types
Expand Down
2 changes: 1 addition & 1 deletion Sources/UCF/Codelinks/Grammar/UCF.TypePatternRule.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Grammar

extension UCF
{
/// TypePattern ::= TypeElement ( '&' TypeElement ) *
/// TypePattern ::= TypeElement ( \s * '&' \s * TypeElement ) *
enum TypePatternRule:ParsingRule
{
typealias Location = String.Index
Expand Down
18 changes: 14 additions & 4 deletions Sources/UCF/Codelinks/UCF.Selector.Suffix.swift
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,21 @@ extension UCF.Selector.Suffix
/// The `string` must start with a hyphen (`-`)!
static func parse(legacy string:Substring) -> Self?
{
if let pattern:UCF.SignaturePattern = try? UCF.SignatureSuffixRule.parse(
string.unicodeScalars)
let (signature, clauses):(UCF.SignaturePattern?, [(String, String?)])
do
{
(signature, clauses) = try UCF.DisambiguationSuffixRule.parse(string.unicodeScalars)

if let disambiguator:UCF.Disambiguator = .init(
signature: signature,
clauses: clauses,
source: string)
{
return .unidoc(disambiguator)
}
}
catch
{
return .unidoc(.init(conditions: [],
signature: .init(parsed: pattern, source: string)))
}

assert(string.startIndex < string.endIndex)
Expand Down
13 changes: 0 additions & 13 deletions Sources/UCF/Codelinks/UCF.Selector.swift
Original file line number Diff line number Diff line change
Expand Up @@ -232,16 +232,3 @@ extension UCF.Selector
return nil
}
}
extension UCF.Selector
{
@inlinable public
static func equivalent(to doclink:Doclink) -> Self?
{
if doclink.absolute
{
return nil
}

return .init(doclink.path.joined(separator: "/"))
}
}
51 changes: 47 additions & 4 deletions Sources/UCF/Doclinks/Doclink.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,40 @@ extension Doclink
{
self.absolute ? self.path.first : nil
}

@inlinable public
var page:String
{
var first:Bool = true
var text:String = self.absolute ? "//" : ""
for component:String in self.path
{
if first
{
first = false
}
else
{
text.append("/")
}

text.append(component)
}
return text
}

@inlinable public
var value:String
{
var text:String = self.page
if let fragment:String = self.fragment
{
text.append("#")
text.append(fragment)
}
return text
}

/// Returns the string value of the doclink, without the `doc:` prefix, percent-encoding any
/// special characters as needed.
@inlinable public
Expand Down Expand Up @@ -53,6 +87,7 @@ extension Doclink
return text
}
}
@available(*, deprecated)
extension Doclink:CustomStringConvertible
{
@inlinable public
Expand Down Expand Up @@ -113,11 +148,19 @@ extension Doclink
end = uri.endIndex
}

if let path:URI.Path = .init(relative: uri[start ..< end])
/// The URI path parser doesn’t know how to handle optionals due to the
/// question character so we need to manually split it off and append
/// it to the last path component.
let question:String.Index? = uri[start ..< end].firstIndex(of: "?")
if let path:URI.Path = .init(relative: uri[start ..< (question ?? end)])
{
self.init(absolute: slashes >= 2,
path: path.normalized(),
fragment: fragment?.decoded)
var path:[String] = path.normalized()
if let question:String.Index,
let i:Int = path.indices.last
{
path[i] += uri[question...]
}
self.init(absolute: slashes >= 2, path: path, fragment: fragment?.decoded)
}
else
{
Expand Down

0 comments on commit 53aa5dd

Please sign in to comment.