tailscale/cmd/jsonimports/jsonimports.go
Joe Tsai fcb614a53e
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>
2025-10-28 14:48:02 -07:00

125 lines
4.0 KiB
Go

// 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)
}
}