Skip to content

Commit

Permalink
feat(error_templates/java): implement edge case for expected error
Browse files Browse the repository at this point in the history
  • Loading branch information
nedpals committed Mar 13, 2024
1 parent 85e3035 commit 511eacb
Show file tree
Hide file tree
Showing 4 changed files with 225 additions and 15 deletions.
113 changes: 98 additions & 15 deletions error_templates/java/identifier_expected_error.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(`<identifier> expected`),
Pattern: comptimeErrorPattern(`(?P<reason>\<identifier\>|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,
Expand All @@ -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)
Expand All @@ -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(),
})
})
}
},
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
public clas Main {
public static void main(String args[]) {
String text = null;
}
}
Original file line number Diff line number Diff line change
@@ -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;
```
89 changes: 89 additions & 0 deletions utils/levenshtein/levenshtein.go
Original file line number Diff line number Diff line change
@@ -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
}

0 comments on commit 511eacb

Please sign in to comment.