Skip to content

Commit

Permalink
The first commit.
Browse files Browse the repository at this point in the history
  • Loading branch information
looshch committed Aug 4, 2022
0 parents commit 5ff62da
Show file tree
Hide file tree
Showing 14 changed files with 538 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
gouse
testenv
.DS_Store
57 changes: 57 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# gouse
Toggle ‘declared but not used’ errors in Go by using idiomatic `_ = notUsedVar`
and leaving a TODO comment. ![a demo gif](demo.gif)

## Installation
```
go install github.com/looshch/gouse@latest
```

## Usage
By default, gouse accepts code from stdin and writes a toggled version to
stdout. If any file paths provided, it takes code from them and writes a
toggled version to stdout unless ‘-w’ flag is passed — then it will write
back to the file paths.
### Examples
```
$ gouse
...input...
notUsed = false
...input...
...output...
notUsed = false; _ = notUsed /* TODO: gouse */
...output...
```
```
$ gouse main.go
...output...
notUsed = false; _ = notUsed /* TODO: gouse */
...output...
```
```
$ gouse -w main.go io.go core.go
```

## How it works
First it tries to remove fake usages. If there is nothing to remove, it tries
to build an input and checks a build stdout for the errors. If there is any,
it creates fake usages for unused variables from the errors.

## Why
To automate automatable and speed up feedback loop.

## Integrations
* Vim: just bind `<cmd>!gouse -w %<cr><cr>` to some mapping.
* [Visual Studio Code plugin](https://github.com/looshch/gouse-vsc).
### Help Wanted
I have zero willingness to touch Java world to implement a wrapper for GoLand.
If anyone wants to help, I’ll be glad to include a link to your wrapper.

## Credits
Inspired by [Ilya Polyakov](https://github.com/PolyakovIlya)’s idea.
Binary file added demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/looshch/gouse

go 1.18
231 changes: 231 additions & 0 deletions gouse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
// gouse allows to toggle ‘declared but not used’ errors by using idiomatic
// _ = notUsedVar and leaving a TODO comment.
//
// Usage:
//
// gouse [-w] [file ...]
//
// By default, gouse accepts code from stdin and writes a toggled version to
// stdout. If any file paths provided, it takes code from them and writes a
// toggled version to stdout unless ‘-w’ flag is passed — then it will write
// back to the file paths.
//
// First it tries to remove fake usages. If there is nothing to remove, it tries
// to build an input and checks a build stdout for the errors. If there is any,
// it creates fake usages for unused variables from the errors.
//
// Examples
//
// $ gouse
// ...input...
//
// notUsed = false
//
// ...input...
// ...output...
//
// notUsed = false; _ = notUsed /* TODO: gouse */
//
// ...output...
//
// $ gouse main.go
// ...output...
//
// notUsed = false; _ = notUsed /* TODO: gouse */
//
// ...output...
//
// $ gouse -w main.go io.go core.go
package main

import (
"bytes"
"flag"
"fmt"
"io"
"log"
"os"
"os/exec"
"regexp"
"strconv"
"strings"
)

func usage() {
fmt.Println("usage: gouse [-w] [file ...]")
os.Exit(2)
}

var write = flag.Bool("w", false, "write results to files")

func main() {
flag.Usage = usage
flag.Parse()

paths := flag.Args()
if len(paths) == 0 {
if *write {
log.SetFlags(log.Flags() &^ (log.Ldate | log.Ltime))
log.Fatal("cannot use -w with standard input")
}
if err := handle(os.Stdin, os.Stdout, false); err != nil {
log.Fatal(err)
}
return
}
for _, p := range paths {
var in *os.File
var out **os.File
var flag int
if *write {
out = &in
flag = os.O_RDWR
} else {
out = &os.Stdout
flag = os.O_RDONLY
}
in, err := os.OpenFile(p, flag, os.ModeExclusive)
if err != nil {
log.Fatal(err)
}
defer in.Close()
if err := handle(in, *out, *write); err != nil {
log.Fatal(err)
}
}
}

// handle manages IO.
func handle(in *os.File, out *os.File, write bool) error {
code, err := io.ReadAll(in)
if err != nil {
return err
}
toggled, err := toggle(code)
if err != nil {
return err
}
if write {
if _, err := out.Seek(0, 0); err != nil {
return err
}
if err := out.Truncate(0); err != nil {
return err
}
}
if _, err := out.Write(toggled); err != nil {
return err
}
return nil
}

const (
COMMENT_PREFIX = "// "

USAGE_PREFIX = "; _ ="
USAGE_POSTFIX = " /* TODO: gouse */"
)

var commentPrefixPadding = len([]rune(COMMENT_PREFIX))

var (
escapedUsagePostfix = regexp.QuoteMeta(USAGE_POSTFIX)

used = regexp.MustCompile(USAGE_PREFIX + ".*" + escapedUsagePostfix)
usedAndGofmted = regexp.MustCompile(`\s*_\s*= \w*` + escapedUsagePostfix)
)

// ERROR_INFO_REGEXP catches position and name of the variable in a build error.
const ERROR_INFO_REGEXP = `\d+:\d+: \w+`

var (
errorInfo = regexp.MustCompile(ERROR_INFO_REGEXP)

noProviderErr = regexp.MustCompile(ERROR_INFO_REGEXP + " required module provides package")
notUsedErr = regexp.MustCompile(ERROR_INFO_REGEXP + " declared but not used")
)

// toggle returns toggled code. First it tries to remove fake usages. If there
// is nothing to remove, it creates them.
func toggle(code []byte) ([]byte, error) {
if used.Match(code) { // used must be before usedAndGofmted because it also removes ‘;’.
return used.ReplaceAll(code, []byte("")), nil
}
if usedAndGofmted.Match(code) {
return usedAndGofmted.ReplaceAll(code, []byte("")), nil
}

lines := bytes.Split(code, []byte("\n"))
// Check for problematic imports and comment them out if any, storing commented
// out lines numbers to commentedLinesNums.
noProviderVarsInfo, err := getVarsInfoFrom(code, noProviderErr)
if err != nil {
return nil, err
}
var commentedLinesNums []int
for _, info := range noProviderVarsInfo {
l := &lines[info.lineNum]
*l = append([]byte(COMMENT_PREFIX), *l...)
commentedLinesNums = append(commentedLinesNums, info.lineNum)
}
// Check for ‘declared but not used’ errors and create fake usages for them if
// any.
notUsedVarsInfo, err := getVarsInfoFrom(bytes.Join(lines, []byte("\n")), notUsedErr)
if err != nil {
return nil, err
}
for _, info := range notUsedVarsInfo {
l := &lines[info.lineNum]
*l = append(*l, []byte(USAGE_PREFIX+info.name+USAGE_POSTFIX)...)
}
// Un-comment commented out lines.
for _, line := range commentedLinesNums {
l := &lines[line]
uncommentedLine := []rune(string(*l))[commentPrefixPadding:]
*l = []byte(string(uncommentedLine))
}
return bytes.Join(lines, []byte("\n")), nil
}

type VarInfo struct {
name string
lineNum int
}

// getVarsInfoFrom tries to build code and checks a build stdout for errors
// catched by r. If any, it returns a slice of tuples with a line and a name of
// every catched symbol.
func getVarsInfoFrom(code []byte, r *regexp.Regexp) ([]VarInfo, error) {
td, err := os.MkdirTemp(os.TempDir(), "gouse")
if err != nil {
return nil, err
}
defer os.RemoveAll(td)
tf, err := os.CreateTemp(td, "*.go")
if err != nil {
return nil, err
}
defer tf.Close()
tf.Write(code)
bo, err := exec.Command("go", "build", "-o", os.DevNull, tf.Name()).CombinedOutput()
if err == nil {
return nil, nil
}
buildErrors := strings.Split(string(bo), "\n")
var info []VarInfo
for _, e := range buildErrors {
if !r.MatchString(e) {
continue
}
varInfo := strings.Split(errorInfo.FindString(e), ":")
lineNum, err := strconv.Atoi(varInfo[0])
if err != nil {
return nil, err
}
info = append(info, VarInfo{
name: varInfo[2],
lineNum: lineNum - 1, // An adjustment for 0-based count.
})
}
return info, nil
}
Loading

0 comments on commit 5ff62da

Please sign in to comment.