diff --git a/errgoengine.go b/errgoengine.go index 7ced40b..80c5f6c 100644 --- a/errgoengine.go +++ b/errgoengine.go @@ -13,6 +13,7 @@ type ErrgoEngine struct { ErrorTemplates ErrorTemplates FS *MultiReadFileFS OutputGen *OutputGenerator + IsTesting bool } func New() *ErrgoEngine { @@ -56,6 +57,7 @@ func (e *ErrgoEngine) Analyze(workingPath, msg string) (*CompiledErrorTemplate, contextData := NewContextData(e.SharedStore, workingPath) contextData.Analyzer = template.Language.AnalyzerFactory(contextData) contextData.AddVariable("message", msg) + contextData.FS = e.FS // extract variables from the error message contextData.AddVariables(template.ExtractVariables(msg)) @@ -125,19 +127,64 @@ func (e *ErrgoEngine) Translate(template *CompiledErrorTemplate, contextData *Co template.OnGenBugFixFn(contextData, fixGen) } - output := e.OutputGen.Generate(contextData, expGen, fixGen) + if e.IsTesting { + // add a code snippet that points to the error + e.OutputGen.GenAfterExplain = func(gen *OutputGenerator) { + err := contextData.MainError + if err == nil { + return + } + + doc := err.Document + if doc == nil || err.Nearest.IsNull() { + return + } + + startLineNr := err.Nearest.StartPosition().Line + startLines := doc.LinesAt(max(startLineNr-1, 0), startLineNr) + endLines := doc.LinesAt(min(startLineNr+1, doc.TotalLines()), min(startLineNr+2, doc.TotalLines())) + arrowLength := int(err.Nearest.EndByte() - err.Nearest.StartByte()) + if arrowLength == 0 { + arrowLength = 1 + } + + startArrowPos := err.Nearest.StartPosition().Column + gen.Writeln("```") + gen.WriteLines(startLines...) + + for i := 0; i < startArrowPos; i++ { + if startLines[len(startLines)-1][i] == '\t' { + gen.Builder.WriteString(" ") + } else { + gen.Builder.WriteByte(' ') + } + } + + for i := 0; i < arrowLength; i++ { + gen.Builder.WriteByte('^') + } + + gen.Break() + gen.WriteLines(endLines...) + gen.Writeln("```") + } + } + + output := e.OutputGen.Generate(expGen, fixGen) defer e.OutputGen.Reset() return expGen.Builder.String(), output } -func ParseFromStackTrace(contextData *ContextData, defaultLanguage *Language, files fs.ReadFileFS) error { +func ParseFiles(contextData *ContextData, defaultLanguage *Language, files fs.ReadFileFS, fileNames []string) error { + if files == nil { + return fmt.Errorf("files is nil") + } + parser := sitter.NewParser() analyzer := &SymbolAnalyzer{ContextData: contextData} - for _, node := range contextData.TraceStack { - path := node.DocumentPath - + for _, path := range fileNames { contents, err := files.ReadFile(path) if err != nil { // return err @@ -153,6 +200,11 @@ func ParseFromStackTrace(contextData *ContextData, defaultLanguage *Language, fi // check if document already exists existingDoc, docExists := contextData.Documents[path] + if docExists && existingDoc.BytesContentEquals(contents) { + // do not parse if content is the same + continue + } + // check matched languages selectedLanguage := defaultLanguage if docExists { @@ -183,3 +235,36 @@ func ParseFromStackTrace(contextData *ContextData, defaultLanguage *Language, fi return nil } + +func ParseFromStackTrace(contextData *ContextData, defaultLanguage *Language, files fs.ReadFileFS) error { + filesToParse := []string{} + + for _, node := range contextData.TraceStack { + path := node.DocumentPath + if path == "" { + continue + } + + // check if file is already parsed + if _, ok := contextData.Documents[path]; ok { + continue + } + + // check if file is already in the list + found := false + for _, f := range filesToParse { + if f == path { + found = true + break + } + } + + if found { + continue + } + + filesToParse = append(filesToParse, path) + } + + return ParseFiles(contextData, defaultLanguage, files, filesToParse) +} diff --git a/error_template.go b/error_template.go index 880e02b..4b69cce 100644 --- a/error_template.go +++ b/error_template.go @@ -7,15 +7,13 @@ import ( "strings" ) -type GenAnalyzeErrorFn func(cd *ContextData, m *MainError) - type ErrorTemplate struct { Name string Pattern string StackTracePattern string - OnAnalyzeErrorFn GenAnalyzeErrorFn - OnGenExplainFn GenExplainFn - OnGenBugFixFn GenBugFixFn + OnAnalyzeErrorFn func(cd *ContextData, m *MainError) + OnGenExplainFn func(cd *ContextData, gen *ExplainGenerator) + OnGenBugFixFn func(cd *ContextData, gen *BugFixGenerator) } func CustomErrorPattern(pattern string) string { diff --git a/error_templates/java/identifier_expected_error.go b/error_templates/java/identifier_expected_error.go index 7bbfbec..33fcab6 100644 --- a/error_templates/java/identifier_expected_error.go +++ b/error_templates/java/identifier_expected_error.go @@ -2,31 +2,119 @@ package java import ( "context" + "strings" lib "github.com/nedpals/errgoengine" + "github.com/nedpals/errgoengine/utils/levenshtein" + "github.com/nedpals/errgoengine/utils/slice" sitter "github.com/smacker/go-tree-sitter" ) +type identifiedExpectedReasonKind int + +const ( + identifierExpectedReasonUnknown identifiedExpectedReasonKind = 0 + identifierExpectedReasonClassInterfaceEnum identifiedExpectedReasonKind = iota + identifierExpectedReasonTypo 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 { + accessTokens := []string{"public"} + statementTokens := []string{"class", "interface", "enum"} + + // use levenstein distance to check if the word is a typo + tokens := append(accessTokens, statementTokens...) + + // 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 { + distance := levenshtein.ComputeDistance(token, lineToken) + if distance >= 1 && distance <= 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 + } + + // if nearestword is not a statement token, then it's a typo + if !slice.ContainsString(statementTokens, nearestWord) { + iCtx.reasonKind = identifierExpectedReasonTypo + } + } + } else if tree, err := sitter.ParseCtx( context.Background(), []byte(m.Nearest.Text()), m.Document.Language.SitterLanguage, @@ -37,18 +125,17 @@ 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.") + case identifierExpectedReasonTypo: + gen.Add("This error indicates there's a typo or misspelled word in your code.") + 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 +162,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`.", ctx.typoWord, ctx.wordForTypo). + AddFix(lib.FixSuggestion{ + NewText: ctx.wordForTypo, + StartPosition: cd.MainError.Nearest.StartPosition(), + EndPosition: cd.MainError.Nearest.EndPosition(), + }) + }) } }, } diff --git a/error_templates/java/java.go b/error_templates/java/java.go index 74a6d54..7cefec4 100644 --- a/error_templates/java/java.go +++ b/error_templates/java/java.go @@ -184,19 +184,18 @@ func getSpaceBoundaryIndiv(line string, idx int, defaultDirection spaceComputeDi } if defaultDirection == spaceComputeDirectionLeft { + // if idx is... + // 1. greater than 0 + // 2. not a space character (if stopOnSpace is false) + // 3. or a space character (if stopOnSpace is true) + // then return the current index if idx-1 >= 0 && ((!stopOnSpace && !isSpace(line, idx-1)) || (stopOnSpace && isSpace(line, idx-1))) { return idx } + // if idx is less than 0, return the current index if idx-1 < 0 { - // check if the current index is not a space - if !isSpace(line, idx) { - // go to the reverse direction to get the nearest space - newIdx := getSpaceBoundaryIndiv(line, idx, spaceComputeDirectionRight) - return newIdx - } - return idx } diff --git a/error_templates/java/symbol_not_found_error.go b/error_templates/java/symbol_not_found_error.go index b624f5a..c8827d3 100644 --- a/error_templates/java/symbol_not_found_error.go +++ b/error_templates/java/symbol_not_found_error.go @@ -8,25 +8,32 @@ import ( ) type symbolNotFoundErrorCtx struct { - symbolType string - symbolName string - locationClass string - locationNode lib.SyntaxNode - rootNode lib.SyntaxNode - parentNode lib.SyntaxNode + symbolType string + symbolName string + locationClass string + locationVariable string + locationVarType string + variableType lib.Symbol // only if the locationVariable is present + classLocation lib.Location + locationNode lib.SyntaxNode + rootNode lib.SyntaxNode + parentNode lib.SyntaxNode } var SymbolNotFoundError = lib.ErrorTemplate{ Name: "SymbolNotFoundError", - Pattern: comptimeErrorPattern("cannot find symbol", `symbol:\s+(?Pvariable|method|class) (?P\S+)\s+location\:\s+class (?P\S+)`), + Pattern: comptimeErrorPattern("cannot find symbol", `symbol:\s+(?Pvariable|method|class) (?P\S+)\s+location\:\s+(?:(?:class (?P\S+))|(?:variable (?P\S+) of type (?P\S+)))`), StackTracePattern: comptimeStackTracePattern, OnAnalyzeErrorFn: func(cd *lib.ContextData, m *lib.MainError) { symbolName := cd.Variables["symbolName"] errorCtx := symbolNotFoundErrorCtx{ - symbolType: cd.Variables["symbolType"], - locationClass: cd.Variables["locationClass"], - symbolName: symbolName, - rootNode: m.Nearest, + symbolType: cd.Variables["symbolType"], + locationClass: cd.Variables["locationClass"], + locationVariable: cd.Variables["locationVariable"], + locationVarType: cd.Variables["locationVarType"], + symbolName: symbolName, + variableType: lib.UnresolvedSymbol, + rootNode: m.Nearest, } nodeTypeToFind := "identifier" @@ -43,11 +50,73 @@ var SymbolNotFoundError = lib.ErrorTemplate{ } // locate the location node - rootNode := m.Nearest.Doc.RootNode() - for q := rootNode.Query(`(class_declaration name: (identifier) @class-name (#eq? @class-name "%s"))`, errorCtx.locationClass); q.Next(); { - node := q.CurrentNode().Parent() - errorCtx.locationNode = node - break + if len(errorCtx.locationClass) > 0 { + rootNode := m.Nearest.Doc.RootNode() + for q := rootNode.Query(`(class_declaration name: (identifier) @class-name (#eq? @class-name "%s"))`, errorCtx.locationClass); q.Next(); { + node := q.CurrentNode().Parent() + errorCtx.locationNode = node + break + } + } else if len(errorCtx.locationVariable) > 0 && len(errorCtx.locationVarType) > 0 { + rootNode := m.Nearest.Doc.RootNode() + for q := rootNode.Query(` + (local_variable_declaration + type: (_) @var-type + declarator: (variable_declarator + name: (identifier) @var-name + (#eq? @var-name "%s")) + (#eq? @var-type "%s"))`, + errorCtx.locationVariable, + errorCtx.locationVarType); q.Next(); q.Next() { + node := q.CurrentNode().Parent() + errorCtx.locationNode = node + break + } + } else if len(errorCtx.locationVariable) > 0 { + rootNode := m.Nearest.Doc.RootNode() + for q := rootNode.Query(`(variable_declarator name: (identifier) @var-name (#eq? @var-name "%s"))`, errorCtx.locationVariable); q.Next(); { + node := q.CurrentNode().Parent() + errorCtx.locationNode = node + break + } + + // get type of the variable + declNode := errorCtx.locationNode.Parent() // assumes that this is local_variable_declaration + typeNode := declNode.ChildByFieldName("type") + + errorCtx.locationVarType = typeNode.Text() + } + + if len(errorCtx.locationVarType) != 0 { + errorCtx.locationClass = errorCtx.locationVarType + foundVariableType := cd.FindSymbol(errorCtx.locationVarType, -1) + if foundVariableType != nil { + errorCtx.variableType = foundVariableType + + // cast to top level symbol + if topLevelSym, ok := foundVariableType.(*lib.TopLevelSymbol); ok { + errorCtx.classLocation = topLevelSym.Location() + } + } else { + errorCtx.variableType = lib.UnresolvedSymbol + } + } + + if !errorCtx.locationNode.IsNull() && errorCtx.locationNode.Type() != "class_declaration" { + if errorCtx.classLocation.DocumentPath != "" { + // go to the class declaration of that specific class + doc := cd.Store.Documents[errorCtx.classLocation.DocumentPath] + foundNode := doc.RootNode().NamedDescendantForPointRange(errorCtx.classLocation) + + if !foundNode.IsNull() { + errorCtx.locationNode = foundNode + } + } else { + // go up to the class declaration + for !errorCtx.locationNode.IsNull() && errorCtx.locationNode.Type() != "class_declaration" { + errorCtx.locationNode = errorCtx.locationNode.Parent() + } + } } m.Context = errorCtx @@ -56,7 +125,7 @@ var SymbolNotFoundError = lib.ErrorTemplate{ ctx := cd.MainError.Context.(symbolNotFoundErrorCtx) switch ctx.symbolType { case "variable": - gen.Add(`The program cannot find variable "%s"`, ctx.symbolName) + gen.Add(`The error indicates that the compiler cannot find variable "%s"`, ctx.symbolName) case "method": gen.Add("The error indicates that the compiler cannot find the method `%s` in the `%s` class.", ctx.symbolName, ctx.locationClass) case "class": @@ -80,6 +149,11 @@ var SymbolNotFoundError = lib.ErrorTemplate{ }) }) case "method": + // change the doc to use for defining the missing method + if ctx.classLocation.DocumentPath != "" { + gen.Document = cd.Store.Documents[ctx.classLocation.DocumentPath] + } + gen.Add("Define the missing method.", func(s *lib.BugFixSuggestion) { bodyNode := ctx.locationNode.ChildByFieldName("body") lastMethodNode := bodyNode.LastNamedChild() @@ -91,7 +165,12 @@ var SymbolNotFoundError = lib.ErrorTemplate{ } // TODO: smartly infer the method signature for the missing method - s.AddStep("Add the missing method `%s` to the `%s` class", methodName, ctx.locationClass). + prefix := "Add" + if ctx.classLocation.DocumentPath != "" { + prefix = fmt.Sprintf("In `%s`, add", ctx.classLocation.DocumentPath) + } + + s.AddStep("%s the missing method `%s` to the `%s` class", prefix, methodName, ctx.locationClass). AddFix(lib.FixSuggestion{ NewText: fmt.Sprintf("\n\n\tprivate static void %s(%s) {\n\t\t// Add code here\n\t}\n", methodName, strings.Join(parameters, ", ")), StartPosition: lastMethodNode.EndPosition().Add(lib.Position{Column: 1}), // add 1 column so that the parenthesis won't be replaced @@ -118,7 +197,14 @@ var SymbolNotFoundError = lib.ErrorTemplate{ } func parseMethodSignature(symbolName string) (methodName string, parameterTypes []string) { - methodName = symbolName[:strings.Index(symbolName, "(")] - parameterTypes = strings.Split(symbolName[strings.Index(symbolName, "(")+1:strings.Index(symbolName, ")")], ",") + openingPar := strings.Index(symbolName, "(") + closingPar := strings.Index(symbolName, ")") + + methodName = symbolName[:openingPar] + if openingPar+1 == closingPar { + return + } + + parameterTypes = strings.Split(symbolName[openingPar+1:closingPar], ",") return methodName, parameterTypes } 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..91b55f3 --- /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`. +```diff +- public clas Main { ++ public class Main { + public static void main(String args[]) { + String text = null; +``` diff --git a/error_templates/java/test_files/identifier_expected_error_public/Main.java b/error_templates/java/test_files/identifier_expected_error_public/Main.java new file mode 100644 index 0000000..1460a12 --- /dev/null +++ b/error_templates/java/test_files/identifier_expected_error_public/Main.java @@ -0,0 +1,3 @@ +publc class Main { + public static void main(String[] args) {} +} diff --git a/error_templates/java/test_files/identifier_expected_error_public/test.txt b/error_templates/java/test_files/identifier_expected_error_public/test.txt new file mode 100644 index 0000000..d97605a --- /dev/null +++ b/error_templates/java/test_files/identifier_expected_error_public/test.txt @@ -0,0 +1,27 @@ +name: "Public" +template: "Java.IdentifierExpectedError" +--- +Main.java:1: error: class, interface, or enum expected +publc class Main { +^ +1 error +=== +template: "Java.IdentifierExpectedError" +--- +# IdentifierExpectedError +This error indicates there's a typo or misspelled word in your code. +``` +publc class Main { +^^^^^ + public static void main(String[] args) {} +} +``` +## Steps to fix +### Correct the typo +Change `publc` to `public`. +```diff +- publc class Main { ++ public class Main { + public static void main(String[] args) {} +} +``` diff --git a/error_templates/java/test_files/symbol_not_found_error_missing_method_imported/Test.java b/error_templates/java/test_files/symbol_not_found_error_missing_method_imported/Test.java new file mode 100644 index 0000000..e7e2bcc --- /dev/null +++ b/error_templates/java/test_files/symbol_not_found_error_missing_method_imported/Test.java @@ -0,0 +1,6 @@ +public class Test { + public static void main(String[] args) { + Triangle tri = new Triangle(); + tri.getArea(); + } +} diff --git a/error_templates/java/test_files/symbol_not_found_error_missing_method_imported/Triangle.java b/error_templates/java/test_files/symbol_not_found_error_missing_method_imported/Triangle.java new file mode 100644 index 0000000..412143d --- /dev/null +++ b/error_templates/java/test_files/symbol_not_found_error_missing_method_imported/Triangle.java @@ -0,0 +1,5 @@ +public class Triangle { + public static int getSides() { + return 3; + } +} diff --git a/error_templates/java/test_files/symbol_not_found_error_missing_method_imported/test.txt b/error_templates/java/test_files/symbol_not_found_error_missing_method_imported/test.txt new file mode 100644 index 0000000..8bb6124 --- /dev/null +++ b/error_templates/java/test_files/symbol_not_found_error_missing_method_imported/test.txt @@ -0,0 +1,35 @@ +name: "MissingMethodImported" +template: "Java.SymbolNotFoundError" +--- +Test.java:4: error: cannot find symbol + tri.getArea(); + ^ + symbol: method getArea() + location: variable tri of type Triangle +1 error +=== +template: "Java.SymbolNotFoundError" +--- +# SymbolNotFoundError +The error indicates that the compiler cannot find the method `getArea()` in the `Triangle` class. +``` + Triangle tri = new Triangle(); + tri.getArea(); + ^^^^^^^^^^^^^^ + } +} +``` +## Steps to fix +### Define the missing method. +In `Triangle.java`, add the missing method `getArea` to the `Triangle` class. +```diff + public static int getSides() { + return 3; + } ++ ++ private static void getArea() { ++ // Add code here ++ } +} + +``` diff --git a/error_templates/java/test_files/symbol_not_found_error_missing_variable/test.txt b/error_templates/java/test_files/symbol_not_found_error_missing_variable/test.txt index 21bdf7d..a16f406 100644 --- a/error_templates/java/test_files/symbol_not_found_error_missing_variable/test.txt +++ b/error_templates/java/test_files/symbol_not_found_error_missing_variable/test.txt @@ -11,7 +11,7 @@ Program.java:3: error: cannot find symbol template: "Java.SymbolNotFoundError" --- # SymbolNotFoundError -The program cannot find variable "a" +The error indicates that the compiler cannot find variable "a" ``` public static void main(String[] args) { System.out.println(a); diff --git a/error_templates/test_utils/test_utils.go b/error_templates/test_utils/test_utils.go index 704a3b5..378891b 100644 --- a/error_templates/test_utils/test_utils.go +++ b/error_templates/test_utils/test_utils.go @@ -57,7 +57,7 @@ func SetupTest(tb testing.TB, cfg SetupTestConfig) TestCases { // load error templates engine := lib.New() - engine.OutputGen.IsTesting = true + engine.IsTesting = true cfg.TemplateLoader(&engine.ErrorTemplates) diff --git a/languages/java/language.go b/languages/java/language.go index 2de7e20..d40d740 100644 --- a/languages/java/language.go +++ b/languages/java/language.go @@ -5,6 +5,8 @@ import ( "embed" _ "embed" "fmt" + "path/filepath" + "strings" lib "github.com/nedpals/errgoengine" "github.com/smacker/go-tree-sitter/java" @@ -22,7 +24,7 @@ var Language = &lib.Language{ SitterLanguage: java.GetLanguage(), StackTracePattern: `\s+at (?P\S+)\((?P\S+):(?P\d+)\)`, AnalyzerFactory: func(cd *lib.ContextData) lib.LanguageAnalyzer { - return &javaAnalyzer{cd} + return &javaAnalyzer{cd, map[string][]string{}} }, SymbolsToCapture: symbols, ExternFS: externs, @@ -41,6 +43,7 @@ var Language = &lib.Language{ type javaAnalyzer struct { *lib.ContextData + markedAsUnresolved map[string][]string // map of file path to symbol names } func (an *javaAnalyzer) FallbackSymbol() lib.Symbol { @@ -48,8 +51,67 @@ func (an *javaAnalyzer) FallbackSymbol() lib.Symbol { } func (an *javaAnalyzer) FindSymbol(name string) lib.Symbol { - sym, _ := builtinTypesStore.FindByName(name) - return sym + return an.findSymbolWithRetries(name, 0) +} + +func (an *javaAnalyzer) findSymbolWithRetries(name string, retry int) lib.Symbol { + if retry > 3 { + // do not retry more than 3 times or it will cause stack overflow + return lib.UnresolvedSymbol + } + + sym, ok := builtinTypesStore.FindByName(name) + if ok { + return sym + } + + // check if symbol name is included in the unresolved list + if list, ok := an.markedAsUnresolved[an.ContextData.CurrentDocumentPath]; ok { + for _, n := range list { + if n == name { + // if it's in the list, return unresolved symbol + return lib.UnresolvedSymbol + } + } + } + + // maybe it's a class from another file? + dir := filepath.Dir(an.ContextData.CurrentDocumentPath) + targetPath := filepath.Join(dir, fmt.Sprintf("%s.java", strings.ReplaceAll(name, ".", "/"))) + if sym := an.Store.FindSymbol(targetPath, name, -1); sym != nil { + return sym + } + + // if not yet parsed, parse the file and mark the symbol as unresolved (if not found) + if an.MainError == nil || an.MainError.Document == nil { + return an.markAsUnresolved(name) + } + + // if error, return nil symbol + if err := lib.ParseFiles(an.ContextData, an.MainError.Document.Language, an.FS, []string{ + targetPath, + }); err != nil { + return an.markAsUnresolved(name) + } + + // if parsed, find the symbol again + return an.findSymbolWithRetries(name, retry+1) +} + +func (an *javaAnalyzer) markAsUnresolved(name string) lib.Symbol { + // add to unresolved list to avoid looping + if _, ok := an.markedAsUnresolved[an.ContextData.CurrentDocumentPath]; !ok { + an.markedAsUnresolved[an.ContextData.CurrentDocumentPath] = []string{} + } + + an.markedAsUnresolved[an.ContextData.CurrentDocumentPath] = append( + an.markedAsUnresolved[an.ContextData.CurrentDocumentPath], + name, + ) + + // nil symbol handling will be done in the next call + // can be a unresolved symbol or a builtin symbol. depends + return nil } func (an *javaAnalyzer) AnalyzeNode(ctx context.Context, n lib.SyntaxNode) lib.Symbol { diff --git a/node.go b/node.go index 102a8cd..3b8eec9 100644 --- a/node.go +++ b/node.go @@ -116,6 +116,10 @@ func (n SyntaxNode) Query(q string, d ...any) *QueryNodeCursor { return queryNode2(n, fmt.Sprintf(q, d...)) } +func (n SyntaxNode) TreeCursor() *sitter.TreeCursor { + return sitter.NewTreeCursor(n.Node) +} + func WrapNode(doc *Document, n *sitter.Node) SyntaxNode { return SyntaxNode{ isTextCached: false, diff --git a/output_gen.go b/output_gen.go index 9481e9f..66424c2 100644 --- a/output_gen.go +++ b/output_gen.go @@ -6,124 +6,104 @@ import ( ) type OutputGenerator struct { - IsTesting bool - wr *strings.Builder + GenAfterExplain func(*OutputGenerator) + Builder *strings.Builder } -func (gen *OutputGenerator) heading(level int, text string) { +func (gen *OutputGenerator) Heading(level int, text string) { // dont go below zero, dont go above 6 level = max(min(6, level), 0) for i := 0; i < level; i++ { - gen.wr.WriteByte('#') + gen.Builder.WriteByte('#') } - gen.wr.WriteByte(' ') - gen.writeln(text) + gen.Builder.WriteByte(' ') + gen.Writeln(text) } -func (gen *OutputGenerator) _break() { - gen.wr.WriteByte('\n') +func (gen *OutputGenerator) Break() { + gen.Builder.WriteByte('\n') } -func (gen *OutputGenerator) generateFromExp(level int, explain *ExplainGenerator) { +func (gen *OutputGenerator) FromExplanation(level int, explain *ExplainGenerator) { + if level == 1 && (explain.Builder == nil || explain.Builder.Len() == 0) && explain.Sections == nil { + gen.Writeln("No explanation found for this error.") + return + } + if explain.Builder != nil { - gen.write(explain.Builder.String()) + gen.Write(explain.Builder.String()) } if explain.Sections != nil { for sectionName, exp := range explain.Sections { - gen._break() - gen.heading(level+1, sectionName) - gen.generateFromExp(level+1, exp) + gen.Break() + gen.Heading(level+1, sectionName) + gen.FromExplanation(level+1, exp) } } else { - gen._break() + gen.Break() } } -func (gen *OutputGenerator) writeln(str string, d ...any) { +func (gen *OutputGenerator) Writeln(str string, d ...any) { if len(str) == 0 { return } - gen.write(str, d...) - gen._break() + gen.Write(str, d...) + gen.Break() } -func (gen *OutputGenerator) write(str string, d ...any) { +func (gen *OutputGenerator) Write(str string, d ...any) { final := fmt.Sprintf(str, d...) for _, c := range final { if c == '\t' { // 1 tab = 4 spaces - gen.wr.WriteString(" ") + gen.Builder.WriteString(" ") } else { - gen.wr.WriteRune(c) + gen.Builder.WriteRune(c) } } } -func (gen *OutputGenerator) writeLines(lines ...string) { +func (gen *OutputGenerator) WriteLines(lines ...string) { for _, line := range lines { if len(line) == 0 { - gen._break() + gen.Break() } else { - gen.writeln(line) + gen.Writeln(line) } } } -func (gen *OutputGenerator) Generate(cd *ContextData, explain *ExplainGenerator, bugFix *BugFixGenerator) string { - if gen.wr == nil { - gen.wr = &strings.Builder{} +func (gen *OutputGenerator) Generate(explain *ExplainGenerator, bugFix *BugFixGenerator) string { + if gen.Builder == nil { + gen.Builder = &strings.Builder{} } if len(explain.ErrorName) != 0 { - gen.heading(1, explain.ErrorName) + gen.Heading(1, explain.ErrorName) } - gen.generateFromExp(1, explain) - doc := cd.MainError.Document - - if doc != nil && gen.IsTesting && !cd.MainError.Nearest.IsNull() { - startLineNr := cd.MainError.Nearest.StartPosition().Line - startLines := doc.LinesAt(max(startLineNr-1, 0), startLineNr) - endLines := doc.LinesAt(min(startLineNr+1, doc.TotalLines()), min(startLineNr+2, doc.TotalLines())) - arrowLength := int(cd.MainError.Nearest.EndByte() - cd.MainError.Nearest.StartByte()) - if arrowLength == 0 { - arrowLength = 1 - } - - startArrowPos := cd.MainError.Nearest.StartPosition().Column - gen.writeln("```") - gen.writeLines(startLines...) - for i := 0; i < startArrowPos; i++ { - if startLines[len(startLines)-1][i] == '\t' { - gen.wr.WriteString(" ") - } else { - gen.wr.WriteByte(' ') - } - } - for i := 0; i < arrowLength; i++ { - gen.wr.WriteByte('^') - } - gen._break() - gen.writeLines(endLines...) - gen.writeln("```") + gen.FromExplanation(1, explain) + if gen.GenAfterExplain != nil { + gen.GenAfterExplain(gen) } - gen.heading(2, "Steps to fix") + gen.Heading(2, "Steps to fix") if bugFix.Suggestions != nil && len(bugFix.Suggestions) != 0 { for sIdx, s := range bugFix.Suggestions { if len(bugFix.Suggestions) == 1 { - gen.heading(3, s.Title) + gen.Heading(3, s.Title) } else { - gen.heading(3, fmt.Sprintf("%d. %s", sIdx+1, s.Title)) + gen.Heading(3, fmt.Sprintf("%d. %s", sIdx+1, s.Title)) } for idx, step := range s.Steps { if len(s.Steps) == 1 { - gen.writeln(step.Content) + gen.Writeln(step.Content) } else { - gen.writeln(fmt.Sprintf("%d. %s", idx+1, step.Content)) + gen.Writeln(fmt.Sprintf("%d. %s", idx+1, step.Content)) } if step.Fixes == nil && len(step.Fixes) == 0 { @@ -131,6 +111,7 @@ func (gen *OutputGenerator) Generate(cd *ContextData, explain *ExplainGenerator, } if len(step.Fixes) != 0 { + doc := step.Doc.Document descriptionBuilder := &strings.Builder{} // get the start and end line after applying the diff @@ -141,7 +122,7 @@ func (gen *OutputGenerator) Generate(cd *ContextData, explain *ExplainGenerator, origStartLine := step.OrigStartLine origAfterLine := step.OrigAfterLine - gen.writeln("```diff") + gen.Writeln("```diff") // use origStartLine instead of startLine because we want to show the original lines if startLine > 0 { @@ -149,19 +130,19 @@ func (gen *OutputGenerator) Generate(cd *ContextData, explain *ExplainGenerator, if step.DiffPosition.Line < 0 { deduct += step.DiffPosition.Line } - gen.writeLines(step.Doc.LinesAt(origStartLine+deduct, origStartLine-1)...) + gen.WriteLines(step.Doc.LinesAt(origStartLine+deduct, origStartLine-1)...) } modified := step.Doc.ModifiedLinesAt(startLine, afterLine) original := step.Doc.LinesAt(origStartLine, origAfterLine) for i, origLine := range original { if i >= len(modified) || modified[i] != origLine { - gen.write("- ") + gen.Write("- ") } if len(origLine) == 0 { - gen._break() + gen.Break() } else { - gen.writeln(origLine) + gen.Writeln(origLine) } } @@ -179,22 +160,22 @@ func (gen *OutputGenerator) Generate(cd *ContextData, explain *ExplainGenerator, if i < len(originalLines) && modifiedLine == originalLines[i] { // write only if the line is not the last line if startLine+i < origAfterLine { - gen.write(modifiedLine) - gen._break() + gen.Write(modifiedLine) + gen.Break() } continue } - gen.write("+") + gen.Write("+") if len(modifiedLine) != 0 { - gen.write(" ") + gen.Write(" ") } - gen.write(modifiedLine) - gen._break() + gen.Write(modifiedLine) + gen.Break() } } - gen.writeLines(step.Doc.LinesAt(origAfterLine+1, min(origAfterLine+2, step.Doc.TotalLines()))...) - gen.writeln("```") + gen.WriteLines(step.Doc.LinesAt(origAfterLine+1, min(origAfterLine+2, step.Doc.TotalLines()))...) + gen.Writeln("```") for fIdx, fix := range step.Fixes { if len(fix.Description) != 0 { @@ -207,23 +188,26 @@ func (gen *OutputGenerator) Generate(cd *ContextData, explain *ExplainGenerator, } if descriptionBuilder.Len() != 0 { - gen.writeln(descriptionBuilder.String()) + gen.Writeln(descriptionBuilder.String()) } } } if sIdx < len(bugFix.Suggestions)-1 { - gen._break() + gen.Break() } } } else { - gen.writeln("Nothing to fix") + gen.Writeln("No bug fixes found for this error.") } - return strings.TrimSpace(gen.wr.String()) + return strings.TrimSpace(gen.Builder.String()) } func (gen *OutputGenerator) Reset() { - gen.wr.Reset() + if gen.GenAfterExplain != nil { + gen.GenAfterExplain = nil + } + gen.Builder.Reset() } diff --git a/output_gen_test.go b/output_gen_test.go index 19c2721..49374f9 100644 --- a/output_gen_test.go +++ b/output_gen_test.go @@ -1,3 +1,389 @@ -package errgoengine +package errgoengine_test -// TODO: +import ( + "strings" + "testing" + + lib "github.com/nedpals/errgoengine" + sitter "github.com/smacker/go-tree-sitter" +) + +func TestOutputGenerator(t *testing.T) { + parser := sitter.NewParser() + doc, err := lib.ParseDocument("program.test", strings.NewReader("a = xyz\nb = 123"), parser, lib.TestLanguage, nil) + if err != nil { + t.Fatal(err) + } + + gen := &lib.OutputGenerator{} + + t.Run("Simple", func(t *testing.T) { + defer gen.Reset() + + bugFix := lib.NewBugFixGenerator(doc) + explain := lib.NewExplainGeneratorForError("NameError") + + // create a fake name error explanation + explain.Add("The variable you are trying to use is not defined. In this case, the variable `xyz` is not defined.") + + // create a fake bug fix suggestion + bugFix.Add("Define the variable `xyz` before using it.", func(s *lib.BugFixSuggestion) { + s.AddStep("In line 1, replace `xyz` with `\"test\"`."). + AddFix(lib.FixSuggestion{ + NewText: "\"test\"", + StartPosition: lib.Position{ + Line: 0, + Column: 4, + }, + EndPosition: lib.Position{ + Line: 0, + Column: 7, + }, + }) + }) + + // generate the output + output := gen.Generate(explain, bugFix) + + // check if the output is correct + expected := `# NameError +The variable you are trying to use is not defined. In this case, the variable ` + "`xyz`" + ` is not defined. +## Steps to fix +### Define the variable ` + "`xyz`" + ` before using it. +In line 1, replace ` + "`xyz`" + ` with ` + "`\"test\"`" + `. +` + "```diff" + ` +- a = xyz ++ a = "test" +b = 123 +` + "```" + `` + + if output != expected { + t.Errorf("exp %s, got %s", expected, output) + } + }) + + t.Run("With sections", func(t *testing.T) { + defer gen.Reset() + + bugFix := lib.NewBugFixGenerator(doc) + explain := lib.NewExplainGeneratorForError("NameError") + + // create a fake name error explanation + explain.Add("The variable you are trying to use is not defined. In this case, the variable `xyz` is not defined.") + + // add a section + explain.CreateSection("More info"). + Add("This error is usually caused by a typo or a missing variable definition.") + + // create a fake bug fix suggestion + bugFix.Add("Define the variable `xyz` before using it.", func(s *lib.BugFixSuggestion) { + s.AddStep("In line 1, replace `xyz` with `\"test\"`."). + AddFix(lib.FixSuggestion{ + NewText: "\"test\"", + StartPosition: lib.Position{ + Line: 0, + Column: 4, + }, + EndPosition: lib.Position{ + Line: 0, + Column: 7, + }, + }) + }) + + // generate the output + output := gen.Generate(explain, bugFix) + + // check if the output is correct + expected := `# NameError +The variable you are trying to use is not defined. In this case, the variable ` + "`xyz`" + ` is not defined. +## More info +This error is usually caused by a typo or a missing variable definition. +## Steps to fix +### Define the variable ` + "`xyz`" + ` before using it. +In line 1, replace ` + "`xyz`" + ` with ` + "`\"test\"`" + `. +` + "```diff" + ` +- a = xyz ++ a = "test" +b = 123 +` + "```" + `` + if output != expected { + t.Errorf("exp %s, got %s", expected, output) + } + }) + + t.Run("With multiple suggestions", func(t *testing.T) { + defer gen.Reset() + + bugFix := lib.NewBugFixGenerator(doc) + explain := lib.NewExplainGeneratorForError("NameError") + + // create a fake name error explanation + explain.Add("The variable you are trying to use is not defined. In this case, the variable `xyz` is not defined.") + + // create a fake bug fix suggestion + bugFix.Add("Define the variable `xyz` before using it.", func(s *lib.BugFixSuggestion) { + s.AddStep("In line 1, replace `xyz` with `\"test\"`."). + AddFix(lib.FixSuggestion{ + NewText: "\"test\"", + StartPosition: lib.Position{ + Line: 0, + Column: 4, + }, + EndPosition: lib.Position{ + Line: 0, + Column: 7, + }, + }) + }) + + bugFix.Add("Define the variable `xyz` before using it.", func(s *lib.BugFixSuggestion) { + s.AddStep("In line 1, declare a new variable named `xyz`"). + AddFix(lib.FixSuggestion{ + NewText: "xyz = \"test\"\n", + StartPosition: lib.Position{ + Line: 0, + Column: 0, + }, + EndPosition: lib.Position{ + Line: 0, + Column: 0, + }, + }) + }) + + // generate the output + output := gen.Generate(explain, bugFix) + + // check if the output is correct + expected := `# NameError +The variable you are trying to use is not defined. In this case, the variable ` + "`xyz`" + ` is not defined. +## Steps to fix +### 1. Define the variable ` + "`xyz`" + ` before using it. +In line 1, replace ` + "`xyz`" + ` with ` + "`\"test\"`" + `. +` + "```diff" + ` +- a = xyz ++ a = "test" +b = 123 +` + "```" + ` + +### 2. Define the variable ` + "`xyz`" + ` before using it. +In line 1, declare a new variable named ` + "`xyz`" + `. +` + "```diff" + ` +- a = xyz ++ xyz = "test" ++ a = xyz +b = 123 +` + "```" + `` + + if output != expected { + t.Errorf("exp %s, got %s", expected, output) + } + }) + + t.Run("With fix description", func(t *testing.T) { + defer gen.Reset() + + bugFix := lib.NewBugFixGenerator(doc) + explain := lib.NewExplainGeneratorForError("NameError") + + // create a fake name error explanation + explain.Add("The variable you are trying to use is not defined. In this case, the variable `xyz` is not defined.") + + // create a fake bug fix suggestion + bugFix.Add("Define the variable `xyz` before using it.", func(s *lib.BugFixSuggestion) { + s.AddStep("In line 1, replace `xyz` with `\"test\"`."). + AddFix(lib.FixSuggestion{ + Description: "This is a test description.", + NewText: "\"test\"", + StartPosition: lib.Position{ + Line: 0, + Column: 4, + }, + EndPosition: lib.Position{ + Line: 0, + Column: 7, + }, + }) + }) + + // generate the output + output := gen.Generate(explain, bugFix) + + // check if the output is correct + expected := `# NameError +The variable you are trying to use is not defined. In this case, the variable ` + "`xyz`" + ` is not defined. +## Steps to fix +### Define the variable ` + "`xyz`" + ` before using it. +In line 1, replace ` + "`xyz`" + ` with ` + "`\"test\"`" + `. +` + "```diff" + ` +- a = xyz ++ a = "test" +b = 123 +` + "```" + ` +This is a test description.` + + if output != expected { + t.Errorf("exp %s, got %s", expected, output) + } + }) + + t.Run("With GenAfterExplain", func(t *testing.T) { + defer gen.Reset() + + bugFix := lib.NewBugFixGenerator(doc) + explain := lib.NewExplainGeneratorForError("NameError") + + // create a fake name error explanation + explain.Add("The variable you are trying to use is not defined. In this case, the variable `xyz` is not defined.") + + // create a fake bug fix suggestion + bugFix.Add("Define the variable `xyz` before using it.", func(s *lib.BugFixSuggestion) { + s.AddStep("In line 1, replace `xyz` with `\"test\"`."). + AddFix(lib.FixSuggestion{ + NewText: "\"test\"", + StartPosition: lib.Position{ + Line: 0, + Column: 4, + }, + EndPosition: lib.Position{ + Line: 0, + Column: 7, + }, + }) + }) + + // add a code snippet that points to the error + gen.GenAfterExplain = func(gen *lib.OutputGenerator) { + startLineNr := 0 + startLines := doc.LinesAt(startLineNr, startLineNr+1) + endLines := doc.LinesAt(startLineNr+1, startLineNr+2) + + gen.Writeln("```") + gen.WriteLines(startLines...) + + for i := 0; i < 4; i++ { + if startLines[len(startLines)-1][i] == '\t' { + gen.Builder.WriteString(" ") + } else { + gen.Builder.WriteByte(' ') + } + } + + for i := 0; i < 3; i++ { + gen.Builder.WriteByte('^') + } + + gen.Break() + gen.WriteLines(endLines...) + gen.Writeln("```") + } + + // generate the output + output := gen.Generate(explain, bugFix) + + // check if the output is correct + expected := `# NameError +The variable you are trying to use is not defined. In this case, the variable ` + "`xyz`" + ` is not defined. +` + "```" + ` +a = xyz +b = 123 + ^^^ +b = 123 +` + "```" + ` +## Steps to fix +### Define the variable ` + "`xyz`" + ` before using it. +In line 1, replace ` + "`xyz`" + ` with ` + "`\"test\"`" + `. +` + "```diff" + ` +- a = xyz ++ a = "test" +b = 123 +` + "```" + `` + + if output != expected { + t.Errorf("exp %s, got %s", expected, output) + } + }) + + t.Run("Empty explanation", func(t *testing.T) { + defer gen.Reset() + + bugFix := lib.NewBugFixGenerator(doc) + explain := lib.NewExplainGeneratorForError("NameError") + + // generate bug fix + bugFix.Add("Define the variable `xyz` before using it.", func(s *lib.BugFixSuggestion) { + s.AddStep("In line 1, replace `xyz` with `\"test\"`."). + AddFix(lib.FixSuggestion{ + NewText: "\"test\"", + StartPosition: lib.Position{ + Line: 0, + Column: 4, + }, + EndPosition: lib.Position{ + Line: 0, + Column: 7, + }, + }) + }) + + // generate the output + output := gen.Generate(explain, bugFix) + + // check if the output is correct + expected := `# NameError +No explanation found for this error. +## Steps to fix +### Define the variable ` + "`xyz`" + ` before using it. +In line 1, replace ` + "`xyz`" + ` with ` + "`\"test\"`" + `. +` + "```diff" + ` +- a = xyz ++ a = "test" +b = 123 +` + "```" + `` + if output != expected { + t.Errorf("exp %s, got %s", expected, output) + } + }) + + t.Run("Empty bug fixes", func(t *testing.T) { + defer gen.Reset() + + bugFix := lib.NewBugFixGenerator(doc) + explain := lib.NewExplainGeneratorForError("NameError") + + // create a fake name error explanation + explain.Add("The variable you are trying to use is not defined. In this case, the variable `xyz` is not defined.") + + // generate the output + output := gen.Generate(explain, bugFix) + + // check if the output is correct + expected := `# NameError +The variable you are trying to use is not defined. In this case, the variable ` + "`xyz`" + ` is not defined. +## Steps to fix +No bug fixes found for this error.` + if output != expected { + t.Errorf("exp %s, got %s", expected, output) + } + }) + + t.Run("Empty explanation + bug fixes", func(t *testing.T) { + defer gen.Reset() + + bugFix := lib.NewBugFixGenerator(doc) + explain := lib.NewExplainGeneratorForError("NameError") + + // generate the output + output := gen.Generate(explain, bugFix) + + // check if the output is correct + expected := `# NameError +No explanation found for this error. +## Steps to fix +No bug fixes found for this error.` + if output != expected { + t.Errorf("exp %s, got %s", expected, output) + } + }) +} diff --git a/source.go b/source.go index e920d87..0569a3a 100644 --- a/source.go +++ b/source.go @@ -496,6 +496,7 @@ func (doc *EditableDocument) Reset() { } type Document struct { + Version int Path string Contents string cachedLines []string @@ -503,6 +504,14 @@ type Document struct { Tree *sitter.Tree } +func (doc *Document) StringContentEquals(str string) bool { + return doc.Contents == str +} + +func (doc *Document) BytesContentEquals(cnt []byte) bool { + return doc.Contents == string(cnt) +} + func (doc *Document) RootNode() SyntaxNode { return WrapNode(doc, doc.Tree.RootNode()) } @@ -537,6 +546,11 @@ func (doc *Document) TotalLines() int { } func ParseDocument(path string, r io.Reader, parser *sitter.Parser, selectLang *Language, existingDoc *Document) (*Document, error) { + version := 1 + if existingDoc != nil { + version = existingDoc.Version + 1 + } + inputBytes, err := io.ReadAll(r) if err != nil { return nil, err @@ -571,5 +585,6 @@ func ParseDocument(path string, r io.Reader, parser *sitter.Parser, selectLang * Language: selectLang, Contents: string(inputBytes), Tree: tree, + Version: version, }, nil } diff --git a/store.go b/store.go index 8e483d1..8162aaa 100644 --- a/store.go +++ b/store.go @@ -1,9 +1,12 @@ package errgoengine +import "io/fs" + type Store struct { DepGraph DepGraph Documents map[string]*Document Symbols map[string]*SymbolTree + FS fs.ReadFileFS } func NewEmptyStore() *Store { diff --git a/symbol_analyzer.go b/symbol_analyzer.go index 3d49363..b288990 100644 --- a/symbol_analyzer.go +++ b/symbol_analyzer.go @@ -384,10 +384,12 @@ func (an *SymbolAnalyzer) captureAndAnalyze(parent *SymbolTree, rootNode SyntaxN } func (an *SymbolAnalyzer) Analyze(doc *Document) { + oldCurrentDocumentPath := an.ContextData.CurrentDocumentPath + an.doc = doc rootNode := doc.RootNode() symTree := an.ContextData.InitOrGetSymbolTree(an.doc.Path) an.ContextData.CurrentDocumentPath = an.doc.Path an.captureAndAnalyze(symTree, rootNode, an.doc.Language.SymbolsToCapture) - an.ContextData.CurrentDocumentPath = "" + an.ContextData.CurrentDocumentPath = oldCurrentDocumentPath } diff --git a/translation.go b/translation.go index 9351e1e..145d718 100644 --- a/translation.go +++ b/translation.go @@ -6,8 +6,6 @@ import ( "unicode" ) -type GenExplainFn func(*ContextData, *ExplainGenerator) - type ExplainGenerator struct { ErrorName string Builder *strings.Builder @@ -41,7 +39,11 @@ func (gen *ExplainGenerator) CreateSection(name string) *ExplainGenerator { return gen.Sections[name] } -type GenBugFixFn func(*ContextData, *BugFixGenerator) +func NewExplainGeneratorForError(name string) *ExplainGenerator { + return &ExplainGenerator{ + ErrorName: name, + } +} type BugFixSuggestion struct { Title string @@ -215,3 +217,9 @@ func (gen *BugFixGenerator) Add(title string, makerFn func(s *BugFixSuggestion)) gen.Suggestions = append(gen.Suggestions, suggestion) return nil } + +func NewBugFixGenerator(doc *Document) *BugFixGenerator { + return &BugFixGenerator{ + Document: doc, + } +} diff --git a/translation_test.go b/translation_test.go index d94f467..5449857 100644 --- a/translation_test.go +++ b/translation_test.go @@ -10,7 +10,7 @@ import ( func TestExplainGenerator(t *testing.T) { t.Run("errorName", func(t *testing.T) { - gen := &lib.ExplainGenerator{ErrorName: "TestError"} + gen := lib.NewExplainGeneratorForError("TestError") if gen.ErrorName != "TestError" { t.Errorf("Expected 'TestError', got %s", gen.ErrorName) @@ -20,87 +20,87 @@ func TestExplainGenerator(t *testing.T) { t.Run("Add", func(t *testing.T) { t.Run("Simple", func(t *testing.T) { gen := &lib.ExplainGenerator{} - gen.Add("This is a simple error message.") + gen.Add("This is a simple error explanation.") - if gen.Builder.String() != "This is a simple error message." { - t.Errorf("Expected 'This is a simple error message.', got %s", gen.Builder.String()) + if gen.Builder.String() != "This is a simple error explanation." { + t.Errorf("Expected 'This is a simple error explanation.', got %s", gen.Builder.String()) } }) t.Run("Simple with string data", func(t *testing.T) { gen := &lib.ExplainGenerator{} - gen.Add("This is a simple error message with data: %s", "Hello") + gen.Add("This is a simple error explanation with data: %s", "Hello") - if gen.Builder.String() != "This is a simple error message with data: Hello" { - t.Errorf("Expected 'This is a simple error message with data: Hello', got %s", gen.Builder.String()) + if gen.Builder.String() != "This is a simple error explanation with data: Hello" { + t.Errorf("Expected 'This is a simple error explanation with data: Hello', got %s", gen.Builder.String()) } }) t.Run("Simple with int data", func(t *testing.T) { gen := &lib.ExplainGenerator{} - gen.Add("This is a simple error message with data: %d", 10) + gen.Add("This is a simple error explanation with data: %d", 10) - if gen.Builder.String() != "This is a simple error message with data: 10" { - t.Errorf("Expected 'This is a simple error message with data: 10', got %s", gen.Builder.String()) + if gen.Builder.String() != "This is a simple error explanation with data: 10" { + t.Errorf("Expected 'This is a simple error explanation with data: 10', got %s", gen.Builder.String()) } }) t.Run("Simple with mixed data", func(t *testing.T) { gen := &lib.ExplainGenerator{} - gen.Add("This is a simple error message with data: %s and %d", "Hello", 10) + gen.Add("This is a simple error explanation with data: %s and %d", "Hello", 10) - if gen.Builder.String() != "This is a simple error message with data: Hello and 10" { - t.Errorf("Expected 'This is a simple error message with data: Hello and 10', got %s", gen.Builder.String()) + if gen.Builder.String() != "This is a simple error explanation with data: Hello and 10" { + t.Errorf("Expected 'This is a simple error explanation with data: Hello and 10', got %s", gen.Builder.String()) } }) t.Run("Append", func(t *testing.T) { gen := &lib.ExplainGenerator{} - gen.Add("This is a simple error message.") + gen.Add("This is a simple error explanation.") gen.Add("This is another error message.") - if gen.Builder.String() != "This is a simple error message.This is another error message." { - t.Errorf("Expected 'This is a simple error message.This is another error message.', got %s", gen.Builder.String()) + if gen.Builder.String() != "This is a simple error explanation.This is another error message." { + t.Errorf("Expected 'This is a simple error explanation.This is another error message.', got %s", gen.Builder.String()) } }) t.Run("Append with newline", func(t *testing.T) { - gen := &lib.ExplainGenerator{ErrorName: "TestError"} - gen.Add("This is a simple error message.\n") + gen := lib.NewExplainGeneratorForError("TestError") + gen.Add("This is a simple error explanation.\n") gen.Add("This is another error message.") - if gen.Builder.String() != "This is a simple error message.\nThis is another error message." { - t.Errorf("Expected 'This is a simple error message.\nThis is another error message.', got %s", gen.Builder.String()) + if gen.Builder.String() != "This is a simple error explanation.\nThis is another error message." { + t.Errorf("Expected 'This is a simple error explanation.\nThis is another error message.', got %s", gen.Builder.String()) } }) t.Run("Append with string data", func(t *testing.T) { gen := &lib.ExplainGenerator{} - gen.Add("This is a simple error message with data: %s", "Hello") + gen.Add("This is a simple error explanation with data: %s", "Hello") gen.Add("This is another error message with data: %s", "World") - if gen.Builder.String() != "This is a simple error message with data: HelloThis is another error message with data: World" { - t.Errorf("Expected 'This is a simple error message with data: HelloThis is another error message with data: World', got %s", gen.Builder.String()) + if gen.Builder.String() != "This is a simple error explanation with data: HelloThis is another error message with data: World" { + t.Errorf("Expected 'This is a simple error explanation with data: HelloThis is another error message with data: World', got %s", gen.Builder.String()) } }) t.Run("Append with int data", func(t *testing.T) { gen := &lib.ExplainGenerator{} - gen.Add("This is a simple error message with data: %d", 10) + gen.Add("This is a simple error explanation with data: %d", 10) gen.Add("This is another error message with data: %d", 20) - if gen.Builder.String() != "This is a simple error message with data: 10This is another error message with data: 20" { - t.Errorf("Expected 'This is a simple error message with data: 10This is another error message with data: 20', got %s", gen.Builder.String()) + if gen.Builder.String() != "This is a simple error explanation with data: 10This is another error message with data: 20" { + t.Errorf("Expected 'This is a simple error explanation with data: 10This is another error message with data: 20', got %s", gen.Builder.String()) } }) t.Run("Append with mixed data", func(t *testing.T) { gen := &lib.ExplainGenerator{} - gen.Add("This is a simple error message with data: %s", "Hello") + gen.Add("This is a simple error explanation with data: %s", "Hello") gen.Add("This is another error message with data: %d", 20) - if gen.Builder.String() != "This is a simple error message with data: HelloThis is another error message with data: 20" { - t.Errorf("Expected 'This is a simple error message with data: HelloThis is another error message with data: 20', got %s", gen.Builder.String()) + if gen.Builder.String() != "This is a simple error explanation with data: HelloThis is another error message with data: 20" { + t.Errorf("Expected 'This is a simple error explanation with data: HelloThis is another error message with data: 20', got %s", gen.Builder.String()) } }) @@ -170,13 +170,11 @@ func TestBugFixGenerator(t *testing.T) { t.Run("Add", func(t *testing.T) { t.Run("Simple", func(t *testing.T) { - gen := &lib.BugFixGenerator{ - Document: doc, - } + gen := lib.NewBugFixGenerator(doc) - gen.Add("This is a simple error message.", func(s *lib.BugFixSuggestion) { - if s.Title != "This is a simple error message." { - t.Errorf("Expected 'This is a simple error message.', got %s", s.Title) + gen.Add("A descriptive suggestion sentence or phrase", func(s *lib.BugFixSuggestion) { + if s.Title != "A descriptive suggestion sentence or phrase" { + t.Errorf("Expected 'A descriptive suggestion sentence or phrase', got %s", s.Title) } if s.Doc == nil { @@ -186,9 +184,7 @@ func TestBugFixGenerator(t *testing.T) { }) t.Run("Empty title", func(t *testing.T) { - gen := &lib.BugFixGenerator{ - Document: doc, - } + gen := lib.NewBugFixGenerator(doc) err := gen.Add("", func(s *lib.BugFixSuggestion) {}) if err == nil { @@ -201,11 +197,9 @@ func TestBugFixGenerator(t *testing.T) { }) t.Run("Empty function", func(t *testing.T) { - gen := &lib.BugFixGenerator{ - Document: doc, - } + gen := lib.NewBugFixGenerator(doc) - err := gen.Add("This is a simple error message.", nil) + err := gen.Add("A descriptive suggestion sentence or phrase", nil) if err == nil { t.Errorf("Expected error, got nil") } @@ -216,11 +210,9 @@ func TestBugFixGenerator(t *testing.T) { }) t.Run("Multiple", func(t *testing.T) { - gen := &lib.BugFixGenerator{ - Document: doc, - } + gen := lib.NewBugFixGenerator(doc) - gen.Add("This is a simple error message.", func(s *lib.BugFixSuggestion) {}) + gen.Add("A descriptive suggestion sentence or phrase", func(s *lib.BugFixSuggestion) {}) gen.Add("This is another error message.", func(s *lib.BugFixSuggestion) {}) if len(gen.Suggestions) != 2 { @@ -231,14 +223,12 @@ func TestBugFixGenerator(t *testing.T) { t.Run("Suggestion/AddStep", func(t *testing.T) { t.Run("Simple", func(t *testing.T) { - gen := &lib.BugFixGenerator{ - Document: doc, - } + gen := lib.NewBugFixGenerator(doc) - gen.Add("This is a simple error message.", func(s *lib.BugFixSuggestion) { - s.AddStep("This is a simple step.", func(step *lib.BugFixStep) { - if step.Content != "This is a simple step." { - t.Errorf("Expected 'This is a simple step.', got %s", step.Content) + gen.Add("A descriptive suggestion sentence or phrase", func(s *lib.BugFixSuggestion) { + s.AddStep("This is a step.", func(step *lib.BugFixStep) { + if step.Content != "This is a step." { + t.Errorf("Expected 'This is a step.', got %s", step.Content) } if step.Doc == nil { @@ -249,25 +239,21 @@ func TestBugFixGenerator(t *testing.T) { }) t.Run("Without period", func(t *testing.T) { - gen := &lib.BugFixGenerator{ - Document: doc, - } + gen := lib.NewBugFixGenerator(doc) - gen.Add("This is a simple error message.", func(s *lib.BugFixSuggestion) { - s.AddStep("This is a simple step", func(step *lib.BugFixStep) { - if step.Content != "This is a simple step." { - t.Errorf("Expected 'This is a simple step.', got %s", step.Content) + gen.Add("A descriptive suggestion sentence or phrase", func(s *lib.BugFixSuggestion) { + s.AddStep("This is a step", func(step *lib.BugFixStep) { + if step.Content != "This is a step." { + t.Errorf("Expected 'This is a step.', got %s", step.Content) } }) }) }) t.Run("With punctuation", func(t *testing.T) { - gen := &lib.BugFixGenerator{ - Document: doc, - } + gen := lib.NewBugFixGenerator(doc) - gen.Add("This is a simple error message.", func(s *lib.BugFixSuggestion) { + gen.Add("A descriptive suggestion sentence or phrase", func(s *lib.BugFixSuggestion) { s.AddStep("Oh wow!", func(step *lib.BugFixStep) { if step.Content != "Oh wow!" { t.Errorf("Expected 'Oh wow!', got %s", step.Content) @@ -277,53 +263,45 @@ func TestBugFixGenerator(t *testing.T) { }) t.Run("With string data", func(t *testing.T) { - gen := &lib.BugFixGenerator{ - Document: doc, - } + gen := lib.NewBugFixGenerator(doc) - gen.Add("This is a simple error message.", func(s *lib.BugFixSuggestion) { - s.AddStep("This is a simple step with data: %s", "Hello", func(step *lib.BugFixStep) { - if step.Content != "This is a simple step with data: Hello." { - t.Errorf("Expected 'This is a simple step with data: Hello.', got %s", step.Content) + gen.Add("A descriptive suggestion sentence or phrase", func(s *lib.BugFixSuggestion) { + s.AddStep("This is a step with data: %s", "Hello", func(step *lib.BugFixStep) { + if step.Content != "This is a step with data: Hello." { + t.Errorf("Expected 'This is a step with data: Hello.', got %s", step.Content) } }) }) }) t.Run("With int data", func(t *testing.T) { - gen := &lib.BugFixGenerator{ - Document: doc, - } + gen := lib.NewBugFixGenerator(doc) - gen.Add("This is a simple error message.", func(s *lib.BugFixSuggestion) { - s.AddStep("This is a simple step with data: %d", 10, func(step *lib.BugFixStep) { - if step.Content != "This is a simple step with data: 10." { - t.Errorf("Expected 'This is a simple step with data: 10.', got %s", step.Content) + gen.Add("A descriptive suggestion sentence or phrase", func(s *lib.BugFixSuggestion) { + s.AddStep("This is a step with data: %d", 10, func(step *lib.BugFixStep) { + if step.Content != "This is a step with data: 10." { + t.Errorf("Expected 'This is a step with data: 10.', got %s", step.Content) } }) }) }) t.Run("With mixed data", func(t *testing.T) { - gen := &lib.BugFixGenerator{ - Document: doc, - } + gen := lib.NewBugFixGenerator(doc) - gen.Add("This is a simple error message.", func(s *lib.BugFixSuggestion) { - s.AddStep("This is a simple step with data: %s and %d", "Hello", 10, func(step *lib.BugFixStep) { - if step.Content != "This is a simple step with data: Hello and 10." { - t.Errorf("Expected 'This is a simple step with data: Hello and 10.', got %s", step.Content) + gen.Add("A descriptive suggestion sentence or phrase", func(s *lib.BugFixSuggestion) { + s.AddStep("This is a step with data: %s and %d", "Hello", 10, func(step *lib.BugFixStep) { + if step.Content != "This is a step with data: Hello and 10." { + t.Errorf("Expected 'This is a step with data: Hello and 10.', got %s", step.Content) } }) }) }) t.Run("Empty content", func(t *testing.T) { - gen := &lib.BugFixGenerator{ - Document: doc, - } + gen := lib.NewBugFixGenerator(doc) - gen.Add("This is a simple error message.", func(s *lib.BugFixSuggestion) { + gen.Add("A descriptive suggestion sentence or phrase", func(s *lib.BugFixSuggestion) { // recover defer func() { if r := recover(); r != nil { @@ -344,9 +322,9 @@ func TestBugFixGenerator(t *testing.T) { Document: doc, } - gen.Add("This is a simple error message.", func(s *lib.BugFixSuggestion) { + gen.Add("A descriptive suggestion sentence or phrase", func(s *lib.BugFixSuggestion) { // add a = 1 fix - step := s.AddStep("This is a simple step."). + step := s.AddStep("This is a step."). AddFix(lib.FixSuggestion{ NewText: "\na = 1", StartPosition: lib.Position{ @@ -367,7 +345,6 @@ func TestBugFixGenerator(t *testing.T) { t.Errorf("Expected 1 fix, got %d", len(s.Steps[0].Fixes)) } }) - }) t.Run("Suggestion/AddFix/Update", func(t *testing.T) { @@ -375,8 +352,8 @@ func TestBugFixGenerator(t *testing.T) { Document: doc, } - gen.Add("This is a simple error message.", func(s *lib.BugFixSuggestion) { - step := s.AddStep("This is a simple step."). + gen.Add("A descriptive suggestion sentence or phrase", func(s *lib.BugFixSuggestion) { + step := s.AddStep("This is a step."). AddFix(lib.FixSuggestion{ NewText: "Welcome to the world", StartPosition: lib.Position{ @@ -417,9 +394,9 @@ func TestBugFixGenerator(t *testing.T) { Document: doc, } - gen.Add("This is a simple error message.", func(s *lib.BugFixSuggestion) { + gen.Add("A descriptive suggestion sentence or phrase", func(s *lib.BugFixSuggestion) { // removes the content inside the print function - step := s.AddStep("This is a simple step."). + step := s.AddStep("This is a step."). AddFix(lib.FixSuggestion{ NewText: "", StartPosition: lib.Position{ @@ -443,9 +420,9 @@ func TestBugFixGenerator(t *testing.T) { Document: doc, } - gen.Add("This is a simple error message.", func(s *lib.BugFixSuggestion) { + gen.Add("A descriptive suggestion sentence or phrase", func(s *lib.BugFixSuggestion) { // removes the content inside the print function - step := s.AddStep("This is a simple step."). + step := s.AddStep("This is a step."). AddFix(lib.FixSuggestion{ NewText: "", StartPosition: lib.Position{ 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 +} diff --git a/utils/slice/slice.go b/utils/slice/slice.go new file mode 100644 index 0000000..81b023c --- /dev/null +++ b/utils/slice/slice.go @@ -0,0 +1,10 @@ +package slice + +func ContainsString(slice []string, value string) bool { + for _, v := range slice { + if v == value { + return true + } + } + return false +}