| // Copyright 2021 The Dawn & Tint Authors | |
| // | |
| // Redistribution and use in source and binary forms, with or without | |
| // modification, are permitted provided that the following conditions are met: | |
| // | |
| // 1. Redistributions of source code must retain the above copyright notice, this | |
| // list of conditions and the following disclaimer. | |
| // | |
| // 2. Redistributions in binary form must reproduce the above copyright notice, | |
| // this list of conditions and the following disclaimer in the documentation | |
| // and/or other materials provided with the distribution. | |
| // | |
| // 3. Neither the name of the copyright holder nor the names of its | |
| // contributors may be used to endorse or promote products derived from | |
| // this software without specific prior written permission. | |
| // | |
| // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" | |
| // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE | |
| // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |
| // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE | |
| // FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL | |
| // DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR | |
| // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER | |
| // CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, | |
| // OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| // OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| // trim-includes is a tool to try removing unnecessary include statements from | |
| // all .cc and .h files in the tint project. | |
| // | |
| // trim-includes removes each #include from each file, then runs the provided | |
| // build script to determine whether the line was necessary. If the include is | |
| // required, it is restored, otherwise it is left deleted. | |
| // After all the #include statements have been tested, the file is | |
| // clang-formatted and git staged. | |
| package main | |
| import ( | |
| "flag" | |
| "fmt" | |
| "io/ioutil" | |
| "os" | |
| "os/exec" | |
| "path/filepath" | |
| "regexp" | |
| "strings" | |
| "sync" | |
| "time" | |
| "dawn.googlesource.com/dawn/tools/src/fileutils" | |
| "dawn.googlesource.com/dawn/tools/src/glob" | |
| ) | |
| var ( | |
| // Path to the build script to run after each attempting to remove each | |
| // #include | |
| buildScript = "" | |
| ) | |
| func main() { | |
| if err := run(); err != nil { | |
| fmt.Println(err) | |
| os.Exit(1) | |
| } | |
| } | |
| func showUsage() { | |
| fmt.Println(` | |
| trim-includes is a tool to try removing unnecessary include statements from all | |
| .cc and .h files in the tint project. | |
| trim-includes removes each #include from each file, then runs the provided build | |
| script to determine whether the line was necessary. If the include is required, | |
| it is restored, otherwise it is left deleted. | |
| After all the #include statements have been tested, the file is clang-formatted | |
| and git staged. | |
| Usage: | |
| trim-includes <path-to-build-script>`) | |
| os.Exit(1) | |
| } | |
| func run() error { | |
| flag.Parse() | |
| args := flag.Args() | |
| if len(args) < 1 { | |
| showUsage() | |
| } | |
| var err error | |
| buildScript, err = exec.LookPath(args[0]) | |
| if err != nil { | |
| return err | |
| } | |
| buildScript, err = filepath.Abs(buildScript) | |
| if err != nil { | |
| return err | |
| } | |
| cfg, err := glob.LoadConfig("config.cfg") | |
| if err != nil { | |
| return err | |
| } | |
| fmt.Println("Checking the project builds with no changes...") | |
| ok, err := tryBuild() | |
| if err != nil { | |
| return err | |
| } | |
| if !ok { | |
| return fmt.Errorf("Project does not build without edits") | |
| } | |
| fmt.Println("Scanning for files...") | |
| paths, err := glob.Scan(fileutils.DawnRoot(), cfg) | |
| if err != nil { | |
| return err | |
| } | |
| fmt.Printf("Loading %v source files...\n", len(paths)) | |
| files, err := loadFiles(paths) | |
| if err != nil { | |
| return err | |
| } | |
| for fileIdx, file := range files { | |
| fmt.Printf("[%d/%d]: %v\n", fileIdx+1, len(files), file.path) | |
| includeLines := file.includesLineNumbers() | |
| enabled := make(map[int]bool, len(file.lines)) | |
| for i := range file.lines { | |
| enabled[i] = true | |
| } | |
| for includeIdx, line := range includeLines { | |
| fmt.Printf(" [%d/%d]: %v", includeIdx+1, len(includeLines), file.lines[line]) | |
| enabled[line] = false | |
| if err := file.save(enabled); err != nil { | |
| return err | |
| } | |
| ok, err := tryBuild() | |
| if err != nil { | |
| return err | |
| } | |
| if ok { | |
| fmt.Printf(" removed\n") | |
| // Wait a bit so file timestamps get an opportunity to change. | |
| // Attempting to save too soon after a successful build may | |
| // result in a false-positive build. | |
| time.Sleep(time.Second) | |
| } else { | |
| fmt.Printf(" required\n") | |
| enabled[line] = true | |
| } | |
| } | |
| if err := file.save(enabled); err != nil { | |
| return err | |
| } | |
| if err := file.format(); err != nil { | |
| return err | |
| } | |
| if err := file.stage(); err != nil { | |
| return err | |
| } | |
| } | |
| fmt.Println("Done") | |
| return nil | |
| } | |
| // Attempt to build the project. Returns true on successful build, false if | |
| // there was a build failure. | |
| func tryBuild() (bool, error) { | |
| cmd := exec.Command("sh", "-c", buildScript) | |
| out, err := cmd.CombinedOutput() | |
| switch err := err.(type) { | |
| case nil: | |
| return cmd.ProcessState.Success(), nil | |
| case *exec.ExitError: | |
| return false, nil | |
| default: | |
| return false, fmt.Errorf("Test failed with error: %v\n%v", err, string(out)) | |
| } | |
| } | |
| type file struct { | |
| path string | |
| lines []string | |
| } | |
| var includeRE = regexp.MustCompile(`^\s*#include (?:\"([^"]*)\"|:?\<([^"]*)\>)`) | |
| // Returns the file path with the extension stripped | |
| func stripExtension(path string) string { | |
| if dot := strings.IndexRune(path, '.'); dot > 0 { | |
| return path[:dot] | |
| } | |
| return path | |
| } | |
| // Returns the zero-based line numbers of all #include statements in the file | |
| func (f *file) includesLineNumbers() []int { | |
| out := []int{} | |
| for i, l := range f.lines { | |
| matches := includeRE.FindStringSubmatch(l) | |
| if len(matches) == 0 { | |
| continue | |
| } | |
| include := matches[1] | |
| if include == "" { | |
| include = matches[2] | |
| } | |
| if strings.HasSuffix(stripExtension(f.path), stripExtension(include)) { | |
| // Don't remove #include for header of cc | |
| continue | |
| } | |
| out = append(out, i) | |
| } | |
| return out | |
| } | |
| // Saves the file, omitting the lines with the zero-based line number that are | |
| // either not in `lines` or have a `false` value. | |
| func (f *file) save(lines map[int]bool) error { | |
| content := []string{} | |
| for i, l := range f.lines { | |
| if lines[i] { | |
| content = append(content, l) | |
| } | |
| } | |
| data := []byte(strings.Join(content, "\n")) | |
| return ioutil.WriteFile(f.path, data, 0666) | |
| } | |
| // Runs clang-format on the file | |
| func (f *file) format() error { | |
| err := exec.Command("clang-format", "-i", f.path).Run() | |
| if err != nil { | |
| return fmt.Errorf("Couldn't format file '%v': %w", f.path, err) | |
| } | |
| return nil | |
| } | |
| // Runs git add on the file | |
| func (f *file) stage() error { | |
| err := exec.Command("git", "-C", fileutils.DawnRoot(), "add", f.path).Run() | |
| if err != nil { | |
| return fmt.Errorf("Couldn't stage file '%v': %w", f.path, err) | |
| } | |
| return nil | |
| } | |
| // Loads all the files with the given file paths, splitting their content into | |
| // into lines. | |
| func loadFiles(paths []string) ([]file, error) { | |
| wg := sync.WaitGroup{} | |
| wg.Add(len(paths)) | |
| files := make([]file, len(paths)) | |
| errs := make([]error, len(paths)) | |
| for i, path := range paths { | |
| i, path := i, filepath.Join(fileutils.DawnRoot(), path) | |
| go func() { | |
| defer wg.Done() | |
| body, err := ioutil.ReadFile(path) | |
| if err != nil { | |
| errs[i] = fmt.Errorf("Failed to open %v: %w", path, err) | |
| } else { | |
| content := string(body) | |
| lines := strings.Split(content, "\n") | |
| files[i] = file{path: path, lines: lines} | |
| } | |
| }() | |
| } | |
| wg.Wait() | |
| for _, err := range errs { | |
| if err != nil { | |
| return nil, err | |
| } | |
| } | |
| return files, nil | |
| } |