#!/usr/bin/env python3 """What may appear in common/, checked. common/ is compiled twice — by jolt and by ClojureDart — and only one of those happens on the way to a desktop build. So the way this breaks is always the same: shared code reaches for something only the JVM has, everything the author ran still works, and the phone stops compiling at a namespace nobody touched. `Math/ceil` in the compose bar was the third time. The rule being enforced is CLAUDE.md's, unchanged: code under common/ may not require jolt.*, glimmer* or a dart: library, and if it needs the host it asks frq.io. This only sees the first half of that — a host call has to be named to be caught, and the list below is the ones that have actually turned up. Two things are stripped before anything is matched, and both are the difference between a check people keep and one they route around. Comments and strings, because the two namespaces that got this right explain themselves by naming the very thing they avoid — a checker that fires on `frq.clock`'s docstring teaches people to stop reading it. And the reader-conditional branches ClojureDart does not read. A `#?(:jolt [glimmer.ratom ...])` is not a violation, it is the sanctioned way to say "desktop only": the cljd compiler never sees inside it. So this reads the conditionals the way the compiler does — first branch whose feature is on, with :cljd, :clj and :default on — and looks only at what is left. """ import re import sys from pathlib import Path # Java that has no ClojureDart counterpart. Spelled as whole tokens: `Math/` # catches java.lang.Math without catching a namespace alias ending in "math". INTEROP = re.compile( r"""(?= len(src) or src[open_paren] != "(": i = hit + 2 continue end = _close(src, open_paren) if end is None: return src forms = _forms(src, open_paren + 1, end - 1) # keyword, form, keyword, form — take the first keyword that is on. keep = None for k in range(0, len(forms) - 1, 2): kw = src[forms[k][0]:forms[k][1]] if kw.lstrip(":") in ACTIVE and keep is None: keep = forms[k + 1] # Blank everything in the conditional except the branch that survives, # including the `#?` itself so it is not rescanned. cut = end if keep is None else keep[0] src = _blank(src, hit, cut) if keep is not None: src = _blank(src, keep[1], end) i = hit + 2 def main(): root = Path(sys.argv[1] if len(sys.argv) > 1 else "common") bad = [] for path in sorted(root.rglob("*.cljc")): source = select(strip(path.read_text())) for lineno, line in enumerate(source.splitlines(), 1): for pattern, why in CHECKS: m = pattern.search(line) if m: bad.append((path, lineno, m.group(0).strip(), why)) for path, lineno, tok, why in bad: print(f"{path}:{lineno}: {tok!r} is {why}", file=sys.stderr) if bad: print( f"\n{len(bad)} thing(s) under {root}/ that ClojureDart cannot compile.\n" "common/ is built by both backends: ask frq.io for the host, and add\n" "the call to both implementations. See CLAUDE.md.", file=sys.stderr, ) return 1 print(f"{root}/ is clean: nothing here that only one backend has.") return 0 if __name__ == "__main__": sys.exit(main())