coretrace-python-analyzer 0.1.0__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.
Files changed (126) hide show
  1. coretrace_python/__init__.py +4 -0
  2. coretrace_python/__main__.py +4 -0
  3. coretrace_python/abstract/__init__.py +45 -0
  4. coretrace_python/abstract/constants.py +226 -0
  5. coretrace_python/abstract/heap.py +252 -0
  6. coretrace_python/abstract/ranges.py +285 -0
  7. coretrace_python/abstract/values.py +56 -0
  8. coretrace_python/analysis/__init__.py +31 -0
  9. coretrace_python/analysis/manager.py +165 -0
  10. coretrace_python/analysis/provider.py +73 -0
  11. coretrace_python/bundled/dependency/dependency_policy/dependency_policy.py +46 -0
  12. coretrace_python/bundled/dependency/dependency_policy/plugin.toml +9 -0
  13. coretrace_python/bundled/dependency/reachable_vulnerability/plugin.toml +9 -0
  14. coretrace_python/bundled/dependency/reachable_vulnerability/reachable_vulnerability.py +56 -0
  15. coretrace_python/bundled/dependency/sample_advisories/plugin.toml +9 -0
  16. coretrace_python/bundled/dependency/sample_advisories/sample_advisories.py +109 -0
  17. coretrace_python/bundled/dependency/vulnerable_dependency/plugin.toml +9 -0
  18. coretrace_python/bundled/dependency/vulnerable_dependency/vulnerable_dependency.py +43 -0
  19. coretrace_python/bundled/models/cli/cli_models.py +35 -0
  20. coretrace_python/bundled/models/cli/plugin.toml +9 -0
  21. coretrace_python/bundled/models/credentials/credential_models.py +43 -0
  22. coretrace_python/bundled/models/credentials/plugin.toml +9 -0
  23. coretrace_python/bundled/models/django/django_models.py +123 -0
  24. coretrace_python/bundled/models/django/plugin.toml +9 -0
  25. coretrace_python/bundled/models/fastapi/fastapi_models.py +29 -0
  26. coretrace_python/bundled/models/fastapi/plugin.toml +9 -0
  27. coretrace_python/bundled/models/flask/flask_models.py +53 -0
  28. coretrace_python/bundled/models/flask/plugin.toml +9 -0
  29. coretrace_python/bundled/models/http_clients/http_client_models.py +43 -0
  30. coretrace_python/bundled/models/http_clients/plugin.toml +9 -0
  31. coretrace_python/bundled/models/python_stdlib/plugin.toml +9 -0
  32. coretrace_python/bundled/models/python_stdlib/python_stdlib.py +68 -0
  33. coretrace_python/bundled/models/sqlalchemy/plugin.toml +9 -0
  34. coretrace_python/bundled/models/sqlalchemy/sqlalchemy_models.py +47 -0
  35. coretrace_python/bundled/secrets/config_secrets/config_secrets.py +38 -0
  36. coretrace_python/bundled/secrets/config_secrets/plugin.toml +9 -0
  37. coretrace_python/bundled/secrets/hardcoded_secrets/hardcoded_secrets.py +19 -0
  38. coretrace_python/bundled/secrets/hardcoded_secrets/plugin.toml +9 -0
  39. coretrace_python/bundled/security/command_injection/command_injection.py +17 -0
  40. coretrace_python/bundled/security/command_injection/plugin.toml +9 -0
  41. coretrace_python/bundled/security/insecure_deserialization/insecure_deserialization.py +17 -0
  42. coretrace_python/bundled/security/insecure_deserialization/plugin.toml +9 -0
  43. coretrace_python/bundled/security/open_redirect/open_redirect.py +17 -0
  44. coretrace_python/bundled/security/open_redirect/plugin.toml +9 -0
  45. coretrace_python/bundled/security/path_traversal/path_traversal.py +17 -0
  46. coretrace_python/bundled/security/path_traversal/plugin.toml +9 -0
  47. coretrace_python/bundled/security/plaintext_credentials/plaintext_credentials.py +21 -0
  48. coretrace_python/bundled/security/plaintext_credentials/plugin.toml +9 -0
  49. coretrace_python/bundled/security/sql_injection/plugin.toml +9 -0
  50. coretrace_python/bundled/security/sql_injection/sql_injection.py +17 -0
  51. coretrace_python/bundled/security/ssrf/plugin.toml +9 -0
  52. coretrace_python/bundled/security/ssrf/ssrf.py +17 -0
  53. coretrace_python/bundled/security/xss/plugin.toml +9 -0
  54. coretrace_python/bundled/security/xss/xss.py +17 -0
  55. coretrace_python/bundled/syntax/dangerous_eval/dangerous_eval.py +19 -0
  56. coretrace_python/bundled/syntax/dangerous_eval/plugin.toml +9 -0
  57. coretrace_python/bundled/syntax/flask_debug/flask_debug.py +63 -0
  58. coretrace_python/bundled/syntax/flask_debug/plugin.toml +9 -0
  59. coretrace_python/bundled/syntax/missing_timeout/missing_timeout.py +51 -0
  60. coretrace_python/bundled/syntax/missing_timeout/plugin.toml +9 -0
  61. coretrace_python/bundled/syntax/weak_crypto/plugin.toml +9 -0
  62. coretrace_python/bundled/syntax/weak_crypto/weak_crypto.py +19 -0
  63. coretrace_python/cache.py +310 -0
  64. coretrace_python/cfg/__init__.py +46 -0
  65. coretrace_python/cfg/builder.py +589 -0
  66. coretrace_python/cfg/dominance.py +183 -0
  67. coretrace_python/cfg/model.py +166 -0
  68. coretrace_python/cli.py +216 -0
  69. coretrace_python/dataflow/__init__.py +29 -0
  70. coretrace_python/dataflow/lattice.py +78 -0
  71. coretrace_python/dataflow/solver.py +96 -0
  72. coretrace_python/dependency/__init__.py +44 -0
  73. coretrace_python/dependency/advisories.py +168 -0
  74. coretrace_python/dependency/correlation.py +87 -0
  75. coretrace_python/dependency/graph.py +274 -0
  76. coretrace_python/dependency/policy.py +65 -0
  77. coretrace_python/dependency/sbom.py +65 -0
  78. coretrace_python/engine.py +688 -0
  79. coretrace_python/findings/__init__.py +13 -0
  80. coretrace_python/findings/coverage.py +45 -0
  81. coretrace_python/findings/model.py +49 -0
  82. coretrace_python/findings/refutation.py +427 -0
  83. coretrace_python/frontend/__init__.py +18 -0
  84. coretrace_python/frontend/ast_adapter.py +447 -0
  85. coretrace_python/frontend/parser.py +23 -0
  86. coretrace_python/hir/__init__.py +5 -0
  87. coretrace_python/hir/nodes.py +584 -0
  88. coretrace_python/hir/visitors.py +36 -0
  89. coretrace_python/interprocedural/__init__.py +49 -0
  90. coretrace_python/interprocedural/callgraph.py +291 -0
  91. coretrace_python/interprocedural/modulegraph.py +218 -0
  92. coretrace_python/interprocedural/summaries.py +463 -0
  93. coretrace_python/ir/__init__.py +5 -0
  94. coretrace_python/ir/defuse.py +74 -0
  95. coretrace_python/ir/lowering.py +575 -0
  96. coretrace_python/ir/model.py +481 -0
  97. coretrace_python/ir/printer.py +201 -0
  98. coretrace_python/ir/ssa.py +277 -0
  99. coretrace_python/plugins/__init__.py +58 -0
  100. coretrace_python/plugins/api.py +153 -0
  101. coretrace_python/plugins/detectors.py +114 -0
  102. coretrace_python/plugins/loader.py +84 -0
  103. coretrace_python/plugins/manifest.py +112 -0
  104. coretrace_python/plugins/registry.py +34 -0
  105. coretrace_python/plugins/secrets.py +330 -0
  106. coretrace_python/reporters/__init__.py +22 -0
  107. coretrace_python/reporters/json_format.py +48 -0
  108. coretrace_python/reporters/report.py +23 -0
  109. coretrace_python/reporters/sarif.py +70 -0
  110. coretrace_python/reporters/text.py +24 -0
  111. coretrace_python/semantic/__init__.py +9 -0
  112. coretrace_python/semantic/identity.py +39 -0
  113. coretrace_python/semantic/imports.py +131 -0
  114. coretrace_python/semantic/scopes.py +473 -0
  115. coretrace_python/semantic/symbols.py +87 -0
  116. coretrace_python/source/__init__.py +13 -0
  117. coretrace_python/source/manager.py +81 -0
  118. coretrace_python/source/model.py +57 -0
  119. coretrace_python/taint/__init__.py +55 -0
  120. coretrace_python/taint/engine.py +800 -0
  121. coretrace_python/taint/models.py +317 -0
  122. coretrace_python/taint/routes.py +99 -0
  123. coretrace_python_analyzer-0.1.0.dist-info/METADATA +74 -0
  124. coretrace_python_analyzer-0.1.0.dist-info/RECORD +126 -0
  125. coretrace_python_analyzer-0.1.0.dist-info/WHEEL +4 -0
  126. coretrace_python_analyzer-0.1.0.dist-info/entry_points.txt +2 -0
@@ -0,0 +1,4 @@
1
+ """CoreTrace's Python static analysis frontend."""
2
+
3
+ __version__ = "0.1.0"
4
+
@@ -0,0 +1,4 @@
1
+ from coretrace_python.cli import main
2
+
3
+ raise SystemExit(main())
4
+
@@ -0,0 +1,45 @@
1
+ """Abstract values and the analyses that compute them (architecture §18)."""
2
+
3
+ from coretrace_python.abstract.constants import (
4
+ ConstantFacts,
5
+ ConstantPropagation,
6
+ propagate_constants,
7
+ )
8
+ from coretrace_python.abstract.heap import (
9
+ ATTRIBUTES,
10
+ ELEMENTS,
11
+ MUTATORS,
12
+ AbstractObject,
13
+ AliasSet,
14
+ AllocationSite,
15
+ HeapAnalysis,
16
+ HeapFacts,
17
+ HeapLocation,
18
+ analyze_heap,
19
+ mutated_by,
20
+ )
21
+ from coretrace_python.abstract.ranges import Interval, RangeAnalysis, RangeFacts, analyze_ranges
22
+ from coretrace_python.abstract.values import AbstractValue, Truth
23
+
24
+ __all__ = [
25
+ "ATTRIBUTES",
26
+ "ELEMENTS",
27
+ "MUTATORS",
28
+ "AbstractObject",
29
+ "AbstractValue",
30
+ "AliasSet",
31
+ "AllocationSite",
32
+ "ConstantFacts",
33
+ "ConstantPropagation",
34
+ "HeapAnalysis",
35
+ "HeapFacts",
36
+ "HeapLocation",
37
+ "Interval",
38
+ "RangeAnalysis",
39
+ "RangeFacts",
40
+ "Truth",
41
+ "analyze_heap",
42
+ "analyze_ranges",
43
+ "mutated_by",
44
+ "propagate_constants",
45
+ ]
@@ -0,0 +1,226 @@
1
+ """Constant propagation: the reference client of the data-flow solver (§18, §38)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import operator
6
+ from collections.abc import Callable, Mapping
7
+ from types import MappingProxyType
8
+ from typing import Any, ClassVar
9
+
10
+ from coretrace_python.abstract.values import AbstractValue, Truth
11
+ from coretrace_python.analysis import AnalysisContext, AnyAnalysis, FunctionAnalysis
12
+ from coretrace_python.cfg import CFG, BlockId, CFGAnalysis
13
+ from coretrace_python.dataflow import BOTTOM, TOP, DataflowProblem, Direction, solve
14
+ from coretrace_python.hir import nodes
15
+ from coretrace_python.ir.model import (
16
+ BasicBlock,
17
+ BinaryOp,
18
+ Branch,
19
+ Compare,
20
+ Constant,
21
+ ForNext,
22
+ FunctionIR,
23
+ Instruction,
24
+ Jump,
25
+ Phi,
26
+ UnaryOp,
27
+ Value,
28
+ )
29
+ from coretrace_python.ir.ssa import SSAAnalysis
30
+
31
+ State = Mapping[Value, AbstractValue]
32
+
33
+ _SAFE_TYPES = (int, float, bool, str, bytes, type(None))
34
+ _NUMERIC = frozenset({"int", "float", "bool"})
35
+
36
+ _BINARY: Mapping[str, Callable[..., Any]] = {
37
+ "add": operator.add,
38
+ "sub": operator.sub,
39
+ "mul": operator.mul,
40
+ "div": operator.truediv,
41
+ "floor_div": operator.floordiv,
42
+ "mod": operator.mod,
43
+ "bit_or": operator.or_,
44
+ "bit_xor": operator.xor,
45
+ "bit_and": operator.and_,
46
+ }
47
+ _COMPARE: Mapping[str, Callable[..., Any]] = {
48
+ "eq": operator.eq,
49
+ "not_eq": operator.ne,
50
+ "lt": operator.lt,
51
+ "lt_eq": operator.le,
52
+ "gt": operator.gt,
53
+ "gt_eq": operator.ge,
54
+ "is": operator.is_,
55
+ "is_not": operator.is_not,
56
+ "in": lambda a, b: a in b,
57
+ "not_in": lambda a, b: a not in b,
58
+ }
59
+ _UNARY: Mapping[str, Callable[..., Any]] = {
60
+ "neg": operator.neg,
61
+ "pos": operator.pos,
62
+ "invert": operator.invert,
63
+ }
64
+
65
+
66
+ class ConstantFacts:
67
+ def __init__(self, values: Mapping[Value, AbstractValue], reachable: frozenset[BlockId]):
68
+ self._values = MappingProxyType(dict(values))
69
+ self._reachable = reachable
70
+
71
+ def value(self, value: Value) -> AbstractValue:
72
+ return self._values.get(value, AbstractValue.bottom())
73
+
74
+ def reachable(self, block: BlockId) -> bool:
75
+ return block in self._reachable
76
+
77
+
78
+ class _ConstantProblem(DataflowProblem[State]):
79
+ direction: ClassVar[Direction] = Direction.FORWARD
80
+
81
+ def __init__(self, function: FunctionIR) -> None:
82
+ self.function = function
83
+ self.blocks = {block.id: block for block in function.blocks}
84
+
85
+ def initial(self) -> State:
86
+ return MappingProxyType({p: AbstractValue.unknown() for p in self.function.parameters})
87
+
88
+ def join(self, a: State, b: State) -> State:
89
+ merged = dict(a)
90
+ for value, fact in b.items():
91
+ merged[value] = merged[value].join(fact) if value in merged else fact
92
+ return MappingProxyType(merged)
93
+
94
+ def evaluate(self, block: BasicBlock, incoming: Mapping[BlockId, State]) -> State:
95
+ """State after ``block`` given the states on its executable incoming edges."""
96
+
97
+ states = list(incoming.values())
98
+ state = dict(states[0])
99
+ for other in states[1:]:
100
+ state = dict(self.join(state, other))
101
+ for instruction in block.instructions:
102
+ if instruction.result is None:
103
+ continue
104
+ state[instruction.result] = (
105
+ self.phi(instruction, incoming)
106
+ if isinstance(instruction, Phi)
107
+ else self.instruction(instruction, state)
108
+ )
109
+ if isinstance(block.terminator, ForNext) and block.terminator.result is not None:
110
+ state[block.terminator.result] = AbstractValue.unknown()
111
+ return MappingProxyType(state)
112
+
113
+ def flow(self, cfg: CFG, block_id: BlockId, incoming: Mapping[BlockId, State]) -> Mapping[BlockId, State]:
114
+ block = self.blocks[block_id]
115
+ state = self.evaluate(block, incoming)
116
+ exits = {target: state for target in block.exception_targets}
117
+ terminator = block.terminator
118
+ if isinstance(terminator, Branch):
119
+ truth = state[terminator.condition].truthiness
120
+ targets = {
121
+ Truth.TRUE: (terminator.then_block,),
122
+ Truth.FALSE: (terminator.else_block,),
123
+ Truth.UNKNOWN: (terminator.then_block, terminator.else_block),
124
+ }[truth]
125
+ return {**exits, **{target: state for target in targets}}
126
+ if isinstance(terminator, Jump):
127
+ return {**exits, terminator.target: state}
128
+ if isinstance(terminator, ForNext):
129
+ return {**exits, terminator.body: state, terminator.exit: state}
130
+ return exits
131
+
132
+ # ------------------------------------------------------------------ transfer
133
+
134
+ def phi(self, phi: Phi, incoming: Mapping[BlockId, State]) -> AbstractValue:
135
+ result = AbstractValue.bottom()
136
+ for value, predecessor in phi.incoming:
137
+ if predecessor in incoming:
138
+ result = result.join(incoming[predecessor].get(value, AbstractValue.unknown()))
139
+ return result
140
+
141
+ def instruction(self, instruction: Instruction, state: Mapping[Value, AbstractValue]) -> AbstractValue:
142
+ if isinstance(instruction, Constant):
143
+ return AbstractValue.of(instruction.value)
144
+ if isinstance(instruction, BinaryOp):
145
+ return self.binary(instruction, state[instruction.left], state[instruction.right])
146
+ if isinstance(instruction, Compare):
147
+ return self.fold(_COMPARE[instruction.operator], state[instruction.left], state[instruction.right])
148
+ if isinstance(instruction, UnaryOp):
149
+ return self.unary(instruction, state[instruction.operand])
150
+ return AbstractValue.unknown()
151
+
152
+ def binary(self, op: BinaryOp, left: AbstractValue, right: AbstractValue) -> AbstractValue:
153
+ folder = _BINARY.get(op.operator)
154
+ if folder is not None:
155
+ numeric_only = op.operator == "mul"
156
+ folded = self.fold(folder, left, right, numeric_only=numeric_only)
157
+ if folded.constant is not TOP:
158
+ return folded
159
+ return AbstractValue.unknown(self.result_types(op.operator, left, right))
160
+
161
+ @staticmethod
162
+ def result_types(operator_name: str, left: AbstractValue, right: AbstractValue) -> frozenset[str] | None:
163
+ if left.types is None or right.types is None:
164
+ return None
165
+ if left.types <= _NUMERIC and right.types <= _NUMERIC:
166
+ if operator_name == "div" or "float" in left.types | right.types:
167
+ return frozenset({"float"})
168
+ if operator_name in _BINARY:
169
+ return frozenset({"int"})
170
+ if operator_name == "add" and left.types == right.types == frozenset({"str"}):
171
+ return frozenset({"str"})
172
+ return None
173
+
174
+ def unary(self, op: UnaryOp, operand: AbstractValue) -> AbstractValue:
175
+ if op.operator == "not":
176
+ if operand.truthiness is Truth.UNKNOWN:
177
+ return AbstractValue.unknown(frozenset({"bool"}))
178
+ return AbstractValue.of(operand.truthiness is Truth.FALSE)
179
+ folder = _UNARY.get(op.operator)
180
+ if folder is None or operand.constant is TOP or operand.constant is BOTTOM:
181
+ return AbstractValue.unknown()
182
+ if not isinstance(operand.constant, (int, float, bool)):
183
+ return AbstractValue.unknown()
184
+ try:
185
+ return AbstractValue.of(folder(operand.constant))
186
+ except (TypeError, ValueError, ArithmeticError):
187
+ return AbstractValue.unknown()
188
+
189
+ @staticmethod
190
+ def fold(
191
+ folder: Callable[..., Any],
192
+ left: AbstractValue,
193
+ right: AbstractValue,
194
+ *,
195
+ numeric_only: bool = False,
196
+ ) -> AbstractValue:
197
+ for side in (left.constant, right.constant):
198
+ if side is TOP or side is BOTTOM or not isinstance(side, _SAFE_TYPES):
199
+ return AbstractValue.unknown()
200
+ if numeric_only and not all(isinstance(c, (int, float, bool)) for c in (left.constant, right.constant)):
201
+ return AbstractValue.unknown()
202
+ try:
203
+ return AbstractValue.of(folder(left.constant, right.constant))
204
+ except (TypeError, ValueError, ArithmeticError):
205
+ return AbstractValue.unknown()
206
+
207
+
208
+ def propagate_constants(function: FunctionIR, cfg: CFG) -> ConstantFacts:
209
+ problem = _ConstantProblem(function)
210
+ solution = solve(problem, cfg)
211
+ values: dict[Value, AbstractValue] = {}
212
+ reachable: set[BlockId] = set()
213
+ for block in function.blocks:
214
+ if solution.reached(block.id):
215
+ reachable.add(block.id)
216
+ values.update(problem.evaluate(block, solution.incoming(block.id)))
217
+ return ConstantFacts(values, frozenset(reachable))
218
+
219
+
220
+ class ConstantPropagation(FunctionAnalysis[ConstantFacts]):
221
+ name: ClassVar[str] = "abstract.constants"
222
+ requires: ClassVar[frozenset[AnyAnalysis]] = frozenset({SSAAnalysis, CFGAnalysis})
223
+
224
+ @classmethod
225
+ def compute(cls, ctx: AnalysisContext, function: nodes.Function) -> ConstantFacts:
226
+ return propagate_constants(ctx.get(SSAAnalysis, function), ctx.get(CFGAnalysis, function))
@@ -0,0 +1,252 @@
1
+ """Coarse heap and aliasing abstraction (architecture §22).
2
+
3
+ SSA names values, not objects. This domain gives every allocation site one
4
+ ``AbstractObject``: containers and calls at their instruction, parameters, module
5
+ globals and imported symbols by name, and the fields loaded from an object by field.
6
+ Each value points to an ``AliasSet`` of objects, computed by a flow-insensitive
7
+ points-to fixpoint over the SSA form; each object has two ``HeapLocation`` fields,
8
+ ``elements`` for its items and ``attributes`` for its attributes, field-insensitive
9
+ within each. Taint and dependence analyses key their states by these locations, so a
10
+ store, a mutating method call or a load on any alias reads and writes the same place.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from collections.abc import Mapping
16
+ from dataclasses import dataclass
17
+ from types import MappingProxyType
18
+ from typing import ClassVar
19
+
20
+ from coretrace_python.analysis import AnalysisContext, AnyAnalysis, FunctionAnalysis
21
+ from coretrace_python.hir import nodes
22
+ from coretrace_python.ir.model import (
23
+ Await,
24
+ BuildDict,
25
+ BuildList,
26
+ BuildSet,
27
+ BuildTuple,
28
+ Call,
29
+ Catch,
30
+ ForNext,
31
+ FunctionIR,
32
+ GetAttr,
33
+ GetItem,
34
+ GetIter,
35
+ Global,
36
+ Instruction,
37
+ Phi,
38
+ SetAttr,
39
+ SetItem,
40
+ Symbol,
41
+ Value,
42
+ WithEnter,
43
+ Yield,
44
+ )
45
+ from coretrace_python.ir.ssa import SSAAnalysis
46
+ from coretrace_python.source import SourceSpan
47
+
48
+ ELEMENTS = "elements"
49
+ ATTRIBUTES = "attributes"
50
+
51
+ # Method names that store their arguments into the receiver's elements.
52
+ MUTATORS = frozenset(
53
+ {"append", "appendleft", "extend", "extendleft", "insert", "add", "update", "setdefault", "put"}
54
+ )
55
+
56
+
57
+ @dataclass(frozen=True)
58
+ class AllocationSite:
59
+ kind: str
60
+ location: SourceSpan
61
+ name: str = ""
62
+ ordinal: int = 0
63
+
64
+ def __str__(self) -> str:
65
+ text = f"{self.kind}@{self.location.source_id}:{self.location.start_line}"
66
+ if self.name:
67
+ text += f":{self.name}"
68
+ if self.ordinal:
69
+ text += f"#{self.ordinal}"
70
+ return text
71
+
72
+
73
+ @dataclass(frozen=True)
74
+ class AbstractObject:
75
+ site: AllocationSite
76
+
77
+ def __str__(self) -> str:
78
+ return str(self.site)
79
+
80
+
81
+ @dataclass(frozen=True)
82
+ class HeapLocation:
83
+ object: AbstractObject
84
+ field: str
85
+
86
+ def __str__(self) -> str:
87
+ return f"{self.object}.{self.field}"
88
+
89
+
90
+ AliasSet = frozenset[AbstractObject]
91
+ NOTHING: AliasSet = frozenset()
92
+
93
+
94
+ class HeapFacts:
95
+ def __init__(self, objects: Mapping[Value, AliasSet]) -> None:
96
+ self._objects: Mapping[Value, AliasSet] = MappingProxyType(dict(objects))
97
+ self.values = tuple(objects)
98
+
99
+ def objects(self, value: Value) -> AliasSet:
100
+ return self._objects.get(value, NOTHING)
101
+
102
+ def locations(self, value: Value, field: str) -> tuple[HeapLocation, ...]:
103
+ return tuple(HeapLocation(o, field) for o in sorted(self.objects(value), key=str))
104
+
105
+
106
+ def mutated_by(call: Call, defs: Mapping[Value, Instruction]) -> Value | None:
107
+ """The receiver of a call to a mutating method (``xs.append(v)``), if any."""
108
+
109
+ callee = defs.get(call.callee)
110
+ if isinstance(callee, GetAttr) and callee.attribute in MUTATORS:
111
+ return callee.object
112
+ return None
113
+
114
+
115
+ class _PointsTo:
116
+ def __init__(self, function: FunctionIR) -> None:
117
+ self.function = function
118
+ self.defs: dict[Value, Instruction] = {
119
+ i.result: i for block in function.blocks for i in block.instructions if i.result
120
+ }
121
+ self.points: dict[Value, set[AbstractObject]] = {}
122
+ self.heap: dict[HeapLocation, set[AbstractObject]] = {}
123
+ self.named: dict[tuple[str, str], AbstractObject] = {}
124
+ for index, parameter in enumerate(function.parameters):
125
+ self.points[parameter] = {
126
+ AbstractObject(AllocationSite("parameter", function.location, ordinal=index))
127
+ }
128
+
129
+ def named_object(self, kind: str, name: str) -> AbstractObject:
130
+ key = (kind, name)
131
+ if key not in self.named:
132
+ self.named[key] = AbstractObject(AllocationSite(kind, self.function.location, name))
133
+ return self.named[key]
134
+
135
+ @staticmethod
136
+ def field_object(owner: AbstractObject, field: str) -> AbstractObject:
137
+ # A field of a field collapses onto itself, so chains such as ``node.next``
138
+ # in a loop stay finite.
139
+ if owner.site.kind == "field":
140
+ return owner
141
+ return AbstractObject(AllocationSite("field", owner.site.location, f"{owner.site}.{field}"))
142
+
143
+ def of(self, value: Value) -> set[AbstractObject]:
144
+ return self.points.setdefault(value, set())
145
+
146
+ def at(self, objects: set[AbstractObject], field: str) -> set[AbstractObject]:
147
+ found: set[AbstractObject] = set()
148
+ for owner in objects:
149
+ found |= self.heap.setdefault(HeapLocation(owner, field), set())
150
+ found.add(self.field_object(owner, field))
151
+ return found
152
+
153
+ def store(self, objects: set[AbstractObject], field: str, value: Value) -> bool:
154
+ changed = False
155
+ for owner in objects:
156
+ location = self.heap.setdefault(HeapLocation(owner, field), set())
157
+ before = len(location)
158
+ location |= self.of(value)
159
+ changed |= len(location) != before
160
+ return changed
161
+
162
+ def solve(self) -> None:
163
+ changed = True
164
+ while changed:
165
+ changed = False
166
+ for block in self.function.blocks:
167
+ for instruction in block.instructions:
168
+ changed |= self.instruction(instruction)
169
+ terminator = block.terminator
170
+ if isinstance(terminator, ForNext) and terminator.result is not None:
171
+ changed |= self.assign(terminator.result, self.at(self.of(terminator.iterator), ELEMENTS))
172
+
173
+ def assign(self, value: Value, objects: set[AbstractObject]) -> bool:
174
+ current = self.of(value)
175
+ before = len(current)
176
+ current |= objects
177
+ return len(current) != before
178
+
179
+ def instruction(self, instruction: Instruction) -> bool:
180
+ if isinstance(instruction, SetAttr):
181
+ return self.store(self.of(instruction.object), ATTRIBUTES, instruction.value)
182
+ if isinstance(instruction, SetItem):
183
+ return self.store(self.of(instruction.object), ELEMENTS, instruction.value)
184
+ result = instruction.result
185
+ if result is None:
186
+ return False
187
+ if isinstance(instruction, BuildList | BuildTuple | BuildDict | BuildSet | Call | WithEnter | Catch | Yield):
188
+ kind = {
189
+ BuildList: "list",
190
+ BuildTuple: "tuple",
191
+ BuildDict: "dict",
192
+ BuildSet: "set",
193
+ Call: "call",
194
+ WithEnter: "context",
195
+ Catch: "exception",
196
+ Yield: "sent",
197
+ }[type(instruction)]
198
+ site = AbstractObject(AllocationSite(kind, instruction.location))
199
+ changed = self.assign(result, {site})
200
+ if isinstance(instruction, BuildList | BuildTuple | BuildDict | BuildSet):
201
+ values = (
202
+ tuple(v for _, v in instruction.items)
203
+ if isinstance(instruction, BuildDict)
204
+ else instruction.elements
205
+ )
206
+ for element in values:
207
+ changed |= self.store({site}, ELEMENTS, element)
208
+ for unpacked in instruction.unpacked:
209
+ contents = self.at(self.of(unpacked), ELEMENTS)
210
+ location = self.heap.setdefault(HeapLocation(site, ELEMENTS), set())
211
+ before = len(location)
212
+ location |= contents
213
+ changed |= len(location) != before
214
+ elif isinstance(instruction, Call):
215
+ receiver = mutated_by(instruction, self.defs)
216
+ if receiver is not None:
217
+ for argument in instruction.argument_values():
218
+ changed |= self.store(self.of(receiver), ELEMENTS, argument)
219
+ return changed
220
+ if isinstance(instruction, Global):
221
+ return self.assign(result, {self.named_object("global", instruction.name)})
222
+ if isinstance(instruction, Symbol):
223
+ return self.assign(result, {self.named_object("symbol", instruction.symbol_id.canonical_name)})
224
+ if isinstance(instruction, Phi):
225
+ objects: set[AbstractObject] = set()
226
+ for value, _ in instruction.incoming:
227
+ objects |= self.of(value)
228
+ return self.assign(result, objects)
229
+ if isinstance(instruction, GetAttr):
230
+ return self.assign(result, self.at(self.of(instruction.object), ATTRIBUTES))
231
+ if isinstance(instruction, GetItem):
232
+ return self.assign(result, self.at(self.of(instruction.object), ELEMENTS))
233
+ if isinstance(instruction, GetIter):
234
+ return self.assign(result, self.of(instruction.iterable))
235
+ if isinstance(instruction, Await):
236
+ return self.assign(result, self.of(instruction.value))
237
+ return False
238
+
239
+
240
+ def analyze_heap(function: FunctionIR) -> HeapFacts:
241
+ solver = _PointsTo(function)
242
+ solver.solve()
243
+ return HeapFacts({value: frozenset(objects) for value, objects in solver.points.items() if objects})
244
+
245
+
246
+ class HeapAnalysis(FunctionAnalysis[HeapFacts]):
247
+ name: ClassVar[str] = "abstract.heap"
248
+ requires: ClassVar[frozenset[AnyAnalysis]] = frozenset({SSAAnalysis})
249
+
250
+ @classmethod
251
+ def compute(cls, ctx: AnalysisContext, function: nodes.Function) -> HeapFacts:
252
+ return analyze_heap(ctx.get(SSAAnalysis, function))