224 """Comment each candidate out, rebuild its object, keep only what still compiles."""
225 confirmed, rejected = [], []
227 for path, cands
in sorted(results.items()):
228 if not path.endswith((
'.c',
'.cc',
'.cpp')):
233 original =
read(path)
238 patched = original.replace(
'#include "%s"' % c[
'include'],
239 '/* IWYU-TEST */ //#include "%s"' % c[
'include'], 1)
240 with open(path,
'w', encoding=
'utf-8')
as fh:
242 rc = subprocess.run([
'ninja',
'-C', BUILD, target],
243 capture_output=
True, text=
True).returncode
244 with open(path,
'w', encoding=
'utf-8')
as fh:
246 (confirmed
if rc == 0
else rejected).append((path, c[
'include']))
247 subprocess.run([
'ninja',
'-C', BUILD, target], capture_output=
True, text=
True)
250 return confirmed, rejected
254 only_h =
'--headers' in sys.argv
255 only_c =
'--sources' in sys.argv
258 if '--file' in sys.argv:
259 want = sys.argv[sys.argv.index(
'--file') + 1]
260 for inc
in results.get(want, []):
261 print(
'%s: %s%s' % (want, inc[
'include'],
262 ' [platform-guarded]' if inc[
'platform_guarded']
else ''))
263 if want
not in results:
264 print(
'%s: no candidates' % want)
268 results = {k: v
for k, v
in results.items()
if k.endswith((
'.h',
'.hpp'))}
270 results = {k: v
for k, v
in results.items()
if k.endswith((
'.c',
'.cc',
'.cpp'))}
272 if '--json' in sys.argv:
273 out = sys.argv[sys.argv.index(
'--json') + 1]
274 with open(out,
'w', encoding=
'utf-8')
as fh:
275 json.dump(results, fh, indent=1, sort_keys=
True)
276 print(
'wrote %s' % out)
278 if '--push-down' in sys.argv:
281 if '--apply' in sys.argv:
282 prefix = sys.argv[sys.argv.index(
'--prefix') + 1]
if '--prefix' in sys.argv
else 'src/'
283 return apply_mode(results, prefix,
'--include-guarded' not in sys.argv)
285 if '--verify' in sys.argv:
287 if '--limit' in sys.argv:
288 limit = int(sys.argv[sys.argv.index(
'--limit') + 1])
289 confirmed, rejected =
verify(results, limit)
290 print(
'\n=== VERIFIED REMOVABLE (compiles without it) ===')
291 for p, i
in confirmed:
292 print(
' %s: %s' % (p, i))
293 print(
'\n=== NEEDED AFTER ALL (transitive dependency) ===')
294 for p, i
in rejected:
295 print(
' %s: %s' % (p, i))
296 print(
'\n%d removable / %d tested' % (len(confirmed), len(confirmed) + len(rejected)))
299 total = sum(len(v)
for v
in results.values())
300 hdr = sum(len(v)
for k, v
in results.items()
if k.endswith((
'.h',
'.hpp')))
301 print(
'candidate unneeded includes: %d in %d files (%d in headers, %d in sources)'
302 % (total, len(results), hdr, total - hdr))
304 per_dir = defaultdict(int)
305 for k, v
in results.items():
306 per_dir[k.split(os.sep)[1]] += len(v)
307 print(
'\nby directory:')
308 for d, n
in sorted(per_dir.items(), key=
lambda kv: -kv[1]):
309 print(
' %5d %s' % (n, d))
311 per_header = defaultdict(int)
312 for v
in results.values():
314 per_header[c[
'include']] += 1
315 print(
'\nmost often included without being used:')
316 for h, n
in sorted(per_header.items(), key=
lambda kv: -kv[1])[:20]:
317 print(
' %5d %s' % (n, h))
319 worst = sorted(results.items(), key=
lambda kv: -len(kv[1]))[:15]
320 print(
'\nfiles with the most candidates:')
322 print(
' %5d %s' % (len(v), p))
393 """Largest subset of `files` whose removals still build. Removals are independent
394 in practice, so recursive halving is sound; the caller re-verifies the union."""
403 mid = len(files) // 2
404 left =
_bisect_set(files[:mid], originals, targets)
405 right =
_bisect_set(files[mid:], originals, targets)
410 good = left
if len(left) >= len(right)
else right
417 targets = {p: v
for p, v
in results.items()
if p.startswith(prefix)}
419 targets = {p: v
for p, v
in targets.items()
420 if not any(c[
'platform_guarded']
for c
in v)}
424 annotated = [(p, c[
'include'])
for p, v
in targets.items()
for c
in v
if c.get(
'annotated')]
425 targets = {p: [c
for c
in v
if not c.get(
'annotated')]
for p, v
in targets.items()}
426 targets = {p: v
for p, v
in targets.items()
if v}
427 for p, i
in annotated:
428 print(
' skipping annotated include (documented intent): %s: %s' % (p, i))
430 print(
'nothing to do for prefix %r' % prefix)
433 originals = {p:
read(p)
for p
in targets}
434 print(
'applying %d candidate removals across %d files (prefix %r)'
435 % (sum(len(v)
for v
in targets.values()), len(targets), prefix))
436 for p, cands
in targets.items():
437 _strip(p, [c[
'include']
for c
in cands])
439 removed = dict(targets)
440 for attempt
in range(6):
450 print(
' build fails and blames nothing we edited -- bisecting the file set')
451 keep =
_bisect_set(sorted(removed), originals, targets)
452 removed = {p: targets[p]
for p
in keep}
454 print(
' round %d: restoring %d implicated file(s)' % (attempt + 1, len(bad)))
456 open(p,
'w', encoding=
'utf-8').write(originals[p])
459 print(
'did not converge; reverting all', file=sys.stderr)
460 for p, s
in originals.items():
461 open(p,
'w', encoding=
'utf-8').write(s)
467 for p
in [f
for f
in targets
if f
not in removed]:
468 cands = [c[
'include']
for c
in targets[p]]
472 removed[p] = targets[p]
473 salvaged += len(cands)
475 open(p,
'w', encoding=
'utf-8').write(originals[p])
483 open(p,
'w', encoding=
'utf-8').write(originals[p])
487 removed[p] = [{
'include': i}
for i
in kept]
488 salvaged += len(kept)
491 total = sum(len(v)
for v
in removed.values())
492 print(
'\nREMOVED %d includes from %d files (%d salvaged by bisection); final build rc=%d'
493 % (total, len(removed), salvaged, rc))
494 return 0
if rc == 0
else 1
543 """Files that include `header` directly (either spelling)."""
544 rel = header[len(SRC) + 1:]
if header.startswith(SRC + os.sep)
else header
545 base = os.path.basename(header)
547 for root, dirs, names
in os.walk(SRC):
548 dirs[:] = [d
for d
in dirs
if d !=
'external']
550 if not n.endswith((
'.c',
'.cc',
'.cpp',
'.h',
'.hpp')):
552 p = os.path.join(root, n)
554 if '#include "%s"' % rel
in t
or '#include "%s"' % base
in t:
555 hits.add(os.path.normpath(p))
560 headers = {p: v
for p, v
in results.items()
if p.endswith((
'.h',
'.hpp'))}
562 moved = kept_universal = reverted = 0
564 for hdr, cands
in sorted(headers.items()):
568 original_hdr =
read(hdr)
572 print(
' %s: dropped %s (nobody needed it)' % (hdr, inc))
577 c_consumers = {f
for f
in consumers
if f.endswith((
'.c',
'.cc',
'.cpp'))}
578 if needy
and c_consumers
and needy >= c_consumers:
580 open(hdr,
'w', encoding=
'utf-8').write(original_hdr)
582 print(
' %s: KEPT %s (all %d consumers need it)' % (hdr, inc, len(c_consumers)))
587 for f
in sorted(needy):
589 if '#include "%s"' % inc
in t:
591 lines = [l
for l
in t.splitlines()
if l.startswith(
'#include "')]
594 anchor = lines[0] +
'\n'
595 t = t.replace(anchor, anchor +
'#include "%s"\n' % inc, 1)
596 with open(f,
'w', encoding=
'utf-8')
as fh:
602 print(
' %s: moved %s down into %d consumer(s)' % (hdr, inc, len(patched)))
605 open(hdr,
'w', encoding=
'utf-8').write(original_hdr)
607 t =
read(f).replace(
'#include "%s"\n' % inc,
'', 1)
608 with open(f,
'w', encoding=
'utf-8')
as fh:
611 print(
' %s: reverted %s (did not converge)' % (hdr, inc))
615 print(
'\nmoved=%d kept-as-universal=%d reverted=%d final build rc=%d'
616 % (moved, kept_universal, reverted, rc))
617 return 0
if rc == 0
else 1