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)
def _rank(key_parts: tuple[str, ...], sources: list[tuple[str, ...]]) -> tuple[int, int] | None:
"""How well one report key names one of the source paths.
def _as_suffix(longer: tuple[str, ...], shorter: tuple[str, ...]) -> int | None:
"""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.
Rank `(1, missing)` is a shorter key that the source path ends with. The
smaller rank is the longer shared suffix. Two different keys at the same
rank are ambiguous.
if len(longer) >= len(shorter) and longer[-len(shorter) :] == shorter:
return len(longer) - len(shorter)
return None
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
for source in sources:
if not source or not key_parts:
continue
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:
rank = _suffix_rank(key_parts, source)
if rank is None:
continue
if best is None or rank < best:
best = rank
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):
if not index:
return None
@@ -67,26 +119,7 @@ def _lookup(index: dict, source_path: str):
found = _exact(normalized, candidates)
if found is not None:
return found
sources: list[tuple[str, ...]] = []
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]
return _best_value(normalized, _source_parts(candidates))
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)
def _render(value) -> str:
if value is None:
return "nil"
if isinstance(value, tuple) and len(value) == 2 and value[0] == "keyword":
return f":{value[1]}"
if isinstance(value, bool):
return "true" if value else "false"
if isinstance(value, int) and not isinstance(value, bool):
return str(value)
def _render_bool(value: bool) -> str:
return "true" if value else "false"
def _render_number(value) -> str:
if isinstance(value, float):
text = f"{value:.4f}".rstrip("0").rstrip(".")
if "." not in text:
text += ".0"
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):
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)):
body = " ".join(_render(item) for item in value)
return f"[{body}]" if body else "[]"
return _render_sequence(value)
if isinstance(value, dict):
parts = []
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 + "}"
return _render_map(value)
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"]
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]:
"""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()
if override:
return override, root
if language == "clojure":
directory = nearest(source, "deps.edn", root) or nearest(source, "bb.edn", root) or root
return _clojure_command(directory), directory
if language == "java":
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
plan = _PLANS.get(language)
if plan is None:
return ["false"], root
return plan(root, source)
+67 -24
View File
@@ -235,33 +235,76 @@ def _visit_clojure(node, data: bytes, found: list[RawSite]) -> None:
stack.extend(reversed(current.children))
def _visit_node(node, data: bytes, language: str, found: list[RawSite]) -> None:
parent = node.parent.type if node.parent is not None else ""
if node.child_count == 0:
token = _text(data, node)
# 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:
mutant = _binary_mutant(language, token)
if mutant is not None:
_add(found, node, token, mutant, _category(token))
if language == "typescript" and token == "?.":
optional = _optional_mutant(node)
if optional is not None:
_add(found, node, "?.", optional, "optional")
if token in {"!", "not"} and parent in _UNARY_PARENTS | {"comparison_operator"}:
_add(found, node, token, "", "unary")
if token == "-" and parent in _UNARY_PARENTS:
_add(found, node, token, "", "unary")
if node.type in {"true", "false"} and token in _BOOLEANS:
_add(found, node, token, _BOOLEANS[token], "boolean")
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:
text = _text(data, node)
def _parent_type(node) -> str:
if node.parent is None:
return ""
return node.parent.type
def _visit_binary(node, token: str, parent: str, language: str, found: list[RawSite]) -> None:
if parent not in _BINARY_PARENTS:
return
mutant = _binary_mutant(language, token)
if mutant is not None:
_add(found, node, token, mutant, _category(token))
def _visit_optional(node, token: str, language: str, found: list[RawSite]) -> None:
if language != "typescript" or token != "?.":
return
optional = _optional_mutant(node)
if optional is not None:
_add(found, node, "?.", optional, "optional")
def _visit_not(node, token: str, parent: str, found: list[RawSite]) -> None:
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")
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:
# An explicit stack: a 1,200-term expression must not raise RecursionError.
stack = [node]