Files
mantra-kmp/docs/scripts/m3-spacing-positions.py
T

110 lines
3.9 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""Count .dp literals by whether they sit in a spacing position or a dimension one.
m3-audit.sh used to split them by value, exempting a list of numbers that "looked like"
dimensions -- which is wrong twice over. 16.dp is a spacing stop *and* a plausible icon
size, and 50.dp was a Spacer height in 53 places and a divider width in one. Only the call
shape says which, so this reads the shape.
spacing padding(...), PaddingValues(...), Arrangement.spacedBy(...), and a
.height()/.width() whose enclosing call is Spacer(
dimension .size(), a non-Spacer .height()/.width(), RoundedCornerShape(),
BorderStroke(), a `size =` argument
Spacing positions are what phase 2 drives to zero; dimensions are out of its scope and are
reported so the number is visible rather than exempted.
Usage: m3-spacing-positions.py [--list]
"""
import os, re, sys
UI = 'composeApp/src/commonMain/kotlin/press/mantra/compose/ui'
THEME = os.path.join(UI, 'theme')
def _spans(text, opener):
"""Brace-matched character ranges of every call matching `opener`."""
spans = []
for m in re.finditer(opener, text):
depth, i = 0, m.end() - 1
while i < len(text):
if text[i] == '(':
depth += 1
elif text[i] == ')':
depth -= 1
if depth == 0:
spans.append((m.start(), i))
break
i += 1
return spans
def classify(path):
text = open(path, encoding='utf-8').read()
pads = _spans(text, r'\.?\bpadding\s*\(|\bPaddingValues\s*\(')
gaps = _spans(text, r'\bspacedBy\s*\(')
spacers = _spans(text, r'\bSpacer\s*\(')
def inside(pos, spans):
return any(a <= pos <= b for a, b in spans)
hw = [
(m.start(1), m.end(1))
for m in re.finditer(r'\.(?:height|width)\s*\(\s*(\d+\.dp)\s*\)', text)
if inside(m.start(), spacers)
]
spacing, dimension = [], []
for m in re.finditer(r'\b(\d+)\.dp\b', text):
pos = m.start()
line_start = text.rfind('\n', 0, pos) + 1
line = text[line_start:text.find('\n', pos)]
if re.match(r'\s*(//|\*|/\*)', line):
continue
# An explicit, reasoned opt-out. A value that is genuinely not a step in the
# spacing rhythm -- room reserved below a list, say -- is marked at the site
# rather than exempted by value in this file, so the reason travels with it
# and a reviewer sees it in the diff.
# Walk back over the unbroken run of comment lines directly above.
exempt = False
prev = text[:line_start].rstrip('\n').split('\n')
for candidate in reversed(prev[-8:]):
stripped = candidate.strip()
if not stripped.startswith('//'):
break
if 'm3-spacing-exempt' in stripped:
exempt = True
break
if exempt:
continue
entry = (path, text[:pos].count('\n') + 1, m.group(0), line.strip()[:90])
if inside(pos, pads) or inside(pos, gaps) or any(a <= pos < b for a, b in hw):
spacing.append(entry)
else:
dimension.append(entry)
return spacing, dimension
def main():
spacing, dimension = [], []
for root, _, files in os.walk(UI):
if root.startswith(THEME):
continue
for f in sorted(files):
if f.endswith('.kt'):
s, d = classify(os.path.join(root, f))
spacing += s
dimension += d
print(f' {"dp literals in spacing positions":42s} {len(spacing):6d}')
print(f' {"dp literals in dimension positions":42s} {len(dimension):6d} (out of scope)')
if '--list' in sys.argv:
for path, line, lit, ctx in spacing:
print(f' {path.replace(UI + "/", "")}:{line} {lit:>7s} {ctx}')
return 1 if spacing else 0
if __name__ == '__main__':
sys.exit(main())