diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml new file mode 100644 index 0000000..d95aff9 --- /dev/null +++ b/.github/workflows/test.yaml @@ -0,0 +1,17 @@ +name: Test + +on: [push] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.20' + + - name: Test + run: go test -v ./... \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cb629a9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,25 @@ +# If you prefer the allow list template instead of the deny list, see community template: +# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore +# +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ + +# Go workspace file +go.work + +cmd/noyoda/noyoda + +.idea/ \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e3108d8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Emin Umut Gerçek + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..72a5296 --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# `noyoda` Go Linter + +`noyoda` is a Go linter, which reports Yoda Conditions. + +# What are Yoda Conditions? +Yoda Condition style involves writing conditions expression/statements in a way that resembles the Yoda from Star Wars. +Instead of writing `if x == 10`, a Yoda Condition would be `if 10 == x`. +This approach aims to prevent unintended assignment by breaking the program at compile time. +For instance, it would prevent code like `if 10 = x`, which is invalid and would not compile. + +# Why Yoda Conditions are Unnecessary in Go + +`if x = 10 { ... } ` is invalid syntax in Go. + +# Install and Usage + +```bash +go install github.com/eugercek/noyoda/cmd/noyoda +cd mycode # go to your code's main package +noyoda ./... +``` + +# Roadmap + +- [ ] switch case check +- [ ] `const` Check +- [ ] flag for `const` check +- [ ] Auto fix +- [ ] Run tests for comprehensive set of popular go codebases, if there are many maybe Open a PR to golangci-lint \ No newline at end of file diff --git a/cmd/noyoda/main.go b/cmd/noyoda/main.go new file mode 100644 index 0000000..52ac34a --- /dev/null +++ b/cmd/noyoda/main.go @@ -0,0 +1,10 @@ +package main + +import ( + "golang.org/x/tools/go/analysis/singlechecker" +) +import "github.com/eugercek/noyoda" + +func main() { + singlechecker.Main(noyoda.NewAnalyzer()) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..aaff589 --- /dev/null +++ b/go.mod @@ -0,0 +1,10 @@ +module github.com/eugercek/noyoda + +go 1.20 + +require golang.org/x/tools v0.12.0 + +require ( + golang.org/x/mod v0.12.0 // indirect + golang.org/x/sys v0.11.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..14a6610 --- /dev/null +++ b/go.sum @@ -0,0 +1,7 @@ +golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= +golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/tools v0.12.0 h1:YW6HUoUmYBpwSgyaGaZq1fHjrBjX1rlpZ54T6mu2kss= +golang.org/x/tools v0.12.0/go.mod h1:Sc0INKfu04TlqNoRA1hgpFZbhYXHPr4V5DzpSBTPqQM= diff --git a/noyoda.go b/noyoda.go new file mode 100644 index 0000000..03fa32c --- /dev/null +++ b/noyoda.go @@ -0,0 +1,66 @@ +package noyoda + +import ( + "fmt" + "go/ast" + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" +) + +const doc = `remove yoda conditions + +Yoda condition is a expression/statement style to prevent accidental assignments like if x = 3 instead if x == 3. +Go does not needs this check. +` + +func NewAnalyzer() *analysis.Analyzer { + return &analysis.Analyzer{ + Name: "noyoda", + Doc: doc, + Run: run, + Requires: []*analysis.Analyzer{inspect.Analyzer}, + } +} + +func run(pass *analysis.Pass) (interface{}, error) { + ins := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + + nodeFilter := []ast.Node{ + (*ast.IfStmt)(nil), + } + + ins.Preorder(nodeFilter, func(node ast.Node) { + stmt, ok := node.(*ast.IfStmt) + + if !ok { + fmt.Println("no if") + return + } + + bexpr, ok := stmt.Cond.(*ast.BinaryExpr) + + if !ok { + return + } + + lit, ok := bexpr.X.(*ast.BasicLit) + + if !ok { + return + } + + y, ok := bexpr.Y.(*ast.Ident) + + if !ok { + return + } + + pass.Reportf(node.Pos(), "yoda condition: %s %s %s should be %s %s %s", + lit.Value, bexpr.Op.String(), y.Name, + y.Name, bexpr.Op.String(), lit.Value, + ) + }) + + return nil, nil +} diff --git a/noyoda_test.go b/noyoda_test.go new file mode 100644 index 0000000..91ca031 --- /dev/null +++ b/noyoda_test.go @@ -0,0 +1,18 @@ +package noyoda + +import ( + "golang.org/x/tools/go/analysis/analysistest" + "os" + "testing" +) + +func Test(t *testing.T) { + var TestDir string + cur, _ := os.Getwd() + TestDir = cur + "/testdata" + + t.Run("valid yoda", func(t *testing.T) { + analysistest.Run(t, TestDir, NewAnalyzer()) + }) + +} diff --git a/testdata/noyoda.go b/testdata/noyoda.go new file mode 100644 index 0000000..797f2d7 --- /dev/null +++ b/testdata/noyoda.go @@ -0,0 +1,32 @@ +package testdata + +func IfNoYodaCondition(n int) { + if n == 10 { + return + } + + return +} + +func ElseIfNoYodaCondition(n int) { + if n == 100 { + return + } else if n == 10 { + return + } +} + +func SwitchNoYodaCondition(n int) { + switch { + case n == 10: + return + } +} + +func ConstNoYodaCondition(n int) { + const x = 10 + + if n == x { + return + } +} diff --git a/testdata/yoda.go b/testdata/yoda.go new file mode 100644 index 0000000..dacc41d --- /dev/null +++ b/testdata/yoda.go @@ -0,0 +1,30 @@ +package testdata + +func IfYodaCondition(n int) { + if 10 == n { // want "yoda condition: 10 == n should be n == 10" + return + } +} + +func ElseIfYodaCondition(n int) { + if n == 100 { + return + } else if 10 == n { // want "yoda condition: 10 == n should be n == 10" + return + } +} + +func SwitchYodaCondition(n int) { + switch { + case 10 == n: + return + } +} + +func ConstYodaCondition(n int) { + const x = 10 + + if n == x { + return + } +}