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'.
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
22 import multiprocessing
34 def ignoreRemoval(include, toAdd, absFileName, moduleRules, noexclude):
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")
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", ),
56 for k, values in debugStl.items():
62 # Avoid proposing to use libstdc++ internal headers.
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",
71 for k, v in bits.items():
72 if include == k and v in toAdd:
75 # Avoid proposing o3tl fw declaration
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; }",
80 for k, v, in o3tl.items():
81 if include == k and v in toAdd:
84 # Follow boost documentation.
85 if include == "boost/optional.hpp" and "boost/optional/optional.hpp" in toAdd:
87 if include == "boost/intrusive_ptr.hpp" and "boost/smart_ptr/intrusive_ptr.hpp" in toAdd:
89 if include == "boost/shared_ptr.hpp" and "boost/smart_ptr/shared_ptr.hpp" in toAdd:
91 if include == "boost/variant.hpp" and "boost/variant/variant.hpp" in toAdd:
93 if include == "boost/unordered_map.hpp" and "boost/unordered/unordered_map.hpp" in toAdd:
95 if include == "boost/functional/hash.hpp" and "boost/container_hash/extensions.hpp" in toAdd:
98 # Avoid .hxx to .h proposals in basic css/uno/* API
100 "com/sun/star/uno/Any.hxx": "com/sun/star/uno/Any.h",
101 "com/sun/star/uno/Reference.hxx": "com/sun/star/uno/Reference.h",
102 "com/sun/star/uno/Sequence.hxx": "com/sun/star/uno/Sequence.h",
103 "com/sun/star/uno/Type.hxx": "com/sun/star/uno/Type.h"
105 for k, v in unoapi.items():
106 if include == k and v in toAdd:
109 # 3rd-party, non-self-contained headers.
110 if include == "libepubgen/libepubgen.h" and "libepubgen/libepubgen-decls.h" in toAdd:
112 if include == "librevenge/librevenge.h" and "librevenge/RVNGPropertyList.h" in toAdd:
114 if include == "libetonyek/libetonyek.h" and "libetonyek/EtonyekDocument.h" in toAdd:
118 # <https://www.openoffice.org/tools/CodingGuidelines.sxw> insists on not
121 # Works around a build breakage specific to the broken Android
123 "android/compatibility.hxx",
124 # Removing this would change the meaning of '#if defined OSL_BIGENDIAN'.
127 if include in noRemove:
130 # Ignore when <foo> is to be replaced with "foo".
134 fileName = os.path.relpath(absFileName, os.getcwd())
136 # Skip headers used only for compile test
137 if fileName == "cppu/qa/cppumaker/test_cppumaker.cxx":
138 if include.endswith(".hpp"):
141 # yaml rules, except when --noexclude is given
143 if "excludelist" in moduleRules.keys() and not noexclude:
144 excludelistRules = moduleRules["excludelist"]
145 if fileName in excludelistRules.keys():
146 if include in excludelistRules[fileName]:
152 def unwrapInclude(include):
153 # Drop <> or "" around the include.
157 def processIWYUOutput(iwyuOutput, moduleRules, fileName, noexclude):
162 currentFileName = None
164 for line in iwyuOutput:
167 # Bail out if IWYU gave an error due to non self-containedness
168 if re.match ("(.*): error: (.*)", line):
179 shouldAdd = fileName + " should add these lines:"
180 match = re.match(shouldAdd, line)
182 currentFileName = match.group(0).split(' ')[0]
186 shouldRemove = fileName + " should remove these lines:"
187 match = re.match(shouldRemove, line)
189 currentFileName = match.group(0).split(' ')[0]
194 match = re.match('#include ([^ ]+)', line)
196 include = unwrapInclude(match.group(1))
197 toAdd.append(include)
199 # Forward declaration.
203 match = re.match("- #include (.*) // lines (.*)-.*", line)
205 # Only suggest removals for now. Removing fwd decls is more complex: they may be
206 # indeed unused or they may removed to be replaced with an include. And we want to
208 include = unwrapInclude(match.group(1))
209 lineno = match.group(2)
210 if not ignoreRemoval(include, toAdd, currentFileName, moduleRules, noexclude):
211 toRemove.append("%s:%s: %s" % (currentFileName, lineno, include))
213 for remove in sorted(toRemove):
214 print("ERROR: %s: remove not needed include" % remove)
218 def run_tool(task_queue, failed_files, dontstop, noexclude):
220 invocation, moduleRules = task_queue.get()
221 if not len(failed_files):
222 print("[IWYU] " + invocation.split(' ')[-1])
223 p = subprocess.Popen(invocation, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
224 retcode = processIWYUOutput(p.communicate()[0].decode('utf-8').splitlines(), moduleRules, invocation.split(' ')[-1], noexclude)
226 print("ERROR: A file is probably not self contained, check this commands output:\n" + invocation)
228 print("ERROR: The following command found unused includes:\n" + invocation)
230 failed_files.append(invocation)
231 task_queue.task_done()
234 def isInUnoIncludeFile(path):
235 return path.startswith("include/com/") \
236 or path.startswith("include/cppu/") \
237 or path.startswith("include/cppuhelper/") \
238 or path.startswith("include/osl/") \
239 or path.startswith("include/rtl/") \
240 or path.startswith("include/sal/") \
241 or path.startswith("include/salhelper/") \
242 or path.startswith("include/systools/") \
243 or path.startswith("include/typelib/") \
244 or path.startswith("include/uno/")
247 def tidy(compileCommands, paths, dontstop, noexclude):
251 max_task = multiprocessing.cpu_count()
252 task_queue = queue.Queue(max_task)
254 for _ in range(max_task):
255 t = threading.Thread(target=run_tool, args=(task_queue, failed_files, dontstop, noexclude))
259 for path in sorted(paths):
260 if isInUnoIncludeFile(path):
263 # IWYU fails on these with #error: don't use this in new code
264 if path.startswith("include/vcl/toolkit"):
267 moduleName = path.split("/")[0]
269 rulePath = os.path.join(moduleName, "IwyuFilter_" + moduleName + ".yaml")
271 if os.path.exists(rulePath):
272 moduleRules = yaml.full_load(open(rulePath))
274 pathAbs = os.path.abspath(path)
275 compileFile = pathAbs
276 matches = [i for i in compileCommands if i["file"] == compileFile]
278 # Only use assume-filename for headers, so we don't try to analyze e.g. Windows-only
280 if "assumeFilename" in moduleRules.keys() and not path.endswith("cxx"):
281 assume = moduleRules["assumeFilename"]
283 assumeAbs = os.path.abspath(assume)
284 compileFile = assumeAbs
285 matches = [i for i in compileCommands if i["file"] == compileFile]
287 print("WARNING: no compile commands for '" + path + "' (assumed filename: '" + assume + "'")
290 print("WARNING: no compile commands for '" + path + "'")
293 _, _, args = matches[0]["command"].partition(" ")
295 args = args.replace(assumeAbs, "-x c++ " + pathAbs)
297 invocation = "include-what-you-use -Xiwyu --no_fwd_decls -Xiwyu --max_line_length=200 " + args
298 task_queue.put((invocation, moduleRules))
301 if len(failed_files):
304 except KeyboardInterrupt:
305 print('\nCtrl-C detected, goodbye.')
308 sys.exit(return_code)
312 parser = argparse.ArgumentParser(description='Check source files for unneeded includes.')
313 parser.add_argument('--continue', action='store_true',
314 help='Don\'t stop on errors. Useful for periodic re-check of large amount of files')
315 parser.add_argument('Files' , nargs='*',
316 help='The files to be checked')
317 parser.add_argument('--recursive', metavar='DIR', nargs=1, type=str,
318 help='Recursively search a directory for source files to check')
319 parser.add_argument('--headers', action='store_true',
320 help='Check header files. If omitted, check source files. Use with --recursive.')
321 parser.add_argument('--noexclude', action='store_true',
322 help='Ignore excludelist. Useful to check whether its exclusions are still all valid.')
324 args = parser.parse_args()
332 for root, dirs, files in os.walk(args.recursive[0]):
335 if (file.endswith(".hxx") or file.endswith(".hrc") or file.endswith(".h")):
336 list_of_files.append(os.path.join(root,file))
338 if (file.endswith(".cxx") or file.endswith(".c")):
339 list_of_files.append(os.path.join(root,file))
341 list_of_files = args.Files
344 with open("compile_commands.json", 'r') as compileCommandsSock:
345 compileCommands = json.load(compileCommandsSock)
346 except FileNotFoundError:
347 print ("File 'compile_commands.json' does not exist, please run:\nmake vim-ide-integration")
350 # quickly sanity check whether files with exceptions in yaml still exists
351 # only check for the module of the very first filename passed
353 # Verify there are files selected for checking, with --recursive it
354 # may happen that there are in fact no C/C++ files in a module directory
355 if not list_of_files:
356 print("No files found to check!")
359 moduleName = sorted(list_of_files)[0].split("/")[0]
360 rulePath = os.path.join(moduleName, "IwyuFilter_" + moduleName + ".yaml")
362 if os.path.exists(rulePath):
363 moduleRules = yaml.full_load(open(rulePath))
364 if "excludelist" in moduleRules.keys():
365 excludelistRules = moduleRules["excludelist"]
366 for pathname in excludelistRules.keys():
367 file = pathlib.Path(pathname)
368 if not file.exists():
369 print("WARNING: File listed in " + rulePath + " no longer exists: " + pathname)
371 tidy(compileCommands, paths=list_of_files, dontstop=vars(args)["continue"], noexclude=args.noexclude)
373 if __name__ == '__main__':
376 # vim:set shiftwidth=4 softtabstop=4 expandtab: