Colibre: tdf#148029 Remove dark area in dark variant
[LibreOffice.git] / bin / find-unneeded-includes
blob8197e9bf681491eabf0f8d49dbbfe0160c9cb830
1 #!/usr/bin/env python3
3 # This Source Code Form is subject to the terms of the Mozilla Public
4 # License, v. 2.0. If a copy of the MPL was not distributed with this
5 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
7 # This parses the output of 'include-what-you-use', focusing on just removing
8 # not needed includes and providing a relatively conservative output by
9 # filtering out a number of LibreOffice-specific false positives.
11 # It assumes you have a 'compile_commands.json' around (similar to clang-tidy),
12 # you can generate one with 'make vim-ide-integration'.
14 # Design goals:
15 # - excludelist mechanism, so a warning is either fixed or excluded
16 # - works in a plugins-enabled clang build
17 # - no custom configure options required
18 # - no need to generate a dummy library to build a header
20 import glob
21 import json
22 import multiprocessing
23 import os
24 import queue
25 import re
26 import subprocess
27 import sys
28 import threading
29 import yaml
30 import argparse
31 import pathlib
34 def ignoreRemoval(include, toAdd, absFileName, moduleRules):
35     # global rules
37     # Avoid replacing .hpp with .hdl in the com::sun::star and  ooo::vba namespaces.
38     if ( include.startswith("com/sun/star") or include.startswith("ooo/vba") ) and include.endswith(".hpp"):
39         hdl = include.replace(".hpp", ".hdl")
40         if hdl in toAdd:
41             return True
43     # Avoid debug STL.
44     debugStl = {
45         "array": ("debug/array", ),
46         "bitset": ("debug/bitset", ),
47         "deque": ("debug/deque", ),
48         "forward_list": ("debug/forward_list", ),
49         "list": ("debug/list", ),
50         "map": ("debug/map.h", "debug/multimap.h"),
51         "set": ("debug/set.h", "debug/multiset.h"),
52         "unordered_map": ("debug/unordered_map", ),
53         "unordered_set": ("debug/unordered_set", ),
54         "vector": ("debug/vector", ),
55     }
56     for k, values in debugStl.items():
57         if include == k:
58             for value in values:
59                 if value in toAdd:
60                     return True
62     # Avoid proposing to use libstdc++ internal headers.
63     bits = {
64         "exception": "bits/exception.h",
65         "memory": "bits/shared_ptr.h",
66         "functional": "bits/std_function.h",
67         "cmath": "bits/std_abs.h",
68         "ctime": "bits/types/clock_t.h",
69         "cstdint": "bits/stdint-uintn.h",
70     }
71     for k, v in bits.items():
72         if include == k and v in toAdd:
73             return True
75     # Avoid proposing o3tl fw declaration
76     o3tl = {
77         "o3tl/typed_flags_set.hxx" : "namespace o3tl { template <typename T> struct typed_flags; }",
78         "o3tl/deleter.hxx" : "namespace o3tl { template <typename T> struct default_delete; }",
79         "o3tl/span.hxx" : "namespace o3tl { template <typename T> class span; }",
80     }
81     for k, v, in o3tl.items():
82         if include == k and v in toAdd:
83             return True
85     # Follow boost documentation.
86     if include == "boost/optional.hpp" and "boost/optional/optional.hpp" in toAdd:
87         return True
88     if include == "boost/intrusive_ptr.hpp" and "boost/smart_ptr/intrusive_ptr.hpp" in toAdd:
89         return True
90     if include == "boost/shared_ptr.hpp" and "boost/smart_ptr/shared_ptr.hpp" in toAdd:
91         return True
92     if include == "boost/variant.hpp" and "boost/variant/variant.hpp" in toAdd:
93         return True
94     if include == "boost/unordered_map.hpp" and "boost/unordered/unordered_map.hpp" in toAdd:
95         return True
96     if include == "boost/functional/hash.hpp" and "boost/container_hash/extensions.hpp" in toAdd:
97         return True
99     # Avoid .hxx to .h proposals in basic css/uno/* API
100     unoapi = {
101         "com/sun/star/uno/Any.hxx": "com/sun/star/uno/Any.h",
102         "com/sun/star/uno/Reference.hxx": "com/sun/star/uno/Reference.h",
103         "com/sun/star/uno/Sequence.hxx": "com/sun/star/uno/Sequence.h",
104         "com/sun/star/uno/Type.hxx": "com/sun/star/uno/Type.h"
105     }
106     for k, v in unoapi.items():
107         if include == k and v in toAdd:
108             return True
110     # 3rd-party, non-self-contained headers.
111     if include == "libepubgen/libepubgen.h" and "libepubgen/libepubgen-decls.h" in toAdd:
112         return True
113     if include == "librevenge/librevenge.h" and "librevenge/RVNGPropertyList.h" in toAdd:
114         return True
115     if include == "libetonyek/libetonyek.h" and "libetonyek/EtonyekDocument.h" in toAdd:
116         return True
118     noRemove = (
119         # <https://www.openoffice.org/tools/CodingGuidelines.sxw> insists on not
120         # removing this.
121         "sal/config.h",
122         # Works around a build breakage specific to the broken Android
123         # toolchain.
124         "android/compatibility.hxx",
125         # Removing this would change the meaning of '#if defined OSL_BIGENDIAN'.
126         "osl/endian.h",
127     )
128     if include in noRemove:
129         return True
131     # Ignore when <foo> is to be replaced with "foo".
132     if include in toAdd:
133         return True
135     fileName = os.path.relpath(absFileName, os.getcwd())
137     # Skip headers used only for compile test
138     if fileName == "cppu/qa/cppumaker/test_cppumaker.cxx":
139         if include.endswith(".hpp"):
140             return True
142     # yaml rules
144     if "excludelist" in moduleRules.keys():
145         excludelistRules = moduleRules["excludelist"]
146         if fileName in excludelistRules.keys():
147             if include in excludelistRules[fileName]:
148                 return True
150     return False
153 def unwrapInclude(include):
154     # Drop <> or "" around the include.
155     return include[1:-1]
158 def processIWYUOutput(iwyuOutput, moduleRules, fileName):
159     inAdd = False
160     toAdd = []
161     inRemove = False
162     toRemove = []
163     currentFileName = None
165     for line in iwyuOutput:
166         line = line.strip()
168         # Bail out if IWYU gave an error due to non self-containedness
169         if re.match ("(.*): error: (.*)", line):
170             return -1
172         if len(line) == 0:
173             if inRemove:
174                 inRemove = False
175                 continue
176             if inAdd:
177                 inAdd = False
178                 continue
180         shouldAdd = fileName + " should add these lines:"
181         match = re.match(shouldAdd, line)
182         if match:
183             currentFileName = match.group(0).split(' ')[0]
184             inAdd = True
185             continue
187         shouldRemove = fileName + " should remove these lines:"
188         match = re.match(shouldRemove, line)
189         if match:
190             currentFileName = match.group(0).split(' ')[0]
191             inRemove = True
192             continue
194         if inAdd:
195             match = re.match('#include ([^ ]+)', line)
196             if match:
197                 include = unwrapInclude(match.group(1))
198                 toAdd.append(include)
199             else:
200                 # Forward declaration.
201                 toAdd.append(line)
203         if inRemove:
204             match = re.match("- #include (.*)  // lines (.*)-.*", line)
205             if match:
206                 # Only suggest removals for now. Removing fwd decls is more complex: they may be
207                 # indeed unused or they may removed to be replaced with an include. And we want to
208                 # avoid the later.
209                 include = unwrapInclude(match.group(1))
210                 lineno = match.group(2)
211                 if not ignoreRemoval(include, toAdd, currentFileName, moduleRules):
212                     toRemove.append("%s:%s: %s" % (currentFileName, lineno, include))
214     for remove in sorted(toRemove):
215         print("ERROR: %s: remove not needed include" % remove)
216     return len(toRemove)
219 def run_tool(task_queue, failed_files, dontstop):
220     while True:
221         invocation, moduleRules = task_queue.get()
222         if not len(failed_files):
223             print("[IWYU] " + invocation.split(' ')[-1])
224             p = subprocess.Popen(invocation, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
225             retcode = processIWYUOutput(p.communicate()[0].decode('utf-8').splitlines(), moduleRules, invocation.split(' ')[-1])
226             if retcode == -1:
227                 print("ERROR: A file is probably not self contained, check this commands output:\n" + invocation)
228             elif retcode > 0:
229                 print("ERROR: The following command found unused includes:\n" + invocation)
230                 if not dontstop:
231                     failed_files.append(invocation)
232         task_queue.task_done()
235 def isInUnoIncludeFile(path):
236     return path.startswith("include/com/") \
237             or path.startswith("include/cppu/") \
238             or path.startswith("include/cppuhelper/") \
239             or path.startswith("include/osl/") \
240             or path.startswith("include/rtl/") \
241             or path.startswith("include/sal/") \
242             or path.startswith("include/salhelper/") \
243             or path.startswith("include/systools/") \
244             or path.startswith("include/typelib/") \
245             or path.startswith("include/uno/")
248 def tidy(compileCommands, paths, dontstop):
249     return_code = 0
251     try:
252         max_task = multiprocessing.cpu_count()
253         task_queue = queue.Queue(max_task)
254         failed_files = []
255         for _ in range(max_task):
256             t = threading.Thread(target=run_tool, args=(task_queue, failed_files, dontstop))
257             t.daemon = True
258             t.start()
260         for path in sorted(paths):
261             if isInUnoIncludeFile(path):
262                 continue
264             moduleName = path.split("/")[0]
266             rulePath = os.path.join(moduleName, "IwyuFilter_" + moduleName + ".yaml")
267             moduleRules = {}
268             if os.path.exists(rulePath):
269                 moduleRules = yaml.full_load(open(rulePath))
270             assume = None
271             pathAbs = os.path.abspath(path)
272             compileFile = pathAbs
273             matches = [i for i in compileCommands if i["file"] == compileFile]
274             if not len(matches):
275                 # Only use assume-filename for headers, so we don't try to analyze e.g. Windows-only
276                 # code on Linux.
277                 if "assumeFilename" in moduleRules.keys() and not path.endswith("cxx"):
278                     assume = moduleRules["assumeFilename"]
279                 if assume:
280                     assumeAbs = os.path.abspath(assume)
281                     compileFile = assumeAbs
282                     matches = [i for i in compileCommands if i["file"] == compileFile]
283                     if not len(matches):
284                         print("WARNING: no compile commands for '" + path + "' (assumed filename: '" + assume + "'")
285                         continue
286                 else:
287                     print("WARNING: no compile commands for '" + path + "'")
288                     continue
290             _, _, args = matches[0]["command"].partition(" ")
291             if assume:
292                 args = args.replace(assumeAbs, "-x c++ " + pathAbs)
294             invocation = "include-what-you-use -Xiwyu --no_fwd_decls -Xiwyu --max_line_length=200 " + args
295             task_queue.put((invocation, moduleRules))
297         task_queue.join()
298         if len(failed_files):
299             return_code = 1
301     except KeyboardInterrupt:
302         print('\nCtrl-C detected, goodbye.')
303         os.kill(0, 9)
305     sys.exit(return_code)
308 def main(argv):
309     parser = argparse.ArgumentParser(description='Check source files for unneeded includes.')
310     parser.add_argument('--continue', action='store_true',
311                     help='Don\'t stop on errors. Useful for periodic re-check of large amount of files')
312     parser.add_argument('Files' , nargs='*',
313                     help='The files to be checked')
314     parser.add_argument('--recursive', metavar='DIR', nargs=1, type=str,
315                     help='Recursively search a directory for source files to check')
316     parser.add_argument('--headers', action='store_true',
317                     help='Check header files. If omitted, check source files. Use with --recursive.')
319     args = parser.parse_args()
321     if not len(argv):
322         parser.print_help()
323         return
325     list_of_files = []
326     if args.recursive:
327         for root, dirs, files in os.walk(args.recursive[0]):
328             for file in files:
329                 if args.headers:
330                     if (file.endswith(".hxx") or file.endswith(".hrc") or file.endswith(".h")):
331                         list_of_files.append(os.path.join(root,file))
332                 else:
333                     if (file.endswith(".cxx") or file.endswith(".c")):
334                         list_of_files.append(os.path.join(root,file))
335     else:
336         list_of_files = args.Files
338     try:
339         with open("compile_commands.json", 'r') as compileCommandsSock:
340             compileCommands = json.load(compileCommandsSock)
341     except FileNotFoundError:
342         print ("File 'compile_commands.json' does not exist, please run:\nmake vim-ide-integration")
343         sys.exit(-1)
345     # quickly sanity check whether files with exceptions in yaml still exists
346     # only check for the module of the very first filename passed
347     moduleName = sorted(list_of_files)[0].split("/")[0]
348     rulePath = os.path.join(moduleName, "IwyuFilter_" + moduleName + ".yaml")
349     moduleRules = {}
350     if os.path.exists(rulePath):
351         moduleRules = yaml.full_load(open(rulePath))
352     if "excludelist" in moduleRules.keys():
353         excludelistRules = moduleRules["excludelist"]
354         for pathname in excludelistRules.keys():
355             file = pathlib.Path(pathname)
356             if not file.exists():
357                 print("WARNING: File listed in " + rulePath + " no longer exists: " + pathname)
359     tidy(compileCommands, paths=list_of_files, dontstop=vars(args)["continue"])
361 if __name__ == '__main__':
362     main(sys.argv[1:])
364 # vim:set shiftwidth=4 softtabstop=4 expandtab: