diff --git a/error_templates/java/identifier_expected_error.go b/error_templates/java/identifier_expected_error.go index 7bbfbec..d602e70 100644 --- a/error_templates/java/identifier_expected_error.go +++ b/error_templates/java/identifier_expected_error.go @@ -2,31 +2,108 @@ package java import ( "context" + "strings" lib "github.com/nedpals/errgoengine" + "github.com/nedpals/errgoengine/utils/levenshtein" sitter "github.com/smacker/go-tree-sitter" ) +type identifiedExpectedReasonKind int + +const ( + identifierExpectedReasonUnknown identifiedExpectedReasonKind = 0 + identifierExpectedReasonClassInterfaceEnum identifiedExpectedReasonKind = iota +) + type identifierExpectedFixKind int const ( identifierExpectedFixUnknown identifierExpectedFixKind = 0 identifierExpectedFixWrapFunction identifierExpectedFixKind = iota + identifierExpectedCorrectTypo identifierExpectedFixKind = iota ) type identifierExpectedErrorCtx struct { - fixKind identifierExpectedFixKind + reasonKind identifiedExpectedReasonKind + typoWord string // for identifierExpectedCorrectTypo. the word that is a typo + wordForTypo string // for identifierExpectedCorrectTypo. the closest word to replace the typo + fixKind identifierExpectedFixKind } var IdentifierExpectedError = lib.ErrorTemplate{ Name: "IdentifierExpectedError", - Pattern: comptimeErrorPattern(` expected`), + Pattern: comptimeErrorPattern(`(?P\|class, interface, or enum) expected`), StackTracePattern: comptimeStackTracePattern, OnAnalyzeErrorFn: func(cd *lib.ContextData, m *lib.MainError) { iCtx := identifierExpectedErrorCtx{} + // identify the reason + switch cd.Variables["reason"] { + case "class, interface, or enum": + iCtx.reasonKind = identifierExpectedReasonClassInterfaceEnum + default: + iCtx.reasonKind = identifierExpectedReasonUnknown + } + // TODO: check if node is parsable - if tree, err := sitter.ParseCtx( + if iCtx.reasonKind == identifierExpectedReasonClassInterfaceEnum { + // use levenstein distance to check if the word is a typo + tokens := []string{"class", "interface", "enum"} + + // get the nearest word + nearestWord := "" + wordToReplace := "" + + // get the contents of that line + line := m.Document.LineAt(m.Nearest.StartPosition().Line) + lineTokens := strings.Split(line, " ") + + // position + nearestCol := 0 + + for _, token := range tokens { + for ltIdx, lineToken := range lineTokens { + if levenshtein.ComputeDistance(token, lineToken) <= 3 { + wordToReplace = lineToken + nearestWord = token + + // compute the position of the word + for i := 0; i < ltIdx; i++ { + nearestCol += len(lineTokens[i]) + 1 + } + + // add 1 to nearestCol to get the portion of the word + nearestCol++ + break + } + } + } + + if nearestWord != "" { + iCtx.wordForTypo = nearestWord + iCtx.typoWord = wordToReplace + iCtx.fixKind = identifierExpectedCorrectTypo + + targetPos := lib.Position{ + Line: m.Nearest.StartPosition().Line, + Column: nearestCol, + } + + // get the nearest node of the word from the position + initialNearest := m.Document.RootNode().NamedDescendantForPointRange(lib.Location{ + StartPos: targetPos, + EndPos: targetPos, + }) + + rawNearestNode := nearestNodeFromPos2(initialNearest.TreeCursor(), targetPos) + if rawNearestNode != nil { + m.Nearest = lib.WrapNode(m.Document, rawNearestNode) + } else { + m.Nearest = initialNearest + } + } + } else if tree, err := sitter.ParseCtx( context.Background(), []byte(m.Nearest.Text()), m.Document.Language.SitterLanguage, @@ -37,18 +114,15 @@ var IdentifierExpectedError = lib.ErrorTemplate{ m.Context = iCtx }, OnGenExplainFn: func(cd *lib.ContextData, gen *lib.ExplainGenerator) { - gen.Add("This error occurs when an identifier is expected, but an expression is found in a location where a statement or declaration is expected.") - - // ctx := cd.MainError.Context.(IdentifierExpectedErrorCtx) - - // switch ctx.kind { - // case cannotBeAppliedMismatchedArgCount: - // gen.Add("This error occurs when there is an attempt to apply a method with an incorrect number of arguments.") - // case cannotBeAppliedMismatchedArgType: - // gen.Add("This error occurs when there is an attempt to apply a method with arguments that do not match the method signature.") - // default: - // gen.Add("unable to determine.") - // } + iCtx := cd.MainError.Context.(identifierExpectedErrorCtx) + + switch iCtx.reasonKind { + case identifierExpectedReasonClassInterfaceEnum: + gen.Add("This error occurs when there's a typo or the keyword `class`, `interface`, or `enum` is missing.") + default: + gen.Add("This error occurs when an identifier is expected, but an expression is found in a location where a statement or declaration is expected.") + } + }, OnGenBugFixFn: func(cd *lib.ContextData, gen *lib.BugFixGenerator) { ctx := cd.MainError.Context.(identifierExpectedErrorCtx) @@ -75,6 +149,15 @@ var IdentifierExpectedError = lib.ErrorTemplate{ EndPosition: cd.MainError.Nearest.EndPosition(), }) }) + case identifierExpectedCorrectTypo: + gen.Add("Correct the typo", func(s *lib.BugFixSuggestion) { + s.AddStep("Change `%s` to `%s` to properly declare the %s.", ctx.typoWord, ctx.wordForTypo, ctx.wordForTypo). + AddFix(lib.FixSuggestion{ + NewText: ctx.wordForTypo, + StartPosition: cd.MainError.Nearest.StartPosition(), + EndPosition: cd.MainError.Nearest.EndPosition(), + }) + }) } }, } diff --git a/error_templates/java/test_files/identifier_expected_error_complex_2/Main.java b/error_templates/java/test_files/identifier_expected_error_complex_2/Main.java new file mode 100644 index 0000000..f7d2380 --- /dev/null +++ b/error_templates/java/test_files/identifier_expected_error_complex_2/Main.java @@ -0,0 +1,5 @@ +public clas Main { + public static void main(String args[]) { + String text = null; + } +} diff --git a/error_templates/java/test_files/identifier_expected_error_complex_2/test.txt b/error_templates/java/test_files/identifier_expected_error_complex_2/test.txt new file mode 100644 index 0000000..33d09f7 --- /dev/null +++ b/error_templates/java/test_files/identifier_expected_error_complex_2/test.txt @@ -0,0 +1,33 @@ +name: "Complex2" +template: "Java.IdentifierExpectedError" +--- +Main.java:1: error: class, interface, or enum expected +public clas Main { + ^ +Main.java:2: error: class, interface, or enum expected + public static void main(String args[]) { + ^ +Main.java:4: error: class, interface, or enum expected + } + ^ +3 errors +=== +template: "Java.IdentifierExpectedError" +--- +# IdentifierExpectedError +This error occurs when there's a typo or the keyword `class`, `interface`, or `enum` is missing. +``` +public clas Main { + ^^^^ + public static void main(String args[]) { + String text = null; +``` +## Steps to fix +### Correct the typo +Change `clas` to `class` to properly declare the class. +```diff +- public clas Main { ++ public class Main { + public static void main(String args[]) { + String text = null; +``` diff --git a/utils/levenshtein/levenshtein.go b/utils/levenshtein/levenshtein.go new file mode 100644 index 0000000..d495617 --- /dev/null +++ b/utils/levenshtein/levenshtein.go @@ -0,0 +1,89 @@ +// Package levenshtein is a Go implementation to calculate Levenshtein Distance. +// +// Implementation taken from: https://github.com/agnivade/levenshtein/blob/master/levenshtein.go +// and originally from: https://gist.github.com/andrei-m/982927#gistcomment-1931258 +package levenshtein + +import "unicode/utf8" + +// minLengthThreshold is the length of the string beyond which +// an allocation will be made. Strings smaller than this will be +// zero alloc. +const minLengthThreshold = 32 + +// ComputeDistance computes the levenshtein distance between the two +// strings passed as an argument. The return value is the levenshtein distance +// +// Works on runes (Unicode code points) but does not normalize +// the input strings. See https://blog.golang.org/normalization +// and the golang.org/x/text/unicode/norm package. +func ComputeDistance(a, b string) int { + if len(a) == 0 { + return utf8.RuneCountInString(b) + } + + if len(b) == 0 { + return utf8.RuneCountInString(a) + } + + if a == b { + return 0 + } + + // We need to convert to []rune if the strings are non-ASCII. + // This could be avoided by using utf8.RuneCountInString + // and then doing some juggling with rune indices, + // but leads to far more bounds checks. It is a reasonable trade-off. + s1 := []rune(a) + s2 := []rune(b) + + // swap to save some memory O(min(a,b)) instead of O(a) + if len(s1) > len(s2) { + s1, s2 = s2, s1 + } + lenS1 := len(s1) + lenS2 := len(s2) + + // Init the row. + var x []uint16 + if lenS1+1 > minLengthThreshold { + x = make([]uint16, lenS1+1) + } else { + // We make a small optimization here for small strings. + // Because a slice of constant length is effectively an array, + // it does not allocate. So we can re-slice it to the right length + // as long as it is below a desired threshold. + x = make([]uint16, minLengthThreshold) + x = x[:lenS1+1] + } + + // we start from 1 because index 0 is already 0. + for i := 1; i < len(x); i++ { + x[i] = uint16(i) + } + + // make a dummy bounds check to prevent the 2 bounds check down below. + // The one inside the loop is particularly costly. + _ = x[lenS1] + // fill in the rest + for i := 1; i <= lenS2; i++ { + prev := uint16(i) + for j := 1; j <= lenS1; j++ { + current := x[j-1] // match + if s2[i-1] != s1[j-1] { + current = min(min(x[j-1]+1, prev+1), x[j]+1) + } + x[j-1] = prev + prev = current + } + x[lenS1] = prev + } + return int(x[lenS1]) +} + +func min(a, b uint16) uint16 { + if a < b { + return a + } + return b +}