Skip to content

Commit a67cbfb

Browse files
committed
refactor the UCF parser slightly, and port its tests to Swift Testing
1 parent d671536 commit a67cbfb

20 files changed

+674
-637
lines changed

Package.resolved

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -621,10 +621,9 @@ let package:Package = .init(
621621
.product(name: "ArgumentParser", package: "swift-argument-parser"),
622622
]),
623623

624-
.executableTarget(name: "UCFTests",
624+
.testTarget(name: "UCFTests",
625625
dependencies: [
626626
.target(name: "UCF"),
627-
.product(name: "Testing_", package: "swift-grammar"),
628627
]),
629628

630629
.executableTarget(name: "FingerprintingTests",

Sources/UCF/Codelinks/UCF.Selector.Path.swift

Lines changed: 3 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -37,102 +37,10 @@ extension UCF.Selector.Path
3737
}
3838
extension UCF.Selector.Path
3939
{
40-
/// Attempts to extend this path by parsing the given string, returning nil if the string
41-
/// does not begin with a valid path component. This method only changes the path if the
42-
/// parsing succeeds.
4340
mutating
44-
func extend(parsing string:Substring.UnicodeScalarView) -> String.Index?
41+
func append(_ component:UCF.Selector.PathComponent)
4542
{
46-
guard
47-
let i:String.Index = string.indices.first
48-
else
49-
{
50-
return nil
51-
}
52-
53-
var j:String.Index = string.index(after: i)
54-
55-
if let _:IdentifierHead = .init(string[i])
56-
{
57-
loop:
58-
while j < string.endIndex
59-
{
60-
switch string[j]
61-
{
62-
case "0" ... "9",
63-
"\u{0300}" ... "\u{036F}",
64-
"\u{1DC0}" ... "\u{1DFF}",
65-
"\u{20D0}" ... "\u{20FF}",
66-
"\u{FE20}" ... "\u{FE2F}":
67-
j = string.index(after: j)
68-
69-
case let codepoint:
70-
guard
71-
let _:IdentifierHead = .init(codepoint)
72-
else
73-
{
74-
break loop
75-
}
76-
77-
j = string.index(after: j)
78-
}
79-
}
80-
}
81-
else if
82-
let _:OperatorHead = .init(string[i])
83-
{
84-
loop:
85-
while j < string.endIndex
86-
{
87-
switch string[j]
88-
{
89-
case "\u{0300}" ... "\u{036F}",
90-
"\u{1DC0}" ... "\u{1DFF}",
91-
"\u{20D0}" ... "\u{20FF}",
92-
"\u{FE00}" ... "\u{FE0F}",
93-
"\u{FE20}" ... "\u{FE2F}",
94-
"\u{E0100}" ... "\u{E01EF}":
95-
j = string.index(after: j)
96-
97-
case let codepoint:
98-
guard
99-
let _:OperatorHead = .init(codepoint)
100-
else
101-
{
102-
break loop
103-
}
104-
105-
j = string.index(after: j)
106-
}
107-
}
108-
}
109-
else
110-
{
111-
return nil
112-
}
113-
114-
if j < string.endIndex, string[j] == "("
115-
{
116-
let k:String.Index = string.index(after: j)
117-
118-
switch string[k...].firstIndex(of: ")")
119-
{
120-
case nil:
121-
return nil
122-
123-
case k?:
124-
// Special case: ignore empty trailing parentheses
125-
self.components.append(String.init(string[i ..< j]))
126-
self.seal = .trailingParentheses
127-
return string.index(after: k)
128-
129-
case let k?:
130-
self.seal = .trailingArguments
131-
j = string.index(after: k)
132-
}
133-
}
134-
135-
self.components.append(String.init(string[i ..< j]))
136-
return j
43+
self.components.append(component.value)
44+
self.seal = component.seal
13745
}
13846
}

Sources/UCF/Codelinks/UCF.Selector.Path.IdentifierHead.swift renamed to Sources/UCF/Codelinks/UCF.Selector.PathComponent.IdentifierHead.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
extension UCF.Selector.Path
1+
extension UCF.Selector.PathComponent
22
{
33
struct IdentifierHead
44
{
@@ -8,7 +8,7 @@ extension UCF.Selector.Path
88
}
99
}
1010
}
11-
extension UCF.Selector.Path.IdentifierHead
11+
extension UCF.Selector.PathComponent.IdentifierHead
1212
{
1313
init?(_ codepoint:Unicode.Scalar)
1414
{

Sources/UCF/Codelinks/UCF.Selector.Path.OperatorHead.swift renamed to Sources/UCF/Codelinks/UCF.Selector.PathComponent.OperatorHead.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
extension UCF.Selector.Path
1+
extension UCF.Selector.PathComponent
22
{
33
struct OperatorHead
44
{
@@ -8,7 +8,7 @@ extension UCF.Selector.Path
88
}
99
}
1010
}
11-
extension UCF.Selector.Path.OperatorHead
11+
extension UCF.Selector.PathComponent.OperatorHead
1212
{
1313
@inlinable public
1414
init?(_ codepoint:Unicode.Scalar)
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
extension UCF.Selector
2+
{
3+
struct PathComponent
4+
{
5+
/// The string indices corresponding to this path component, which may include
6+
/// additional junk characters not included in the component ``value`` itself.
7+
let range:Range<String.Index>
8+
/// The canonical value of this path component. This never includes empty trailing
9+
/// parentheses.
10+
let value:String
11+
/// Indicates if this path component can be followed by any additional path components,
12+
/// and if non-nil, indicates the reason why.
13+
let seal:Seal?
14+
}
15+
}
16+
extension UCF.Selector.PathComponent
17+
{
18+
static func parse(_ string:Substring.UnicodeScalarView) -> Self?
19+
{
20+
guard
21+
let i:String.Index = string.indices.first
22+
else
23+
{
24+
return nil
25+
}
26+
27+
var j:String.Index = string.index(after: i)
28+
29+
if let _:IdentifierHead = .init(string[i])
30+
{
31+
loop:
32+
while j < string.endIndex
33+
{
34+
switch string[j]
35+
{
36+
case "0" ... "9",
37+
"\u{0300}" ... "\u{036F}",
38+
"\u{1DC0}" ... "\u{1DFF}",
39+
"\u{20D0}" ... "\u{20FF}",
40+
"\u{FE20}" ... "\u{FE2F}":
41+
j = string.index(after: j)
42+
43+
case let codepoint:
44+
guard
45+
let _:IdentifierHead = .init(codepoint)
46+
else
47+
{
48+
break loop
49+
}
50+
51+
j = string.index(after: j)
52+
}
53+
}
54+
}
55+
else if
56+
let _:OperatorHead = .init(string[i])
57+
{
58+
loop:
59+
while j < string.endIndex
60+
{
61+
switch string[j]
62+
{
63+
case "\u{0300}" ... "\u{036F}",
64+
"\u{1DC0}" ... "\u{1DFF}",
65+
"\u{20D0}" ... "\u{20FF}",
66+
"\u{FE00}" ... "\u{FE0F}",
67+
"\u{FE20}" ... "\u{FE2F}",
68+
"\u{E0100}" ... "\u{E01EF}":
69+
j = string.index(after: j)
70+
71+
case let codepoint:
72+
guard
73+
let _:OperatorHead = .init(codepoint)
74+
else
75+
{
76+
break loop
77+
}
78+
79+
j = string.index(after: j)
80+
}
81+
}
82+
}
83+
else
84+
{
85+
return nil
86+
}
87+
88+
guard j < string.endIndex, string[j] == "("
89+
else
90+
{
91+
return .init(
92+
range: i ..< j,
93+
value: String.init(string[i ..< j]),
94+
seal: nil)
95+
}
96+
97+
let k:String.Index = string.index(after: j)
98+
99+
switch string[k...].firstIndex(of: ")")
100+
{
101+
case nil:
102+
return nil
103+
104+
case k?:
105+
// Special case: ignore empty trailing parentheses
106+
return .init(
107+
range: i ..< string.index(after: k),
108+
value: String.init(string[i ..< j]),
109+
seal: .trailingParentheses)
110+
111+
case let k?:
112+
j = string.index(after: k)
113+
return .init(
114+
range: i ..< j,
115+
value: String.init(string[i ..< j]),
116+
seal: .trailingArguments)
117+
}
118+
}
119+
}

Sources/UCF/Codelinks/UCF.Selector.swift

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,10 @@ extension UCF.Selector:LosslessStringConvertible
116116

117117
var path:Path = .init()
118118
// Special case for bare operator references:
119-
if case j? = path.extend(parsing: string.unicodeScalars[i ..< j])
119+
if let sole:PathComponent = .parse(string.unicodeScalars[i ..< j]),
120+
j == sole.range.upperBound
120121
{
122+
path.append(sole)
121123
self.init(base: .relative, path: path)
122124
}
123125
else
@@ -134,16 +136,20 @@ extension UCF.Selector
134136
self.init(base: base)
135137

136138
var i:String.Index = string.startIndex
137-
while let j:String.Index = self.path.extend(parsing: string.unicodeScalars[i...])
139+
while let next:PathComponent = .parse(string.unicodeScalars[i...])
138140
{
139-
guard j < string.endIndex
141+
self.path.append(next)
142+
143+
let j:String.Index = next.range.upperBound
144+
if j < string.endIndex
145+
{
146+
i = string.index(after: j)
147+
}
140148
else
141149
{
142150
return
143151
}
144152

145-
i = string.index(after: j)
146-
147153
switch string[j]
148154
{
149155
case "/":
@@ -286,7 +292,7 @@ extension UCF.Selector
286292
}
287293
extension UCF.Selector
288294
{
289-
@inlinable public
295+
@inlinable public
290296
static func equivalent(to doclink:Doclink) -> Self?
291297
{
292298
if doclink.absolute

Sources/UCFTests/Anchors.swift

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import Testing
2+
import UCF
3+
4+
@Suite
5+
struct Anchors
6+
{
7+
@Test
8+
static func Empty()
9+
{
10+
#expect("" == UCF.AnchorMangling.init(mangling: ""))
11+
}
12+
@Test
13+
static func OneWord()
14+
{
15+
#expect("overview" == UCF.AnchorMangling.init(mangling: "Overview"))
16+
}
17+
@Test
18+
static func TwoWords()
19+
{
20+
#expect("basic-usage" == UCF.AnchorMangling.init(mangling: "Basic usage"))
21+
}
22+
@Test
23+
static func Punctuation()
24+
{
25+
#expect("""
26+
swifties-of-america-dont-forget-to-claim-your-free-hoodie-before-the-deadline
27+
""" == UCF.AnchorMangling.init(
28+
mangling: """
29+
Swifties of America! Don’t forget to claim your FREE hoodie before the deadline!
30+
"""))
31+
}
32+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import FNV1
2+
import Testing
3+
import UCF
4+
5+
@Suite
6+
struct CodelinkDisambiguators:ParsingSuite
7+
{
8+
typealias Format = UCF.Selector
9+
10+
@Test
11+
static func Enum() throws
12+
{
13+
let link:UCF.Selector = try Self.roundtrip("Fake [enum]")
14+
#expect(link.base == .relative)
15+
#expect(link.path.components == ["Fake"])
16+
#expect(!link.path.hasTrailingParentheses)
17+
#expect(link.suffix == .filter(.enum))
18+
}
19+
@Test
20+
static func UncannyHash() throws
21+
{
22+
let link:UCF.Selector = try Self.roundtrip("Fake [ENUM]")
23+
let hash:FNV24 = .init("ENUM", radix: 36)!
24+
25+
#expect(link.base == .relative)
26+
#expect(link.path.components == ["Fake"])
27+
#expect(!link.path.hasTrailingParentheses)
28+
#expect(link.suffix == .hash(hash))
29+
}
30+
@Test
31+
static func ClassVar() throws
32+
{
33+
let link:UCF.Selector = try Self.roundtrip("Fake.max [class var]")
34+
#expect(link.base == .relative)
35+
#expect(link.path.components == ["Fake", "max"])
36+
#expect(!link.path.hasTrailingParentheses)
37+
#expect(link.suffix == .filter(.class_var))
38+
}
39+
}

0 commit comments

Comments
 (0)