Skip to content
This repository has been archived by the owner on Nov 23, 2024. It is now read-only.

Commit

Permalink
feat: detect instance variables generated with @property-functions (#…
Browse files Browse the repository at this point in the history
…281)

Closes #272 

### Summary of Changes

Added the detection of instance variables generated with
`@property`-functions.

<!-- Please provide a summary of changes in this pull request, ensuring
all changes are explained. -->

---------

Co-authored-by: megalinter-bot <[email protected]>
  • Loading branch information
lukarade and megalinter-bot authored Jun 18, 2024
1 parent 4acc5ca commit 492134e
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 3 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,19 @@ def enter_functiondef(self, node: astroid.FunctionDef) -> None:
for decorator in node.decorators.nodes:
if isinstance(decorator, astroid.Name) and decorator.name == "overload":
return
elif isinstance(decorator, astroid.Name) and decorator.name == "property":
if isinstance(self.current_node_stack[-1], ClassScope) and hasattr(
self.current_node_stack[-1],
"instance_variables",
):
self.current_node_stack[-1].instance_variables.setdefault(node.name, []).append(
InstanceVariable(
node=node,
id=NodeID.calc_node_id(node),
name=node.name,
klass=self.current_node_stack[-1].symbol.node,
),
)

self.current_node_stack.append(
FunctionScope(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -768,8 +768,18 @@ def _resolve_references(self) -> tuple[dict[str, list[ReferenceNode]], dict[Node
if isinstance(referenced_symbol, GlobalVariable | ClassVariable | InstanceVariable):
# Since classes and functions are defined as immutable
# reading from them is not a reason for impurity.
# There is an exception to this rule for functions
# that are decorated with a '@property' decorator. These functions define an
# instance variable as a property, which can be read from.
if isinstance(referenced_symbol.node, astroid.ClassDef | astroid.FunctionDef):
continue
if (
isinstance(referenced_symbol.node, astroid.FunctionDef)
and "builtins.property" in referenced_symbol.node.decoratornames()
and isinstance(referenced_symbol, InstanceVariable)
):
pass
else:
continue
# Add the referenced symbol to the list of symbols whom are read from.
if referenced_symbol not in raw_reasons[function.symbol.id].reads_from:
raw_reasons[function.symbol.id].reads_from.add(referenced_symbol)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,10 @@ def transform_scope_node(
super_classes_transformed = []
for child in node.instance_variables.values():
for c1 in child:
c_str = to_string_class(c1.node.node)
if isinstance(c1.node, MemberAccess):
c_str = to_string_class(c1.node.node)
else:
c_str = to_string_class(c1.node)
if c_str is not None:
instance_vars_transformed.append(c_str) # type: ignore[misc]
# it is not possible that c_str is None
Expand Down Expand Up @@ -1884,6 +1887,60 @@ def __post_init__(self):
),
},
),
( # language=Python "Assign Instance Attribute via property"
"""
class A:
def __init__(self, value):
self._value = value
def f(self):
return self.value
@property
def value(self):
return self._value
""", # language=none
{
"A": SimpleClassScope(
"GlobalVariable.ClassDef.A",
[
SimpleFunctionScope(
"ClassVariable.FunctionDef.__init__",
[
SimpleScope("Parameter.AssignName.self", []),
SimpleScope("Parameter.AssignName.value", []),
SimpleScope("InstanceVariable.MemberAccess.self._value", []),
],
["AssignName.self", "Name.self", "AssignName.value", "MemberAccessTarget.self._value"],
["Name.value"],
[],
["AssignName.self", "AssignName.value"],
),
SimpleFunctionScope(
"ClassVariable.FunctionDef.f",
[SimpleScope("Parameter.AssignName.self", [])],
["AssignName.self"],
["MemberAccessValue.self.value", "Name.self"],
[],
["AssignName.self"],
),
SimpleFunctionScope(
"ClassVariable.FunctionDef.value",
[SimpleScope("Parameter.AssignName.self", [])],
["AssignName.self"],
["MemberAccessValue.self._value", "Name.self"],
[],
["AssignName.self"],
),
],
["FunctionDef.__init__", "FunctionDef.f", "FunctionDef.value"],
["AssignAttr._value", "FunctionDef.value"],
None,
"__init__",
None,
),
},
),
],
ids=[
"ClassDef",
Expand All @@ -1898,6 +1955,7 @@ def __post_init__(self):
"Multiple ClassDef",
"ClassDef with super class",
"ClassDef with __new__, __init__ and __post_init__",
"Assign Instance Attribute via property",
],
)
def test_get_module_data_classes(code: str, expected: dict[str, SimpleClassScope]) -> None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -471,6 +471,25 @@ def f():
"f.line2": Pure(),
},
),
( # language=Python "Assign Instance Attribute via property"
"""
class A:
def __init__(self, value):
self._value = value
def f(self):
return self.value
@property
def value(self):
return self._value
""", # language=none
{
"__init__.line3": Pure(),
"f.line6": SimpleImpure({"NonLocalVariableRead.InstanceVariable.A.value"}),
"value.line10": SimpleImpure({"NonLocalVariableRead.InstanceVariable.A._value"}),
},
),
],
ids=[
"Trivial function",
Expand All @@ -496,6 +515,7 @@ def f():
"Builtins for dict",
"Builtins for list",
"Builtins for set",
"Assign Instance Attribute via property",
], # TODO: class inits in cycles
)
def test_infer_purity_pure(code: str, expected: list[ImpurityReason]) -> None:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2851,7 +2851,11 @@ def f():
ReferenceTestNode(
"a.state.line18",
"FunctionDef.f",
["ClassVariable.State.state.line13", "ClassVariable.State.state.line9"],
[
"ClassVariable.State.state.line13",
"ClassVariable.State.state.line9",
"InstanceVariable.State.state.line9",
],
),
ReferenceTestNode("a.line18", "FunctionDef.f", ["LocalVariable.a.line17"]),
],
Expand Down

0 comments on commit 492134e

Please sign in to comment.