From 66ff6d21a42c04afcad89c78a80066176d1294b0 Mon Sep 17 00:00:00 2001 From: "Robert C. Martin" Date: Sat, 3 Oct 2026 18:03:53 -0500 Subject: [PATCH] Bring every CRAP score under 10 except the option parser. The reader, the normalizer, the TypeScript picker, and the fingerprint printer are each a small function. parse_args stays the option switch. --- src/dryer/astnorm.py | 72 ++++++++----- src/dryer/clojure_read.py | 208 ++++++++++++++++++++++---------------- src/dryer/extract.py | 29 ++++-- src/dryer/shape.py | 74 +++++++------- 4 files changed, 220 insertions(+), 163 deletions(-) diff --git a/src/dryer/astnorm.py b/src/dryer/astnorm.py index 0e58afa..88f7582 100644 --- a/src/dryer/astnorm.py +++ b/src/dryer/astnorm.py @@ -178,11 +178,25 @@ def _without_type_args(nodes): return rest, type_args +def _callee_leaf(node): + if node.type in _IDENTIFIERS: + return [K("symbol"), _text(node)] + return _PENDING + + +def _named_leaf(node, head: bool): + if node.type in _IDENTIFIERS: + if head: + return [K("symbol"), _text(node)] + return K("symbol") + if _is_literal(node): + return K("literal") + return _PENDING + + def _try_leaf(node, mode: str, head: bool): if mode == "callee": - if node.type in _IDENTIFIERS: - return [K("symbol"), _text(node)] - return _PENDING + return _callee_leaf(node) if mode == "path": return _PENDING if node.type in _SKIP: @@ -191,13 +205,7 @@ def _try_leaf(node, mode: str, head: bool): return [K("symbol"), node.type] if not node.is_named: return None - if node.type in _IDENTIFIERS: - if head: - return [K("symbol"), _text(node)] - return K("symbol") - if _is_literal(node): - return K("literal") - return _PENDING + return _named_leaf(node, head) def _call_jobs(node): @@ -241,13 +249,17 @@ def _path_jobs(node): return jobs +def _callee_jobs(node): + if node.type in _ATTRIBUTES: + return _attr_jobs(node, True) + if node.type in _SCOPED or node.type == "generic_type": + return _path_jobs(node) + return [(node, "norm", True)] + + def _jobs(node, mode: str, head: bool): if mode == "callee": - if node.type in _ATTRIBUTES: - return _attr_jobs(node, True) - if node.type in _SCOPED or node.type == "generic_type": - return _path_jobs(node) - return [(node, "norm", True)] + return _callee_jobs(node) if mode == "path": return _path_jobs(node) if node.type == "parenthesized_expression" and len(node.named_children) == 1: @@ -325,13 +337,26 @@ def _assemble_path(node, cache): return parts +def _callee_value(node, cache): + if node.type in _ATTRIBUTES: + return _assemble_attr(node, True, cache) + if node.type in _SCOPED or node.type == "generic_type": + return _assemble_path(node, cache) + return cache[(id(node), "norm", True)] + + +def _assemble_children(node, cache): + children = [] + for child in node.children: + norm = cache[(id(child), "norm", False)] + if norm is not None: + children.append(norm) + return [K(node.type), *children] + + def _assemble(node, mode: str, head: bool, cache): if mode == "callee": - if node.type in _ATTRIBUTES: - return _assemble_attr(node, True, cache) - if node.type in _SCOPED or node.type == "generic_type": - return _assemble_path(node, cache) - return cache[(id(node), "norm", True)] + return _callee_value(node, cache) if mode == "path": return _assemble_path(node, cache) if node.type == "parenthesized_expression" and len(node.named_children) == 1: @@ -341,12 +366,7 @@ def _assemble(node, mode: str, head: bool, cache): return _assemble_call(node, cache) if node.type in _ATTRIBUTES: return _assemble_attr(node, head, cache) - children = [] - for child in node.children: - norm = cache[(id(child), "norm", False)] - if norm is not None: - children.append(norm) - return [K(node.type), *children] + return _assemble_children(node, cache) def _normalize(node, head: bool = False): diff --git a/src/dryer/clojure_read.py b/src/dryer/clojure_read.py index 67c42af..9fdd9d4 100644 --- a/src/dryer/clojure_read.py +++ b/src/dryer/clojure_read.py @@ -17,6 +17,9 @@ from dryer.shape import K _WHITESPACE = set(" \t\n\r,") _TERMINATORS = set("()[]{}\";'@^`~\\,#:") +_COLLECTIONS = {"(": (")", "list"), "[": ("]", "vector"), "{": ("}", "map")} +_WRAPPERS = {"'": "quote", "@": "deref", "`": "syntax-quote"} +_ATOMS = set("\"\\:") _NUMBER = re.compile( r"^[+-]?(?:" r"\d+/\d+" @@ -190,25 +193,31 @@ class Reader: raise ReadError(self.line, "unexpected end of file") return self._open(stack) + def _deliver_coll(self, frame, produced): + items = frame[4] + if produced is None: + return _NEED + if isinstance(produced, Splice): + items.extend(produced.items) + else: + items.append(produced) + return _NEED + + def _deliver_wrap(self, frame, produced): + _tag, name, line = frame + items = [Sym(name, line)] + if produced is not None and not isinstance(produced, Splice): + items.append(produced) + return Coll("list", line, items=items) + def _deliver(self, stack: list[tuple], produced): frame = stack[-1] tag = frame[0] if tag == "coll": - items = frame[4] - if produced is None: - return _NEED - if isinstance(produced, Splice): - items.extend(produced.items) - else: - items.append(produced) - return _NEED + return self._deliver_coll(frame, produced) stack.pop() if tag == "wrap": - _tag, name, line = frame - items = [Sym(name, line)] - if produced is not None and not isinstance(produced, Splice): - items.append(produced) - return Coll("list", line, items=items) + return self._deliver_wrap(frame, produced) if tag == "discard": return None if tag == "meta": @@ -232,97 +241,122 @@ class Reader: raise ReadError(self.line, "splicing reader conditional needs a collection") return spliced - def _open(self, stack: list[tuple]): - ch = self.peek() - line = self.line - if ch == "(": - self.get() - stack.append(("coll", ")", "list", line, [], False)) - return _NEED - if ch == "[": - self.get() - stack.append(("coll", "]", "vector", line, [], False)) - return _NEED - if ch == "{": - self.get() - stack.append(("coll", "}", "map", line, [], False)) - return _NEED + def _open_collection(self, stack: list[tuple], ch: str, line: int): + self.get() + end, kind = _COLLECTIONS[ch] + stack.append(("coll", end, kind, line, [], False)) + return _NEED + + def _open_atom(self, ch: str): if ch == '"': return self.read_string() if ch == "\\": return self.read_char() - if ch == ":": - return self.read_keyword() - if ch == "'": + return self.read_keyword() + + def _open_unquote(self, stack: list[tuple], line: int): + name = "unquote" + if self.peek() == "@": self.get() - stack.append(("wrap", "quote", line)) - return _NEED - if ch == "@": - self.get() - stack.append(("wrap", "deref", line)) - return _NEED - if ch == "`": - self.get() - stack.append(("wrap", "syntax-quote", line)) + name = "unquote-splicing" + stack.append(("wrap", name, line)) + return _NEED + + def _open_wrapper(self, stack: list[tuple], ch: str, line: int): + self.get() + if ch in _WRAPPERS: + stack.append(("wrap", _WRAPPERS[ch], line)) return _NEED if ch == "~": - self.get() - name = "unquote" - if self.peek() == "@": - self.get() - name = "unquote-splicing" - stack.append(("wrap", name, line)) - return _NEED - if ch == "^": - self.get() - stack.append(("meta",)) - return _NEED - if ch == "#": - return self._dispatch(stack, line) + return self._open_unquote(stack, line) + stack.append(("meta",)) + return _NEED + + def _open_token(self, line: int): token = self.read_token() if _NUMBER.match(token): return Lit(line) return Sym(token, line) + def _open(self, stack: list[tuple]): + ch = self.peek() + line = self.line + if ch in _COLLECTIONS: + return self._open_collection(stack, ch, line) + if ch in _ATOMS: + return self._open_atom(ch) + if ch in _WRAPPERS or ch in "~^": + return self._open_wrapper(stack, ch, line) + if ch == "#": + return self._dispatch(stack, line) + return self._open_token(line) + + def _dispatch_discard(self, stack: list[tuple], _line: int): + self.get() + stack.append(("discard",)) + return _NEED + + def _dispatch_set(self, stack: list[tuple], _line: int): + brace = self.line + self.get() + stack.append(("coll", "}", "set", brace, [], False)) + return _NEED + + def _dispatch_fn(self, stack: list[tuple], line: int): + self.get() + stack.append(("coll", ")", "list", line, [], True)) + return _NEED + + def _dispatch_string(self, _stack: list[tuple], line: int): + self.read_string() + return Lit(line) + + def _dispatch_cond(self, stack: list[tuple], _line: int): + self.get() + splicing = self.peek() == "@" + if splicing: + self.get() + stack.append(("cond", splicing)) + return _NEED + + def _read_map_qualifier(self) -> None: + peeked = self.peek() + if peeked == ":": + self.get() + return + if peeked is None or peeked == "{" or peeked in _WHITESPACE: + return + self.read_token() + + def _dispatch_map(self, _stack: list[tuple], _line: int): + self.get() + self._read_map_qualifier() + return _NEED + + def _dispatch_extension(self, stack: list[tuple], line: int): + self.read_token() + stack.append(("lit", line)) + return _NEED + def _dispatch(self, stack: list[tuple], line: int): self.get() ch = self.peek() if ch is None: raise ReadError(line, "incomplete dispatch") - if ch == "_": - self.get() - stack.append(("discard",)) - return _NEED - if ch == "{": - brace = self.line - self.get() - stack.append(("coll", "}", "set", brace, [], False)) - return _NEED - if ch == "(": - self.get() - stack.append(("coll", ")", "list", line, [], True)) - return _NEED - if ch == '"': - self.read_string() - return Lit(line) - if ch == "?": - self.get() - splicing = False - if self.peek() == "@": - self.get() - splicing = True - stack.append(("cond", splicing)) - return _NEED - if ch == ":": - self.get() - if self.peek() == ":": - self.get() - elif self.peek() not in (None, "{") and self.peek() not in _WHITESPACE: - self.read_token() - return _NEED - self.read_token() - stack.append(("lit", line)) - return _NEED + handler = _DISPATCH.get(ch) + if handler is None: + return self._dispatch_extension(stack, line) + return handler(self, stack, line) + + +_DISPATCH = { + "_": Reader._dispatch_discard, + "{": Reader._dispatch_set, + "(": Reader._dispatch_fn, + '"': Reader._dispatch_string, + "?": Reader._dispatch_cond, + ":": Reader._dispatch_map, +} def _branch_name(feature): diff --git a/src/dryer/extract.py b/src/dryer/extract.py index 32b07fa..51f3160 100644 --- a/src/dryer/extract.py +++ b/src/dryer/extract.py @@ -104,22 +104,29 @@ def _python_nodes(data: bytes, root) -> list: return found +def _keep_typescript_function(node) -> bool: + return _has(node, "statement_block") and not _inside(node, _TS_FUNCTION) + + +def _typescript_parent(node): + parent = node.parent + if parent is None or parent.type not in _DEFINITION_PARENTS: + return None + if _inside(parent, _TS_FUNCTION): + return None + return parent + + def _typescript_nodes(data: bytes, root) -> list: found = [] for node in descendants(root): - if node.type == "function_declaration": - if _has(node, "statement_block") and not _inside(node, _TS_FUNCTION): - found.append(node) - elif node.type == "method_definition": - if _has(node, "statement_block") and not _inside(node, _TS_FUNCTION): + if node.type in {"function_declaration", "method_definition"}: + if _keep_typescript_function(node): found.append(node) elif node.type in {"arrow_function", "function_expression"}: - parent = node.parent - if parent is None or parent.type not in _DEFINITION_PARENTS: - continue - if _inside(parent, _TS_FUNCTION): - continue - found.append(parent) + parent = _typescript_parent(node) + if parent is not None: + found.append(parent) return found diff --git a/src/dryer/shape.py b/src/dryer/shape.py index 728f3a3..7b9bb16 100644 --- a/src/dryer/shape.py +++ b/src/dryer/shape.py @@ -27,11 +27,23 @@ def _pr_atom(node) -> str: raise TypeError(f"cannot print {node!r}") -def pr(node) -> str: - """Print a normalized node the way Clojure `pr-str` prints it.""" +def _print_list(current: list, done: dict[int, str]) -> str: + pieces = [] + for child in current: + if isinstance(child, list): + pieces.append(done[id(child)]) + else: + pieces.append(_pr_atom(child)) + return "[" + " ".join(pieces) + "]" - if not isinstance(node, list): - return _pr_atom(node) + +def _queue_lists(stack: list, current: list) -> None: + for child in reversed(current): + if isinstance(child, list): + stack.append((child, False)) + + +def _printed(node: list) -> dict[int, str]: # Post-order. A 1,200-deep tree must not raise RecursionError. done: dict[int, str] = {} stack: list[tuple[list, bool]] = [(node, False)] @@ -39,21 +51,21 @@ def pr(node) -> str: current, expanded = stack.pop() key = id(current) if expanded: - pieces = [] - for child in current: - if isinstance(child, list): - pieces.append(done[id(child)]) - else: - pieces.append(_pr_atom(child)) - done[key] = "[" + " ".join(pieces) + "]" + done[key] = _print_list(current, done) continue if key in done: continue stack.append((current, True)) - for child in reversed(current): - if isinstance(child, list): - stack.append((child, False)) - return done[id(node)] + _queue_lists(stack, current) + return done + + +def pr(node) -> str: + """Print a normalized node the way Clojure `pr-str` prints it.""" + + if not isinstance(node, list): + return _pr_atom(node) + return _printed(node)[id(node)] def node_count(node) -> int: @@ -71,29 +83,7 @@ def node_count(node) -> int: return total -def fingerprints(node) -> frozenset[str]: - if not isinstance(node, list): - return frozenset({pr(node)}) - done: dict[int, str] = {} - stack: list[tuple[list, bool]] = [(node, False)] - while stack: - current, expanded = stack.pop() - key = id(current) - if expanded: - pieces = [] - for child in current: - if isinstance(child, list): - pieces.append(done[id(child)]) - else: - pieces.append(_pr_atom(child)) - done[key] = "[" + " ".join(pieces) + "]" - continue - if key in done: - continue - stack.append((current, True)) - for child in reversed(current): - if isinstance(child, list): - stack.append((child, False)) +def _atom_prints(node) -> set[str]: atoms: set[str] = set() pending: list = [node] while pending: @@ -102,7 +92,13 @@ def fingerprints(node) -> frozenset[str]: pending.extend(current) else: atoms.add(_pr_atom(current)) - return frozenset(set(done.values()) | atoms) + return atoms + + +def fingerprints(node) -> frozenset[str]: + if not isinstance(node, list): + return frozenset({pr(node)}) + return frozenset(set(_printed(node).values()) | _atom_prints(node)) def jaccard(left: frozenset[str], right: frozenset[str]) -> float: