Ansel 0.0
A darktable fork - bloat + design vision
Loading...
Searching...
No Matches
misplaced_files.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""Find files that live in the wrong directory.
3
4A file whose only consumers are in ONE other subsystem is not shared infrastructure --
5it belongs to that subsystem. This reports those, and refuses to report the ones that
6would merely move the problem.
7
8Two checks make the difference between a useful list and a misleading one:
9
10 * SIBLING USE. A header used by `iop/` looks like an `iop/` file until you notice that
11 three files in its own directory also use it. Moving it alone then creates a new
12 layering inversion where there was none. Such files are reported separately as
13 "cluster" candidates: they can only move together with the siblings that use them.
14
15 * LAYER DIRECTION. Moving a file UP the layer stack (common/ -> iop/) removes an
16 inversion. Moving it DOWN would create one. Only the former is proposed.
17
18Usage:
19 python3 tools/misplaced_files.py # every source directory
20 python3 tools/misplaced_files.py --dir common # one directory
21 python3 tools/misplaced_files.py --clusters # show the blocked ones and why
22"""
23import collections
24import os
25import re
26import sys
27
28SRC = 'src'
29LAYER = {'external': 0, 'win': 0, 'system': 0, 'common': 1, 'math': 1, 'colorprofiles': 1, 'pixel': 2, 'control': 3,
30 'gui': 4, 'widgets': 4, # widgets/ = reusable GTK widgets, no app state 'develop': 5,
31 'iop': 6, 'imageio': 6, 'libs': 7, 'views': 7, 'chart': 7,
32 'apps': 10, # executables link the orchestrator, so they sit ABOVE it 'app': 9}
33
34# Headers included for side effects, or re-included X-macro headers: never propose these.
35NEVER_MOVE = {'module_api.h', 'view_api.h', 'lib_api.h', 'imageio_format_api.h',
36 'imageio_storage_api.h', 'poison.h', 'win.h', 'darktable.h', 'config.h'}
37
38
39def subsystem(path):
40 parts = path.split(os.sep)
41 return parts[1] if len(parts) > 1 else None
42
43
44def sources():
45 out = []
46 for root, dirs, names in os.walk(SRC):
47 dirs[:] = [d for d in dirs if d not in ('external', 'attic')]
48 for n in names:
49 if n.endswith(('.c', '.cc', '.cpp', '.h', '.hh', '.hpp')):
50 out.append(os.path.join(root, n))
51 return sorted(out)
52
53
55 files = sources()
56 text = {}
57 for f in files:
58 try:
59 text[f] = open(f, encoding='utf-8', errors='replace').read()
60 except OSError:
61 text[f] = ''
62 # who includes what, by resolved path
63 known = set(files)
64 users = collections.defaultdict(set)
65 for f, t in text.items():
66 for inc in re.findall(r'^\s*#\s*include\s+"([^"]+)"', t, re.M):
67 for cand in (os.path.normpath(os.path.join(SRC, inc)),
68 os.path.normpath(os.path.join(os.path.dirname(f), inc))):
69 if cand in known and cand != f:
70 users[cand].add(f)
71 break
72 return files, users
73
74
75def main():
76 only_dir = sys.argv[sys.argv.index('--dir') + 1] if '--dir' in sys.argv else None
77 files, users = build_index()
78
79 movable, clusters = [], []
80 for f in files:
81 if not f.endswith(('.h', '.hh', '.hpp')):
82 continue
83 if os.path.basename(f) in NEVER_MOVE:
84 continue
85 home = subsystem(f)
86 if home is None or (only_dir and home != only_dir):
87 continue
88 consumers = collections.Counter(subsystem(u) for u in users.get(f, ()))
89 own = consumers.pop(home, 0)
90 if len(consumers) != 1:
91 continue
92 dest, n = next(iter(consumers.items()))
93 if LAYER.get(dest, 99) <= LAYER.get(home, 0):
94 continue # would move DOWN the stack: creates an inversion
95 impl = [f[:-2] + e for e in ('c', 'cc') if os.path.exists(f[:-2] + e)]
96 rec = (n, f, dest, own, impl)
97 (clusters if own else movable).append(rec)
98
99 movable.sort(reverse=True)
100 clusters.sort(reverse=True)
101
102 print('=== MOVABLE: only one consuming subsystem, and nothing in its own directory '
103 'uses it ===')
104 by_dest = collections.defaultdict(list)
105 for n, f, dest, _, impl in movable:
106 by_dest[dest].append((f, n, impl))
107 for dest, items in sorted(by_dest.items(), key=lambda kv: -len(kv[1])):
108 print('\n -> %s/ (%d)' % (dest, len(items)))
109 for f, n, impl in items:
110 print(' %-44s %d includer(s)%s'
111 % (f, n, ' + ' + ', '.join(os.path.basename(i) for i in impl) if impl else ''))
112
113 if '--clusters' in sys.argv:
114 print('\n=== BLOCKED: one external consumer, but siblings use it too ===')
115 print(' (moving these alone creates an inversion -- move the cluster or nothing)')
116 for n, f, dest, own, _ in clusters:
117 print(' %-44s -> %-9s %d external, %d sibling(s)' % (f, dest, n, own))
118 else:
119 print('\n%d further files have a single external consumer but are also used by '
120 'siblings; run --clusters to see them.' % len(clusters))
121 return 0
122
123
124if __name__ == '__main__':
125 sys.exit(main())