DRY invariants: one invocation pipeline, one normalizer.
Two rule families, because "duplicate path" has two shapes and each is
invisible to the other check:
- single-source — a second module reaches the chokepoint. Detected from
import records: a non-owner importing
InvocationKernel has just opened a
second way into the pipeline, whether or not it copied any code.
- single-definition — a second module defines the chokepoint. Detected
from top-level definitions: a copy-pasted
normalize_config imports
nothing and would sail through every import rule ever written.
check_single_definition(facts: dict[str, ModuleFacts], rules: tuple[SingleDefinitionRule, ...]) -> list[Violation]
Non-owner modules that define a declared chokepoint name at top level.
Source code in src/symfonic/devtools/archgate/dry.py
| def check_single_definition(
facts: dict[str, ModuleFacts], rules: tuple[SingleDefinitionRule, ...]
) -> list[Violation]:
"""Non-owner modules that define a declared chokepoint name at top level."""
violations: list[Violation] = []
for rule in rules:
for name in sorted(facts):
if owns(rule.owners, name):
continue
defined = [
definition
for definition in facts[name].definitions
if definition.kind == "function"
and any(fnmatch.fnmatchcase(definition.name, pat) for pat in rule.names)
]
if not defined:
continue
listed = ", ".join(
f"{definition.name} (line {definition.lineno})" for definition in defined
)
violations.append(
Violation(
rule=rule.rule,
message=(
f"{rule.id}: {name} defines {listed} — {rule.description}. "
f"Declared owners: {', '.join(rule.owners)}"
),
modules=(name,),
)
)
return violations
|
check_single_source(modules: dict[str, ModuleInfo], rules: tuple[SingleSourceRule, ...]) -> list[Violation]
Non-owner modules that import a declared chokepoint.
Source code in src/symfonic/devtools/archgate/dry.py
| def check_single_source(
modules: dict[str, ModuleInfo], rules: tuple[SingleSourceRule, ...]
) -> list[Violation]:
"""Non-owner modules that import a declared chokepoint."""
violations: list[Violation] = []
for rule in rules:
for name in sorted(modules):
if owns(rule.owners, name):
continue
reaches = [
record
for record in modules[name].imports
if not record.type_checking
and not (record.lazy and rule.allow_lazy)
and any(_hits_target(target, record.target) for target in rule.targets)
]
if not reaches:
continue
first = min(reaches, key=lambda record: record.lineno)
violations.append(
Violation(
rule=rule.rule,
message=(
f"{rule.id}: {name}:{first.lineno} imports {first.target} — "
f"{rule.description}. Declared owners: {', '.join(rule.owners)}"
),
modules=(name,),
)
)
return violations
|
owns(patterns: tuple[str, ...], module: str) -> bool
Owner patterns are dotted prefixes, or globs when they contain wildcards.
Source code in src/symfonic/devtools/archgate/dry.py
| def owns(patterns: tuple[str, ...], module: str) -> bool:
"""Owner patterns are dotted prefixes, or globs when they contain wildcards."""
for pattern in patterns:
if any(ch in pattern for ch in "*?["):
if fnmatch.fnmatchcase(module, pattern):
return True
elif module == pattern or module.startswith(pattern + "."):
return True
return False
|