Skip to content

Instantly share code, notes, and snippets.

@roscopecoltran
Forked from tenntenn/findtypo.go
Created October 26, 2018 13:55
Show Gist options
  • Save roscopecoltran/c2350d16dc5134c3d5f8a7e6a05a7a40 to your computer and use it in GitHub Desktop.
Save roscopecoltran/c2350d16dc5134c3d5f8a7e6a05a7a40 to your computer and use it in GitHub Desktop.
package main
import (
"bufio"
"fmt"
"go/ast"
"go/build"
"go/importer"
"go/parser"
"go/token"
"go/types"
"os"
"path/filepath"
"strconv"
"strings"
)
var (
ctx *build.Context
allPackage map[string][]*PackageInfo
)
type PackageInfo struct {
Package *build.Package
Info *types.Info
}
func (p *PackageInfo) HasSymbol(s string) bool {
for _, o := range p.Info.Defs {
if o == nil {
continue
}
if o.Exported() && o.Name() == s {
return true
}
}
return false
}
type Typo struct {
Pos token.Position
Text string
}
func init() {
c := build.Default // copy
ctx = &c
ctx.CgoEnabled = false
ctx.GOPATH = ""
}
func getAllStdPackages(ctx *build.Context) (map[string][]*PackageInfo, error) {
allpkgs := map[string][]*PackageInfo{}
config := &types.Config{
Importer: importer.Default(),
}
srcDir := filepath.Join(ctx.GOROOT, "src")
err := filepath.Walk(srcDir, func(path string, fi os.FileInfo, err error) error {
if err != nil {
return err
}
if !fi.IsDir() {
return nil
}
if n := fi.Name(); n == "internal" || n == "testdata" || n == "vendor" {
return filepath.SkipDir
}
pkg, err := ctx.ImportDir(path, 0)
if err != nil {
return nil
}
fset := token.NewFileSet()
pkgs, err := parser.ParseDir(fset, pkg.Dir, makeFilter(pkg), 0)
if err != nil {
return nil
}
for pn, p := range pkgs {
info := &types.Info{
Defs: map[*ast.Ident]types.Object{},
}
files := make([]*ast.File, 0, len(p.Files))
for _, f := range p.Files {
files = append(files, f)
}
_, err := config.Check(pn, fset, files, info)
if err != nil {
return nil
}
allpkgs[pn] = append(allpkgs[pn], &PackageInfo{
Package: pkg,
Info: info,
})
}
return nil
})
if err != nil {
return nil, err
}
return allpkgs, nil
}
func main() {
if pkgs, err := getAllStdPackages(ctx); err == nil {
allPackage = pkgs
} else {
fmt.Fprintln(os.Stderr, "Error:", err)
os.Exit(1)
}
typos, err := FindTypo(os.Args[1:])
if err != nil {
fmt.Fprintln(os.Stderr, "Error:", err)
os.Exit(1)
}
for _, typo := range typos {
fmt.Println(strconv.Quote(typo.Text), "at", typo.Pos)
}
}
func FindTypo(paths []string) ([]*Typo, error) {
dir, err := os.Getwd()
if err != nil {
return nil, err
}
var typos []*Typo
for _, path := range paths {
t, err := findTypo(dir, path)
if err != nil {
return nil, err
}
typos = append(typos, t...)
}
return typos, nil
}
func findTypo(dir string, path string) ([]*Typo, error) {
pkg, err := ctx.Import(path, dir, build.IgnoreVendor)
if err != nil {
return nil, err
}
fset := token.NewFileSet()
pkgs, err := parser.ParseDir(fset, pkg.Dir, makeFilter(pkg), parser.ParseComments)
if err != nil {
return nil, err
}
var typos []*Typo
for _, p := range pkgs {
for _, f := range p.Files {
t, err := findTypoByFile(fset, f)
if err != nil {
return nil, err
}
typos = append(typos, t...)
}
}
return typos, nil
}
func makeFilter(pkg *build.Package) func(fi os.FileInfo) bool {
return func(fi os.FileInfo) bool {
if strings.HasSuffix(fi.Name(), "_test.go") {
return false
}
for _, ignored := range pkg.IgnoredGoFiles {
if ignored == fi.Name() {
return false
}
}
for _, cgofile := range pkg.CgoFiles {
if cgofile == fi.Name() {
return false
}
}
return true
}
}
func findTypoByFile(fset *token.FileSet, f *ast.File) ([]*Typo, error) {
var typos []*Typo
for _, cg := range f.Comments {
s := bufio.NewScanner(strings.NewReader(cg.Text()))
s.Split(bufio.ScanWords)
for s.Scan() {
str := strings.TrimRight(s.Text(), ".")
if typo, isTarget := hasTypo(str); typo && isTarget {
// 末尾のsを取り除いてもう一度やってみる
if strings.HasSuffix(str, "s") {
str = strings.TrimRight(str, "s")
if typo, isTarget := hasTypo(str); !typo && isTarget {
// sを取り除いたら大丈夫だった
continue
}
}
typos = append(typos, &Typo{
Text: s.Text(),
Pos: fset.Position(cg.Pos()),
})
}
}
if err := s.Err(); err != nil {
return nil, err
}
}
return typos, nil
}
func hasTypo(s string) (typo, target bool) {
expr, err := parser.ParseExpr(s)
if err != nil {
return false, false
}
pkg, symbol, ok := getPkgSymbol(expr)
if !ok {
return false, false
}
typo = !isExitSymbol(pkg, symbol)
return typo, true
}
func isExistPkg(pkg string) bool {
_, ok := allPackage[pkg]
return ok
}
func getPkgSymbol(expr ast.Expr) (pkg, symbol string, ok bool) {
selExpr, ok := expr.(*ast.SelectorExpr)
if !ok {
return "", "", false
}
if !selExpr.Sel.IsExported() {
return "", "", false
}
pkgIdent, ok := selExpr.X.(*ast.Ident)
if !ok {
return "", "", false
}
pkg = pkgIdent.Name
symbol = selExpr.Sel.Name
if !isExistPkg(pkgIdent.Name) {
return "", "", false
}
return pkg, symbol, true
}
func isExitSymbol(pkg, symbol string) bool {
ps, ok := allPackage[pkg]
if !ok {
return false
}
for _, p := range ps {
if p.HasSymbol(symbol) {
return true
}
}
return false
}
$ go run findtypo.go `go list std`
"user.NewContext" at /usr/local/go/src/context/context.go:105:2
"user.FromContext" at /usr/local/go/src/context/context.go:105:2
"template.Attributes." at /usr/local/go/src/crypto/x509/x509.go:2088:3
"driver.Preparer" at /usr/local/go/src/database/sql/sql.go:1130:2
"time.Format3339Nano" at /usr/local/go/src/database/sql/sql.go:2335:1
"runtime.Memstats" at /usr/local/go/src/expvar/expvar.go:5:1
"io.ReaderCloser" at /usr/local/go/src/net/http/server.go:842:1
"syscall.Note" at /usr/local/go/src/os/signal/doc.go:5:1
"runtime.BitVector" at /usr/local/go/src/reflect/type.go:3159:1
"atomic.Loaduintptr" at /usr/local/go/src/runtime/signal_unix.go:32:1
"atomic.Storeuintptr." at /usr/local/go/src/runtime/signal_unix.go:32:1
"atomic.Or8" at /usr/local/go/src/runtime/mbitmap.go:312:1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment