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.
This commit is contained in:
Robert C. Martin
2026-10-03 18:03:53 -05:00
parent 2be6acadf5
commit 66ff6d21a4
4 changed files with 220 additions and 163 deletions
+46 -26
View File
@@ -178,11 +178,25 @@ def _without_type_args(nodes):
return rest, type_args 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): def _try_leaf(node, mode: str, head: bool):
if mode == "callee": if mode == "callee":
if node.type in _IDENTIFIERS: return _callee_leaf(node)
return [K("symbol"), _text(node)]
return _PENDING
if mode == "path": if mode == "path":
return _PENDING return _PENDING
if node.type in _SKIP: if node.type in _SKIP:
@@ -191,13 +205,7 @@ def _try_leaf(node, mode: str, head: bool):
return [K("symbol"), node.type] return [K("symbol"), node.type]
if not node.is_named: if not node.is_named:
return None return None
if node.type in _IDENTIFIERS: return _named_leaf(node, head)
if head:
return [K("symbol"), _text(node)]
return K("symbol")
if _is_literal(node):
return K("literal")
return _PENDING
def _call_jobs(node): def _call_jobs(node):
@@ -241,13 +249,17 @@ def _path_jobs(node):
return jobs 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): def _jobs(node, mode: str, head: bool):
if mode == "callee": if mode == "callee":
if node.type in _ATTRIBUTES: return _callee_jobs(node)
return _attr_jobs(node, True)
if node.type in _SCOPED or node.type == "generic_type":
return _path_jobs(node)
return [(node, "norm", True)]
if mode == "path": if mode == "path":
return _path_jobs(node) return _path_jobs(node)
if node.type == "parenthesized_expression" and len(node.named_children) == 1: if node.type == "parenthesized_expression" and len(node.named_children) == 1:
@@ -325,13 +337,26 @@ def _assemble_path(node, cache):
return parts 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): def _assemble(node, mode: str, head: bool, cache):
if mode == "callee": if mode == "callee":
if node.type in _ATTRIBUTES: return _callee_value(node, cache)
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)]
if mode == "path": if mode == "path":
return _assemble_path(node, cache) return _assemble_path(node, cache)
if node.type == "parenthesized_expression" and len(node.named_children) == 1: 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) return _assemble_call(node, cache)
if node.type in _ATTRIBUTES: if node.type in _ATTRIBUTES:
return _assemble_attr(node, head, cache) return _assemble_attr(node, head, cache)
children = [] return _assemble_children(node, cache)
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 _normalize(node, head: bool = False): def _normalize(node, head: bool = False):
+121 -87
View File
@@ -17,6 +17,9 @@ from dryer.shape import K
_WHITESPACE = set(" \t\n\r,") _WHITESPACE = set(" \t\n\r,")
_TERMINATORS = set("()[]{}\";'@^`~\\,#:") _TERMINATORS = set("()[]{}\";'@^`~\\,#:")
_COLLECTIONS = {"(": (")", "list"), "[": ("]", "vector"), "{": ("}", "map")}
_WRAPPERS = {"'": "quote", "@": "deref", "`": "syntax-quote"}
_ATOMS = set("\"\\:")
_NUMBER = re.compile( _NUMBER = re.compile(
r"^[+-]?(?:" r"^[+-]?(?:"
r"\d+/\d+" r"\d+/\d+"
@@ -190,25 +193,31 @@ class Reader:
raise ReadError(self.line, "unexpected end of file") raise ReadError(self.line, "unexpected end of file")
return self._open(stack) 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): def _deliver(self, stack: list[tuple], produced):
frame = stack[-1] frame = stack[-1]
tag = frame[0] tag = frame[0]
if tag == "coll": if tag == "coll":
items = frame[4] return self._deliver_coll(frame, produced)
if produced is None:
return _NEED
if isinstance(produced, Splice):
items.extend(produced.items)
else:
items.append(produced)
return _NEED
stack.pop() stack.pop()
if tag == "wrap": if tag == "wrap":
_tag, name, line = frame return self._deliver_wrap(frame, produced)
items = [Sym(name, line)]
if produced is not None and not isinstance(produced, Splice):
items.append(produced)
return Coll("list", line, items=items)
if tag == "discard": if tag == "discard":
return None return None
if tag == "meta": if tag == "meta":
@@ -232,97 +241,122 @@ class Reader:
raise ReadError(self.line, "splicing reader conditional needs a collection") raise ReadError(self.line, "splicing reader conditional needs a collection")
return spliced return spliced
def _open(self, stack: list[tuple]): def _open_collection(self, stack: list[tuple], ch: str, line: int):
ch = self.peek() self.get()
line = self.line end, kind = _COLLECTIONS[ch]
if ch == "(": stack.append(("coll", end, kind, line, [], False))
self.get() return _NEED
stack.append(("coll", ")", "list", line, [], False))
return _NEED def _open_atom(self, ch: str):
if ch == "[":
self.get()
stack.append(("coll", "]", "vector", line, [], False))
return _NEED
if ch == "{":
self.get()
stack.append(("coll", "}", "map", line, [], False))
return _NEED
if ch == '"': if ch == '"':
return self.read_string() return self.read_string()
if ch == "\\": if ch == "\\":
return self.read_char() return self.read_char()
if ch == ":": return self.read_keyword()
return self.read_keyword()
if ch == "'": def _open_unquote(self, stack: list[tuple], line: int):
name = "unquote"
if self.peek() == "@":
self.get() self.get()
stack.append(("wrap", "quote", line)) name = "unquote-splicing"
return _NEED stack.append(("wrap", name, line))
if ch == "@": return _NEED
self.get()
stack.append(("wrap", "deref", line)) def _open_wrapper(self, stack: list[tuple], ch: str, line: int):
return _NEED self.get()
if ch == "`": if ch in _WRAPPERS:
self.get() stack.append(("wrap", _WRAPPERS[ch], line))
stack.append(("wrap", "syntax-quote", line))
return _NEED return _NEED
if ch == "~": if ch == "~":
self.get() return self._open_unquote(stack, line)
name = "unquote" stack.append(("meta",))
if self.peek() == "@": return _NEED
self.get()
name = "unquote-splicing" def _open_token(self, line: int):
stack.append(("wrap", name, line))
return _NEED
if ch == "^":
self.get()
stack.append(("meta",))
return _NEED
if ch == "#":
return self._dispatch(stack, line)
token = self.read_token() token = self.read_token()
if _NUMBER.match(token): if _NUMBER.match(token):
return Lit(line) return Lit(line)
return Sym(token, 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): def _dispatch(self, stack: list[tuple], line: int):
self.get() self.get()
ch = self.peek() ch = self.peek()
if ch is None: if ch is None:
raise ReadError(line, "incomplete dispatch") raise ReadError(line, "incomplete dispatch")
if ch == "_": handler = _DISPATCH.get(ch)
self.get() if handler is None:
stack.append(("discard",)) return self._dispatch_extension(stack, line)
return _NEED return handler(self, stack, line)
if ch == "{":
brace = self.line
self.get() _DISPATCH = {
stack.append(("coll", "}", "set", brace, [], False)) "_": Reader._dispatch_discard,
return _NEED "{": Reader._dispatch_set,
if ch == "(": "(": Reader._dispatch_fn,
self.get() '"': Reader._dispatch_string,
stack.append(("coll", ")", "list", line, [], True)) "?": Reader._dispatch_cond,
return _NEED ":": Reader._dispatch_map,
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
def _branch_name(feature): def _branch_name(feature):
+18 -11
View File
@@ -104,22 +104,29 @@ def _python_nodes(data: bytes, root) -> list:
return found 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: def _typescript_nodes(data: bytes, root) -> list:
found = [] found = []
for node in descendants(root): for node in descendants(root):
if node.type == "function_declaration": if node.type in {"function_declaration", "method_definition"}:
if _has(node, "statement_block") and not _inside(node, _TS_FUNCTION): if _keep_typescript_function(node):
found.append(node)
elif node.type == "method_definition":
if _has(node, "statement_block") and not _inside(node, _TS_FUNCTION):
found.append(node) found.append(node)
elif node.type in {"arrow_function", "function_expression"}: elif node.type in {"arrow_function", "function_expression"}:
parent = node.parent parent = _typescript_parent(node)
if parent is None or parent.type not in _DEFINITION_PARENTS: if parent is not None:
continue found.append(parent)
if _inside(parent, _TS_FUNCTION):
continue
found.append(parent)
return found return found
+35 -39
View File
@@ -27,11 +27,23 @@ def _pr_atom(node) -> str:
raise TypeError(f"cannot print {node!r}") raise TypeError(f"cannot print {node!r}")
def pr(node) -> str: def _print_list(current: list, done: dict[int, str]) -> str:
"""Print a normalized node the way Clojure `pr-str` prints it.""" 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. # Post-order. A 1,200-deep tree must not raise RecursionError.
done: dict[int, str] = {} done: dict[int, str] = {}
stack: list[tuple[list, bool]] = [(node, False)] stack: list[tuple[list, bool]] = [(node, False)]
@@ -39,21 +51,21 @@ def pr(node) -> str:
current, expanded = stack.pop() current, expanded = stack.pop()
key = id(current) key = id(current)
if expanded: if expanded:
pieces = [] done[key] = _print_list(current, done)
for child in current:
if isinstance(child, list):
pieces.append(done[id(child)])
else:
pieces.append(_pr_atom(child))
done[key] = "[" + " ".join(pieces) + "]"
continue continue
if key in done: if key in done:
continue continue
stack.append((current, True)) stack.append((current, True))
for child in reversed(current): _queue_lists(stack, current)
if isinstance(child, list): return done
stack.append((child, False))
return done[id(node)]
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: def node_count(node) -> int:
@@ -71,29 +83,7 @@ def node_count(node) -> int:
return total return total
def fingerprints(node) -> frozenset[str]: def _atom_prints(node) -> set[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))
atoms: set[str] = set() atoms: set[str] = set()
pending: list = [node] pending: list = [node]
while pending: while pending:
@@ -102,7 +92,13 @@ def fingerprints(node) -> frozenset[str]:
pending.extend(current) pending.extend(current)
else: else:
atoms.add(_pr_atom(current)) 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: def jaccard(left: frozenset[str], right: frozenset[str]) -> float: