1// Copyright 2021 The Tint Authors. 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15// trim-includes is a tool to try removing unnecessary include statements from 16// all .cc and .h files in the tint project. 17// 18// trim-includes removes each #include from each file, then runs the provided 19// build script to determine whether the line was necessary. If the include is 20// required, it is restored, otherwise it is left deleted. 21// After all the #include statements have been tested, the file is 22// clang-formatted and git staged. 23package main 24 25import ( 26 "flag" 27 "fmt" 28 "io/ioutil" 29 "os" 30 "os/exec" 31 "path/filepath" 32 "regexp" 33 "strings" 34 "sync" 35 "time" 36 37 "dawn.googlesource.com/tint/tools/src/fileutils" 38 "dawn.googlesource.com/tint/tools/src/glob" 39) 40 41var ( 42 // Path to the build script to run after each attempting to remove each 43 // #include 44 buildScript = "" 45) 46 47func main() { 48 if err := run(); err != nil { 49 fmt.Println(err) 50 os.Exit(1) 51 } 52} 53 54func showUsage() { 55 fmt.Println(` 56trim-includes is a tool to try removing unnecessary include statements from all 57.cc and .h files in the tint project. 58 59trim-includes removes each #include from each file, then runs the provided build 60script to determine whether the line was necessary. If the include is required, 61it is restored, otherwise it is left deleted. 62After all the #include statements have been tested, the file is clang-formatted 63and git staged. 64 65Usage: 66 trim-includes <path-to-build-script>`) 67 os.Exit(1) 68} 69 70func run() error { 71 flag.Parse() 72 args := flag.Args() 73 if len(args) < 1 { 74 showUsage() 75 } 76 77 var err error 78 buildScript, err = exec.LookPath(args[0]) 79 if err != nil { 80 return err 81 } 82 buildScript, err = filepath.Abs(buildScript) 83 if err != nil { 84 return err 85 } 86 87 cfg, err := glob.LoadConfig("config.cfg") 88 if err != nil { 89 return err 90 } 91 92 fmt.Println("Checking the project builds with no changes...") 93 ok, err := tryBuild() 94 if err != nil { 95 return err 96 } 97 if !ok { 98 return fmt.Errorf("Project does not build without edits") 99 } 100 101 fmt.Println("Scanning for files...") 102 paths, err := glob.Scan(fileutils.ProjectRoot(), cfg) 103 if err != nil { 104 return err 105 } 106 107 fmt.Printf("Loading %v source files...\n", len(paths)) 108 files, err := loadFiles(paths) 109 if err != nil { 110 return err 111 } 112 113 for fileIdx, file := range files { 114 fmt.Printf("[%d/%d]: %v\n", fileIdx+1, len(files), file.path) 115 includeLines := file.includesLineNumbers() 116 enabled := make(map[int]bool, len(file.lines)) 117 for i := range file.lines { 118 enabled[i] = true 119 } 120 for includeIdx, line := range includeLines { 121 fmt.Printf(" [%d/%d]: %v", includeIdx+1, len(includeLines), file.lines[line]) 122 enabled[line] = false 123 if err := file.save(enabled); err != nil { 124 return err 125 } 126 ok, err := tryBuild() 127 if err != nil { 128 return err 129 } 130 if ok { 131 fmt.Printf(" removed\n") 132 // Wait a bit so file timestamps get an opportunity to change. 133 // Attempting to save too soon after a successful build may 134 // result in a false-positive build. 135 time.Sleep(time.Second) 136 } else { 137 fmt.Printf(" required\n") 138 enabled[line] = true 139 } 140 } 141 if err := file.save(enabled); err != nil { 142 return err 143 } 144 if err := file.format(); err != nil { 145 return err 146 } 147 if err := file.stage(); err != nil { 148 return err 149 } 150 } 151 fmt.Println("Done") 152 return nil 153} 154 155// Attempt to build the project. Returns true on successful build, false if 156// there was a build failure. 157func tryBuild() (bool, error) { 158 cmd := exec.Command("sh", "-c", buildScript) 159 out, err := cmd.CombinedOutput() 160 switch err := err.(type) { 161 case nil: 162 return cmd.ProcessState.Success(), nil 163 case *exec.ExitError: 164 return false, nil 165 default: 166 return false, fmt.Errorf("Test failed with error: %v\n%v", err, string(out)) 167 } 168} 169 170type file struct { 171 path string 172 lines []string 173} 174 175var includeRE = regexp.MustCompile(`^\s*#include (?:\"([^"]*)\"|:?\<([^"]*)\>)`) 176 177// Returns the file path with the extension stripped 178func stripExtension(path string) string { 179 if dot := strings.IndexRune(path, '.'); dot > 0 { 180 return path[:dot] 181 } 182 return path 183} 184 185// Returns the zero-based line numbers of all #include statements in the file 186func (f *file) includesLineNumbers() []int { 187 out := []int{} 188 for i, l := range f.lines { 189 matches := includeRE.FindStringSubmatch(l) 190 if len(matches) == 0 { 191 continue 192 } 193 194 include := matches[1] 195 if include == "" { 196 include = matches[2] 197 } 198 199 if strings.HasSuffix(stripExtension(f.path), stripExtension(include)) { 200 // Don't remove #include for header of cc 201 continue 202 } 203 204 out = append(out, i) 205 } 206 return out 207} 208 209// Saves the file, omitting the lines with the zero-based line number that are 210// either not in `lines` or have a `false` value. 211func (f *file) save(lines map[int]bool) error { 212 content := []string{} 213 for i, l := range f.lines { 214 if lines[i] { 215 content = append(content, l) 216 } 217 } 218 data := []byte(strings.Join(content, "\n")) 219 return ioutil.WriteFile(f.path, data, 0666) 220} 221 222// Runs clang-format on the file 223func (f *file) format() error { 224 err := exec.Command("clang-format", "-i", f.path).Run() 225 if err != nil { 226 return fmt.Errorf("Couldn't format file '%v': %w", f.path, err) 227 } 228 return nil 229} 230 231// Runs git add on the file 232func (f *file) stage() error { 233 err := exec.Command("git", "-C", fileutils.ProjectRoot(), "add", f.path).Run() 234 if err != nil { 235 return fmt.Errorf("Couldn't stage file '%v': %w", f.path, err) 236 } 237 return nil 238} 239 240// Loads all the files with the given file paths, splitting their content into 241// into lines. 242func loadFiles(paths []string) ([]file, error) { 243 wg := sync.WaitGroup{} 244 wg.Add(len(paths)) 245 files := make([]file, len(paths)) 246 errs := make([]error, len(paths)) 247 for i, path := range paths { 248 i, path := i, filepath.Join(fileutils.ProjectRoot(), path) 249 go func() { 250 defer wg.Done() 251 body, err := ioutil.ReadFile(path) 252 if err != nil { 253 errs[i] = fmt.Errorf("Failed to open %v: %w", path, err) 254 } else { 255 content := string(body) 256 lines := strings.Split(content, "\n") 257 files[i] = file{path: path, lines: lines} 258 } 259 }() 260 } 261 wg.Wait() 262 for _, err := range errs { 263 if err != nil { 264 return nil, err 265 } 266 } 267 return files, nil 268} 269