cobraphp 0.1.0.dev1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,195 @@
1
+ """Call graph node and edge data types."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import cast
6
+
7
+ from cpg2py import AbcEdgeQuerier, AbcNodeQuerier, Storage
8
+
9
+ from ._errors import StaticAnalysisError
10
+
11
+
12
+ class CallNode(AbcNodeQuerier): # type: ignore[misc]
13
+ """Abstract base for call graph node metadata.
14
+
15
+ Extends AbcNodeQuerier to access call graph node properties
16
+ from Storage. Do NOT instantiate directly — use FuncNode or
17
+ SiteNode subclasses.
18
+
19
+ Attributes:
20
+ node_id: AST node ID (readonly, inherited).
21
+ node_type: "function_declaration" or "call_site" (readonly).
22
+
23
+ """
24
+
25
+ def __init__(self, storage: Storage, node_id: str, node_type: str) -> None:
26
+ """Initialize CallNode via AbcNodeQuerier.
27
+
28
+ Args:
29
+ storage: Storage instance containing node data.
30
+ node_id: AST node ID.
31
+ node_type: "function_declaration" or "call_site".
32
+
33
+ Raises:
34
+ NodeNotFoundError: If node doesn't exist in storage.
35
+
36
+ """
37
+ super().__init__(storage, node_id)
38
+ self._storage_ref = storage
39
+ storage.set_node_prop(node_id, "node_type", node_type)
40
+
41
+ @property
42
+ def node_type(self) -> str:
43
+ """Node type: 'function_declaration' or 'call_site'."""
44
+ return self.get_property("node_type") or ""
45
+
46
+
47
+ class FuncNode(CallNode):
48
+ """Storage wrapper for function declaration or file-level container.
49
+
50
+ Represents function declarations or file-level containers in
51
+ call graph. When is_file=True, represents a file-level container
52
+ for top-level code.
53
+
54
+ Attributes:
55
+ name: Function name or file path (mutable).
56
+ parameter_ids: AST node IDs of parameter declarations (mutable).
57
+ return_type_hint: Return type annotation if present (mutable).
58
+ is_file: Whether this is a file-level container (readonly).
59
+
60
+ """
61
+
62
+ def __init__(self, storage: Storage, node_id: str) -> None:
63
+ """Initialize FuncNode.
64
+
65
+ Args:
66
+ storage: Storage instance containing node data.
67
+ node_id: AST node ID of function declaration or file ID.
68
+
69
+ Raises:
70
+ NodeNotFoundError: If node doesn't exist in storage.
71
+
72
+ """
73
+ super().__init__(storage, node_id, "function_declaration")
74
+
75
+ @property
76
+ def is_file(self) -> bool:
77
+ """Whether this node represents a file-level container."""
78
+ value = self.get_property("is_file")
79
+ return bool(value) if value is not None else False
80
+
81
+ @property
82
+ def name(self) -> str:
83
+ """Function name."""
84
+ return self.get_property("name") or ""
85
+
86
+ @name.setter
87
+ def name(self, value: str) -> None:
88
+ """Set function name."""
89
+ self._storage_ref.set_node_prop(self.node_id, "name", value)
90
+
91
+ @property
92
+ def parameter_ids(self) -> list[str]:
93
+ """AST node IDs of parameter declarations."""
94
+ return self.get_property("parameter_ids") or []
95
+
96
+ @parameter_ids.setter
97
+ def parameter_ids(self, value: list[str]) -> None:
98
+ """Set parameter IDs."""
99
+ self._storage_ref.set_node_prop(self.node_id, "parameter_ids", value)
100
+
101
+ @property
102
+ def return_type_hint(self) -> str | None:
103
+ """Type hint for return value, or None if not specified."""
104
+ value = self.get_property("return_type_hint")
105
+ return cast(str, value) if value else None
106
+
107
+ @return_type_hint.setter
108
+ def return_type_hint(self, value: str) -> None:
109
+ """Set return type hint."""
110
+ self._storage_ref.set_node_prop(self.node_id, "return_type_hint", value)
111
+
112
+
113
+ class SiteNode(CallNode):
114
+ """Storage wrapper for call site node metadata.
115
+
116
+ Attributes:
117
+ argument_ids: AST node IDs of argument expressions (mutable).
118
+ caller_id: AST node ID of containing function or file (mutable).
119
+
120
+ """
121
+
122
+ def __init__(self, storage: Storage, node_id: str) -> None:
123
+ """Initialize SiteNode.
124
+
125
+ Args:
126
+ storage: Storage instance containing node data.
127
+ node_id: AST node ID of call expression.
128
+
129
+ Raises:
130
+ NodeNotFoundError: If node doesn't exist in storage.
131
+
132
+ """
133
+ super().__init__(storage, node_id, "call_site")
134
+
135
+ @property
136
+ def argument_ids(self) -> list[str]:
137
+ """AST node IDs of argument expressions."""
138
+ return self.get_property("argument_ids") or []
139
+
140
+ @argument_ids.setter
141
+ def argument_ids(self, value: list[str]) -> None:
142
+ """Set argument IDs."""
143
+ self._storage_ref.set_node_prop(self.node_id, "argument_ids", value)
144
+
145
+ @property
146
+ def caller_id(self) -> str:
147
+ """AST node ID of the function or file containing this call site.
148
+
149
+ Raises:
150
+ StaticAnalysisError: If caller_id has not been set.
151
+
152
+ """
153
+ value = self.get_property("caller_id")
154
+ if value is None:
155
+ raise StaticAnalysisError(f"Property 'caller_id' not found for node {self.node_id}")
156
+ return cast(str, value)
157
+
158
+ @caller_id.setter
159
+ def caller_id(self, value: str) -> None:
160
+ """Set caller function ID."""
161
+ self._storage_ref.set_node_prop(self.node_id, "caller_id", value)
162
+
163
+
164
+ class CallEdge(AbcEdgeQuerier): # type: ignore[misc]
165
+ """Storage wrapper for call graph edge (call site to callee).
166
+
167
+ Attributes:
168
+ edge_id: Edge ID tuple (from, to, type) (readonly, inherited).
169
+ callsite_node_id: Alias for from_nid (readonly).
170
+ callee_node_id: Alias for to_nid (readonly).
171
+
172
+ """
173
+
174
+ def __init__(self, storage: Storage, f_nid: str, t_nid: str, e_type: str) -> None:
175
+ """Initialize CallEdge.
176
+
177
+ Args:
178
+ storage: Storage instance containing edge data.
179
+ f_nid: Source node ID (callsite).
180
+ t_nid: Target node ID (callee).
181
+ e_type: Edge type.
182
+
183
+ """
184
+ super().__init__(storage, f_nid, t_nid, e_type)
185
+ self._storage_ref = storage
186
+
187
+ @property
188
+ def callsite_node_id(self) -> str:
189
+ """AST node ID of call expression (alias for from_nid)."""
190
+ return cast(str, self.from_nid)
191
+
192
+ @property
193
+ def callee_node_id(self) -> str:
194
+ """AST node ID of function declaration (alias for to_nid)."""
195
+ return cast(str, self.to_nid)
@@ -0,0 +1,112 @@
1
+ """Resolve PHP call sites to named callable declarations using the receiver class."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from php_parser_py import AST, Node
6
+
7
+ # PHP AST node types of the declarations and calls callee resolution relates.
8
+ FUNCTION_DECL_TYPE = "Stmt_Function"
9
+ METHOD_DECL_TYPE = "Stmt_ClassMethod"
10
+ NAMED_CALLABLE_TYPES = (FUNCTION_DECL_TYPE, METHOD_DECL_TYPE)
11
+ FUNCTION_CALL_TYPE = "Expr_FuncCall"
12
+ METHOD_CALL_TYPES = ("Expr_MethodCall", "Expr_StaticCall")
13
+ CALL_SITE_TYPES = (FUNCTION_CALL_TYPE, *METHOD_CALL_TYPES)
14
+ # Node types php-parser emits for a name written literally in source.
15
+ _NAME_TYPES = frozenset({"Name", "Name_FullyQualified", "Name_Relative"})
16
+ _IDENTIFIER_TYPES = frozenset({"Identifier"})
17
+ _VARIABLE_TYPE = "Expr_Variable"
18
+ # Declarations that own methods; $this, self, and static refer to the nearest one.
19
+ _CLASS_LIKE_TYPES = frozenset({"Stmt_Class", "Stmt_Trait", "Stmt_Interface", "Stmt_Enum"})
20
+ # PHP's receiver variable and class keywords bound to the enclosing class (keywords are case-insensitive).
21
+ _THIS_VARIABLE = "this"
22
+ _ENCLOSING_CLASS_KEYWORDS = frozenset({"self", "static"})
23
+ NAMESPACE_SEPARATOR = "\\"
24
+
25
+
26
+ class CalleeIndex:
27
+ """Named function and method declarations indexed for callee resolution.
28
+
29
+ A function call resolves to a free function only: its exact name, else
30
+ the short name after the last namespace separator. A method call
31
+ resolves to its receiver class's method of that name, where $this,
32
+ self, and static denote the enclosing class and a literal class name
33
+ denotes that class by short name. With no receiver class (parent, a
34
+ variable, an expression) or no such method in it, the call resolves to
35
+ the first method of that name in AST order. A call whose name is an
36
+ expression has no callee. For every key, the first declaration wins.
37
+ """
38
+
39
+ def __init__(self, ast: AST) -> None:
40
+ self._ast = ast
41
+ self._functions: dict[str, Node] = {}
42
+ self._class_methods: dict[tuple[str, str], Node] = {}
43
+ self._methods: dict[str, Node] = {}
44
+
45
+ def add(self, decl: Node, name: str) -> None:
46
+ """Index a Stmt_Function or Stmt_ClassMethod declaration under its name."""
47
+ if decl.node_type == FUNCTION_DECL_TYPE:
48
+ self._functions.setdefault(name, decl)
49
+ return
50
+ class_name = _enclosing_class_name(self._ast, decl)
51
+ if class_name is not None:
52
+ self._class_methods.setdefault((class_name, name), decl)
53
+ self._methods.setdefault(name, decl)
54
+
55
+ def resolve(self, call_site: Node) -> Node | None:
56
+ """Return the declaration a call site targets, or None when unresolved."""
57
+ if call_site.node_type == FUNCTION_CALL_TYPE:
58
+ return self._resolve_function(call_site)
59
+ return self._resolve_method(call_site)
60
+
61
+ # Free function by exact name, else by namespace short name.
62
+ def _resolve_function(self, call_site: Node) -> Node | None:
63
+ name = _child_name(self._ast, call_site, _NAME_TYPES)
64
+ if name is None:
65
+ return None
66
+ exact = self._functions.get(name)
67
+ if exact is not None or NAMESPACE_SEPARATOR not in name:
68
+ return exact
69
+ return self._functions.get(_short_name(name))
70
+
71
+ # Receiver class's method, else the first method of that name.
72
+ def _resolve_method(self, call_site: Node) -> Node | None:
73
+ name = _child_name(self._ast, call_site, _IDENTIFIER_TYPES)
74
+ if name is None:
75
+ return None
76
+ class_name = self._receiver_class_name(call_site)
77
+ own = None if class_name is None else self._class_methods.get((class_name, name))
78
+ return own if own is not None else self._methods.get(name)
79
+
80
+ # Class a method call's receiver (its first child) statically denotes, or None.
81
+ def _receiver_class_name(self, call_site: Node) -> str | None:
82
+ receiver = next(iter(self._ast.succ(call_site)))
83
+ name = str(receiver.all_properties.get("name", ""))
84
+ if receiver.node_type == _VARIABLE_TYPE:
85
+ return _enclosing_class_name(self._ast, call_site) if name == _THIS_VARIABLE else None
86
+ if receiver.node_type not in _NAME_TYPES or name.lower() == "parent":
87
+ return None
88
+ if name.lower() in _ENCLOSING_CLASS_KEYWORDS:
89
+ return _enclosing_class_name(self._ast, call_site)
90
+ return _short_name(name)
91
+
92
+
93
+ # Name of the nearest enclosing class-like declaration; None when absent or anonymous.
94
+ def _enclosing_class_name(ast: AST, node: Node) -> str | None:
95
+ enclosing = next((a for a in ast.ancestors(node) if a.node_type in _CLASS_LIKE_TYPES), None)
96
+ if enclosing is None:
97
+ return None
98
+ return _child_name(ast, enclosing, _IDENTIFIER_TYPES)
99
+
100
+
101
+ # Non-empty name property of the first direct child of the given types, else None.
102
+ def _child_name(ast: AST, node: Node, child_types: frozenset[str]) -> str | None:
103
+ child = next((c for c in ast.succ(node) if c.node_type in child_types), None)
104
+ if child is None:
105
+ return None
106
+ name = child.all_properties.get("name", "")
107
+ return str(name) if name else None
108
+
109
+
110
+ # Segment after the last namespace separator.
111
+ def _short_name(name: str) -> str:
112
+ return name.rsplit(NAMESPACE_SEPARATOR, 1)[-1]