Bring every CRAP score under 10 except the option parser.

Rendering, mutation-site rules, coverage ranking, and the per-language test command 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 ba4065f3f1
commit c57f03879a
4 changed files with 242 additions and 100 deletions
+66 -33
View File
@@ -33,30 +33,82 @@ def _parts(path: str) -> tuple[str, ...]:
return tuple(part for part in path.split("/") if part) return tuple(part for part in path.split("/") if part)
def _rank(key_parts: tuple[str, ...], sources: list[tuple[str, ...]]) -> tuple[int, int] | None: def _as_suffix(longer: tuple[str, ...], shorter: tuple[str, ...]) -> int | None:
"""How well one report key names one of the source paths. """How many extra parts `longer` adds in front of `shorter`, when it ends with it."""
Rank `(0, extra)` is a key that ends with the source and may add a prefix. if len(longer) >= len(shorter) and longer[-len(shorter) :] == shorter:
Rank `(1, missing)` is a shorter key that the source path ends with. The return len(longer) - len(shorter)
smaller rank is the longer shared suffix. Two different keys at the same return None
rank are ambiguous.
def _suffix_rank(key_parts: tuple[str, ...], source: tuple[str, ...]) -> tuple[int, int] | None:
"""Rank `(0, extra)` is a key that ends with the source. `(1, missing)` is the reverse.
An equal path is rank `(0, 0)`. A shorter key matches only when the source
is strictly longer, so an equal path is not also a missing-part match.
"""
if not source or not key_parts:
return None
extra = _as_suffix(key_parts, source)
if extra is not None:
return (0, extra)
if len(source) <= len(key_parts):
return None
missing = _as_suffix(source, key_parts)
if missing is None:
return None
return (1, missing)
def _rank(key_parts: tuple[str, ...], sources: list[tuple[str, ...]]) -> tuple[int, int] | None:
"""The best suffix rank of one report key against the source paths.
The smaller rank is the longer shared suffix. Two different keys at the
same rank are ambiguous.
""" """
best: tuple[int, int] | None = None best: tuple[int, int] | None = None
for source in sources: for source in sources:
if not source or not key_parts: rank = _suffix_rank(key_parts, source)
continue if rank is None:
if len(key_parts) >= len(source) and key_parts[-len(source) :] == source:
rank = (0, len(key_parts) - len(source))
elif len(source) > len(key_parts) and source[-len(key_parts) :] == key_parts:
rank = (1, len(source) - len(key_parts))
else:
continue continue
if best is None or rank < best: if best is None or rank < best:
best = rank best = rank
return best return best
def _source_parts(candidates: list[str]) -> list[tuple[str, ...]]:
sources: list[tuple[str, ...]] = []
for candidate in candidates:
parts = _parts(candidate)
if parts and parts not in sources:
sources.append(parts)
return sources
def _keep_match(best, best_rank, key_parts, value, rank):
if best_rank is None or rank < best_rank:
return rank, [(key_parts, value)]
if rank == best_rank:
best.append((key_parts, value))
return best_rank, best
def _best_value(normalized: dict, sources: list[tuple[str, ...]]):
best_rank: tuple[int, int] | None = None
best: list[tuple[tuple[str, ...], object]] = []
for key, value in normalized.items():
key_parts = _parts(key)
rank = _rank(key_parts, sources)
if rank is None:
continue
best_rank, best = _keep_match(best, best_rank, key_parts, value, rank)
if len({parts for parts, _value in best}) != 1:
return None
return best[0][1]
def _lookup(index: dict, source_path: str): def _lookup(index: dict, source_path: str):
if not index: if not index:
return None return None
@@ -67,26 +119,7 @@ def _lookup(index: dict, source_path: str):
found = _exact(normalized, candidates) found = _exact(normalized, candidates)
if found is not None: if found is not None:
return found return found
sources: list[tuple[str, ...]] = [] return _best_value(normalized, _source_parts(candidates))
for candidate in candidates:
parts = _parts(candidate)
if parts and parts not in sources:
sources.append(parts)
best_rank: tuple[int, int] | None = None
best: list[tuple[tuple[str, ...], object]] = []
for key, value in normalized.items():
key_parts = _parts(key)
rank = _rank(key_parts, sources)
if rank is None:
continue
if best_rank is None or rank < best_rank:
best_rank = rank
best = [(key_parts, value)]
elif rank == best_rank:
best.append((key_parts, value))
if len({parts for parts, _value in best}) != 1:
return None
return best[0][1]
def _covered_from_pairs(lines: dict[int, tuple[int, int]] | None) -> set[int] | None: def _covered_from_pairs(lines: dict[int, tuple[int, int]] | None) -> set[int] | None:
+50 -17
View File
@@ -31,32 +31,65 @@ def keyword(name: str) -> tuple:
return ("keyword", name) return ("keyword", name)
def _render(value) -> str: def _render_bool(value: bool) -> str:
if value is None: return "true" if value else "false"
return "nil"
if isinstance(value, tuple) and len(value) == 2 and value[0] == "keyword":
return f":{value[1]}" def _render_number(value) -> str:
if isinstance(value, bool):
return "true" if value else "false"
if isinstance(value, int) and not isinstance(value, bool):
return str(value)
if isinstance(value, float): if isinstance(value, float):
text = f"{value:.4f}".rstrip("0").rstrip(".") text = f"{value:.4f}".rstrip("0").rstrip(".")
if "." not in text: if "." not in text:
text += ".0" text += ".0"
return text return text
return str(value)
def _render_atom(value) -> str | None:
if value is None:
return "nil"
# bool is a subclass of int, so it has to be chosen first.
if isinstance(value, bool):
return _render_bool(value)
if isinstance(value, (int, float)):
return _render_number(value)
if isinstance(value, str): if isinstance(value, str):
return f'"{_escape(value)}"' return f'"{_escape(value)}"'
return None
def _render_keyword(value) -> str | None:
if isinstance(value, tuple) and len(value) == 2 and value[0] == "keyword":
return f":{value[1]}"
return None
def _render_sequence(value) -> str:
body = " ".join(_render(item) for item in value)
return f"[{body}]" if body else "[]"
def _render_key(key) -> str:
if isinstance(key, str) and _keyword_key(key):
return f":{key}"
return _render(key)
def _render_map(value: dict) -> str:
parts = [f"{_render_key(key)} {_render(item)}" for key, item in value.items()]
return "{" + " ".join(parts) + "}"
def _render(value) -> str:
atom = _render_atom(value)
if atom is not None:
return atom
rendered = _render_keyword(value)
if rendered is not None:
return rendered
if isinstance(value, (list, tuple)): if isinstance(value, (list, tuple)):
body = " ".join(_render(item) for item in value) return _render_sequence(value)
return f"[{body}]" if body else "[]"
if isinstance(value, dict): if isinstance(value, dict):
parts = [] return _render_map(value)
for key, item in value.items():
rendered_key = f":{key}" if isinstance(key, str) and _keyword_key(key) else _render(key)
parts.append(f"{rendered_key} {_render(item)}")
body = " ".join(parts)
return "{" + body + "}"
raise TypeError(f"cannot render {type(value).__name__} as EDN") raise TypeError(f"cannot render {type(value).__name__} as EDN")
+59 -26
View File
@@ -177,6 +177,61 @@ def _python_command(directory: Path) -> list[str]:
return [interpreter, "-m", "unittest", "discover"] return [interpreter, "-m", "unittest", "discover"]
def _nearest_marker(source: Path, root: Path, markers: tuple[str, ...]) -> Path:
for marker in markers:
found = nearest(source, marker, root)
if found is not None:
return found
return root
def _go_package(directory: Path, source: Path) -> str:
relative = source.resolve().parent.relative_to(directory.resolve())
if relative == Path("."):
return "."
return "./" + relative.as_posix()
def _clojure_plan(root: Path, source: Path) -> tuple[Command, Path]:
directory = _nearest_marker(source, root, ("deps.edn", "bb.edn"))
return _clojure_command(directory), directory
def _java_plan(root: Path, source: Path) -> tuple[Command, Path]:
directory = _nearest_marker(source, root, ("pom.xml",))
return ["mvn", "-q", "test", "-DexcludeTags=no-mutate"], directory
def _go_plan(root: Path, source: Path) -> tuple[Command, Path]:
directory = _nearest_marker(source, root, ("go.mod",))
return ["go", "test", "-count=1", _go_package(directory, source)], directory
def _typescript_plan(root: Path, source: Path) -> tuple[Command, Path]:
directory = _nearest_marker(source, root, ("package.json",))
return ["npm", "test"], directory
def _rust_plan(root: Path, source: Path) -> tuple[Command, Path]:
directory = _nearest_marker(source, root, ("Cargo.toml",))
return ["cargo", "test"], directory
def _python_plan(root: Path, source: Path) -> tuple[Command, Path]:
directory = _nearest_marker(source, root, ("pyproject.toml", "pytest.ini", "setup.cfg"))
return _python_command(directory), directory
_PLANS = {
"clojure": _clojure_plan,
"java": _java_plan,
"go": _go_plan,
"typescript": _typescript_plan,
"rust": _rust_plan,
"python": _python_plan,
}
def test_plan(root: Path, source: Path, language: str, override: str | None) -> tuple[Command, Path]: def test_plan(root: Path, source: Path, language: str, override: str | None) -> tuple[Command, Path]:
"""The test command and the directory it runs in. """The test command and the directory it runs in.
@@ -187,29 +242,7 @@ def test_plan(root: Path, source: Path, language: str, override: str | None) ->
root = root.resolve() root = root.resolve()
if override: if override:
return override, root return override, root
if language == "clojure": plan = _PLANS.get(language)
directory = nearest(source, "deps.edn", root) or nearest(source, "bb.edn", root) or root if plan is None:
return _clojure_command(directory), directory return ["false"], root
if language == "java": return plan(root, source)
directory = nearest(source, "pom.xml", root) or root
return ["mvn", "-q", "test", "-DexcludeTags=no-mutate"], directory
if language == "go":
directory = nearest(source, "go.mod", root) or root
relative = source.resolve().parent.relative_to(directory.resolve())
package = "." if relative == Path(".") else "./" + relative.as_posix()
return ["go", "test", "-count=1", package], directory
if language == "typescript":
directory = nearest(source, "package.json", root) or root
return ["npm", "test"], directory
if language == "rust":
directory = nearest(source, "Cargo.toml", root) or root
return ["cargo", "test"], directory
if language == "python":
directory = (
nearest(source, "pyproject.toml", root)
or nearest(source, "pytest.ini", root)
or nearest(source, "setup.cfg", root)
or root
)
return _python_command(directory), directory
return ["false"], root
+67 -24
View File
@@ -235,33 +235,76 @@ def _visit_clojure(node, data: bytes, found: list[RawSite]) -> None:
stack.extend(reversed(current.children)) stack.extend(reversed(current.children))
def _visit_node(node, data: bytes, language: str, found: list[RawSite]) -> None: def _parent_type(node) -> str:
parent = node.parent.type if node.parent is not None else "" if node.parent is None:
if node.child_count == 0: return ""
token = _text(data, node) return node.parent.type
# A literal sits inside the same expression as an operator, so these
# checks are independent. `0` is a child of `a > 0`, not an operator.
if parent in _BINARY_PARENTS: def _visit_binary(node, token: str, parent: str, language: str, found: list[RawSite]) -> None:
mutant = _binary_mutant(language, token) if parent not in _BINARY_PARENTS:
if mutant is not None: return
_add(found, node, token, mutant, _category(token)) mutant = _binary_mutant(language, token)
if language == "typescript" and token == "?.": if mutant is not None:
optional = _optional_mutant(node) _add(found, node, token, mutant, _category(token))
if optional is not None:
_add(found, node, "?.", optional, "optional")
if token in {"!", "not"} and parent in _UNARY_PARENTS | {"comparison_operator"}: def _visit_optional(node, token: str, language: str, found: list[RawSite]) -> None:
_add(found, node, token, "", "unary") if language != "typescript" or token != "?.":
if token == "-" and parent in _UNARY_PARENTS: return
_add(found, node, token, "", "unary") optional = _optional_mutant(node)
if node.type in {"true", "false"} and token in _BOOLEANS: if optional is not None:
_add(found, node, token, _BOOLEANS[token], "boolean") _add(found, node, "?.", optional, "optional")
if node.type in _NUMBERS and token in {"0", "1"}:
_add(found, node, token, "1" if token == "0" else "0", "constant")
elif node.type == "bool_lit" and _text(data, node) in _BOOLEANS: def _visit_not(node, token: str, parent: str, found: list[RawSite]) -> None:
text = _text(data, node) if token in {"!", "not"} and parent in _UNARY_PARENTS | {"comparison_operator"}:
_add(found, node, token, "", "unary")
def _visit_negation(node, token: str, parent: str, found: list[RawSite]) -> None:
if token == "-" and parent in _UNARY_PARENTS:
_add(found, node, token, "", "unary")
def _visit_boolean_token(node, token: str, found: list[RawSite]) -> None:
if node.type in {"true", "false"} and token in _BOOLEANS:
_add(found, node, token, _BOOLEANS[token], "boolean")
def _visit_constant(node, token: str, found: list[RawSite]) -> None:
if node.type in _NUMBERS and token in {"0", "1"}:
_add(found, node, token, _flip01(token), "constant")
def _visit_bool_lit(node, data: bytes, found: list[RawSite]) -> None:
if node.type != "bool_lit":
return
text = _text(data, node)
if text in _BOOLEANS:
_add(found, node, text, _BOOLEANS[text], "boolean") _add(found, node, text, _BOOLEANS[text], "boolean")
def _visit_leaf(node, data: bytes, language: str, found: list[RawSite]) -> None:
# A literal sits inside the same expression as an operator, so these
# checks are independent. `0` is a child of `a > 0`, not an operator.
token = _text(data, node)
parent = _parent_type(node)
_visit_binary(node, token, parent, language, found)
_visit_optional(node, token, language, found)
_visit_not(node, token, parent, found)
_visit_negation(node, token, parent, found)
_visit_boolean_token(node, token, found)
_visit_constant(node, token, found)
def _visit_node(node, data: bytes, language: str, found: list[RawSite]) -> None:
if node.child_count == 0:
_visit_leaf(node, data, language, found)
else:
_visit_bool_lit(node, data, found)
def _visit(node, data: bytes, language: str, found: list[RawSite]) -> None: def _visit(node, data: bytes, language: str, found: list[RawSite]) -> None:
# An explicit stack: a 1,200-term expression must not raise RecursionError. # An explicit stack: a 1,200-term expression must not raise RecursionError.
stack = [node] stack = [node]