| // Copyright 2026 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| // specfill checks the public SIMD API against internal/spec and reports | |
| // signature mismatches, comment structure violations, and documentation diffs. | |
| package main | |
| import ( | |
| "bytes" | |
| "errors" | |
| "flag" | |
| "fmt" | |
| "os" | |
| "os/exec" | |
| "path/filepath" | |
| "simd/archsimd/_gen/gentools" | |
| "simd/archsimd/_gen/specdoc" | |
| "simd/archsimd/_gen/specgen" | |
| "strings" | |
| ) | |
| func main() { | |
| genOpts := gentools.RegisterFlags(nil) | |
| noFillDoc := flag.Bool("no-fill-doc", false, "disable filling doc comments from spec") | |
| noFillNames := flag.Bool("no-fill-names", false, "disable filling parameter and result names from spec") | |
| flag.Usage = func() { | |
| w := flag.CommandLine.Output() | |
| fmt.Fprintf(w, "usage: specfill [flags]\n") | |
| flag.CommandLine.PrintDefaults() | |
| } | |
| flag.Parse() | |
| if flag.NArg() != 0 { | |
| flag.Usage() | |
| os.Exit(1) | |
| } | |
| specDir := specgen.MustFindSpecDir(genOpts.GOROOT) | |
| specFuncs, err := specgen.Load(specDir, nil) | |
| if err != nil { | |
| fmt.Fprintf(os.Stderr, "loading spec: %v\n", err) | |
| os.Exit(1) | |
| } | |
| specIdx := specgen.NewIndex(specFuncs) | |
| dirs := []string{ | |
| filepath.Join(genOpts.GOROOT, "src/simd"), | |
| filepath.Join(genOpts.GOROOT, "src/simd/archsimd"), | |
| } | |
| var files gentools.Files | |
| defer files.FlushOrExit() | |
| var combinedReport specdoc.Report | |
| for _, dir := range dirs { | |
| entries, err := os.ReadDir(dir) | |
| if err != nil { | |
| fmt.Fprintf(os.Stderr, "reading %s: %v\n", dir, err) | |
| os.Exit(1) | |
| } | |
| for _, e := range entries { | |
| if e.IsDir() || !strings.HasSuffix(e.Name(), ".go") || strings.HasSuffix(e.Name(), "_test.go") { | |
| continue | |
| } | |
| filePath := filepath.Join(dir, e.Name()) | |
| relPath, err := filepath.Rel(genOpts.GOROOT, filePath) | |
| if err != nil { | |
| relPath = filePath | |
| } | |
| srcRelPath, err := filepath.Rel(filepath.Join(genOpts.GOROOT, "src"), filePath) | |
| if err != nil { | |
| srcRelPath = strings.TrimPrefix(filepath.ToSlash(relPath), "src/") | |
| } | |
| src, err := os.ReadFile(filePath) | |
| if err != nil { | |
| fmt.Fprintf(os.Stderr, "reading %s: %v\n", filePath, err) | |
| os.Exit(1) | |
| } | |
| gen, err := isGenerated(src) | |
| if err != nil { | |
| fmt.Fprintf(os.Stderr, "%s: %v\n", filePath, err) | |
| os.Exit(1) | |
| } | |
| if gen { | |
| continue | |
| } | |
| rewritten, err := specdoc.Fill(src, specIdx, specdoc.Options{ | |
| Filename: filepath.ToSlash(relPath), | |
| AllowDocRewrite: true, | |
| NoFillDoc: *noFillDoc, | |
| NoFillNames: *noFillNames, | |
| }) | |
| var rep *specdoc.Report | |
| if errors.As(err, &rep) { | |
| combinedReport.Merge(rep) | |
| } else if err != nil { | |
| fmt.Fprintf(os.Stderr, "checking %s: %v\n", filePath, err) | |
| os.Exit(1) | |
| } | |
| if rewritten != nil && !bytes.Equal(src, rewritten) { | |
| buf := files.NewGoFile(filepath.ToSlash(srcRelPath)) | |
| buf.Write(rewritten) | |
| } | |
| } | |
| } | |
| combinedReport.Print(os.Stderr) | |
| if genOpts.Write { | |
| if err := checkCleanWorkingCopy(genOpts.GOROOT); err != nil { | |
| fmt.Fprintf(os.Stderr, "refusing to write: %v\n", err) | |
| os.Exit(1) | |
| } | |
| if !combinedReport.Empty() { | |
| fmt.Fprintln(os.Stderr, "errors reported; not writing files") | |
| os.Exit(1) | |
| } | |
| } | |
| } | |
| // checkCleanWorkingCopy verifies that the working directory has no uncommitted | |
| // changes before specfill rewrites files in place (-w). | |
| // | |
| // We rely on "git status --porcelain" rather than invoking "jj" commands directly. | |
| // In a standard Git repository or a colocated jj/git repository, "git status" | |
| // checks disk cleanliness without side-effects. | |
| // | |
| // # Interoperation with colocated jj repos | |
| // | |
| // Jujutsu keeps Git's HEAD and index in sync with the parent of the working-copy | |
| // commit (@-). If the user starts from a fresh, empty commit (e.g. via "jj new"), | |
| // the working copy matches Git's HEAD and git status reports clean. If there are | |
| // modified or untracked files on disk (or if @ contains changes), git status | |
| // reports them as uncommitted changes. By querying git rather than running "jj" | |
| // commands (like "jj diff" or "jj status"), we avoid jj's snapshotting side-effects, | |
| // such as automatically snapshotting dirty files into @, writing to the operation | |
| // log, and auto-tracking newly created files. | |
| func checkCleanWorkingCopy(dir string) error { | |
| cmd := exec.Command("git", "status", "--porcelain") | |
| cmd.Dir = dir | |
| out, err := cmd.Output() | |
| if err != nil { | |
| // Not a git repository or git binary not available; skip the check. | |
| return nil | |
| } | |
| if len(bytes.TrimSpace(out)) > 0 { | |
| return fmt.Errorf("working copy has uncommitted changes:\n%s", out) | |
| } | |
| return nil | |
| } | |
| // isGenerated reports whether src is a generated Go file. | |
| // | |
| // It checks for the standard "// Code generated by" comment marker at the beginning | |
| // of the file or at the beginning of any line. If missing, it checks for | |
| // "DO NOT EDIT" as a backstop; if found, it returns an error to catch non-standard | |
| // or malformed generator headers rather than erroneously treating the file as hand-written. | |
| func isGenerated(src []byte) (bool, error) { | |
| const marker = "// Code generated by" | |
| if bytes.HasPrefix(src, []byte(marker)) || bytes.Contains(src, []byte("\n"+marker)) { | |
| return true, nil | |
| } | |
| if bytes.Contains(src, []byte("DO NOT EDIT")) { | |
| return false, errors.New("file contains 'DO NOT EDIT' but lacks '// Code generated by' header") | |
| } | |
| return false, nil | |
| } | |