diff --git a/server/events/comment_parser.go b/server/events/comment_parser.go index 71bda2e52c..a822267955 100644 --- a/server/events/comment_parser.go +++ b/server/events/comment_parser.go @@ -149,7 +149,12 @@ func (e *CommentParser) Parse(rawComment string, vcsHost models.VCSHostType) Com // Helpfully warn the user if they're using "terraform" instead of "atlantis" if args[0] == "terraform" && e.ExecutableName != "terraform" { - return CommentParseResult{CommentResponse: fmt.Sprintf(DidYouMeanAtlantisComment, e.ExecutableName)} + return CommentParseResult{CommentResponse: fmt.Sprintf(DidYouMeanAtlantisComment, e.ExecutableName, "terraform")} + } + + // Helpfully warn the user that the command might be misspelled + if utils.IsSimilarWord(args[0], e.ExecutableName) { + return CommentParseResult{CommentResponse: fmt.Sprintf(DidYouMeanAtlantisComment, e.ExecutableName, args[0])} } // Atlantis can be invoked using the name of the VCS host user we're @@ -541,8 +546,8 @@ Use "{{ .ExecutableName }} [command] --help" for more information about a comman "\n```" // DidYouMeanAtlantisComment is the comment we add to the pull request when -// someone runs a command with terraform instead of atlantis. -var DidYouMeanAtlantisComment = "Did you mean to use `%s` instead of `terraform`?" +// someone runs a misspelled command or terraform instead of atlantis. +var DidYouMeanAtlantisComment = "Did you mean to use `%s` instead of `%s`?" // UnlockUsage is the comment we add to the pull request when someone runs // `atlantis unlock` with flags. diff --git a/server/events/comment_parser_test.go b/server/events/comment_parser_test.go index e5974c88f5..4583920999 100644 --- a/server/events/comment_parser_test.go +++ b/server/events/comment_parser_test.go @@ -251,7 +251,7 @@ func TestParse_DidYouMeanAtlantis(t *testing.T) { } for _, c := range comments { r := commentParser.Parse(c, models.Github) - Assert(t, r.CommentResponse == fmt.Sprintf(events.DidYouMeanAtlantisComment, "atlantis"), + Assert(t, r.CommentResponse == fmt.Sprintf(events.DidYouMeanAtlantisComment, "atlantis", "terraform"), "For comment %q expected CommentResponse==%q but got %q", c, events.DidYouMeanAtlantisComment, r.CommentResponse) } } diff --git a/server/utils/spellcheck.go b/server/utils/spellcheck.go new file mode 100644 index 0000000000..f3530a08ae --- /dev/null +++ b/server/utils/spellcheck.go @@ -0,0 +1,17 @@ +package utils + +import ( + "github.com/agext/levenshtein" +) + +// IsSimilarWord calculates "The Levenshtein Distance" between two strings which +// represents the minimum total cost of edits that would convert the first string +// into the second. If the distance is less than 3, the word is considered misspelled. +func IsSimilarWord(given string, suggestion string) bool { + dist := levenshtein.Distance(given, suggestion, nil) + if dist > 0 && dist < 3 { + return true + } + + return false +} diff --git a/server/utils/spellcheck_test.go b/server/utils/spellcheck_test.go new file mode 100644 index 0000000000..de7c9e24b7 --- /dev/null +++ b/server/utils/spellcheck_test.go @@ -0,0 +1,65 @@ +package utils_test + +import ( + "fmt" + "testing" + + "github.com/runatlantis/atlantis/server/utils" + . "github.com/runatlantis/atlantis/testing" +) + +func Test_IsSimilarWord(t *testing.T) { + t.Log("check if given executable name is misspelled or just an unrelated word") + + spellings := []struct { + Misspelled bool + Given string + Want string + }{ + { + false, + "atlantis", + "atlantis", + }, + { + false, + "maybe", + "atlantis", + }, + { + false, + "atlantis-qa", + "atlantis-prod", + }, + { + true, + "altantis", + "atlantis", + }, + { + true, + "atlants", + "atlantis", + }, + { + true, + "teraform", + "terraform", + }, + } + + for _, s := range spellings { + t.Run(fmt.Sprintf("given %s want %s", s.Given, s.Want), func(t *testing.T) { + isMisspelled := utils.IsSimilarWord(s.Given, s.Want) + + if s.Misspelled { + Equals(t, isMisspelled, true) + } + + if !s.Misspelled { + Equals(t, isMisspelled, false) + } + }) + } + +}