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

Commit

Permalink
feat: correct origins for CombinedCallGraphNodes (#269)
Browse files Browse the repository at this point in the history
Closes #267 

### Summary of Changes

<!-- 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 492134e commit 07304da
Show file tree
Hide file tree
Showing 12 changed files with 446 additions and 375 deletions.
4 changes: 3 additions & 1 deletion src/library_analyzer/cli/_run_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,6 @@ def _run_api_command(

api_purity = get_purity_results(src_dir_path)
out_file_api_purity = out_dir_path.joinpath(f"{package}__api_purity.json")
api_purity.to_json_file(out_file_api_purity)
api_purity.to_json_file(
out_file_api_purity,
) # Shorten is set to True by default, therefore the results will only contain the count of each reason.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
Parameter,
Reasons,
Symbol,
UnknownProto,
)


Expand All @@ -21,21 +22,21 @@ class CallGraphBuilder:
Attributes
----------
classes : dict[str, ClassScope]
classes :
Classnames in the module as key and their corresponding ClassScope instance as value.
raw_reasons : dict[NodeID, Reasons]
raw_reasons :
The raw reasons for impurity for all functions.
Keys are the ids of the functions.
call_graph_forest : CallGraphForest
call_graph_forest :
The call graph forest for the given functions.
visited : set[NodeID]
visited :
A set of all visited nodes.
Parameters
----------
classes : dict[str, ClassScope]
classes :
Classnames in the module as key and their corresponding ClassScope instance as value.
raw_reasons : dict[NodeID, Reasons]
raw_reasons :
The raw reasons for impurity for all functions.
Keys are the ids of the functions.
"""
Expand All @@ -59,7 +60,7 @@ def _build_call_graph_forest(self) -> CallGraphForest:
Returns
-------
call_graph_forest : CallGraphForest
call_graph_forest :
The call graph forest for the given functions.
"""
# Prepare the classes for the call graph.
Expand Down Expand Up @@ -131,7 +132,7 @@ def _built_call_graph(self, reason: Reasons) -> None:
Parameters
----------
reason : Reasons
reason :
The raw reasons of the function.
"""
# If the node has already been visited, return
Expand All @@ -144,13 +145,16 @@ def _built_call_graph(self, reason: Reasons) -> None:
# If the node is already inside the forest and does not have any calls left, it is considered to be finished.
if self.call_graph_forest.has_graph(reason.id) and not reason.calls:
return

# If the node is already inside the forest but still has calls left, it needs to be updated.
if self.call_graph_forest.has_graph(reason.id):
cgn = self.call_graph_forest.get_graph(reason.id)
# Create a new node and add it to the forest.
cgn = CallGraphNode(
symbol=reason.function_scope.symbol, # type: ignore[union-attr] # function_scope is never None here
reasons=reason,
)
self.call_graph_forest.add_graph(reason.id, cgn)
else:
cgn = CallGraphNode(
symbol=reason.function_scope.symbol, # type: ignore[union-attr] # function_scope is never None here
reasons=reason,
)
self.call_graph_forest.add_graph(reason.id, cgn)

# The node has calls, which need to be added to the forest and to the children of the current node.
# They are sorted to ensure a deterministic order of the children (especially but not only for testing).
Expand All @@ -172,14 +176,14 @@ def _built_call_graph(self, reason: Reasons) -> None:

# Check if the node was declared inside the current module.
elif call.id not in self.raw_reasons:
self._handle_unknown_call(call, reason.id)
self._handle_unknown_call(call, reason)

# Build the call graph for the child function and add it to the children of the current node.
else:
self._built_call_graph(self.raw_reasons[call.id])
self.call_graph_forest.get_graph(reason.id).add_child(self.call_graph_forest.get_graph(call.id))

def _handle_unknown_call(self, call: Symbol, reason_id: NodeID) -> None:
def _handle_unknown_call(self, call: Symbol, reason: Reasons) -> None:
"""Handle unknown calls.
Deal with unknown calls and add them to the forest.
Expand All @@ -188,10 +192,10 @@ def _handle_unknown_call(self, call: Symbol, reason_id: NodeID) -> None:
Parameters
----------
call : Symbol
call :
The call that is unknown.
reason_id : NodeID
The id of the function that the call is in.
reason :
The reason of the function that contains the unknown call.
"""
# Deal with the case that the call calls an imported function.
if isinstance(call, Import):
Expand All @@ -200,26 +204,32 @@ def _handle_unknown_call(self, call: Symbol, reason_id: NodeID) -> None:
reasons=Reasons(id=call.id),
)
self.call_graph_forest.add_graph(call.id, imported_cgn)
self.call_graph_forest.get_graph(reason_id).add_child(self.call_graph_forest.get_graph(call.id))
self.call_graph_forest.get_graph(reason.id).add_child(self.call_graph_forest.get_graph(call.id))

# If the call was used as a member of an MemberAccessValue, it needs to be removed from the unknown_calls.
# This is due to the improved analysis that can determine the module through the receiver of that call.
# Hence, the call is handled as a call of an imported function and not as an unknown_call
# when inferring the purity later.
for unknown_call in self.call_graph_forest.get_graph(reason_id).reasons.unknown_calls:
if unknown_call.node == call.call:
for unknown_call in self.call_graph_forest.get_graph(reason.id).reasons.unknown_calls.copy().values():
if unknown_call.symbol.node == call.call:
(
self.call_graph_forest.get_graph(reason_id).reasons.remove_unknown_call(
self.call_graph_forest.get_graph(reason.id).reasons.remove_unknown_call(
NodeID.calc_node_id(call.call),
)
)

# Deal with the case that the call calls a function parameter.
elif isinstance(call, Parameter):
self.call_graph_forest.get_graph(reason_id).reasons.unknown_calls.add(call)
self.call_graph_forest.get_graph(reason.id).reasons.unknown_calls[call.id] = UnknownProto(
symbol=call,
origin=reason.function_scope.symbol if reason.function_scope else None,
)

else:
self.call_graph_forest.get_graph(reason_id).reasons.unknown_calls.add(call)
self.call_graph_forest.get_graph(reason.id).reasons.unknown_calls[call.id] = UnknownProto(
symbol=call,
origin=reason.function_scope.symbol if reason.function_scope else None,
)

def _handle_cycles(self, removed_nodes: set[NodeID] | None = None) -> None:
"""Handle cycles in the call graph.
Expand All @@ -231,7 +241,7 @@ def _handle_cycles(self, removed_nodes: set[NodeID] | None = None) -> None:
Parameters
----------
removed_nodes : set[NodeID] | None
removed_nodes :
A set of all removed nodes.
If not given, a new set is created.
"""
Expand Down Expand Up @@ -262,16 +272,16 @@ def _test_cgn_for_cycles(
Parameters
----------
cgn : CallGraphNode
cgn :
The current node in the graph that is visited.
visited_nodes : set[NewCallGraphNode] | None
visited_nodes :
A set of all visited nodes.
path : list[NodeID] | None
path :
A list of all nodes in the current path.
Returns
-------
cycle : dict[NodeID, NewCallGraphNode]
cycle :
Dict of all nodes in the cycle.
Keys are the NodeIDs of the nodes.
Returns an empty dict if no cycle is found.
Expand Down Expand Up @@ -316,7 +326,7 @@ def _contract_cycle(self, cycle: dict[NodeID, CallGraphNode]) -> None:
Parameters
----------
cycle : dict[NodeID, CallGraphNode]
cycle :
A dict of all nodes in the cycle.
Keys are the NodeIDs of the CallGraphNodes.
"""
Expand Down Expand Up @@ -368,10 +378,10 @@ def _update_pointers(self, cycle: dict[NodeID, CallGraphNode], combined_node: Co
Parameters
----------
cycle : dict[NodeID, CallGraphNode]
cycle :
A dict of all nodes in the cycle.
Keys are the NodeIDs of the nodes.
combined_node : CombinedCallGraphNode
combined_node :
The combined node that replaces all nodes in the cycle.
"""
for graph in self.call_graph_forest.graphs.values():
Expand All @@ -386,15 +396,15 @@ def build_call_graph(classes: dict[str, ClassScope], raw_reasons: dict[NodeID, R
Parameters
----------
classes : dict[str, ClassScope]
classes :
Classnames in the module as key and their corresponding ClassScope instance as value.
raw_reasons : dict[NodeID, Reasons]
raw_reasons :
The raw reasons for impurity for all functions.
Keys are the ids of the functions.
Returns
-------
call_graph_forest : CallGraphForest
call_graph_forest :
The call graph forest for the given functions.
"""
return CallGraphBuilder(classes, raw_reasons).call_graph_forest
Loading

0 comments on commit 07304da

Please sign in to comment.