Skip to content

Commit 87a554b

Browse files
EPage-Edaciidgh
authored andcommitted
[SR-10794] Close option suggestions (#2144)
* Suggest Close Options [SR-10794] * Add Test for Suggestion * Added a comment about #available requirement, and some additional test cases * Add `bestMatch` function and clean up description * Tweak comment
1 parent dd10996 commit 87a554b

File tree

3 files changed

+51
-10
lines changed

3 files changed

+51
-10
lines changed

Sources/Basic/EditDistance.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
/// - Complexity: O(_n*m_), where *n* is the length of the first String and
1414
/// *m* is the length of the second one.
1515
public func editDistance(_ first: String, _ second: String) -> Int {
16+
// FIXME: We should use the new `CollectionDifference` API once the
17+
// deployment target is bumped.
1618
let a = Array(first.utf16)
1719
let b = Array(second.utf16)
1820
var distance = [[Int]](repeating: [Int](repeating: 0, count: b.count + 1), count: a.count + 1)
@@ -34,3 +36,21 @@ public func editDistance(_ first: String, _ second: String) -> Int {
3436
}
3537
return distance[a.count][b.count]
3638
}
39+
40+
/// Finds the "best" match for a `String` from an array of possible options.
41+
///
42+
/// - Parameters:
43+
/// - input: The input `String` to match.
44+
/// - options: The available options for `input`.
45+
///
46+
/// - Returns: The best match from the given `options`, or `nil` if none were sufficiently close.
47+
public func bestMatch(for input: String, from options: [String]) -> String? {
48+
return options
49+
.map { ($0, editDistance(input, $0)) }
50+
// Filter out unreasonable edit distances. Based on:
51+
// https://github.com/apple/swift/blob/37daa03b7dc8fb3c4d91dc560a9e0e631c980326/lib/Sema/TypeCheckNameLookup.cpp#L606
52+
.filter { $0.1 <= ($0.0.count + 2) / 3 }
53+
// Sort by edit distance
54+
.sorted { $0.1 < $1.1 }
55+
.first?.0
56+
}

Sources/SPMUtility/ArgumentParser.swift

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ import func SPMLibc.exit
1616
public enum ArgumentParserError: Swift.Error {
1717

1818
/// An unknown option is encountered.
19-
case unknownOption(String)
19+
case unknownOption(String, suggestion: String?)
2020

2121
/// The value of an argument is invalid.
2222
case invalidValue(argument: String, error: ArgumentConversionError)
@@ -43,8 +43,12 @@ extension ArgumentParserError: LocalizedError {
4343
extension ArgumentParserError: CustomStringConvertible {
4444
public var description: String {
4545
switch self {
46-
case .unknownOption(let option):
47-
return "unknown option \(option); use --help to list available options"
46+
case .unknownOption(let option, let suggestion):
47+
var desc = "unknown option \(option); use --help to list available options"
48+
if let suggestion = suggestion {
49+
desc += "\nDid you mean \(suggestion)?"
50+
}
51+
return desc
4852
case .invalidValue(let argument, let error):
4953
return "\(error) for argument \(argument); use --help to print usage"
5054
case .expectedValue(let option):
@@ -847,7 +851,8 @@ public final class ArgumentParser {
847851
let (argumentString, value) = argumentString.spm_split(around: "=")
848852
// Get the corresponding option for the option argument.
849853
guard let optionArgument = optionsMap[argumentString] else {
850-
throw ArgumentParserError.unknownOption(argumentString)
854+
let suggestion = bestMatch(for: argumentString, from: Array(optionsMap.keys))
855+
throw ArgumentParserError.unknownOption(argumentString, suggestion: suggestion)
851856
}
852857

853858
argument = optionArgument

Tests/UtilityTests/ArgumentParserTests.swift

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -120,8 +120,24 @@ class ArgumentParserTests: XCTestCase {
120120
do {
121121
_ = try parser.parse(["foo", "--bar"])
122122
XCTFail("unexpected success")
123-
} catch ArgumentParserError.unknownOption(let option) {
123+
} catch ArgumentParserError.unknownOption(let option, let suggestion) {
124124
XCTAssertEqual(option, "--bar")
125+
XCTAssertNil(suggestion)
126+
}
127+
128+
do {
129+
_ = try parser.parse(["--food"])
130+
XCTFail("unexpected success")
131+
} catch ArgumentParserError.unknownOption(let option, let suggestion) {
132+
XCTAssertEqual(option, "--food")
133+
XCTAssertEqual(suggestion, "--foo")
134+
}
135+
do {
136+
_ = try parser.parse(["--verb"])
137+
XCTFail("unexpected success")
138+
} catch ArgumentParserError.unknownOption(let option, let suggestion) {
139+
XCTAssertEqual(option, "--verb")
140+
XCTAssertNil(suggestion)
125141
}
126142

127143
do {
@@ -286,19 +302,19 @@ class ArgumentParserTests: XCTestCase {
286302

287303
do {
288304
args = try parser.parse(["--foo", "foo", "b", "--no-fly", "--branch", "bugfix"])
289-
} catch ArgumentParserError.unknownOption(let arg) {
305+
} catch ArgumentParserError.unknownOption(let arg, _) {
290306
XCTAssertEqual(arg, "--branch")
291307
}
292308

293309
do {
294310
args = try parser.parse(["--foo", "foo", "a", "--branch", "bugfix", "--no-fly"])
295-
} catch ArgumentParserError.unknownOption(let arg) {
311+
} catch ArgumentParserError.unknownOption(let arg, _) {
296312
XCTAssertEqual(arg, "--no-fly")
297313
}
298314

299315
do {
300316
args = try parser.parse(["a", "--branch", "bugfix", "--foo"])
301-
} catch ArgumentParserError.unknownOption(let arg) {
317+
} catch ArgumentParserError.unknownOption(let arg, _) {
302318
XCTAssertEqual(arg, "--foo")
303319
}
304320

@@ -374,7 +390,7 @@ class ArgumentParserTests: XCTestCase {
374390

375391
do {
376392
args = try parser.parse(["foo", "bar", "--no-fly"])
377-
} catch ArgumentParserError.unknownOption(let arg) {
393+
} catch ArgumentParserError.unknownOption(let arg, _) {
378394
XCTAssertEqual(arg, "--no-fly")
379395
}
380396
}
@@ -717,7 +733,7 @@ class ArgumentParserTests: XCTestCase {
717733
do {
718734
_ = try parser.parse(["-18"])
719735
XCTFail("unexpected success")
720-
} catch ArgumentParserError.unknownOption(let option) {
736+
} catch ArgumentParserError.unknownOption(let option, _) {
721737
XCTAssertEqual(option, "-18")
722738
}
723739

0 commit comments

Comments
 (0)