Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 16 additions & 9 deletions nominal_refactor_advisor/audit/source_measures.py
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,10 @@ class FamilyFlattened(PerFileModuleMeasure):
"""
weight = 3
names: ClassVar[frozenset[str]] = frozenset({"declared_name", "family_name"})
# Standard-library read-only descriptors whose getter result is the attribute's value.
descriptors: ClassVar[frozenset[tuple[str, str]]] = frozenset({
("builtins", "property"), ("functools", "cached_property"),
})

@classmethod
def _flattened(cls, node: ast.AST | None) -> int:
Expand All @@ -531,13 +535,14 @@ def _flattened(cls, node: ast.AST | None) -> int:
return 0

@classmethod
def identity_return(cls, function: ast.FunctionDef, properties: set[str], builtins: set[str]) -> ast.Return | None:
def identity_return(cls, function: ast.FunctionDef, properties: set[str],
modules: dict[str, str]) -> ast.Return | None:
if len(function.decorator_list) != 1:
return None
decorator = function.decorator_list[0]
if not (isinstance(decorator, ast.Name) and decorator.id in properties
or isinstance(decorator, ast.Attribute) and decorator.attr == "property"
and isinstance(decorator.value, ast.Name) and decorator.value.id in builtins):
or isinstance(decorator, ast.Attribute) and isinstance(decorator.value, ast.Name)
and (modules.get(decorator.value.id), decorator.attr) in cls.descriptors):
return None
parameters = (*function.args.posonlyargs, *function.args.args)
body = function.body
Expand Down Expand Up @@ -572,17 +577,19 @@ def flattened(cls, node: ast.Module) -> tuple[ast.AST, ...]:
if writes[name] == 1:
strings[name] = value
properties = {"property"} if not writes["property"] else set()
builtins: set[str] = set()
modules: dict[str, str] = {}
sources = {module for module, _ in cls.descriptors}
for statement in node.body:
if isinstance(statement, ast.ImportFrom) and statement.module == "builtins":
if isinstance(statement, ast.ImportFrom):
properties.update(alias.asname or alias.name for alias in statement.names
if alias.name == "property" and writes[alias.asname or alias.name] == 1)
if (statement.module, alias.name) in cls.descriptors
and writes[alias.asname or alias.name] == 1)
elif isinstance(statement, ast.Import):
builtins.update(alias.asname or alias.name for alias in statement.names
if alias.name == "builtins" and writes[alias.asname or alias.name] == 1)
modules.update((alias.asname or alias.name, alias.name) for alias in statement.names
if alias.name in sources and writes[alias.asname or alias.name] == 1)
identities = {returned for item in nodes
if isinstance(item, ast.FunctionDef)
and (returned := cls.identity_return(item, properties, builtins)) is not None}
and (returned := cls.identity_return(item, properties, modules)) is not None}

def string_value(value: ast.AST) -> str | None:
if isinstance(value, ast.Constant) and isinstance(value.value, str):
Expand Down
4 changes: 3 additions & 1 deletion tests/audit/test_audit_regressions.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,9 @@ def test_family_identity_and_selector_from_real_history(self):
def test_identity_projection_is_not_string_redispatch(self):
for header, decorator in (("", "property"),
("from builtins import property as identity", "identity"),
("import builtins as native", "native.property")):
("import builtins as native", "native.property"),
("from functools import cached_property", "cached_property"),
("import functools", "functools.cached_property")):
source = header + f'''
SEND_TOOL = "comms_send"
class CodingCall:
Expand Down
Loading