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

Commit 726ba5b

Browse files
refactor: data structure (#223)
### Summary of Changes In this fix, the data structure is reworked to fit the model discussed in the requirements. Also, the analysis is changed regarding the resolving of references. `FunctionScope` class now holds target, value and call nodes for the function, so we can iterate over these for each function. It should now be more efficient and easier to understand since the changes to the data structures below were used. Added the class `Reference` to represent a node that references a `Symbol`. The `ReferenceNode` class was extended with two subclasses `TargetReference` - representing a Symbol referencing another Symbol, and `ValueReference` - representing a Reference referencing a Symbol. Both classes store the referenced symbols in a list. Reworked the `Reasons` class so it now holds the Symbols for the variables written to/ read from. This blocks the merge of #211! <!-- Please provide a summary of changes in this pull request, ensuring all changes are explained. --> --------- Co-authored-by: megalinter-bot <129584137+megalinter-bot@users.noreply.github.com>
1 parent 4e51c55 commit 726ba5b

19 files changed

+8876
-5673
lines changed

src/library_analyzer/cli/_run_api.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def _run_api_command(
2323
out_dir_path : Path
2424
The path to the output directory.
2525
docstring_style : DocstringStyle
26-
The style of docstrings that used in the library.
26+
The style of docstrings that is used in the library.
2727
"""
2828
api = get_api(package, src_dir_path, docstring_style)
2929
out_file_api = out_dir_path.joinpath(f"{package}__api.json")
@@ -32,5 +32,3 @@ def _run_api_command(
3232
api_dependencies = get_dependencies(api)
3333
out_file_api_dependencies = out_dir_path.joinpath(f"{package}__api_dependencies.json")
3434
api_dependencies.to_json_file(out_file_api_dependencies)
35-
36-
# TODO: call resolve_references here

src/library_analyzer/processing/api/purity_analysis/_build_call_graph.py

Lines changed: 294 additions & 95 deletions
Large diffs are not rendered by default.

src/library_analyzer/processing/api/purity_analysis/_get_module_data.py

Lines changed: 589 additions & 399 deletions
Large diffs are not rendered by default.

src/library_analyzer/processing/api/purity_analysis/_infer_purity.py

Lines changed: 239 additions & 249 deletions
Large diffs are not rendered by default.

src/library_analyzer/processing/api/purity_analysis/_resolve_references.py

Lines changed: 379 additions & 344 deletions
Large diffs are not rendered by default.

src/library_analyzer/processing/api/purity_analysis/model/__init__.py

Lines changed: 35 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,14 @@
11
"""Data model for purity analysis."""
22

3-
from library_analyzer.processing.api.purity_analysis.model._purity import (
4-
CallOfParameter,
5-
Expression,
6-
FileRead,
7-
FileWrite,
8-
Impure,
9-
ImpurityReason,
10-
NativeCall,
11-
NonLocalVariableRead,
12-
NonLocalVariableWrite,
13-
OpenMode,
14-
ParameterAccess,
15-
Pure,
16-
PurityResult,
17-
StringLiteral,
18-
UnknownCall,
19-
)
20-
from library_analyzer.processing.api.purity_analysis.model._reference import (
3+
from library_analyzer.processing.api.purity_analysis.model._call_graph import (
214
CallGraphForest,
225
CallGraphNode,
23-
ReferenceNode,
246
)
25-
from library_analyzer.processing.api.purity_analysis.model._scope import (
7+
from library_analyzer.processing.api.purity_analysis.model._module_data import (
268
Builtin,
9+
BuiltinOpen,
2710
ClassScope,
2811
ClassVariable,
29-
FunctionReference,
3012
FunctionScope,
3113
GlobalVariable,
3214
Import,
@@ -38,17 +20,42 @@
3820
ModuleData,
3921
NodeID,
4022
Parameter,
41-
Reasons,
23+
Reference,
4224
Scope,
4325
Symbol,
4426
)
27+
from library_analyzer.processing.api.purity_analysis.model._purity import (
28+
APIPurity,
29+
CallOfParameter,
30+
Expression,
31+
FileRead,
32+
FileWrite,
33+
Impure,
34+
ImpurityReason,
35+
NativeCall,
36+
NonLocalVariableRead,
37+
NonLocalVariableWrite,
38+
OpenMode,
39+
ParameterAccess,
40+
Pure,
41+
PurityResult,
42+
StringLiteral,
43+
UnknownCall,
44+
)
45+
from library_analyzer.processing.api.purity_analysis.model._reference import (
46+
ModuleAnalysisResult,
47+
Reasons,
48+
ReferenceNode,
49+
TargetReference,
50+
ValueReference,
51+
)
4552

4653
__all__ = [
54+
"ModuleAnalysisResult",
4755
"ModuleData",
4856
"Scope",
4957
"ClassScope",
5058
"FunctionScope",
51-
"FunctionReference",
5259
"MemberAccess",
5360
"MemberAccessTarget",
5461
"MemberAccessValue",
@@ -80,4 +87,9 @@
8087
"NativeCall",
8188
"UnknownCall",
8289
"CallOfParameter",
90+
"Reference",
91+
"TargetReference",
92+
"ValueReference",
93+
"APIPurity",
94+
"BuiltinOpen",
8395
]
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
from __future__ import annotations
2+
3+
from dataclasses import dataclass, field
4+
from typing import TYPE_CHECKING
5+
6+
if TYPE_CHECKING:
7+
from library_analyzer.processing.api.purity_analysis.model._module_data import (
8+
ClassScope,
9+
FunctionScope,
10+
NodeID,
11+
)
12+
from library_analyzer.processing.api.purity_analysis.model._reference import Reasons
13+
14+
15+
@dataclass
16+
class CallGraphNode:
17+
"""Class for call graph nodes.
18+
19+
A call graph node represents a function in the call graph.
20+
21+
Attributes
22+
----------
23+
scope : FunctionScope | ClassScope
24+
The function that the node represents.
25+
This is a ClassScope if the class has a __init__ method.
26+
In this case, the node is used for propagating the reasons of the
27+
__init__ method to function calling the class.
28+
reasons : Reasons
29+
The raw Reasons for the node.
30+
children : set[CallGraphNode]
31+
The set of children of the node, (i.e., the set of nodes that this node calls)
32+
combined_node_ids : list[NodeID]
33+
A list of the names of all nodes that are combined into this node.
34+
This is only set if the node is a combined node.
35+
This is later used for transferring the reasons of the combined node to the original nodes.
36+
is_builtin : bool
37+
True if the function is a builtin function, False otherwise.
38+
"""
39+
40+
scope: FunctionScope | ClassScope # TODO: change to symbol
41+
reasons: (
42+
Reasons # TODO: remove calls from reasons after they were added to the call graph (except for unknown calls)
43+
)
44+
children: set[CallGraphNode] = field(default_factory=set)
45+
combined_node_ids: list[NodeID] = field(default_factory=list)
46+
is_builtin: bool = False
47+
48+
def __hash__(self) -> int:
49+
return hash(str(self))
50+
51+
def __repr__(self) -> str:
52+
return f"{self.scope.symbol.id}"
53+
54+
def add_child(self, child: CallGraphNode) -> None:
55+
"""Add a child to the node.
56+
57+
Parameters
58+
----------
59+
child : CallGraphNode
60+
The child to add.
61+
"""
62+
self.children.add(child)
63+
64+
def is_leaf(self) -> bool:
65+
"""Check if the node is a leaf node.
66+
67+
Returns
68+
-------
69+
bool
70+
True if the node is a leaf node, False otherwise.
71+
"""
72+
return len(self.children) == 0
73+
74+
def combined_node_id_to_string(self) -> list[str]:
75+
"""Return the combined node IDs as a string.
76+
77+
Returns
78+
-------
79+
str
80+
The combined node IDs as a string.
81+
"""
82+
return [str(node_id) for node_id in self.combined_node_ids]
83+
84+
85+
@dataclass
86+
class CallGraphForest:
87+
"""Class for call graph forests.
88+
89+
A call graph forest represents a collection of call graph trees.
90+
91+
Attributes
92+
----------
93+
graphs : dict[str, CallGraphNode]
94+
The dictionary of call graph trees.
95+
The key is the name of the tree, the value is the root CallGraphNode of the tree.
96+
"""
97+
98+
graphs: dict[NodeID, CallGraphNode] = field(default_factory=dict)
99+
100+
def add_graph(self, graph_id: NodeID, graph: CallGraphNode) -> None:
101+
"""Add a call graph tree to the forest.
102+
103+
Parameters
104+
----------
105+
graph_id : NodeID
106+
The NodeID of the tree node.
107+
graph : CallGraphNode
108+
The root of the tree.
109+
"""
110+
self.graphs[graph_id] = graph
111+
112+
def get_graph(self, graph_id: NodeID) -> CallGraphNode:
113+
"""Get a call graph tree from the forest.
114+
115+
Parameters
116+
----------
117+
graph_id : NodeID
118+
The NodeID of the tree node to get.
119+
120+
Returns
121+
-------
122+
CallGraphNode
123+
The CallGraphNode that is the root of the tree.
124+
125+
Raises
126+
------
127+
KeyError
128+
If the graph_id is not in the forest.
129+
"""
130+
result = self.graphs.get(graph_id)
131+
if result is None:
132+
raise KeyError(f"Graph with id {graph_id} not found inside the call graph.")
133+
return result
134+
135+
def has_graph(self, graph_id: NodeID) -> bool:
136+
"""Check if the forest contains a call graph tree with the given NodeID.
137+
138+
Parameters
139+
----------
140+
graph_id : NodeID
141+
The NodeID of the tree to check for.
142+
143+
Returns
144+
-------
145+
bool
146+
True if the forest contains a tree with the given NodeID, False otherwise.
147+
"""
148+
return graph_id in self.graphs
149+
150+
def delete_graph(self, graph_id: NodeID) -> None:
151+
"""Delete a call graph tree from the forest.
152+
153+
Parameters
154+
----------
155+
graph_id : NodeID
156+
The NodeID of the tree to delete.
157+
"""
158+
del self.graphs[graph_id]

0 commit comments

Comments
 (0)