feat(cli): prompt for misspelled parameter names (#10350)

* feat(cli): add cliutil/levenshtein package
* feat(cli): attempt to catch misspelled parameter names
This commit is contained in:
Cian Johnston
2023-11-06 13:44:39 +00:00
committed by GitHub
parent bb5acb0332
commit 4277ca02e5
4 changed files with 338 additions and 3 deletions
+99
View File
@@ -0,0 +1,99 @@
package levenshtein
import (
"golang.org/x/exp/constraints"
"golang.org/x/xerrors"
)
// Matches returns the closest matches to the needle from the haystack.
// The maxDistance parameter is the maximum Matches distance to consider.
// If no matches are found, an empty slice is returned.
func Matches(needle string, maxDistance int, haystack ...string) (matches []string) {
for _, hay := range haystack {
if d, err := Distance(needle, hay, maxDistance); err == nil && d <= maxDistance {
matches = append(matches, hay)
}
}
return matches
}
var ErrMaxDist = xerrors.New("levenshtein: maxDist exceeded")
// Distance returns the edit distance between a and b using the
// Wagner-Fischer algorithm.
// A and B must be less than 255 characters long.
// maxDist is the maximum distance to consider.
// A value of -1 for maxDist means no maximum.
func Distance(a, b string, maxDist int) (int, error) {
if len(a) > 255 {
return 0, xerrors.Errorf("levenshtein: a must be less than 255 characters long")
}
if len(b) > 255 {
return 0, xerrors.Errorf("levenshtein: b must be less than 255 characters long")
}
m := uint8(len(a))
n := uint8(len(b))
// Special cases for empty strings
if m == 0 {
return int(n), nil
}
if n == 0 {
return int(m), nil
}
// Allocate a matrix of size m+1 * n+1
d := make([][]uint8, 0)
var i, j uint8
for i = 0; i < m+1; i++ {
di := make([]uint8, n+1)
d = append(d, di)
}
// Source prefixes
for i = 1; i < m+1; i++ {
d[i][0] = i
}
// Target prefixes
for j = 1; j < n; j++ {
d[0][j] = j // nolint:gosec // this cannot overflow
}
// Compute the distance
for j = 0; j < n; j++ {
for i = 0; i < m; i++ {
var subCost uint8
// Equal
if a[i] != b[j] {
subCost = 1
}
// Don't forget: matrix is +1 size
d[i+1][j+1] = min(
d[i][j+1]+1, // deletion
d[i+1][j]+1, // insertion
d[i][j]+subCost, // substitution
)
// check maxDist on the diagonal
if maxDist > -1 && i == j && d[i+1][j+1] > uint8(maxDist) {
return int(d[i+1][j+1]), ErrMaxDist
}
}
}
return int(d[m][n]), nil
}
func min[T constraints.Ordered](ts ...T) T {
if len(ts) == 0 {
panic("min: no arguments")
}
m := ts[0]
for _, t := range ts[1:] {
if t < m {
m = t
}
}
return m
}
+194
View File
@@ -0,0 +1,194 @@
package levenshtein_test
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/coder/coder/v2/cli/cliutil/levenshtein"
)
func Test_Levenshtein_Matches(t *testing.T) {
t.Parallel()
for _, tt := range []struct {
Name string
Needle string
MaxDistance int
Haystack []string
Expected []string
}{
{
Name: "empty",
Needle: "",
MaxDistance: 0,
Haystack: []string{},
Expected: []string{},
},
{
Name: "empty haystack",
Needle: "foo",
MaxDistance: 0,
Haystack: []string{},
Expected: []string{},
},
{
Name: "empty needle",
Needle: "",
MaxDistance: 0,
Haystack: []string{"foo"},
Expected: []string{},
},
{
Name: "exact match distance 0",
Needle: "foo",
MaxDistance: 0,
Haystack: []string{"foo", "fob"},
Expected: []string{"foo"},
},
{
Name: "exact match distance 1",
Needle: "foo",
MaxDistance: 1,
Haystack: []string{"foo", "bar"},
Expected: []string{"foo"},
},
{
Name: "not found",
Needle: "foo",
MaxDistance: 1,
Haystack: []string{"bar"},
Expected: []string{},
},
{
Name: "1 deletion",
Needle: "foo",
MaxDistance: 1,
Haystack: []string{"bar", "fo"},
Expected: []string{"fo"},
},
{
Name: "one deletion, two matches",
Needle: "foo",
MaxDistance: 1,
Haystack: []string{"bar", "fo", "fou"},
Expected: []string{"fo", "fou"},
},
{
Name: "one deletion, one addition",
Needle: "foo",
MaxDistance: 1,
Haystack: []string{"bar", "fo", "fou", "f"},
Expected: []string{"fo", "fou"},
},
{
Name: "distance 2",
Needle: "foo",
MaxDistance: 2,
Haystack: []string{"bar", "boo", "boof"},
Expected: []string{"boo", "boof"},
},
{
Name: "longer input",
Needle: "kuberenetes",
MaxDistance: 5,
Haystack: []string{"kubernetes", "kubeconfig", "kubectl", "kube"},
Expected: []string{"kubernetes"},
},
} {
tt := tt
t.Run(tt.Name, func(t *testing.T) {
t.Parallel()
actual := levenshtein.Matches(tt.Needle, tt.MaxDistance, tt.Haystack...)
require.ElementsMatch(t, tt.Expected, actual)
})
}
}
func Test_Levenshtein_Distance(t *testing.T) {
t.Parallel()
for _, tt := range []struct {
Name string
A string
B string
MaxDist int
Expected int
Error string
}{
{
Name: "empty",
A: "",
B: "",
MaxDist: -1,
Expected: 0,
},
{
Name: "a empty",
A: "",
B: "foo",
MaxDist: -1,
Expected: 3,
},
{
Name: "b empty",
A: "foo",
B: "",
MaxDist: -1,
Expected: 3,
},
{
Name: "a is b",
A: "foo",
B: "foo",
MaxDist: -1,
Expected: 0,
},
{
Name: "one addition",
A: "foo",
B: "fooo",
MaxDist: -1,
Expected: 1,
},
{
Name: "one deletion",
A: "fooo",
B: "foo",
MaxDist: -1,
Expected: 1,
},
{
Name: "one substitution",
A: "foo",
B: "fou",
MaxDist: -1,
Expected: 1,
},
{
Name: "different strings entirely",
A: "foo",
B: "bar",
MaxDist: -1,
Expected: 3,
},
{
Name: "different strings, max distance 2",
A: "foo",
B: "bar",
MaxDist: 2,
Error: levenshtein.ErrMaxDist.Error(),
},
} {
tt := tt
t.Run(tt.Name, func(t *testing.T) {
t.Parallel()
actual, err := levenshtein.Distance(tt.A, tt.B, tt.MaxDist)
if tt.Error == "" {
require.NoError(t, err)
require.Equal(t, tt.Expected, actual)
} else {
require.EqualError(t, err, tt.Error)
}
})
}
}