• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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