cmd/jsonimports: add static analyzer for consistent "json" imports (#17669)
This migrates an internal tool to open source so that we can run it on the tailscale.com module as well. We add the "util/safediff" also as a dependency of the tool. This PR does not yet set up a CI to run this analyzer. Updates tailscale/corp#791 Signed-off-by: Joe Tsai <joetsai@digital-static.net>
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// The jsonimports tool formats all Go source files in the repository
|
||||
// to enforce that "json" imports are consistent.
|
||||
//
|
||||
// With Go 1.25, the "encoding/json/v2" and "encoding/json/jsontext"
|
||||
// packages are now available under goexperiment.jsonv2.
|
||||
// This leads to possible confusion over the following:
|
||||
//
|
||||
// - "encoding/json"
|
||||
// - "encoding/json/v2"
|
||||
// - "encoding/json/jsontext"
|
||||
// - "github.com/go-json-experiment/json/v1"
|
||||
// - "github.com/go-json-experiment/json"
|
||||
// - "github.com/go-json-experiment/json/jsontext"
|
||||
//
|
||||
// In order to enforce consistent usage, we apply the following rules:
|
||||
//
|
||||
// - Until the Go standard library formally accepts "encoding/json/v2"
|
||||
// and "encoding/json/jsontext" into the standard library
|
||||
// (i.e., they are no longer considered experimental),
|
||||
// we forbid any code from directly importing those packages.
|
||||
// Go code should instead import "github.com/go-json-experiment/json"
|
||||
// and "github.com/go-json-experiment/json/jsontext".
|
||||
// The latter packages contain aliases to the standard library
|
||||
// if built on Go 1.25 with the goexperiment.jsonv2 tag specified.
|
||||
//
|
||||
// - Imports of "encoding/json" or "github.com/go-json-experiment/json/v1"
|
||||
// must be explicitly imported under the package name "jsonv1".
|
||||
// If both packages need to be imported, then the former should
|
||||
// be imported under the package name "jsonv1std".
|
||||
//
|
||||
// - Imports of "github.com/go-json-experiment/json"
|
||||
// must be explicitly imported under the package name "jsonv2".
|
||||
//
|
||||
// The latter two rules exist to provide clarity when reading code.
|
||||
// Without them, it is unclear whether "json.Marshal" refers to v1 or v2.
|
||||
// With them, however, it is clear that "jsonv1.Marshal" is calling v1 and
|
||||
// that "jsonv2.Marshal" is calling v2.
|
||||
//
|
||||
// TODO(@joetsai): At this present moment, there is no guidance given on
|
||||
// whether to use v1 or v2 for newly written Go source code.
|
||||
// I will write a document in the near future providing more guidance.
|
||||
// Feel free to continue using v1 "encoding/json" as you are accustomed to.
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"tailscale.com/syncs"
|
||||
"tailscale.com/util/must"
|
||||
"tailscale.com/util/safediff"
|
||||
)
|
||||
|
||||
func main() {
|
||||
update := flag.Bool("update", false, "update all Go source files")
|
||||
flag.Parse()
|
||||
|
||||
// Change working directory to Git repository root.
|
||||
repoRoot := strings.TrimSuffix(string(must.Get(exec.Command(
|
||||
"git", "rev-parse", "--show-toplevel",
|
||||
).Output())), "\n")
|
||||
must.Do(os.Chdir(repoRoot))
|
||||
|
||||
// Iterate over all indexed files in the Git repository.
|
||||
var printMu sync.Mutex
|
||||
var group sync.WaitGroup
|
||||
sema := syncs.NewSemaphore(runtime.NumCPU())
|
||||
var numDiffs int
|
||||
files := string(must.Get(exec.Command("git", "ls-files").Output()))
|
||||
for file := range strings.Lines(files) {
|
||||
sema.Acquire()
|
||||
group.Go(func() {
|
||||
defer sema.Release()
|
||||
|
||||
// Ignore non-Go source files.
|
||||
file = strings.TrimSuffix(file, "\n")
|
||||
if !strings.HasSuffix(file, ".go") {
|
||||
return
|
||||
}
|
||||
|
||||
// Format all "json" imports in the Go source file.
|
||||
srcIn := must.Get(os.ReadFile(file))
|
||||
srcOut := mustFormatFile(srcIn)
|
||||
|
||||
// Print differences with each formatted file.
|
||||
if !bytes.Equal(srcIn, srcOut) {
|
||||
numDiffs++
|
||||
|
||||
printMu.Lock()
|
||||
fmt.Println(file)
|
||||
lines, _ := safediff.Lines(string(srcIn), string(srcOut), -1)
|
||||
for line := range strings.Lines(lines) {
|
||||
fmt.Print("\t", line)
|
||||
}
|
||||
fmt.Println()
|
||||
printMu.Unlock()
|
||||
|
||||
// If -update is specified, write out the changes.
|
||||
if *update {
|
||||
mode := must.Get(os.Stat(file)).Mode()
|
||||
must.Do(os.WriteFile(file, srcOut, mode))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
group.Wait()
|
||||
|
||||
// Report whether any differences were detected.
|
||||
if numDiffs > 0 && !*update {
|
||||
fmt.Printf(`%d files with "json" imports that need formatting`+"\n", numDiffs)
|
||||
fmt.Println("Please run:")
|
||||
fmt.Println("\t./tool/go run tailscale.com/cmd/jsonimports -update")
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user