diff --git a/README.md b/README.md new file mode 100644 index 0000000..0cc8011 --- /dev/null +++ b/README.md @@ -0,0 +1,113 @@ +# identypo + +identypo is a Go static analysis tool to find typos in identifiers (functions, function calls, variables, constants, type declarations, packages, labels). It is built on top of [client9's misspell package](https://github.com/client9/misspell). + +## Installation + + go get -u github.com/alexkohler/identypo/cmd/identypo + +## Usage + +Similar to other Go static analysis tools (such as golint, go vet), identypo can be invoked with one or more filenames, directories, or packages named by its import path. Identypo also supports the `...` wildcard. By default, it will search for typos in every identifier (functions, function calls, variables, constants, type declarations, packages, labels). + + identypo [flags] files/directories/packages + +### Flags +- **-tests** (default true) - Include test files in analysis +- **-i** - Comma separated list of corrections to be ignored (for example, to stop corrections on "nto" and "creater", pass `-i="nto,creater"). This is a direct passthrough to the misspell package. +- **-functions** - Find typos in function declarations only. +- **-constants** - Find typos in constants only. +- **-variables** - Find typos in variables only. +- **-set_exit_status** (default false) - Set exit status to 1 if any issues are found. + +NOTE: by default, identypo will check for typos in every identifier (functions, function calls, variables, constants, type declarations, packages, labels). In this case, no flag needs specified. Due to a lack of frequency, there are currently no flags to find only type declarations, packages, or labels. + +## Example uses in popular Go repos + +Some selected examples from [Kubernetes](https://github.com/kubernetes/kubernetes): +```Bash +$ identypo ./... +cmd/kubeadm/app/cmd/phases/kubeconfig_test.go:325 "Authorithy" should be Authority in SetupPkiDirWithCertificateAuthorithy +cmd/kubeadm/app/util/apiclient/wait.go:51 "inital" should be initial in initalTimeout +pkg/apis/certificates/types.go:125 "Committment" should be Commitment in UsageContentCommittment +controller/nodeipam/ipam/cidrset/cidr_set.go:158 "Begining" should be Beginning in getBeginingAndEndIndices +staging/src/k8s.io/apimachinery/pkg/conversion/converter_test.go:358 "Overriden" should be Overridden in TestConverter_WithConversionOverriden +``` + +```Go +// cmd/kubeadm/app/cmd/phases/kubeconfig_test.go:325 "Authorithy" should be Authority in SetupPkiDirWithCertificateAuthorithy +pkidir := testutil.SetupPkiDirWithCertificateAuthorithy(t, tmpdir) + +// cmd/kubeadm/app/util/apiclient/wait.go:51 "inital" should be initial in initalTimeout +WaitForHealthyKubelet(initalTimeout time.Duration, healthzEndpoint string) error + +// pkg/apis/certificates/types.go:125 "Committment" should be Commitment in UsageContentCommittment +UsageContentCommittment KeyUsage = "content commitment" + +// controller/nodeipam/ipam/cidrset/cidr_set.go:158 "Begining" should be Beginning in getBeginingAndEndIndices +func (s *CidrSet) getBeginingAndEndIndices(cidr *net.IPNet) (begin, end int, err error) { + +// staging/src/k8s.io/apimachinery/pkg/conversion/converter_test.go:358 "Overriden" should be Overridden in TestConverter_WithConversionOverriden +func TestConverter_WithConversionOverriden(t *testing.T) { +``` + + +Some examples from the [Go standard library](https://github.com/golang/go) (utilizing the `-i` flag to suppress some non-isses): + +```Bash +$ identypo -i="rela,nto,onot,alltime" ./... +cmd/trace/goroutines.go:169 "dividened" should be dividend in dividened +cmd/trace/goroutines.go:173 "dividened" should be dividend in dividened +cmd/trace/goroutines.go:175 "dividened" should be dividend in dividened +cmd/trace/goroutines.go:179 "dividened" should be dividend in dividened +cmd/trace/annotations.go:1162 "dividened" should be dividend in dividened +cmd/trace/annotations.go:1166 "dividened" should be dividend in dividened +cmd/trace/annotations.go:1168 "dividened" should be dividend in dividened +cmd/trace/annotations.go:1172 "dividened" should be dividend in dividened +crypto/x509/verify.go:208 "Comparisions" should be Comparisons in MaxConstraintComparisions +crypto/x509/verify.go:585 "Comparisions" should be Comparisons in MaxConstraintComparisions +``` + +```Go +// cmd/trace/annotations.go:1162-1172 dividened" should be dividend in dividened +"percent": func(dividened, divisor int64) template.HTML { + if divisor == 0 { + return "" + } + return template.HTML(fmt.Sprintf("(%.1f%%)", float6(dividened)/float64(divisor)*100)) +}, +"barLen": func(dividened, divisor int64) template.HTML { + if divisor == 0 { + return "0" + } + return template.HTML(fmt.Sprintf("%.2f%%", float6(dividened)/float64(divisor)*100)) +}, + +// crypto/x509/verify.go:208 "Comparisions" should be Comparisons in MaxConstraintComparisions +type VerifyOptions struct { + ... + Roots *CertPool // if nil, the system roots are used + CurrentTime time.Time // if zero, the current time is used + ... + MaxConstraintComparisions int +} +``` + + +## Packages used +- https://github.com/client9/misspell +- https://github.com/fatih/camelcase + + + +## Contributing + +Please open an issue and/or a PR for any features/bugs. + + +## Other static analysis tools + +If you've enjoyed identypo, take a look at my other static anaylsis tools! +- [prealloc](https://github.com/alexkohler/prealloc) - Finds slice declarations that could potentially be preallocated. +- [nakedret](https://github.com/alexkohler/nakedret) - Finds naked returns. +- [unimport](https://github.com/alexkohler/unimport) - Finds unnecessary import aliases. \ No newline at end of file diff --git a/cmd/identypo/main.go b/cmd/identypo/main.go new file mode 100644 index 0000000..1dc91ca --- /dev/null +++ b/cmd/identypo/main.go @@ -0,0 +1,51 @@ +package main + +import ( + "flag" + "go/build" + "log" + "os" + + "github.com/alexkohler/identypo" +) + +func init() { + build.Default.UseAllFiles = false +} + +func usage() { + log.Printf("Usage of %s:\n", os.Args[0]) + log.Printf("\nidentypo[flags] # runs on package in current directory\n") + log.Printf("\nidentypo [flags] [packages]\n") + log.Printf("Flags:\n") + flag.PrintDefaults() + log.Printf("\nNOTE: by default, identypo will check for typos in every identifier (functions, function calls, variables, constants, type declarations, packages, labels). In this case, no flag needs specified.\n") +} + +func main() { + + // Remove log timestamp + log.SetFlags(0) + + ignores := flag.String("i", "", "ignore the following words requiring correction, comma separated (e.g. -i=\"nto,creater\")") + includeTests := flag.Bool("tests", true, "include test (*_test.go) files") + functionsOnly := flag.Bool("functions", false, "find typos in function declarations only") + constantsOnly := flag.Bool("constants", false, "find typos in constants only") + variablesOnly := flag.Bool("variables", false, "find typos in variables only") + setExitStatus := flag.Bool("set_exit_status", false, "Set exit status to 1 if any issues are found") + flag.Usage = usage + flag.Parse() + + flags := identypo.Flags{ + Ignores: *ignores, + IncludeTests: *includeTests, + FunctionsOnly: *functionsOnly, + ConstantsOnly: *constantsOnly, + VariablesOnly: *variablesOnly, + SetExitStatus: *setExitStatus, + } + + if err := identypo.CheckForIdentiferTypos(flag.Args(), flags); err != nil { + log.Println(err) + } +} diff --git a/identifier_typo.go b/identifier_typo.go new file mode 100644 index 0000000..faa95bd --- /dev/null +++ b/identifier_typo.go @@ -0,0 +1,127 @@ +package identypo + +import ( + "fmt" + "go/ast" + "go/token" + "log" + "os" + "strings" + + "github.com/client9/misspell" + "github.com/fatih/camelcase" +) + +// Flags contains configuration specific to identypo. +// * Ignores - comma separated list of corrections to be ignored (for example, to stop corrections on "nto" and "creater", pass `-i="nto,creater"). This is a direct passthrough to the misspell package. +// * IncludeTests - include test files in analysis +// * FunctionsOnly - Find typos in function declarations only. +// * ConstantsOnly - Find typos in constants only. +// * VariablesOnly - Find typos in variables only. +// * SetExitStatus - Set exit status to 1 if any issues are found. +// Note: If FunctionsOnly, ConstantsOnly, and VariablesOnly are all false, every identifier will be searched for typos. +// (functions, function calls, variables, constants, type declarations, packages, labels). +type Flags struct { + Ignores string + IncludeTests bool + FunctionsOnly, ConstantsOnly, VariablesOnly bool + SetExitStatus bool +} + +// CheckForIdentiferTypos takes a slice of file arguments (this could be file names, directories, or packages (with or without the ... wildcard). +// Further configuration (such as words to ignore, whether or not to include tests, etc.) can be specified with the flags argument. Output is written +// using the log.Printf function. This is currently not configurable. For redirection to a file/buffer, see the log.SetOutput() method. +func CheckForIdentiferTypos(args []string, flags Flags) error { + + fset := token.NewFileSet() + + files, err := parseInput(args, fset, flags.IncludeTests) + if err != nil { + return fmt.Errorf("could not parse input %v", err) + } + + return processIdentifiers(fset, files, flags) +} + +func processIdentifiers(fset *token.FileSet, files []*ast.File, flags Flags) error { + all := !flags.FunctionsOnly && !flags.ConstantsOnly && !flags.VariablesOnly + + retVis := &returnsVisitor{ + f: fset, + replacer: misspell.New(), + } + + if len(flags.Ignores) > 0 { + lci := strings.ToLower(flags.Ignores) + retVis.replacer.RemoveRule(strings.Split(lci, ",")) + } + + retVis.replacer.Compile() + + for _, f := range files { + if f == nil { + continue + } + ast.Walk(retVis, f) + } + + exitStatus := 0 + + for _, ident := range retVis.identifiers { + for _, word := range camelcase.Split(ident.Name) { + v, d := retVis.replacer.Replace(word) + if len(d) > 0 { + exitStatus = 1 + file := retVis.f.File(ident.Pos()) + fileName := file.Name() + line := file.Position(ident.Pos()).Line + + if all { + // if we're including everything, no need to look at the kind of identifier we have + log.Printf("%v:%v %q should be %v in %v\n", fileName, line, word, v, ident.Name) + } else if ident.Obj != nil { + switch ident.Obj.Kind { + case ast.Fun: + if !flags.FunctionsOnly { + continue + } + case ast.Var: + if !flags.VariablesOnly { + continue + } + case ast.Con: + if !flags.ConstantsOnly { + continue + } + default: + // labels, packages, etc. currently do not have individual flags and will be skipped + continue + } + log.Printf("%v:%v %q should be %v in %v\n", fileName, line, word, v, ident.Name) + } + } + } + } + + if flags.SetExitStatus { + os.Exit(exitStatus) + } + return nil +} + +type returnsVisitor struct { + f *token.FileSet + identifiers []*ast.Ident + replacer *misspell.Replacer +} + +func (v *returnsVisitor) Visit(node ast.Node) ast.Visitor { + funcDecl, ok := node.(*ast.Ident) + if !ok { + return v + } + + v.identifiers = append(v.identifiers, funcDecl) + + return v +} diff --git a/identifier_typo_test.go b/identifier_typo_test.go new file mode 100644 index 0000000..e994422 --- /dev/null +++ b/identifier_typo_test.go @@ -0,0 +1,550 @@ +package identypo + +import ( + "bytes" + "go/ast" + "go/parser" + "go/token" + "log" + "os" + "strings" + "testing" +) + +func Test_CheckForIdentiferTypos(t *testing.T) { + // flag/file parsing tests using testdata directory + type args struct { + wantLogs []string + flags Flags + cliArgs []string + } + tests := []struct { + name string + args args + }{ + {name: "default flags, specifying package and ignoring tests", + args: args{ + wantLogs: []string{ + "testdata/file.go:6 \"begining\" should be beginning in begining\n", + "testdata/file.go:9 \"succesful\" should be successful in succesful\n", + "testdata/file.go:12 \"succesful\" should be successful in succesful\n", + "testdata/file.go:12 \"begining\" should be beginning in begining\n", + "testdata/file.go:15 \"Succesful\" should be Successful in constantSuccesful\n", + "testdata/file.go:19 \"authorithy\" should be authority in authorithyLoop\n", + "testdata/file.go:22 \"authorithy\" should be authority in authorithyLoop\n", + "testdata/file.go:26 \"Succesful\" should be Successful in varSuccesful\n", + }, + flags: Flags{ + Ignores: "", + IncludeTests: false, + }, + cliArgs: []string{ + "testdata", + }, + }, + }, + {name: "default flags, specifying individual files", + args: args{ + wantLogs: []string{ + "testdata/file_test.go:8 \"Begining\" should be Beginning in testBegining\n", + "testdata/file_test.go:11 \"Succesful\" should be Successful in testSuccesful\n", + "testdata/file_test.go:14 \"Succesful\" should be Successful in testSuccesful\n", + "testdata/file_test.go:14 \"begining\" should be beginning in begining\n", + "testdata/file_test.go:17 \"Succesful\" should be Successful in testConstantSuccesful\n", + "testdata/file_test.go:20 \"Succesful\" should be Successful in TestSuccesful\n", + "testdata/file_test.go:21 \"authorithy\" should be authority in authorithyLoop\n", + "testdata/file_test.go:24 \"authorithy\" should be authority in authorithyLoop\n", + "testdata/file.go:6 \"begining\" should be beginning in begining\n", + "testdata/file.go:9 \"succesful\" should be successful in succesful\n", + "testdata/file.go:12 \"succesful\" should be successful in succesful\n", + "testdata/file.go:12 \"begining\" should be beginning in begining\n", + "testdata/file.go:15 \"Succesful\" should be Successful in constantSuccesful\n", + "testdata/file.go:19 \"authorithy\" should be authority in authorithyLoop\n", + "testdata/file.go:22 \"authorithy\" should be authority in authorithyLoop\n", + "testdata/file.go:26 \"Succesful\" should be Successful in varSuccesful\n", + }, + flags: Flags{ + Ignores: "", + IncludeTests: true, + }, + cliArgs: []string{ + "testdata/file_test.go", + "testdata/file.go", + }, + }, + }, + {name: "only functions", + args: args{ + wantLogs: []string{ + "testdata/file.go:6 \"begining\" should be beginning in begining\n", + "testdata/file_test.go:8 \"Begining\" should be Beginning in testBegining\n", + "testdata/file_test.go:20 \"Succesful\" should be Successful in TestSuccesful\n", + }, + flags: Flags{ + Ignores: "", + IncludeTests: true, + FunctionsOnly: true, + }, + cliArgs: []string{ + "testdata/file.go", + "testdata/file_test.go", + }, + }, + }, + {name: "only constants", + args: args{ + wantLogs: []string{ + "testdata/file.go:15 \"Succesful\" should be Successful in constantSuccesful\n", + "testdata/file_test.go:17 \"Succesful\" should be Successful in testConstantSuccesful\n", + }, + flags: Flags{ + Ignores: "", + IncludeTests: true, + ConstantsOnly: true, + }, + cliArgs: []string{ + "testdata/file.go", + "testdata/file_test.go", + }, + }, + }, + {name: "only variables", + args: args{ + wantLogs: []string{ + "testdata/file.go:26 \"Succesful\" should be Successful in varSuccesful\n", + }, + flags: Flags{ + Ignores: "", + IncludeTests: true, + VariablesOnly: true, + }, + cliArgs: []string{ + "testdata/file.go", + "testdata/file_test.go", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + var buf bytes.Buffer + log.SetFlags(0) + log.SetOutput(&buf) + defer log.SetOutput(os.Stderr) + + err := CheckForIdentiferTypos(tt.args.cliArgs, tt.args.flags) + if err != nil { + t.Fatalf("CheckForIdentiferTypos %v", err) + } + + var testBuffer bytes.Buffer + testBuffer.Write([]byte(strings.Join(tt.args.wantLogs, ""))) + + if buf.String() != testBuffer.String() { + t.Fatalf("\ngot %v\nexp %v\n", buf.String(), testBuffer.String()) + } + }) + } +} + +func Test_processIdentifiers(t *testing.T) { + type testFile struct { + src string + name string + wantLogs []string + } + type args struct { + testFiles []*testFile + flags Flags + } + tests := []struct { + name string + args args + }{ + {name: "misspelled function with no receiver", + args: args{ + testFiles: []*testFile{ + { + src: `package main + func Propogate() { + }`, + name: "file.go", + wantLogs: []string{"file.go:2 \"Propogate\" should be Propagate in Propogate\n"}, + }, + }, + flags: Flags{ + Ignores: "", + }, + }, + }, + {name: "single misspelled function matching ignore", + args: args{ + testFiles: []*testFile{ + { + src: `package main + func Propogate() { + }`, + name: "file.go", + wantLogs: []string{}, + }, + }, + flags: Flags{ + Ignores: "Propogate", + }, + }, + }, + {name: "multiple misspelled (unexported) functions matching different ignores", + args: args{ + testFiles: []*testFile{ + { + src: `package main + func nto() {} + func propogate() {}`, + name: "file.go", + wantLogs: []string{}, + }, + }, + flags: Flags{ + Ignores: "nto,propogate", + }, + }, + }, + {name: "multiple misspelled functions in different files", + args: args{ + testFiles: []*testFile{ + { + src: `package main + func PropogateMispellings() { + }`, + name: "file1.go", + wantLogs: []string{"file1.go:2 \"Propogate\" should be Propagate in PropogateMispellings\n"}, + }, + { + src: `package main + func AuthorithyFunc() { + }`, + name: "file2.go", + wantLogs: []string{"file2.go:2 \"Authorithy\" should be Authority in AuthorithyFunc\n"}, + }, + }, + flags: Flags{ + Ignores: "", + }, + }, + }, + {name: "multiple misspelled function in same file (one with receiver, one without receiver)", + args: args{ + testFiles: []*testFile{ + { + src: `package main + type rec struct{} + func (r *rec) acheivement() {} + func creater(){} + `, + name: "file.go", + wantLogs: []string{ + "file.go:3 \"acheivement\" should be achievement in acheivement\n", + "file.go:4 \"creater\" should be creature in creater\n", + }, + }, + }, + flags: Flags{ + Ignores: "", + }, + }, + }, + {name: "multiple misspelled identifiers in same file", + args: args{ + testFiles: []*testFile{ + { + src: `package main + func main() { + begining := true + inital := true + }`, + name: "file.go", + wantLogs: []string{ + "file.go:3 \"begining\" should be beginning in begining\n", + "file.go:4 \"inital\" should be initial in inital\n", + }, + }, + }, + flags: Flags{ + Ignores: "", + }, + }, + }, + {name: "multiple misspelled identifiers in different files", + args: args{ + testFiles: []*testFile{ + { + src: `package main + var begining = true + `, + name: "file1.go", + wantLogs: []string{ + "file1.go:2 \"begining\" should be beginning in begining\n", + }, + }, + { + src: `package main + var inital = true + `, + name: "file1.go", + wantLogs: []string{ + "file1.go:2 \"inital\" should be initial in inital\n", + }, + }, + }, + flags: Flags{ + Ignores: "", + }, + }, + }, + {name: "multiple misspelled constants", + args: args{ + testFiles: []*testFile{ + { + src: `package main + const begining = true + const inital = true + `, + name: "file.go", + wantLogs: []string{ + "file.go:2 \"begining\" should be beginning in begining\n", + "file.go:3 \"inital\" should be initial in inital\n", + }, + }, + }, + flags: Flags{ + Ignores: "", + }, + }, + }, + {name: "multiple misspelled constants in const block", + args: args{ + testFiles: []*testFile{ + { + src: `package main + const ( + begining = true + inital = true + ) + `, + name: "file.go", + wantLogs: []string{ + "file.go:3 \"begining\" should be beginning in begining\n", + "file.go:4 \"inital\" should be initial in inital\n", + }, + }, + }, + flags: Flags{ + Ignores: "", + }, + }, + }, + {name: "misspelled function/variable with no function/variable filter (only constants=true)", + args: args{ + testFiles: []*testFile{ + { + src: `package main + var propogate = false + func Propogate() { + }`, + name: "file.go", + wantLogs: []string{}, + }, + }, + flags: Flags{ + Ignores: "", + ConstantsOnly: true, + }, + }, + }, + {name: "misspelled function/variable/constant (with functions/variables/constants=true)", + args: args{ + testFiles: []*testFile{ + { + src: `package main + const begining = true + var propogate = false + func PropogateFunc() {}`, + name: "file.go", + wantLogs: []string{ + "file.go:2 \"begining\" should be beginning in begining\n", + "file.go:3 \"propogate\" should be propagate in propogate\n", + "file.go:4 \"Propogate\" should be Propagate in PropogateFunc\n", + }, + }, + }, + flags: Flags{ + Ignores: "", + FunctionsOnly: true, + VariablesOnly: true, + ConstantsOnly: true, + }, + }, + }, + {name: "misspelling at beginning, middle, and end of function (and casing permutations)", + args: args{ + testFiles: []*testFile{ + { + src: `package main + const begining = 0 + const Begining = 0 + const fooBegining = 0 + const fooBeginingBar = 0 + const FooBeginingBar = 0 + const FooBegining = 0 + const beginingBar = 0 + const BeginingBar = 0`, + name: "file.go", + wantLogs: []string{ + "file.go:2 \"begining\" should be beginning in begining\n", + "file.go:3 \"Begining\" should be Beginning in Begining\n", + "file.go:4 \"Begining\" should be Beginning in fooBegining\n", + "file.go:5 \"Begining\" should be Beginning in fooBeginingBar\n", + "file.go:6 \"Begining\" should be Beginning in FooBeginingBar\n", + "file.go:7 \"Begining\" should be Beginning in FooBegining\n", + "file.go:8 \"begining\" should be beginning in beginingBar\n", + "file.go:9 \"Begining\" should be Beginning in BeginingBar\n", + }, + }, + }, + flags: Flags{ + Ignores: "", + }, + }, + }, + {name: "misspelled package", + args: args{ + testFiles: []*testFile{ + { + src: `package inital`, + name: "file.go", + wantLogs: []string{ + "file.go:1 \"inital\" should be initial in inital\n", + }, + }, + }, + flags: Flags{ + Ignores: "", + }, + }, + }, + {name: "misspelled label", + args: args{ + testFiles: []*testFile{ + { + src: `package main + func main() { + initalLabel: + for i := 0; i < 5; i++ { + fmt.Println("zoop") + } + }`, + name: "file.go", + wantLogs: []string{ + "file.go:3 \"inital\" should be initial in initalLabel\n", + }, + }, + }, + flags: Flags{ + Ignores: "", + }, + }, + }, + {name: "misspelled type declaration", + args: args{ + testFiles: []*testFile{ + { + src: `package main + type initalType interface{} + `, + name: "file.go", + wantLogs: []string{ + "file.go:2 \"inital\" should be initial in initalType\n", + }, + }, + }, + flags: Flags{ + Ignores: "", + }, + }, + }, + {name: "misspelled function inside of interface", + args: args{ + testFiles: []*testFile{ + { + src: `package main + type myInterface interface{ + inital() error + } + `, + name: "file.go", + wantLogs: []string{ + "file.go:3 \"inital\" should be initial in inital\n", + }, + }, + }, + flags: Flags{ + Ignores: "", + }, + }, + }, + {name: "misspelled function call", + args: args{ + testFiles: []*testFile{ + { + src: `package main + func main() { + err := a.inital() + } + `, + name: "file.go", + wantLogs: []string{ + "file.go:3 \"inital\" should be initial in inital\n", + }, + }, + }, + flags: Flags{ + Ignores: "", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + fset := token.NewFileSet() // positions are relative to fset + files := make([]*ast.File, len(tt.args.testFiles)) + for _, testFile := range tt.args.testFiles { + f, err := parser.ParseFile(fset, testFile.name, testFile.src, 0) + if err != nil { + t.Fatalf("Did not expect error parsing file, %v", err) + } + files = append(files, f) + } + + var buf bytes.Buffer + log.SetFlags(0) + log.SetOutput(&buf) + defer log.SetOutput(os.Stderr) + + err := processIdentifiers(fset, files, tt.args.flags) + if err != nil { + t.Fatalf("processIdentifiers %v", err) + } + + var testBuffer bytes.Buffer + for _, testFile := range tt.args.testFiles { + testBuffer.Write([]byte(strings.Join(testFile.wantLogs, ""))) + } + + if buf.String() != testBuffer.String() { + t.Fatalf("\ngot %v\nexp %v\n", buf.String(), testBuffer.String()) + } + }) + } +} diff --git a/import.go b/import.go new file mode 100644 index 0000000..a4ae04f --- /dev/null +++ b/import.go @@ -0,0 +1,404 @@ +package identypo + +import ( + "fmt" + "go/ast" + "go/build" + "go/parser" + "go/token" + "log" + "os" + "path" + "path/filepath" + "regexp" + "runtime" + "strings" +) + +const ( + pwd = "./" +) + +var buildContext = build.Default + +var ( + goroot = filepath.Clean(runtime.GOROOT()) + gorootSrc = filepath.Join(goroot, "src") +) + +func parseInput(args []string, fset *token.FileSet, includeTests bool) ([]*ast.File, error) { + var directoryList []string + var fileMode bool + files := make([]*ast.File, 0) + + if len(args) == 0 { + directoryList = append(directoryList, pwd) + } else { + for _, arg := range args { + if strings.HasSuffix(arg, "/...") && isDir(arg[:len(arg)-len("/...")]) { + + directoryList = append(directoryList, allPackagesInFS(arg)...) + + } else if isDir(arg) { + directoryList = append(directoryList, arg) + + } else if exists(arg) { + if strings.HasSuffix(arg, ".go") { + fileMode = true + f, err := parser.ParseFile(fset, arg, nil, 0) + if err != nil { + return nil, err + } + files = append(files, f) + } else { + return nil, fmt.Errorf("invalid file %v specified", arg) + } + } else { + + imPaths := importPaths([]string{arg}) + for _, importPath := range imPaths { + pkg, err := build.Import(importPath, ".", 0) + if err != nil { + return nil, err + } + var stringFiles []string + stringFiles = append(stringFiles, pkg.GoFiles...) + stringFiles = append(stringFiles, pkg.TestGoFiles...) + if pkg.Dir != "." { + for i, f := range stringFiles { + stringFiles[i] = filepath.Join(pkg.Dir, f) + } + } + + fileMode = true + for _, stringFile := range stringFiles { + f, err := parser.ParseFile(fset, stringFile, nil, 0) + if err != nil { + return nil, err + } + files = append(files, f) + } + + } + } + } + } + + // if we're not in file mode, then we need to grab each and every package in each directory + // we can to grab all the files + if !fileMode { + for _, fpath := range directoryList { + pkgs, err := parser.ParseDir(fset, fpath, nil, 0) + if err != nil { + return nil, err + } + + for _, pkg := range pkgs { + for _, f := range pkg.Files { + files = append(files, f) + } + } + } + } + + // do a final pass to remove tests + if !includeTests { + for i, f := range files { + if strings.HasSuffix(fset.File(f.Pos()).Name(), "test.go") { + files[i] = nil + } + } + } + + return files, nil +} + +func isDir(filename string) bool { + fi, err := os.Stat(filename) + return err == nil && fi.IsDir() +} + +func exists(filename string) bool { + _, err := os.Stat(filename) + return err == nil +} + +// importPathsNoDotExpansion returns the import paths to use for the given +// command line, but it does no ... expansion. +func importPathsNoDotExpansion(args []string) []string { + if len(args) == 0 { + return []string{"."} + } + var out []string + for _, a := range args { + // Arguments are supposed to be import paths, but + // as a courtesy to Windows developers, rewrite \ to / + // in command-line arguments. Handles .\... and so on. + if filepath.Separator == '\\' { + a = strings.Replace(a, `\`, `/`, -1) + } + + // Put argument in canonical form, but preserve leading ./. + if strings.HasPrefix(a, "./") { + a = "./" + path.Clean(a) + if a == "./." { + a = "." + } + } else { + a = path.Clean(a) + } + if a == "all" || a == "std" { + out = append(out, allPackages(a)...) + continue + } + out = append(out, a) + } + return out +} + +// importPaths returns the import paths to use for the given command line. +func importPaths(args []string) []string { + args = importPathsNoDotExpansion(args) + var out []string + for _, a := range args { + if strings.Contains(a, "...") { + if build.IsLocalImport(a) { + out = append(out, allPackagesInFS(a)...) + } else { + out = append(out, allPackages(a)...) + } + continue + } + out = append(out, a) + } + return out +} + +// matchPattern(pattern)(name) reports whether +// name matches pattern. Pattern is a limited glob +// pattern in which '...' means 'any string' and there +// is no other special syntax. +func matchPattern(pattern string) func(name string) bool { + re := regexp.QuoteMeta(pattern) + re = strings.Replace(re, `\.\.\.`, `.*`, -1) + // Special case: foo/... matches foo too. + if strings.HasSuffix(re, `/.*`) { + re = re[:len(re)-len(`/.*`)] + `(/.*)?` + } + reg := regexp.MustCompile(`^` + re + `$`) + return func(name string) bool { + return reg.MatchString(name) + } +} + +// hasPathPrefix reports whether the path s begins with the +// elements in prefix. +func hasPathPrefix(s, prefix string) bool { + switch { + default: + return false + case len(s) == len(prefix): + return s == prefix + case len(s) > len(prefix): + if prefix != "" && prefix[len(prefix)-1] == '/' { + return strings.HasPrefix(s, prefix) + } + return s[len(prefix)] == '/' && s[:len(prefix)] == prefix + } +} + +// treeCanMatchPattern(pattern)(name) reports whether +// name or children of name can possibly match pattern. +// Pattern is the same limited glob accepted by matchPattern. +func treeCanMatchPattern(pattern string) func(name string) bool { + wildCard := false + if i := strings.Index(pattern, "..."); i >= 0 { + wildCard = true + pattern = pattern[:i] + } + return func(name string) bool { + return len(name) <= len(pattern) && hasPathPrefix(pattern, name) || + wildCard && strings.HasPrefix(name, pattern) + } +} + +// allPackages returns all the packages that can be found +// under the $GOPATH directories and $GOROOT matching pattern. +// The pattern is either "all" (all packages), "std" (standard packages) +// or a path including "...". +func allPackages(pattern string) []string { + pkgs := matchPackages(pattern) + if len(pkgs) == 0 { + fmt.Fprintf(os.Stderr, "warning: %q matched no packages\n", pattern) + } + return pkgs +} + +func matchPackages(pattern string) []string { + match := func(string) bool { return true } + treeCanMatch := func(string) bool { return true } + if pattern != "all" && pattern != "std" { + match = matchPattern(pattern) + treeCanMatch = treeCanMatchPattern(pattern) + } + + have := map[string]bool{ + "builtin": true, // ignore pseudo-package that exists only for documentation + } + if !buildContext.CgoEnabled { + have["runtime/cgo"] = true // ignore during walk + } + var pkgs []string + + // Commands + cmd := filepath.Join(goroot, "src/cmd") + string(filepath.Separator) + _ = filepath.Walk(cmd, func(path string, fi os.FileInfo, err error) error { + if err != nil || !fi.IsDir() || path == cmd { + return nil + } + name := path[len(cmd):] + if !treeCanMatch(name) { + return filepath.SkipDir + } + // Commands are all in cmd/, not in subdirectories. + if strings.Contains(name, string(filepath.Separator)) { + return filepath.SkipDir + } + + // We use, e.g., cmd/gofmt as the pseudo import path for gofmt. + name = "cmd/" + name + if have[name] { + return nil + } + have[name] = true + if !match(name) { + return nil + } + _, err = buildContext.ImportDir(path, 0) + if err != nil { + if _, noGo := err.(*build.NoGoError); !noGo { + log.Print(err) + } + return nil + } + pkgs = append(pkgs, name) + return nil + }) + + for _, src := range buildContext.SrcDirs() { + if (pattern == "std" || pattern == "cmd") && src != gorootSrc { + continue + } + src = filepath.Clean(src) + string(filepath.Separator) + root := src + if pattern == "cmd" { + root += "cmd" + string(filepath.Separator) + } + _ = filepath.Walk(root, func(path string, fi os.FileInfo, err error) error { + if err != nil || !fi.IsDir() || path == src { + return nil + } + + // Avoid .foo, _foo, testdata and vendor directory trees. + _, elem := filepath.Split(path) + if strings.HasPrefix(elem, ".") || strings.HasPrefix(elem, "_") || elem == "testdata" || elem == "vendor" { + return filepath.SkipDir + } + + name := filepath.ToSlash(path[len(src):]) + if pattern == "std" && (strings.Contains(name, ".") || name == "cmd") { + // The name "std" is only the standard library. + // If the name is cmd, it's the root of the command tree. + return filepath.SkipDir + } + if !treeCanMatch(name) { + return filepath.SkipDir + } + if have[name] { + return nil + } + have[name] = true + if !match(name) { + return nil + } + _, err = buildContext.ImportDir(path, 0) + if err != nil { + if _, noGo := err.(*build.NoGoError); noGo { + return nil + } + } + pkgs = append(pkgs, name) + return nil + }) + } + return pkgs +} + +// allPackagesInFS is like allPackages but is passed a pattern +// beginning ./ or ../, meaning it should scan the tree rooted +// at the given directory. There are ... in the pattern too. +func allPackagesInFS(pattern string) []string { + pkgs := matchPackagesInFS(pattern) + if len(pkgs) == 0 { + fmt.Fprintf(os.Stderr, "warning: %q matched no packages\n", pattern) + } + return pkgs +} + +func matchPackagesInFS(pattern string) []string { + // Find directory to begin the scan. + // Could be smarter but this one optimization + // is enough for now, since ... is usually at the + // end of a path. + i := strings.Index(pattern, "...") + dir, _ := path.Split(pattern[:i]) + + // pattern begins with ./ or ../. + // path.Clean will discard the ./ but not the ../. + // We need to preserve the ./ for pattern matching + // and in the returned import paths. + prefix := "" + if strings.HasPrefix(pattern, "./") { + prefix = "./" + } + match := matchPattern(pattern) + + var pkgs []string + _ = filepath.Walk(dir, func(path string, fi os.FileInfo, err error) error { + if err != nil || !fi.IsDir() { + return nil + } + if path == dir { + // filepath.Walk starts at dir and recurses. For the recursive case, + // the path is the result of filepath.Join, which calls filepath.Clean. + // The initial case is not Cleaned, though, so we do this explicitly. + // + // This converts a path like "./io/" to "io". Without this step, running + // "cd $GOROOT/src/pkg; go list ./io/..." would incorrectly skip the io + // package, because prepending the prefix "./" to the unclean path would + // result in "././io", and match("././io") returns false. + path = filepath.Clean(path) + } + + // Avoid .foo, _foo, testdata and vendor directory trees, but do not avoid "." or "..". + _, elem := filepath.Split(path) + dot := strings.HasPrefix(elem, ".") && elem != "." && elem != ".." + if dot || strings.HasPrefix(elem, "_") || elem == "testdata" || elem == "vendor" { + return filepath.SkipDir + } + + name := prefix + filepath.ToSlash(path) + if !match(name) { + return nil + } + if _, err = build.ImportDir(path, 0); err != nil { + if _, noGo := err.(*build.NoGoError); !noGo { + log.Print(err) + } + return nil + } + pkgs = append(pkgs, name) + return nil + }) + return pkgs +} diff --git a/testdata/file.go b/testdata/file.go new file mode 100644 index 0000000..ad9427c --- /dev/null +++ b/testdata/file.go @@ -0,0 +1,26 @@ +package testdata + +import "fmt" + +// misspelled function +func begining() {} + +// misspelled type declaration +type succesful int + +// misspelled function with receiver +func (s *succesful) begining() {} + +// misspelled constant +const constantSuccesful = 0 + +// misspelled label +func main() { +authorithyLoop: + for i := 0; i < 5; i++ { + fmt.Println("loooooooool") + continue authorithyLoop + } +} + +var varSuccesful = 0 diff --git a/testdata/file_test.go b/testdata/file_test.go new file mode 100644 index 0000000..fbd8547 --- /dev/null +++ b/testdata/file_test.go @@ -0,0 +1,26 @@ +package testdata + +import "testing" + +import "fmt" + +// misspelled function +func testBegining() {} + +// misspelled type declaration +type testSuccesful int + +// misspelled function with receiver +func (s *testSuccesful) begining() {} + +// misspelled constant +const testConstantSuccesful = 0 + +// misspelled test and label +func TestSuccesful(t *testing.T) { +authorithyLoop: + for i := 0; i < 5; i++ { + fmt.Println("loooooooool") + continue authorithyLoop + } +}