From 28083a7bb0e76217e36bc997ced4c0521934c843 Mon Sep 17 00:00:00 2001 From: Ned Palacios Date: Sat, 3 Feb 2024 20:29:15 +0800 Subject: [PATCH 01/24] fix: update translation_test.go texts --- translation_test.go | 125 ++++++++++++++++++++++---------------------- 1 file changed, 62 insertions(+), 63 deletions(-) diff --git a/translation_test.go b/translation_test.go index d94f467..671afda 100644 --- a/translation_test.go +++ b/translation_test.go @@ -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.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()) } }) @@ -174,9 +174,9 @@ func TestBugFixGenerator(t *testing.T) { Document: 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 { @@ -205,7 +205,7 @@ func TestBugFixGenerator(t *testing.T) { Document: 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") } @@ -220,7 +220,7 @@ 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) {}) gen.Add("This is another error message.", func(s *lib.BugFixSuggestion) {}) if len(gen.Suggestions) != 2 { @@ -235,10 +235,10 @@ func TestBugFixGenerator(t *testing.T) { Document: 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 { @@ -253,10 +253,10 @@ func TestBugFixGenerator(t *testing.T) { Document: 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) } }) }) @@ -267,7 +267,7 @@ 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) { s.AddStep("Oh wow!", func(step *lib.BugFixStep) { if step.Content != "Oh wow!" { t.Errorf("Expected 'Oh wow!', got %s", step.Content) @@ -281,10 +281,10 @@ func TestBugFixGenerator(t *testing.T) { Document: 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) } }) }) @@ -295,10 +295,10 @@ func TestBugFixGenerator(t *testing.T) { Document: 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) } }) }) @@ -309,10 +309,10 @@ func TestBugFixGenerator(t *testing.T) { Document: 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) } }) }) @@ -323,7 +323,7 @@ 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) { // recover defer func() { if r := recover(); r != nil { @@ -344,9 +344,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 +367,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 +374,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 +416,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 +442,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{ From 3a501c2b362865871dcc35b2c7f30a26475e0acd Mon Sep 17 00:00:00 2001 From: Ned Palacios Date: Sat, 3 Feb 2024 20:51:56 +0800 Subject: [PATCH 02/24] refactor: expose OutputGen methods to public --- output_gen.go | 94 +++++++++++++++++++++++++-------------------------- 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/output_gen.go b/output_gen.go index 9481e9f..fddb50b 100644 --- a/output_gen.go +++ b/output_gen.go @@ -7,79 +7,79 @@ import ( type OutputGenerator struct { IsTesting bool - wr *strings.Builder + 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') + gen.Builder.WriteByte('\n') } -func (gen *OutputGenerator) generateFromExp(level int, explain *ExplainGenerator) { +func (gen *OutputGenerator) ExpGen(level int, explain *ExplainGenerator) { 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.Heading(level+1, sectionName) + gen.ExpGen(level+1, exp) } } else { 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.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() } 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{} + 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) + gen.ExpGen(1, explain) doc := cd.MainError.Document if doc != nil && gen.IsTesting && !cd.MainError.Nearest.IsNull() { @@ -92,38 +92,38 @@ func (gen *OutputGenerator) Generate(cd *ContextData, explain *ExplainGenerator, } startArrowPos := cd.MainError.Nearest.StartPosition().Column - gen.writeln("```") - gen.writeLines(startLines...) + gen.Writeln("```") + gen.WriteLines(startLines...) for i := 0; i < startArrowPos; i++ { if startLines[len(startLines)-1][i] == '\t' { - gen.wr.WriteString(" ") + gen.Builder.WriteString(" ") } else { - gen.wr.WriteByte(' ') + gen.Builder.WriteByte(' ') } } for i := 0; i < arrowLength; i++ { - gen.wr.WriteByte('^') + gen.Builder.WriteByte('^') } gen._break() - gen.writeLines(endLines...) - gen.writeln("```") + gen.WriteLines(endLines...) + gen.Writeln("```") } - 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 { @@ -141,7 +141,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 +149,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() } else { - gen.writeln(origLine) + gen.Writeln(origLine) } } @@ -179,22 +179,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.Write(modifiedLine) gen._break() } continue } - gen.write("+") + gen.Write("+") if len(modifiedLine) != 0 { - gen.write(" ") + gen.Write(" ") } - gen.write(modifiedLine) + 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,7 +207,7 @@ func (gen *OutputGenerator) Generate(cd *ContextData, explain *ExplainGenerator, } if descriptionBuilder.Len() != 0 { - gen.writeln(descriptionBuilder.String()) + gen.Writeln(descriptionBuilder.String()) } } } @@ -218,12 +218,12 @@ func (gen *OutputGenerator) Generate(cd *ContextData, explain *ExplainGenerator, } } else { - gen.writeln("Nothing to fix") + gen.Writeln("Nothing to fix") } - return strings.TrimSpace(gen.wr.String()) + return strings.TrimSpace(gen.Builder.String()) } func (gen *OutputGenerator) Reset() { - gen.wr.Reset() + gen.Builder.Reset() } From a5937e0dc71065dcfa5113fc3f2a112a3d0a1be3 Mon Sep 17 00:00:00 2001 From: Ned Palacios Date: Sat, 3 Feb 2024 21:06:13 +0800 Subject: [PATCH 03/24] refactor: remove contextData param in OutputGen, add hooks --- errgoengine.go | 46 +++++++++++++++++++++++- error_templates/test_utils/test_utils.go | 2 +- output_gen.go | 39 +++++--------------- 3 files changed, 55 insertions(+), 32 deletions(-) diff --git a/errgoengine.go b/errgoengine.go index 7ced40b..fc13e1b 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 { @@ -125,7 +126,50 @@ 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 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/output_gen.go b/output_gen.go index fddb50b..caff2eb 100644 --- a/output_gen.go +++ b/output_gen.go @@ -6,8 +6,8 @@ import ( ) type OutputGenerator struct { - IsTesting bool - Builder *strings.Builder + GenAfterExplain func(*OutputGenerator) + Builder *strings.Builder } func (gen *OutputGenerator) Heading(level int, text string) { @@ -70,7 +70,7 @@ func (gen *OutputGenerator) WriteLines(lines ...string) { } } -func (gen *OutputGenerator) Generate(cd *ContextData, explain *ExplainGenerator, bugFix *BugFixGenerator) string { +func (gen *OutputGenerator) Generate(explain *ExplainGenerator, bugFix *BugFixGenerator) string { if gen.Builder == nil { gen.Builder = &strings.Builder{} } @@ -80,33 +80,8 @@ func (gen *OutputGenerator) Generate(cd *ContextData, explain *ExplainGenerator, } gen.ExpGen(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.Builder.WriteString(" ") - } else { - gen.Builder.WriteByte(' ') - } - } - for i := 0; i < arrowLength; i++ { - gen.Builder.WriteByte('^') - } - gen._break() - gen.WriteLines(endLines...) - gen.Writeln("```") + if gen.GenAfterExplain != nil { + gen.GenAfterExplain(gen) } gen.Heading(2, "Steps to fix") @@ -131,6 +106,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 @@ -225,5 +201,8 @@ func (gen *OutputGenerator) Generate(cd *ContextData, explain *ExplainGenerator, } func (gen *OutputGenerator) Reset() { + if gen.GenAfterExplain != nil { + gen.GenAfterExplain = nil + } gen.Builder.Reset() } From fefc077948e3d8d6dfec306e35cfdcaf6e51052c Mon Sep 17 00:00:00 2001 From: Ned Palacios Date: Sat, 3 Feb 2024 21:24:51 +0800 Subject: [PATCH 04/24] fix: remove custom types linked to contextData inside translation.go --- error_template.go | 8 +++----- translation.go | 4 ---- 2 files changed, 3 insertions(+), 9 deletions(-) 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/translation.go b/translation.go index 9351e1e..2817f91 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,8 +39,6 @@ func (gen *ExplainGenerator) CreateSection(name string) *ExplainGenerator { return gen.Sections[name] } -type GenBugFixFn func(*ContextData, *BugFixGenerator) - type BugFixSuggestion struct { Title string Steps []*BugFixStep From b2384a24ba9e2c376a46d745e42264e0741cdf73 Mon Sep 17 00:00:00 2001 From: Ned Palacios Date: Sat, 3 Feb 2024 21:36:56 +0800 Subject: [PATCH 05/24] refactor: introduce new function for creating bugfix, update tests --- translation.go | 6 ++++++ translation_test.go | 44 +++++++++++--------------------------------- 2 files changed, 17 insertions(+), 33 deletions(-) diff --git a/translation.go b/translation.go index 2817f91..de88096 100644 --- a/translation.go +++ b/translation.go @@ -211,3 +211,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 671afda..d3fd28f 100644 --- a/translation_test.go +++ b/translation_test.go @@ -170,9 +170,7 @@ 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("A descriptive suggestion sentence or phrase", func(s *lib.BugFixSuggestion) { if s.Title != "A descriptive suggestion sentence or phrase" { @@ -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,9 +197,7 @@ 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("A descriptive suggestion sentence or phrase", nil) if err == nil { @@ -216,9 +210,7 @@ func TestBugFixGenerator(t *testing.T) { }) t.Run("Multiple", func(t *testing.T) { - gen := &lib.BugFixGenerator{ - Document: doc, - } + gen := lib.NewBugFixGenerator(doc) gen.Add("A descriptive suggestion sentence or phrase", func(s *lib.BugFixSuggestion) {}) gen.Add("This is another error message.", func(s *lib.BugFixSuggestion) {}) @@ -231,9 +223,7 @@ 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("A descriptive suggestion sentence or phrase", func(s *lib.BugFixSuggestion) { s.AddStep("This is a step.", func(step *lib.BugFixStep) { @@ -249,9 +239,7 @@ func TestBugFixGenerator(t *testing.T) { }) t.Run("Without period", func(t *testing.T) { - gen := &lib.BugFixGenerator{ - Document: doc, - } + gen := lib.NewBugFixGenerator(doc) gen.Add("A descriptive suggestion sentence or phrase", func(s *lib.BugFixSuggestion) { s.AddStep("This is a step", func(step *lib.BugFixStep) { @@ -263,9 +251,7 @@ func TestBugFixGenerator(t *testing.T) { }) t.Run("With punctuation", func(t *testing.T) { - gen := &lib.BugFixGenerator{ - Document: doc, - } + gen := lib.NewBugFixGenerator(doc) gen.Add("A descriptive suggestion sentence or phrase", func(s *lib.BugFixSuggestion) { s.AddStep("Oh wow!", func(step *lib.BugFixStep) { @@ -277,9 +263,7 @@ 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("A descriptive suggestion sentence or phrase", func(s *lib.BugFixSuggestion) { s.AddStep("This is a step with data: %s", "Hello", func(step *lib.BugFixStep) { @@ -291,9 +275,7 @@ func TestBugFixGenerator(t *testing.T) { }) t.Run("With int data", func(t *testing.T) { - gen := &lib.BugFixGenerator{ - Document: doc, - } + gen := lib.NewBugFixGenerator(doc) 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) { @@ -305,9 +287,7 @@ func TestBugFixGenerator(t *testing.T) { }) t.Run("With mixed data", func(t *testing.T) { - gen := &lib.BugFixGenerator{ - Document: doc, - } + gen := lib.NewBugFixGenerator(doc) 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) { @@ -319,9 +299,7 @@ func TestBugFixGenerator(t *testing.T) { }) t.Run("Empty content", func(t *testing.T) { - gen := &lib.BugFixGenerator{ - Document: doc, - } + gen := lib.NewBugFixGenerator(doc) gen.Add("A descriptive suggestion sentence or phrase", func(s *lib.BugFixSuggestion) { // recover From 91a68607c389a3a9107d1fd3266299c7f9e92734 Mon Sep 17 00:00:00 2001 From: Ned Palacios Date: Sat, 3 Feb 2024 21:39:02 +0800 Subject: [PATCH 06/24] refactor: introduce new fn for creating explaingen with error, update tests --- translation.go | 6 ++++++ translation_test.go | 4 ++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/translation.go b/translation.go index de88096..145d718 100644 --- a/translation.go +++ b/translation.go @@ -39,6 +39,12 @@ func (gen *ExplainGenerator) CreateSection(name string) *ExplainGenerator { return gen.Sections[name] } +func NewExplainGeneratorForError(name string) *ExplainGenerator { + return &ExplainGenerator{ + ErrorName: name, + } +} + type BugFixSuggestion struct { Title string Steps []*BugFixStep diff --git a/translation_test.go b/translation_test.go index d3fd28f..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) @@ -65,7 +65,7 @@ func TestExplainGenerator(t *testing.T) { }) t.Run("Append with newline", func(t *testing.T) { - gen := &lib.ExplainGenerator{ErrorName: "TestError"} + gen := lib.NewExplainGeneratorForError("TestError") gen.Add("This is a simple error explanation.\n") gen.Add("This is another error message.") From 6b8ad0fc5e2abea9b2d0964e8a443e7c94902ec4 Mon Sep 17 00:00:00 2001 From: Ned Palacios Date: Sat, 3 Feb 2024 22:37:15 +0800 Subject: [PATCH 07/24] fix: create outputgen test --- output_gen.go | 13 ++-- output_gen_test.go | 149 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 156 insertions(+), 6 deletions(-) diff --git a/output_gen.go b/output_gen.go index caff2eb..f8bb421 100644 --- a/output_gen.go +++ b/output_gen.go @@ -24,7 +24,12 @@ func (gen *OutputGenerator) _break() { gen.Builder.WriteByte('\n') } -func (gen *OutputGenerator) ExpGen(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()) } @@ -33,7 +38,7 @@ func (gen *OutputGenerator) ExpGen(level int, explain *ExplainGenerator) { for sectionName, exp := range explain.Sections { gen._break() gen.Heading(level+1, sectionName) - gen.ExpGen(level+1, exp) + gen.FromExplanation(level+1, exp) } } else { gen._break() @@ -79,7 +84,7 @@ func (gen *OutputGenerator) Generate(explain *ExplainGenerator, bugFix *BugFixGe gen.Heading(1, explain.ErrorName) } - gen.ExpGen(1, explain) + gen.FromExplanation(1, explain) if gen.GenAfterExplain != nil { gen.GenAfterExplain(gen) } @@ -194,7 +199,7 @@ func (gen *OutputGenerator) Generate(explain *ExplainGenerator, bugFix *BugFixGe } } else { - gen.Writeln("Nothing to fix") + gen.Writeln("No bug fixes found for this error.") } return strings.TrimSpace(gen.Builder.String()) diff --git a/output_gen_test.go b/output_gen_test.go index 19c2721..94bf30d 100644 --- a/output_gen_test.go +++ b/output_gen_test.go @@ -1,3 +1,148 @@ -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\nxyz = \"test\""), 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 +xyz = "test" +` + "```" + `` + + 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 +xyz = "test" +` + "```" + `` + 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) + } + }) +} From 2895e85d8f153070d371d3164eda86923fbe81ce Mon Sep 17 00:00:00 2001 From: Ned Palacios Date: Sat, 3 Feb 2024 23:47:56 +0800 Subject: [PATCH 08/24] refactor: expose outputgen Break() --- errgoengine.go | 2 +- output_gen.go | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/errgoengine.go b/errgoengine.go index fc13e1b..05eec43 100644 --- a/errgoengine.go +++ b/errgoengine.go @@ -163,7 +163,7 @@ func (e *ErrgoEngine) Translate(template *CompiledErrorTemplate, contextData *Co gen.Builder.WriteByte('^') } - gen._break() + gen.Break() gen.WriteLines(endLines...) gen.Writeln("```") } diff --git a/output_gen.go b/output_gen.go index f8bb421..66424c2 100644 --- a/output_gen.go +++ b/output_gen.go @@ -20,7 +20,7 @@ func (gen *OutputGenerator) Heading(level int, text string) { gen.Writeln(text) } -func (gen *OutputGenerator) _break() { +func (gen *OutputGenerator) Break() { gen.Builder.WriteByte('\n') } @@ -36,12 +36,12 @@ func (gen *OutputGenerator) FromExplanation(level int, explain *ExplainGenerator if explain.Sections != nil { for sectionName, exp := range explain.Sections { - gen._break() + gen.Break() gen.Heading(level+1, sectionName) gen.FromExplanation(level+1, exp) } } else { - gen._break() + gen.Break() } } @@ -50,7 +50,7 @@ func (gen *OutputGenerator) Writeln(str string, d ...any) { return } gen.Write(str, d...) - gen._break() + gen.Break() } func (gen *OutputGenerator) Write(str string, d ...any) { @@ -68,7 +68,7 @@ func (gen *OutputGenerator) Write(str string, d ...any) { func (gen *OutputGenerator) WriteLines(lines ...string) { for _, line := range lines { if len(line) == 0 { - gen._break() + gen.Break() } else { gen.Writeln(line) } @@ -140,7 +140,7 @@ func (gen *OutputGenerator) Generate(explain *ExplainGenerator, bugFix *BugFixGe gen.Write("- ") } if len(origLine) == 0 { - gen._break() + gen.Break() } else { gen.Writeln(origLine) } @@ -161,7 +161,7 @@ func (gen *OutputGenerator) Generate(explain *ExplainGenerator, bugFix *BugFixGe // write only if the line is not the last line if startLine+i < origAfterLine { gen.Write(modifiedLine) - gen._break() + gen.Break() } continue } @@ -170,7 +170,7 @@ func (gen *OutputGenerator) Generate(explain *ExplainGenerator, bugFix *BugFixGe gen.Write(" ") } gen.Write(modifiedLine) - gen._break() + gen.Break() } } @@ -194,7 +194,7 @@ func (gen *OutputGenerator) Generate(explain *ExplainGenerator, bugFix *BugFixGe } if sIdx < len(bugFix.Suggestions)-1 { - gen._break() + gen.Break() } } From 0a775d0fc166cb9d371ac4d78a8d3b97a56e9af6 Mon Sep 17 00:00:00 2001 From: Ned Palacios Date: Sat, 3 Feb 2024 23:48:16 +0800 Subject: [PATCH 09/24] feat: add new tests, update output_gen_test.go --- output_gen_test.go | 247 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 244 insertions(+), 3 deletions(-) diff --git a/output_gen_test.go b/output_gen_test.go index 94bf30d..49374f9 100644 --- a/output_gen_test.go +++ b/output_gen_test.go @@ -10,7 +10,7 @@ import ( func TestOutputGenerator(t *testing.T) { parser := sitter.NewParser() - doc, err := lib.ParseDocument("program.test", strings.NewReader("a = xyz\nb = 123\nxyz = \"test\""), parser, lib.TestLanguage, nil) + doc, err := lib.ParseDocument("program.test", strings.NewReader("a = xyz\nb = 123"), parser, lib.TestLanguage, nil) if err != nil { t.Fatal(err) } @@ -55,7 +55,249 @@ In line 1, replace ` + "`xyz`" + ` with ` + "`\"test\"`" + `. - a = xyz + a = "test" b = 123 -xyz = "test" +` + "```" + `` + + 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 { @@ -98,7 +340,6 @@ In line 1, replace ` + "`xyz`" + ` with ` + "`\"test\"`" + `. - a = xyz + a = "test" b = 123 -xyz = "test" ` + "```" + `` if output != expected { t.Errorf("exp %s, got %s", expected, output) From f57686d8465257d574d59b8f57b44ce63b8e9e65 Mon Sep 17 00:00:00 2001 From: Ned Palacios Date: Mon, 5 Feb 2024 01:21:28 +0800 Subject: [PATCH 10/24] fix: update description for symbol not found error --- error_templates/java/symbol_not_found_error.go | 2 +- .../test_files/symbol_not_found_error_missing_variable/test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/error_templates/java/symbol_not_found_error.go b/error_templates/java/symbol_not_found_error.go index b624f5a..8b96a0d 100644 --- a/error_templates/java/symbol_not_found_error.go +++ b/error_templates/java/symbol_not_found_error.go @@ -56,7 +56,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": 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); From c6b548bad20280f5d9728735acf1a9cd9c56c4a4 Mon Sep 17 00:00:00 2001 From: Ned Palacios Date: Mon, 5 Feb 2024 01:24:18 +0800 Subject: [PATCH 11/24] refactor: add separate function for parsing files without using stacktrace structure --- errgoengine.go | 43 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 39 insertions(+), 4 deletions(-) diff --git a/errgoengine.go b/errgoengine.go index 05eec43..443c17c 100644 --- a/errgoengine.go +++ b/errgoengine.go @@ -175,13 +175,15 @@ func (e *ErrgoEngine) Translate(template *CompiledErrorTemplate, contextData *Co 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 @@ -227,3 +229,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) +} From 4e8c548c4b4a827d0061175a434183eba109b67d Mon Sep 17 00:00:00 2001 From: Ned Palacios Date: Mon, 5 Feb 2024 01:24:59 +0800 Subject: [PATCH 12/24] fix: revert to old name upon finishing analyzing in SymbolAnalyzer --- symbol_analyzer.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 } From cb42a70ee95919ce3316c5781fe7bd052dae3080 Mon Sep 17 00:00:00 2001 From: Ned Palacios Date: Mon, 5 Feb 2024 01:25:23 +0800 Subject: [PATCH 13/24] feat: support for parsing local files in java --- languages/java/language.go | 59 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 3 deletions(-) diff --git a/languages/java/language.go b/languages/java/language.go index 2de7e20..eb75fc1 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,58 @@ func (an *javaAnalyzer) FallbackSymbol() lib.Symbol { } func (an *javaAnalyzer) FindSymbol(name string) lib.Symbol { - sym, _ := builtinTypesStore.FindByName(name) - return sym + 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.FindSymbol(name) +} + +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 { From a8896ef0ac2ca24604c3545e15081827ae2d8bb9 Mon Sep 17 00:00:00 2001 From: Ned Palacios Date: Mon, 5 Feb 2024 01:26:08 +0800 Subject: [PATCH 14/24] feat: embed engine fs inside Store --- errgoengine.go | 1 + store.go | 3 +++ 2 files changed, 4 insertions(+) diff --git a/errgoengine.go b/errgoengine.go index 443c17c..4c645e8 100644 --- a/errgoengine.go +++ b/errgoengine.go @@ -57,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)) 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 { From f4253b2cd98c0fe57b06d9720eda3147f4269aba Mon Sep 17 00:00:00 2001 From: Ned Palacios Date: Mon, 5 Feb 2024 01:26:20 +0800 Subject: [PATCH 15/24] feat: support for symbol not found outside class --- .../java/symbol_not_found_error.go | 124 +++++++++++++++--- .../Test.java | 6 + .../Triangle.java | 5 + .../test.txt | 35 +++++ 4 files changed, 151 insertions(+), 19 deletions(-) create mode 100644 error_templates/java/test_files/symbol_not_found_error_missing_method_imported/Test.java create mode 100644 error_templates/java/test_files/symbol_not_found_error_missing_method_imported/Triangle.java create mode 100644 error_templates/java/test_files/symbol_not_found_error_missing_method_imported/test.txt diff --git a/error_templates/java/symbol_not_found_error.go b/error_templates/java/symbol_not_found_error.go index 8b96a0d..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 @@ -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/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 ++ } +} + +``` From b27f33414652d7960b83177dbbc5f5bb826de248 Mon Sep 17 00:00:00 2001 From: Ned Palacios Date: Sun, 10 Mar 2024 15:16:26 +0800 Subject: [PATCH 16/24] feat: add version support in documents --- source.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/source.go b/source.go index e920d87..d7f0754 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 @@ -537,6 +538,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 +577,6 @@ func ParseDocument(path string, r io.Reader, parser *sitter.Parser, selectLang * Language: selectLang, Contents: string(inputBytes), Tree: tree, + Version: version, }, nil } From 9b5509fadfdf0b8e8b9887d85af89084f81f2f27 Mon Sep 17 00:00:00 2001 From: Ned Palacios Date: Sun, 10 Mar 2024 15:28:18 +0800 Subject: [PATCH 17/24] fix: skip parsing document if content is the same --- errgoengine.go | 5 +++++ source.go | 8 ++++++++ 2 files changed, 13 insertions(+) diff --git a/errgoengine.go b/errgoengine.go index 4c645e8..80c5f6c 100644 --- a/errgoengine.go +++ b/errgoengine.go @@ -200,6 +200,11 @@ func ParseFiles(contextData *ContextData, defaultLanguage *Language, files fs.Re // 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 { diff --git a/source.go b/source.go index d7f0754..0569a3a 100644 --- a/source.go +++ b/source.go @@ -504,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()) } From 5534e94d0b4c982d3cdce8dc96fb82b14728589f Mon Sep 17 00:00:00 2001 From: Ned Palacios Date: Wed, 13 Mar 2024 23:54:46 +0800 Subject: [PATCH 18/24] fix: getSpaceBoundaryIndiv stack overflow, add docs --- error_templates/java/java.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/error_templates/java/java.go b/error_templates/java/java.go index 74a6d54..cf1a45c 100644 --- a/error_templates/java/java.go +++ b/error_templates/java/java.go @@ -184,15 +184,21 @@ 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 + // check if the index next to the current index is a space + // if it is, then we go to the reverse direction to get the nearest space + if isSpace(line, idx+1) { newIdx := getSpaceBoundaryIndiv(line, idx, spaceComputeDirectionRight) return newIdx } From 85e3035b32045983d81e3ffd5fc7b0d8f98672de Mon Sep 17 00:00:00 2001 From: Ned Palacios Date: Thu, 14 Mar 2024 00:38:48 +0800 Subject: [PATCH 19/24] feat: add SyntaxNode#TreeCursor() method --- node.go | 4 ++++ 1 file changed, 4 insertions(+) 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, From 511eacbd0a4dc738f8af6ada566090bbfa52a40f Mon Sep 17 00:00:00 2001 From: Ned Palacios Date: Thu, 14 Mar 2024 00:39:07 +0800 Subject: [PATCH 20/24] feat(error_templates/java): implement edge case for expected error --- .../java/identifier_expected_error.go | 113 +++++++++++++++--- .../Main.java | 5 + .../test.txt | 33 +++++ utils/levenshtein/levenshtein.go | 89 ++++++++++++++ 4 files changed, 225 insertions(+), 15 deletions(-) create mode 100644 error_templates/java/test_files/identifier_expected_error_complex_2/Main.java create mode 100644 error_templates/java/test_files/identifier_expected_error_complex_2/test.txt create mode 100644 utils/levenshtein/levenshtein.go 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 +} From bd33cc1a75854476236ad17104b333cdb24a9e54 Mon Sep 17 00:00:00 2001 From: Ned Palacios Date: Thu, 14 Mar 2024 00:43:56 +0800 Subject: [PATCH 21/24] fix(error_templates/java): remove negative idx check in getSpaceBoundaryIndiv --- error_templates/java/java.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/error_templates/java/java.go b/error_templates/java/java.go index cf1a45c..7cefec4 100644 --- a/error_templates/java/java.go +++ b/error_templates/java/java.go @@ -196,13 +196,6 @@ func getSpaceBoundaryIndiv(line string, idx int, defaultDirection spaceComputeDi // if idx is less than 0, return the current index if idx-1 < 0 { - // check if the index next to the current index is a space - // if it is, then we go to the reverse direction to get the nearest space - if isSpace(line, idx+1) { - newIdx := getSpaceBoundaryIndiv(line, idx, spaceComputeDirectionRight) - return newIdx - } - return idx } From 57ce740a1aacad28b2b3195592aa410aa02e790e Mon Sep 17 00:00:00 2001 From: Ned Palacios Date: Thu, 14 Mar 2024 00:44:13 +0800 Subject: [PATCH 22/24] fix(languages/java): avoid infinite stack overflow for FindSymbol --- languages/java/language.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/languages/java/language.go b/languages/java/language.go index eb75fc1..d40d740 100644 --- a/languages/java/language.go +++ b/languages/java/language.go @@ -51,6 +51,15 @@ func (an *javaAnalyzer) FallbackSymbol() lib.Symbol { } func (an *javaAnalyzer) FindSymbol(name string) lib.Symbol { + 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 @@ -86,7 +95,7 @@ func (an *javaAnalyzer) FindSymbol(name string) lib.Symbol { } // if parsed, find the symbol again - return an.FindSymbol(name) + return an.findSymbolWithRetries(name, retry+1) } func (an *javaAnalyzer) markAsUnresolved(name string) lib.Symbol { From 93cc92e957032956a2fff4566c67d11d38fb54cb Mon Sep 17 00:00:00 2001 From: Ned Palacios Date: Thu, 14 Mar 2024 08:24:47 +0800 Subject: [PATCH 23/24] fix(error_templates/java): do not include "correct" words (distance < 1) --- error_templates/java/identifier_expected_error.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/error_templates/java/identifier_expected_error.go b/error_templates/java/identifier_expected_error.go index d602e70..20bb2c7 100644 --- a/error_templates/java/identifier_expected_error.go +++ b/error_templates/java/identifier_expected_error.go @@ -64,7 +64,8 @@ var IdentifierExpectedError = lib.ErrorTemplate{ for _, token := range tokens { for ltIdx, lineToken := range lineTokens { - if levenshtein.ComputeDistance(token, lineToken) <= 3 { + distance := levenshtein.ComputeDistance(token, lineToken) + if distance >= 1 && distance <= 3 { wordToReplace = lineToken nearestWord = token From 5f2d3102ac976ff6427c405008861805e8be50e6 Mon Sep 17 00:00:00 2001 From: Ned Palacios Date: Thu, 14 Mar 2024 08:25:15 +0800 Subject: [PATCH 24/24] fix(error_templates/java): include access modifiers in typo --- .../java/identifier_expected_error.go | 16 +++++++++-- .../test.txt | 2 +- .../Main.java | 3 +++ .../identifier_expected_error_public/test.txt | 27 +++++++++++++++++++ utils/slice/slice.go | 10 +++++++ 5 files changed, 55 insertions(+), 3 deletions(-) create mode 100644 error_templates/java/test_files/identifier_expected_error_public/Main.java create mode 100644 error_templates/java/test_files/identifier_expected_error_public/test.txt create mode 100644 utils/slice/slice.go diff --git a/error_templates/java/identifier_expected_error.go b/error_templates/java/identifier_expected_error.go index 20bb2c7..33fcab6 100644 --- a/error_templates/java/identifier_expected_error.go +++ b/error_templates/java/identifier_expected_error.go @@ -6,6 +6,7 @@ import ( lib "github.com/nedpals/errgoengine" "github.com/nedpals/errgoengine/utils/levenshtein" + "github.com/nedpals/errgoengine/utils/slice" sitter "github.com/smacker/go-tree-sitter" ) @@ -14,6 +15,7 @@ type identifiedExpectedReasonKind int const ( identifierExpectedReasonUnknown identifiedExpectedReasonKind = 0 identifierExpectedReasonClassInterfaceEnum identifiedExpectedReasonKind = iota + identifierExpectedReasonTypo identifiedExpectedReasonKind = iota ) type identifierExpectedFixKind int @@ -48,8 +50,11 @@ var IdentifierExpectedError = lib.ErrorTemplate{ // TODO: check if node is parsable if iCtx.reasonKind == identifierExpectedReasonClassInterfaceEnum { + accessTokens := []string{"public"} + statementTokens := []string{"class", "interface", "enum"} + // use levenstein distance to check if the word is a typo - tokens := []string{"class", "interface", "enum"} + tokens := append(accessTokens, statementTokens...) // get the nearest word nearestWord := "" @@ -103,6 +108,11 @@ var IdentifierExpectedError = lib.ErrorTemplate{ } 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(), @@ -120,6 +130,8 @@ var IdentifierExpectedError = lib.ErrorTemplate{ 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.") } @@ -152,7 +164,7 @@ var IdentifierExpectedError = lib.ErrorTemplate{ }) 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). + s.AddStep("Change `%s` to `%s`.", ctx.typoWord, ctx.wordForTypo). AddFix(lib.FixSuggestion{ NewText: ctx.wordForTypo, StartPosition: cd.MainError.Nearest.StartPosition(), 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 index 33d09f7..91b55f3 100644 --- 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 @@ -24,7 +24,7 @@ public clas Main { ``` ## Steps to fix ### Correct the typo -Change `clas` to `class` to properly declare the class. +Change `clas` to `class`. ```diff - public clas Main { + public class Main { 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/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 +}