diff --git a/internal/lint.go b/internal/lint.go index c821b21..c33fcab 100644 --- a/internal/lint.go +++ b/internal/lint.go @@ -81,6 +81,12 @@ func (e *Engine) Run(filename string) ([]Issue, error) { } filtered = append(filtered, unnecessaryElseIssues...) + unusedFunc, err := e.detectUnusedFunctions(tempFile) + if err != nil { + return nil, fmt.Errorf("error detecting unused functions: %w", err) + } + filtered = append(filtered, unusedFunc...) + // map issues back to .gno file if necessary if strings.HasSuffix(filename, ".gno") { for i := range filtered { diff --git a/internal/lint_test.go b/internal/lint_test.go index 810e16d..b64da07 100644 --- a/internal/lint_test.go +++ b/internal/lint_test.go @@ -45,6 +45,10 @@ import ( "strings" ) +func foo() { + println("unused") +} + func main() { x := 1 y := "unused" @@ -55,6 +59,7 @@ func main() { "x declared and not used", "y declared and not used", `"strings" imported and not used`, + "function foo is declared and not used", }, }, } diff --git a/internal/rule_set.go b/internal/rule_set.go index 0d3488d..ad6ea2a 100644 --- a/internal/rule_set.go +++ b/internal/rule_set.go @@ -6,6 +6,9 @@ import ( "go/token" ) +// detectUnnecessaryElse detects unnecessary else blocks. +// This rule considers an else block unnecessary if the if block ends with a return statement. +// In such cases, the else block can be removed and the code can be flattened to improve readability. func (e *Engine) detectUnnecessaryElse(filename string) ([]Issue, error) { fset := token.NewFileSet() node, err := parser.ParseFile(fset, filename, nil, parser.ParseComments) @@ -42,3 +45,49 @@ func (e *Engine) detectUnnecessaryElse(filename string) ([]Issue, error) { return issues, nil } + +// detectUnusedFunctions detects functions that are declared but never used. +// This rule reports all unused functions except for the following cases: +// 1. The main function: It's considered "used" as it's the entry point of the program. +// 2. The init function: It's used for package initialization and runs without explicit calls. +// 3. Exported functions: Functions starting with a capital letter are excluded as they might be used in other packages. +// +// This rule helps in code cleanup and improves maintainability. +func (e *Engine) detectUnusedFunctions(filename string) ([]Issue, error) { + fset := token.NewFileSet() + node, err := parser.ParseFile(fset, filename, nil, parser.ParseComments) + if err != nil { + return nil, err + } + + declaredFuncs := make(map[string]*ast.FuncDecl) + calledFuncs := make(map[string]bool) + + ast.Inspect(node, func(n ast.Node) bool { + switch x := n.(type) { + case *ast.FuncDecl: + declaredFuncs[x.Name.Name] = x + case *ast.CallExpr: + if ident, ok := x.Fun.(*ast.Ident); ok { + calledFuncs[ident.Name] = true + } + } + return true + }) + + var issues []Issue + for funcName, funcDecl := range declaredFuncs { + if !calledFuncs[funcName] && funcName != "main" && funcName != "init" && !ast.IsExported(funcName) { + issue := Issue{ + Rule: "unused-function", + Filename: filename, + Start: fset.Position(funcDecl.Pos()), + End: fset.Position(funcDecl.End()), + Message: "function " + funcName + " is declared but not used", + } + issues = append(issues, issue) + } + } + + return issues, nil +} diff --git a/internal/rule_set_test.go b/internal/rule_set_test.go index cb04628..21a4cd8 100644 --- a/internal/rule_set_test.go +++ b/internal/rule_set_test.go @@ -92,3 +92,108 @@ func example2() int { }) } } + +func TestDetectUnusedFunctions(t *testing.T) { + tests := []struct { + name string + code string + expected int + }{ + { + name: "No unused functions", + code: ` +package main + +func main() { + helper() +} + +func helper() { + println("do something") +}`, + expected: 0, + }, + { + name: "One unused function", + code: ` +package main + +func main() { + println("1") +} + +func unused() { + println("do something") +}`, + expected: 1, + }, + { + name: "Multiple unused functions", + code: ` +package main + +func main() { + used() +} + +func used() { + // this function is called +} + +func unused1() { + // this function is never called +} + +func unused2() { + // this function is also never called +}`, + expected: 2, + }, + // { + // name: "Unused method", + // code: ` + // package main + + // type MyStruct struct{} + + // func (m MyStruct) Used() { + // // this method is used + // } + + // func (m MyStruct) Unused() { + // // this method is never used + // } + + // func main() { + // m := MyStruct{} + // m.Used() + // }`, + // expected: 1, + // }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + tmpDir, err := os.MkdirTemp("", "lint-test") + require.NoError(t, err) + defer os.RemoveAll(tmpDir) + + tmpfile := filepath.Join(tmpDir, "test.go") + err = os.WriteFile(tmpfile, []byte(tt.code), 0o644) + require.NoError(t, err) + + engine := &Engine{} + issues, err := engine.detectUnusedFunctions(tmpfile) + require.NoError(t, err) + + assert.Equal(t, tt.expected, len(issues), "Number of detected unused functions doesn't match expected") + + if len(issues) > 0 { + for _, issue := range issues { + assert.Equal(t, "unused-function", issue.Rule) + assert.Contains(t, issue.Message, "function", "is declared but not used") + } + } + }) + } +} diff --git a/testdata/main.gno b/testdata/main.gno index 00bd4a9..23193f3 100644 --- a/testdata/main.gno +++ b/testdata/main.gno @@ -1,12 +1,15 @@ package main -import "fmt" +import "strings" -func foo(x bool) int { +func foo(x, y bool) int { if x { return 1 } else { - return 2 + if y { + return 2 + } + return 3 } }