pycforge 0.15.2__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.
- pycforge/__init__.py +8 -0
- pycforge/__main__.py +2 -0
- pycforge/_version.py +1 -0
- pycforge/converter/__init__.py +0 -0
- pycforge/converter/analysis/__init__.py +5 -0
- pycforge/converter/analysis/functions.py +723 -0
- pycforge/converter/analysis/model.py +158 -0
- pycforge/converter/analysis/planning.py +464 -0
- pycforge/converter/analysis/stage.py +1976 -0
- pycforge/converter/analysis/symbols.py +237 -0
- pycforge/converter/analysis/validation.py +2166 -0
- pycforge/converter/c_output/__init__.py +2 -0
- pycforge/converter/c_output/conformance.py +297 -0
- pycforge/converter/c_output/renderer.py +220 -0
- pycforge/converter/conditional_regions/__init__.py +71 -0
- pycforge/converter/conditional_regions/analysis.py +481 -0
- pycforge/converter/conditional_regions/lowering.py +579 -0
- pycforge/converter/conditional_regions/model.py +243 -0
- pycforge/converter/conditional_regions/validation.py +690 -0
- pycforge/converter/containers/__init__.py +19 -0
- pycforge/converter/containers/analysis.py +542 -0
- pycforge/converter/containers/lowering.py +290 -0
- pycforge/converter/containers/model.py +146 -0
- pycforge/converter/contracts/__init__.py +0 -0
- pycforge/converter/contracts/configuration.py +178 -0
- pycforge/converter/contracts/identifiers.py +68 -0
- pycforge/converter/contracts/versions.py +71 -0
- pycforge/converter/core/__init__.py +0 -0
- pycforge/converter/core/artifact_io.py +87 -0
- pycforge/converter/core/cancellation.py +7 -0
- pycforge/converter/core/canonicalization.py +254 -0
- pycforge/converter/core/context.py +13 -0
- pycforge/converter/core/diagnostics.py +95 -0
- pycforge/converter/core/enums.py +23 -0
- pycforge/converter/core/fingerprint.py +22 -0
- pycforge/converter/core/pipeline.py +8 -0
- pycforge/converter/core/progress.py +25 -0
- pycforge/converter/core/request.py +50 -0
- pycforge/converter/core/resource_policy.py +31 -0
- pycforge/converter/core/result.py +23 -0
- pycforge/converter/core/serialization.py +36 -0
- pycforge/converter/core/stage.py +10 -0
- pycforge/converter/core/stage_artifact.py +80 -0
- pycforge/converter/core/stage_outcome.py +15 -0
- pycforge/converter/decision_trace/__init__.py +0 -0
- pycforge/converter/decision_trace/recorder.py +52 -0
- pycforge/converter/facade.py +459 -0
- pycforge/converter/frontend/__init__.py +4 -0
- pycforge/converter/frontend/normalizer.py +59 -0
- pycforge/converter/frontend/parser.py +20 -0
- pycforge/converter/frontend/source_document.py +93 -0
- pycforge/converter/frontend/stages.py +518 -0
- pycforge/converter/frontend/validation.py +76 -0
- pycforge/converter/io/__init__.py +0 -0
- pycforge/converter/io/atomic_writer.py +33 -0
- pycforge/converter/ir/__init__.py +1 -0
- pycforge/converter/ir/c_ir/__init__.py +4 -0
- pycforge/converter/ir/c_ir/builder.py +26 -0
- pycforge/converter/ir/c_ir/model.py +318 -0
- pycforge/converter/ir/c_ir/serialization.py +88 -0
- pycforge/converter/ir/c_ir/validation.py +834 -0
- pycforge/converter/ir/python_ir/__init__.py +17 -0
- pycforge/converter/ir/python_ir/nodes.py +86 -0
- pycforge/converter/keyword_calls/__init__.py +68 -0
- pycforge/converter/keyword_calls/analysis.py +458 -0
- pycforge/converter/keyword_calls/lowering.py +271 -0
- pycforge/converter/keyword_calls/model.py +304 -0
- pycforge/converter/keyword_calls/validation.py +802 -0
- pycforge/converter/keyword_only_calls/__init__.py +68 -0
- pycforge/converter/keyword_only_calls/analysis.py +640 -0
- pycforge/converter/keyword_only_calls/lowering.py +376 -0
- pycforge/converter/keyword_only_calls/model.py +328 -0
- pycforge/converter/keyword_only_calls/validation.py +1357 -0
- pycforge/converter/lowering.py +1000 -0
- pycforge/converter/modules/__init__.py +13 -0
- pycforge/converter/modules/analysis.py +1250 -0
- pycforge/converter/modules/lowering.py +200 -0
- pycforge/converter/modules/model.py +73 -0
- pycforge/converter/modules/stage.py +192 -0
- pycforge/converter/numeric_semantics/__init__.py +23 -0
- pycforge/converter/numeric_semantics/analysis.py +420 -0
- pycforge/converter/numeric_semantics/lowering.py +153 -0
- pycforge/converter/numeric_semantics/model.py +123 -0
- pycforge/converter/records/__init__.py +34 -0
- pycforge/converter/records/analysis.py +1394 -0
- pycforge/converter/records/lowering.py +393 -0
- pycforge/converter/records/model.py +319 -0
- pycforge/converter/rules/__init__.py +3 -0
- pycforge/converter/support_templates/__init__.py +49 -0
- pycforge/converter/support_templates/assembly.py +106 -0
- pycforge/converter/support_templates/factories.py +348 -0
- pycforge/converter/support_templates/model.py +159 -0
- pycforge/converter/support_templates/registry.py +388 -0
- pycforge/converter/telemetry/__init__.py +0 -0
- pycforge/converter/telemetry/sink.py +39 -0
- pycforge/ide/__init__.py +19 -0
- pycforge/ide/__main__.py +5 -0
- pycforge/ide/_worker_event_codec.py +276 -0
- pycforge/ide/_worker_protocol_json.py +197 -0
- pycforge/ide/_worker_protocol_types.py +90 -0
- pycforge/ide/_worker_request_codec.py +200 -0
- pycforge/ide/_worker_request_data.py +260 -0
- pycforge/ide/_worker_result_data.py +343 -0
- pycforge/ide/action_contract.py +469 -0
- pycforge/ide/action_validation.py +229 -0
- pycforge/ide/command_palette.py +223 -0
- pycforge/ide/controller.py +904 -0
- pycforge/ide/controller_conversion.py +546 -0
- pycforge/ide/controller_io.py +205 -0
- pycforge/ide/editor.py +951 -0
- pycforge/ide/editor_commands_qt.py +274 -0
- pycforge/ide/editor_lexical.py +43 -0
- pycforge/ide/editor_sidebars.py +236 -0
- pycforge/ide/editor_syntax.py +243 -0
- pycforge/ide/find_replace.py +535 -0
- pycforge/ide/icons.py +91 -0
- pycforge/ide/io_service.py +196 -0
- pycforge/ide/model.py +164 -0
- pycforge/ide/panels.py +903 -0
- pycforge/ide/positions.py +220 -0
- pycforge/ide/process_worker.py +130 -0
- pycforge/ide/qt.py +230 -0
- pycforge/ide/qt_actions.py +418 -0
- pycforge/ide/qt_close.py +169 -0
- pycforge/ide/qt_command_palette.py +297 -0
- pycforge/ide/qt_contract.py +220 -0
- pycforge/ide/qt_documents.py +590 -0
- pycforge/ide/qt_editor_buffers.py +113 -0
- pycforge/ide/qt_editor_surfaces.py +112 -0
- pycforge/ide/qt_menus.py +324 -0
- pycforge/ide/qt_projection.py +500 -0
- pycforge/ide/qt_shell.py +348 -0
- pycforge/ide/qt_shell_interactions.py +320 -0
- pycforge/ide/qt_state.py +393 -0
- pycforge/ide/qt_workspace_features.py +398 -0
- pycforge/ide/qt_workspace_navigation.py +208 -0
- pycforge/ide/qt_workspace_observers.py +173 -0
- pycforge/ide/qt_workspace_panels.py +545 -0
- pycforge/ide/qt_workspace_widgets.py +228 -0
- pycforge/ide/resources/icons/about.svg +1 -0
- pycforge/ide/resources/icons/add-document.svg +1 -0
- pycforge/ide/resources/icons/brand-mark.svg +1 -0
- pycforge/ide/resources/icons/bundle-search.svg +1 -0
- pycforge/ide/resources/icons/cancel.svg +1 -0
- pycforge/ide/resources/icons/check.svg +1 -0
- pycforge/ide/resources/icons/chevron-right.svg +1 -0
- pycforge/ide/resources/icons/close.svg +1 -0
- pycforge/ide/resources/icons/collapse-all.svg +1 -0
- pycforge/ide/resources/icons/command-palette.svg +1 -0
- pycforge/ide/resources/icons/convert.svg +1 -0
- pycforge/ide/resources/icons/copy.svg +1 -0
- pycforge/ide/resources/icons/cut.svg +1 -0
- pycforge/ide/resources/icons/decision-trace.svg +1 -0
- pycforge/ide/resources/icons/details.svg +1 -0
- pycforge/ide/resources/icons/diagnostics.svg +1 -0
- pycforge/ide/resources/icons/duplicate-line.svg +1 -0
- pycforge/ide/resources/icons/exit.svg +1 -0
- pycforge/ide/resources/icons/expand-all.svg +1 -0
- pycforge/ide/resources/icons/export.svg +1 -0
- pycforge/ide/resources/icons/find.svg +1 -0
- pycforge/ide/resources/icons/go-to-line.svg +1 -0
- pycforge/ide/resources/icons/go-to-output.svg +1 -0
- pycforge/ide/resources/icons/go-to-source.svg +1 -0
- pycforge/ide/resources/icons/history.svg +1 -0
- pycforge/ide/resources/icons/indent.svg +1 -0
- pycforge/ide/resources/icons/link-c.svg +1 -0
- pycforge/ide/resources/icons/mappings.svg +1 -0
- pycforge/ide/resources/icons/module.svg +1 -0
- pycforge/ide/resources/icons/move-down.svg +1 -0
- pycforge/ide/resources/icons/move-line-down.svg +1 -0
- pycforge/ide/resources/icons/move-line-up.svg +1 -0
- pycforge/ide/resources/icons/move-up.svg +1 -0
- pycforge/ide/resources/icons/next.svg +1 -0
- pycforge/ide/resources/icons/open.svg +1 -0
- pycforge/ide/resources/icons/outdent.svg +1 -0
- pycforge/ide/resources/icons/outline.svg +1 -0
- pycforge/ide/resources/icons/paste.svg +1 -0
- pycforge/ide/resources/icons/previous.svg +1 -0
- pycforge/ide/resources/icons/primary-module.svg +1 -0
- pycforge/ide/resources/icons/redo.svg +1 -0
- pycforge/ide/resources/icons/remove-document.svg +1 -0
- pycforge/ide/resources/icons/replace.svg +1 -0
- pycforge/ide/resources/icons/save-as.svg +1 -0
- pycforge/ide/resources/icons/save.svg +1 -0
- pycforge/ide/resources/icons/select-all.svg +1 -0
- pycforge/ide/resources/icons/settings.svg +1 -0
- pycforge/ide/resources/icons/show-c.svg +1 -0
- pycforge/ide/resources/icons/split-view.svg +1 -0
- pycforge/ide/resources/icons/summary.svg +1 -0
- pycforge/ide/resources/icons/telemetry.svg +1 -0
- pycforge/ide/resources/icons/toggle-comment.svg +1 -0
- pycforge/ide/resources/icons/toggle-fold.svg +1 -0
- pycforge/ide/resources/icons/undo.svg +1 -0
- pycforge/ide/resources/icons/whitespace.svg +1 -0
- pycforge/ide/revisions.py +400 -0
- pycforge/ide/search_service.py +280 -0
- pycforge/ide/session_history.py +150 -0
- pycforge/ide/source_editing.py +417 -0
- pycforge/ide/source_structure.py +558 -0
- pycforge/ide/source_structure_async.py +191 -0
- pycforge/ide/supervisor.py +503 -0
- pycforge/ide/theme.py +119 -0
- pycforge/ide/theme_stylesheet.py +598 -0
- pycforge/ide/theme_workspace_stylesheet.py +155 -0
- pycforge/ide/visual_tokens.py +142 -0
- pycforge/ide/worker_protocol.py +80 -0
- pycforge/ide/workspace_search.py +462 -0
- pycforge/ide/workspace_session.py +256 -0
- pycforge/laboratory/__init__.py +6 -0
- pycforge/laboratory/audits.py +2244 -0
- pycforge/laboratory/checkpoint_e.py +2685 -0
- pycforge/laboratory/cli.py +119 -0
- pycforge/laboratory/exit_codes.py +11 -0
- pycforge/laboratory/keyword_audit.py +661 -0
- pycforge/laboratory/keyword_only_audit.py +481 -0
- pycforge-0.15.2.dist-info/METADATA +209 -0
- pycforge-0.15.2.dist-info/RECORD +221 -0
- pycforge-0.15.2.dist-info/WHEEL +5 -0
- pycforge-0.15.2.dist-info/entry_points.txt +5 -0
- pycforge-0.15.2.dist-info/licenses/LICENSE +674 -0
- pycforge-0.15.2.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1394 @@
|
|
|
1
|
+
"""Deterministic analyzer for the first Phase 13 static-record slice.
|
|
2
|
+
|
|
3
|
+
The accepted profile is intentionally smaller than Python's object model:
|
|
4
|
+
|
|
5
|
+
* top-level, base-less, undecorated classes;
|
|
6
|
+
* one to sixty-four value-less ``int``/``float``/``bool`` fields;
|
|
7
|
+
* one exact field-copying ``__init__`` and no other methods;
|
|
8
|
+
* fresh automatic locals constructed by a direct function-body assignment;
|
|
9
|
+
* immutable, statically bound field reads and no aliasing or escape.
|
|
10
|
+
|
|
11
|
+
Anything outside that closed profile is rejected before lowering.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import hashlib
|
|
17
|
+
from collections import defaultdict
|
|
18
|
+
from collections.abc import Mapping, Sequence
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
from pycforge.converter.analysis.model import ValueCategory
|
|
23
|
+
from pycforge.converter.analysis.symbols import PythonIRIndex, SymbolScopeAnalyzer
|
|
24
|
+
from pycforge.converter.ir.python_ir import python_ir_reference_ids
|
|
25
|
+
|
|
26
|
+
from .model import (
|
|
27
|
+
MAX_RECORD_FIELDS,
|
|
28
|
+
RecordAnalysisCanceled,
|
|
29
|
+
RecordAnalysisError,
|
|
30
|
+
RecordDefinitionFact,
|
|
31
|
+
RecordFieldAccessFact,
|
|
32
|
+
RecordFieldFact,
|
|
33
|
+
RecordInitializerFact,
|
|
34
|
+
RecordInstanceBindingFact,
|
|
35
|
+
RecordInstanceFact,
|
|
36
|
+
RecordValueCategory,
|
|
37
|
+
StaticRecordAnalysis,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
_FIELD_CATEGORIES = {
|
|
42
|
+
"int": ValueCategory.INTEGER,
|
|
43
|
+
"float": ValueCategory.FLOAT,
|
|
44
|
+
"bool": ValueCategory.BOOLEAN,
|
|
45
|
+
}
|
|
46
|
+
_STORE_PARENT_SLOTS = {
|
|
47
|
+
("Assign", "targets"),
|
|
48
|
+
("AnnAssign", "target"),
|
|
49
|
+
("AugAssign", "target"),
|
|
50
|
+
("Delete", "targets"),
|
|
51
|
+
("NamedExpr", "target"),
|
|
52
|
+
("For", "target"),
|
|
53
|
+
("AsyncFor", "target"),
|
|
54
|
+
("comprehension", "target"),
|
|
55
|
+
("withitem", "optional_vars"),
|
|
56
|
+
}
|
|
57
|
+
_DYNAMIC_ATTRIBUTE_BUILTINS = {"getattr", "setattr", "delattr", "hasattr"}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _stable_id(prefix: str, *parts: str) -> str:
|
|
61
|
+
return prefix + hashlib.sha256("\x1f".join(parts).encode("utf-8")).hexdigest()[:20]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True, slots=True)
|
|
65
|
+
class _Identity:
|
|
66
|
+
module_id: str
|
|
67
|
+
document_id: str
|
|
68
|
+
logical_name: str
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass(frozen=True, slots=True)
|
|
72
|
+
class _RecordSpec:
|
|
73
|
+
definition: RecordDefinitionFact
|
|
74
|
+
fields: tuple[RecordFieldFact, ...]
|
|
75
|
+
initializer: RecordInitializerFact
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
class StaticRecordAnalyzer:
|
|
79
|
+
"""Prove and publish the bounded immutable static-record profile.
|
|
80
|
+
|
|
81
|
+
``module_records`` is keyed by ``ClassDef`` node ID. A record may contain
|
|
82
|
+
``source_name``, ``flattened_name``, ``module_id``, ``document_id``, and
|
|
83
|
+
``logical_name`` (or ``logical_source_name``). This allows the Phase 12
|
|
84
|
+
bundle resolver to qualify class names without losing source identity.
|
|
85
|
+
|
|
86
|
+
``function_records`` supplies equivalent owner-module metadata keyed by
|
|
87
|
+
top-level ``FunctionDef`` node ID. ``bindings`` and ``categories`` are the
|
|
88
|
+
already-published cumulative analysis values; both are optional so this
|
|
89
|
+
semantic gate remains independently testable on a single normalized file.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def __init__(
|
|
93
|
+
self,
|
|
94
|
+
module: dict[str, Any],
|
|
95
|
+
*,
|
|
96
|
+
module_records: Mapping[str, Mapping[str, Any]] | None = None,
|
|
97
|
+
function_records: Mapping[str, Mapping[str, Any]] | None = None,
|
|
98
|
+
bindings: Sequence[Mapping[str, Any]] = (),
|
|
99
|
+
categories: Mapping[str, ValueCategory | str] | None = None,
|
|
100
|
+
cancellation: Any | None = None,
|
|
101
|
+
default_module_id: str = "__main__",
|
|
102
|
+
default_logical_name: str = "<memory>",
|
|
103
|
+
) -> None:
|
|
104
|
+
self.module = module
|
|
105
|
+
self.cancellation = cancellation
|
|
106
|
+
self._check_canceled()
|
|
107
|
+
self.index = PythonIRIndex(module)
|
|
108
|
+
self.module_records = {
|
|
109
|
+
key: dict(value) for key, value in (module_records or {}).items()
|
|
110
|
+
}
|
|
111
|
+
self.function_records = {
|
|
112
|
+
key: dict(value) for key, value in (function_records or {}).items()
|
|
113
|
+
}
|
|
114
|
+
supplied_bindings = tuple(dict(item) for item in bindings)
|
|
115
|
+
class_node_ids = {
|
|
116
|
+
item["node_id"] for item in module.get("nodes", ()) if item.get("kind") == "ClassDef"
|
|
117
|
+
}
|
|
118
|
+
supplied_record_declarations = {
|
|
119
|
+
item.get("declaration_node_id")
|
|
120
|
+
for item in supplied_bindings
|
|
121
|
+
if item.get("binding_kind") == "record-class"
|
|
122
|
+
}
|
|
123
|
+
if supplied_bindings and class_node_ids.issubset(supplied_record_declarations):
|
|
124
|
+
self.bindings = supplied_bindings
|
|
125
|
+
else:
|
|
126
|
+
# Keep the analyzer independently usable while retaining the same
|
|
127
|
+
# lexical-identity proof as the cumulative analysis stage. Older
|
|
128
|
+
# isolated callers may supply ordinary bindings produced without
|
|
129
|
+
# the Phase 13 record switch; those are intentionally recomputed.
|
|
130
|
+
_, inferred_bindings, _ = SymbolScopeAnalyzer().analyze(
|
|
131
|
+
module,
|
|
132
|
+
allow_records=True,
|
|
133
|
+
)
|
|
134
|
+
self.bindings = tuple(item.to_dict() for item in inferred_bindings)
|
|
135
|
+
self.categories = {
|
|
136
|
+
key: self._coerce_category(value)
|
|
137
|
+
for key, value in (categories or {}).items()
|
|
138
|
+
}
|
|
139
|
+
self.default_module_id = default_module_id
|
|
140
|
+
self.default_logical_name = default_logical_name
|
|
141
|
+
self.identity_by_document: dict[str, _Identity] = {}
|
|
142
|
+
for metadata in (
|
|
143
|
+
*(
|
|
144
|
+
self.module_records[key]
|
|
145
|
+
for key in sorted(self.module_records)
|
|
146
|
+
),
|
|
147
|
+
*(
|
|
148
|
+
self.function_records[key]
|
|
149
|
+
for key in sorted(self.function_records)
|
|
150
|
+
),
|
|
151
|
+
):
|
|
152
|
+
self._check_canceled()
|
|
153
|
+
document_id = str(
|
|
154
|
+
metadata.get("document_id")
|
|
155
|
+
or metadata.get("source_document_id")
|
|
156
|
+
or ""
|
|
157
|
+
)
|
|
158
|
+
if not document_id:
|
|
159
|
+
continue
|
|
160
|
+
self.identity_by_document.setdefault(
|
|
161
|
+
document_id,
|
|
162
|
+
_Identity(
|
|
163
|
+
str(metadata.get("module_id") or default_module_id),
|
|
164
|
+
document_id,
|
|
165
|
+
str(
|
|
166
|
+
metadata.get("logical_name")
|
|
167
|
+
or metadata.get("logical_source_name")
|
|
168
|
+
or default_logical_name
|
|
169
|
+
),
|
|
170
|
+
),
|
|
171
|
+
)
|
|
172
|
+
self.binding_by_occurrence = {
|
|
173
|
+
occurrence: binding
|
|
174
|
+
for binding in self.bindings
|
|
175
|
+
for occurrence in binding.get("occurrence_node_ids", ())
|
|
176
|
+
if isinstance(occurrence, str)
|
|
177
|
+
}
|
|
178
|
+
self.binding_by_declaration = {
|
|
179
|
+
declaration: binding
|
|
180
|
+
for binding in self.bindings
|
|
181
|
+
for declaration in (binding.get("declaration_node_id"),)
|
|
182
|
+
if isinstance(declaration, str)
|
|
183
|
+
}
|
|
184
|
+
self.parents: dict[str, list[tuple[str, str]]] = defaultdict(list)
|
|
185
|
+
known_ids = self.index.nodes
|
|
186
|
+
for parent in self.index.nodes.values():
|
|
187
|
+
self._check_canceled()
|
|
188
|
+
for field_name, value in parent.get("fields", {}).items():
|
|
189
|
+
self._check_canceled()
|
|
190
|
+
for child_id in python_ir_reference_ids(
|
|
191
|
+
parent["kind"], field_name, value, known_ids
|
|
192
|
+
):
|
|
193
|
+
self.parents[child_id].append((parent["node_id"], field_name))
|
|
194
|
+
|
|
195
|
+
def analyze(self) -> StaticRecordAnalysis:
|
|
196
|
+
self._check_canceled()
|
|
197
|
+
root = self.index.node(self.module["root_node_id"])
|
|
198
|
+
if root["kind"] != "Module":
|
|
199
|
+
self._reject("PYC3601", "Static-record analysis requires a Module root", root)
|
|
200
|
+
root_body = tuple(root["fields"].get("body", ()))
|
|
201
|
+
root_members = set(root_body)
|
|
202
|
+
class_nodes = tuple(
|
|
203
|
+
sorted(
|
|
204
|
+
(
|
|
205
|
+
node
|
|
206
|
+
for node in self._nodes()
|
|
207
|
+
if node["kind"] == "ClassDef"
|
|
208
|
+
),
|
|
209
|
+
key=lambda item: self._ordinal(item["node_id"]),
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
for node in class_nodes:
|
|
213
|
+
self._check_canceled()
|
|
214
|
+
if node["node_id"] not in root_members:
|
|
215
|
+
self._reject(
|
|
216
|
+
"PYC3601",
|
|
217
|
+
"Static-record classes must be declared directly at module scope",
|
|
218
|
+
node,
|
|
219
|
+
)
|
|
220
|
+
|
|
221
|
+
specs_list: list[_RecordSpec] = []
|
|
222
|
+
for node in class_nodes:
|
|
223
|
+
self._check_canceled()
|
|
224
|
+
specs_list.append(self._definition(node))
|
|
225
|
+
specs = tuple(specs_list)
|
|
226
|
+
by_resolution_name: dict[str, _RecordSpec] = {}
|
|
227
|
+
for spec in specs:
|
|
228
|
+
self._check_canceled()
|
|
229
|
+
name = spec.definition.flattened_name
|
|
230
|
+
if name in by_resolution_name:
|
|
231
|
+
self._reject(
|
|
232
|
+
"PYC3601",
|
|
233
|
+
f"Static-record constructor name {name!r} is not unique",
|
|
234
|
+
self.index.node(spec.definition.class_node_id),
|
|
235
|
+
)
|
|
236
|
+
by_resolution_name[name] = spec
|
|
237
|
+
|
|
238
|
+
self._reject_cross_module_imports(specs)
|
|
239
|
+
self._reject_forward_object_annotations(specs)
|
|
240
|
+
by_class_binding = {
|
|
241
|
+
item.definition.class_binding_id: item for item in specs
|
|
242
|
+
}
|
|
243
|
+
by_record_id = {
|
|
244
|
+
item.definition.record_id: item for item in specs
|
|
245
|
+
}
|
|
246
|
+
if len(by_class_binding) != len(specs):
|
|
247
|
+
self._reject(
|
|
248
|
+
"PYC3601",
|
|
249
|
+
"Static-record class bindings are not unique",
|
|
250
|
+
self.index.node(specs[0].definition.class_node_id),
|
|
251
|
+
)
|
|
252
|
+
instances = self._instances(
|
|
253
|
+
by_resolution_name,
|
|
254
|
+
by_class_binding,
|
|
255
|
+
root_members,
|
|
256
|
+
)
|
|
257
|
+
bindings: list[RecordInstanceBindingFact] = []
|
|
258
|
+
accesses: list[RecordFieldAccessFact] = []
|
|
259
|
+
for instance in instances:
|
|
260
|
+
self._check_canceled()
|
|
261
|
+
binding, instance_accesses = self._validate_instance_uses(
|
|
262
|
+
instance, by_record_id
|
|
263
|
+
)
|
|
264
|
+
bindings.append(binding)
|
|
265
|
+
accesses.extend(instance_accesses)
|
|
266
|
+
|
|
267
|
+
return StaticRecordAnalysis(
|
|
268
|
+
definitions=tuple(item.definition for item in specs),
|
|
269
|
+
fields=tuple(field for item in specs for field in item.fields),
|
|
270
|
+
initializers=tuple(item.initializer for item in specs),
|
|
271
|
+
instances=tuple(instances),
|
|
272
|
+
bindings=tuple(bindings),
|
|
273
|
+
accesses=tuple(
|
|
274
|
+
sorted(accesses, key=lambda item: self._ordinal(item.access_node_id))
|
|
275
|
+
),
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
def _definition(self, node: dict[str, Any]) -> _RecordSpec:
|
|
279
|
+
fields = node["fields"]
|
|
280
|
+
if (
|
|
281
|
+
fields.get("bases")
|
|
282
|
+
or fields.get("keywords")
|
|
283
|
+
or fields.get("decorator_list")
|
|
284
|
+
):
|
|
285
|
+
self._reject(
|
|
286
|
+
"PYC3601",
|
|
287
|
+
"Static-record classes cannot have bases, keywords, metaclasses, or decorators",
|
|
288
|
+
node,
|
|
289
|
+
)
|
|
290
|
+
|
|
291
|
+
metadata = self.module_records.get(node["node_id"], {})
|
|
292
|
+
source_name = str(metadata.get("source_name", fields.get("name", "")))
|
|
293
|
+
flattened_name = str(metadata.get("flattened_name", fields.get("name", "")))
|
|
294
|
+
if not source_name or not flattened_name:
|
|
295
|
+
self._reject("PYC3601", "Static-record class name is missing", node)
|
|
296
|
+
if source_name in {"int", "float", "bool", "str"}:
|
|
297
|
+
self._reject(
|
|
298
|
+
"PYC3601",
|
|
299
|
+
"Static-record class names cannot shadow the closed annotation builtins",
|
|
300
|
+
node,
|
|
301
|
+
)
|
|
302
|
+
identity = self._identity(node, metadata)
|
|
303
|
+
record_id = _stable_id(
|
|
304
|
+
"record-", identity.module_id, node["node_id"], source_name
|
|
305
|
+
)
|
|
306
|
+
lexical_binding = self.binding_by_declaration.get(node["node_id"])
|
|
307
|
+
class_binding_id = str(
|
|
308
|
+
metadata.get("binding_id")
|
|
309
|
+
or (lexical_binding or {}).get("binding_id")
|
|
310
|
+
or _stable_id("bind-record-class-", record_id, flattened_name)
|
|
311
|
+
)
|
|
312
|
+
if lexical_binding is not None and (
|
|
313
|
+
lexical_binding.get("binding_kind") != "record-class"
|
|
314
|
+
or lexical_binding.get("source_name") != flattened_name
|
|
315
|
+
):
|
|
316
|
+
self._reject(
|
|
317
|
+
"PYC3601",
|
|
318
|
+
"Static-record definition lacks its exact lexical class binding",
|
|
319
|
+
node,
|
|
320
|
+
identity,
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
body_ids = tuple(fields.get("body", ()))
|
|
324
|
+
body_nodes = tuple(self.index.node(item) for item in body_ids)
|
|
325
|
+
methods = tuple(item for item in body_nodes if item["kind"] == "FunctionDef")
|
|
326
|
+
for method in methods:
|
|
327
|
+
self._check_canceled()
|
|
328
|
+
if method["fields"].get("name") != "__init__":
|
|
329
|
+
self._reject(
|
|
330
|
+
"PYC3604",
|
|
331
|
+
"The first static-record slice supports __init__ only; other methods are deferred",
|
|
332
|
+
method,
|
|
333
|
+
identity,
|
|
334
|
+
)
|
|
335
|
+
if len(methods) != 1:
|
|
336
|
+
self._reject(
|
|
337
|
+
"PYC3603",
|
|
338
|
+
"A static-record class must declare exactly one __init__",
|
|
339
|
+
methods[1] if len(methods) > 1 else node,
|
|
340
|
+
identity,
|
|
341
|
+
)
|
|
342
|
+
initializer_node = methods[0]
|
|
343
|
+
field_nodes = tuple(item for item in body_nodes if item["kind"] == "AnnAssign")
|
|
344
|
+
unsupported = tuple(
|
|
345
|
+
item for item in body_nodes if item["kind"] not in {"AnnAssign", "FunctionDef"}
|
|
346
|
+
)
|
|
347
|
+
if unsupported:
|
|
348
|
+
self._reject(
|
|
349
|
+
"PYC3604",
|
|
350
|
+
"Static-record class bodies contain only value-less fields followed by __init__",
|
|
351
|
+
unsupported[0],
|
|
352
|
+
identity,
|
|
353
|
+
)
|
|
354
|
+
if body_nodes != field_nodes + (initializer_node,):
|
|
355
|
+
self._reject(
|
|
356
|
+
"PYC3604",
|
|
357
|
+
"Static-record fields must precede the sole __init__ declaration",
|
|
358
|
+
node,
|
|
359
|
+
identity,
|
|
360
|
+
)
|
|
361
|
+
if not field_nodes:
|
|
362
|
+
self._reject(
|
|
363
|
+
"PYC3602",
|
|
364
|
+
"A static-record class must declare at least one field",
|
|
365
|
+
node,
|
|
366
|
+
identity,
|
|
367
|
+
)
|
|
368
|
+
if len(field_nodes) > MAX_RECORD_FIELDS:
|
|
369
|
+
self._reject(
|
|
370
|
+
"PYC3602",
|
|
371
|
+
f"Static-record field count exceeds the fixed limit of {MAX_RECORD_FIELDS}",
|
|
372
|
+
field_nodes[MAX_RECORD_FIELDS],
|
|
373
|
+
identity,
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
field_facts: list[RecordFieldFact] = []
|
|
377
|
+
seen_names: set[str] = set()
|
|
378
|
+
for ordinal, field_node in enumerate(field_nodes):
|
|
379
|
+
self._check_canceled()
|
|
380
|
+
fact = self._field(
|
|
381
|
+
field_node,
|
|
382
|
+
record_id=record_id,
|
|
383
|
+
class_node_id=node["node_id"],
|
|
384
|
+
ordinal=ordinal,
|
|
385
|
+
identity=identity,
|
|
386
|
+
)
|
|
387
|
+
if fact.source_name in seen_names:
|
|
388
|
+
self._reject(
|
|
389
|
+
"PYC3602",
|
|
390
|
+
f"Duplicate static-record field {fact.source_name!r}",
|
|
391
|
+
field_node,
|
|
392
|
+
identity,
|
|
393
|
+
)
|
|
394
|
+
seen_names.add(fact.source_name)
|
|
395
|
+
field_facts.append(fact)
|
|
396
|
+
|
|
397
|
+
initializer = self._initializer(
|
|
398
|
+
initializer_node,
|
|
399
|
+
record_id=record_id,
|
|
400
|
+
fields=tuple(field_facts),
|
|
401
|
+
identity=identity,
|
|
402
|
+
)
|
|
403
|
+
definition = RecordDefinitionFact(
|
|
404
|
+
record_id=record_id,
|
|
405
|
+
class_node_id=node["node_id"],
|
|
406
|
+
class_binding_id=class_binding_id,
|
|
407
|
+
source_name=source_name,
|
|
408
|
+
flattened_name=flattened_name,
|
|
409
|
+
module_id=identity.module_id,
|
|
410
|
+
document_id=identity.document_id,
|
|
411
|
+
logical_name=identity.logical_name,
|
|
412
|
+
field_ids=tuple(item.field_id for item in field_facts),
|
|
413
|
+
initializer_id=initializer.initializer_id,
|
|
414
|
+
category=RecordValueCategory.DEFINITION,
|
|
415
|
+
storage_model="automatic-inline-record",
|
|
416
|
+
ownership_model="unique-lexical-owner",
|
|
417
|
+
lifetime_model="enclosing-function-activation",
|
|
418
|
+
aliasing_model="forbidden",
|
|
419
|
+
cleanup_model="none",
|
|
420
|
+
nullability_model="non-null-by-construction",
|
|
421
|
+
mutable=False,
|
|
422
|
+
)
|
|
423
|
+
return _RecordSpec(definition, tuple(field_facts), initializer)
|
|
424
|
+
|
|
425
|
+
def _field(
|
|
426
|
+
self,
|
|
427
|
+
node: dict[str, Any],
|
|
428
|
+
*,
|
|
429
|
+
record_id: str,
|
|
430
|
+
class_node_id: str,
|
|
431
|
+
ordinal: int,
|
|
432
|
+
identity: _Identity,
|
|
433
|
+
) -> RecordFieldFact:
|
|
434
|
+
fields = node["fields"]
|
|
435
|
+
target = self.index.nodes.get(fields.get("target"))
|
|
436
|
+
annotation = self.index.nodes.get(fields.get("annotation"))
|
|
437
|
+
if (
|
|
438
|
+
target is None
|
|
439
|
+
or target["kind"] != "Name"
|
|
440
|
+
or fields.get("simple") != 1
|
|
441
|
+
or fields.get("value") is not None
|
|
442
|
+
):
|
|
443
|
+
self._reject(
|
|
444
|
+
"PYC3602",
|
|
445
|
+
"Static-record fields must be simple value-less annotated names",
|
|
446
|
+
node,
|
|
447
|
+
identity,
|
|
448
|
+
)
|
|
449
|
+
annotation_name = (
|
|
450
|
+
annotation["fields"].get("id")
|
|
451
|
+
if annotation is not None and annotation["kind"] == "Name"
|
|
452
|
+
else None
|
|
453
|
+
)
|
|
454
|
+
if annotation_name not in _FIELD_CATEGORIES:
|
|
455
|
+
self._reject(
|
|
456
|
+
"PYC3602",
|
|
457
|
+
"Static-record fields require an exact int, float, or bool annotation",
|
|
458
|
+
annotation or node,
|
|
459
|
+
identity,
|
|
460
|
+
)
|
|
461
|
+
source_name = str(target["fields"].get("id", ""))
|
|
462
|
+
if (
|
|
463
|
+
not source_name
|
|
464
|
+
or source_name == "self"
|
|
465
|
+
or (source_name.startswith("__") and source_name.endswith("__"))
|
|
466
|
+
):
|
|
467
|
+
self._reject(
|
|
468
|
+
"PYC3602",
|
|
469
|
+
"Static-record field name is unsupported; Python dunder descriptors are excluded",
|
|
470
|
+
target,
|
|
471
|
+
identity,
|
|
472
|
+
)
|
|
473
|
+
field_id = _stable_id("record-field-", record_id, source_name, str(ordinal))
|
|
474
|
+
return RecordFieldFact(
|
|
475
|
+
field_id=field_id,
|
|
476
|
+
record_id=record_id,
|
|
477
|
+
class_node_id=class_node_id,
|
|
478
|
+
declaration_node_id=node["node_id"],
|
|
479
|
+
target_node_id=target["node_id"],
|
|
480
|
+
annotation_node_id=annotation["node_id"],
|
|
481
|
+
source_name=source_name,
|
|
482
|
+
ordinal=ordinal,
|
|
483
|
+
category=_FIELD_CATEGORIES[annotation_name],
|
|
484
|
+
module_id=identity.module_id,
|
|
485
|
+
document_id=identity.document_id,
|
|
486
|
+
logical_name=identity.logical_name,
|
|
487
|
+
mutable=False,
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
def _initializer(
|
|
491
|
+
self,
|
|
492
|
+
node: dict[str, Any],
|
|
493
|
+
*,
|
|
494
|
+
record_id: str,
|
|
495
|
+
fields: tuple[RecordFieldFact, ...],
|
|
496
|
+
identity: _Identity,
|
|
497
|
+
) -> RecordInitializerFact:
|
|
498
|
+
values = node["fields"]
|
|
499
|
+
if values.get("decorator_list") or values.get("type_comment"):
|
|
500
|
+
self._reject(
|
|
501
|
+
"PYC3603", "Static-record __init__ cannot be decorated or type-commented", node, identity
|
|
502
|
+
)
|
|
503
|
+
if not self._is_none_annotation(values.get("returns")):
|
|
504
|
+
target = self.index.nodes.get(values.get("returns"), node)
|
|
505
|
+
self._reject(
|
|
506
|
+
"PYC3603", "Static-record __init__ must declare -> None", target, identity
|
|
507
|
+
)
|
|
508
|
+
arguments = self.index.nodes.get(values.get("args"))
|
|
509
|
+
if arguments is None or arguments["kind"] != "arguments":
|
|
510
|
+
self._reject("PYC3603", "Static-record __init__ arguments are malformed", node, identity)
|
|
511
|
+
arg_fields = arguments["fields"]
|
|
512
|
+
positional = tuple(arg_fields.get("args", ()))
|
|
513
|
+
if (
|
|
514
|
+
arg_fields.get("posonlyargs")
|
|
515
|
+
or arg_fields.get("vararg") is not None
|
|
516
|
+
or arg_fields.get("kwonlyargs")
|
|
517
|
+
or arg_fields.get("kw_defaults")
|
|
518
|
+
or arg_fields.get("kwarg") is not None
|
|
519
|
+
or arg_fields.get("defaults")
|
|
520
|
+
or len(positional) != len(fields) + 1
|
|
521
|
+
):
|
|
522
|
+
self._reject(
|
|
523
|
+
"PYC3603",
|
|
524
|
+
"Static-record __init__ is exact positional self plus one parameter per field",
|
|
525
|
+
arguments,
|
|
526
|
+
identity,
|
|
527
|
+
)
|
|
528
|
+
parameters = tuple(self.index.node(item) for item in positional)
|
|
529
|
+
self_parameter = parameters[0]
|
|
530
|
+
if (
|
|
531
|
+
self_parameter["kind"] != "arg"
|
|
532
|
+
or self_parameter["fields"].get("arg") != "self"
|
|
533
|
+
or self_parameter["fields"].get("annotation") is not None
|
|
534
|
+
or self_parameter["fields"].get("type_comment") is not None
|
|
535
|
+
):
|
|
536
|
+
self._reject(
|
|
537
|
+
"PYC3603", "Static-record __init__ begins with unannotated self", self_parameter, identity
|
|
538
|
+
)
|
|
539
|
+
value_parameters = parameters[1:]
|
|
540
|
+
for parameter, field in zip(value_parameters, fields):
|
|
541
|
+
self._check_canceled()
|
|
542
|
+
annotation = self.index.nodes.get(parameter["fields"].get("annotation"))
|
|
543
|
+
expected = next(
|
|
544
|
+
name for name, category in _FIELD_CATEGORIES.items() if category is field.category
|
|
545
|
+
)
|
|
546
|
+
if (
|
|
547
|
+
parameter["kind"] != "arg"
|
|
548
|
+
or parameter["fields"].get("arg") != field.source_name
|
|
549
|
+
or annotation is None
|
|
550
|
+
or annotation["kind"] != "Name"
|
|
551
|
+
or annotation["fields"].get("id") != expected
|
|
552
|
+
or parameter["fields"].get("type_comment") is not None
|
|
553
|
+
):
|
|
554
|
+
self._reject(
|
|
555
|
+
"PYC3603",
|
|
556
|
+
"Static-record __init__ parameters must match field names, order, and exact types",
|
|
557
|
+
parameter,
|
|
558
|
+
identity,
|
|
559
|
+
)
|
|
560
|
+
|
|
561
|
+
assignments = tuple(values.get("body", ()))
|
|
562
|
+
if len(assignments) != len(fields):
|
|
563
|
+
self._reject(
|
|
564
|
+
"PYC3603",
|
|
565
|
+
"Static-record __init__ assigns every field exactly once and performs no other work",
|
|
566
|
+
node,
|
|
567
|
+
identity,
|
|
568
|
+
)
|
|
569
|
+
for assignment_id, field, parameter in zip(assignments, fields, value_parameters):
|
|
570
|
+
self._check_canceled()
|
|
571
|
+
assignment = self.index.node(assignment_id)
|
|
572
|
+
assign_fields = assignment["fields"]
|
|
573
|
+
targets = tuple(assign_fields.get("targets", ()))
|
|
574
|
+
target = self.index.nodes.get(targets[0]) if len(targets) == 1 else None
|
|
575
|
+
receiver = (
|
|
576
|
+
self.index.nodes.get(target["fields"].get("value"))
|
|
577
|
+
if target is not None and target["kind"] == "Attribute"
|
|
578
|
+
else None
|
|
579
|
+
)
|
|
580
|
+
source = self.index.nodes.get(assign_fields.get("value"))
|
|
581
|
+
if (
|
|
582
|
+
assignment["kind"] != "Assign"
|
|
583
|
+
or assign_fields.get("type_comment") is not None
|
|
584
|
+
or target is None
|
|
585
|
+
or target["kind"] != "Attribute"
|
|
586
|
+
or target["fields"].get("attr") != field.source_name
|
|
587
|
+
or receiver is None
|
|
588
|
+
or receiver["kind"] != "Name"
|
|
589
|
+
or receiver["fields"].get("id") != "self"
|
|
590
|
+
or source is None
|
|
591
|
+
or source["kind"] != "Name"
|
|
592
|
+
or source["fields"].get("id") != parameter["fields"].get("arg")
|
|
593
|
+
):
|
|
594
|
+
self._reject(
|
|
595
|
+
"PYC3603",
|
|
596
|
+
"Static-record __init__ body is the ordered direct copy self.field = field",
|
|
597
|
+
assignment,
|
|
598
|
+
identity,
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
initializer_id = _stable_id("record-init-", record_id, node["node_id"])
|
|
602
|
+
return RecordInitializerFact(
|
|
603
|
+
initializer_id=initializer_id,
|
|
604
|
+
record_id=record_id,
|
|
605
|
+
function_node_id=node["node_id"],
|
|
606
|
+
arguments_node_id=arguments["node_id"],
|
|
607
|
+
self_parameter_node_id=self_parameter["node_id"],
|
|
608
|
+
parameter_node_ids=tuple(item["node_id"] for item in value_parameters),
|
|
609
|
+
assignment_node_ids=assignments,
|
|
610
|
+
field_ids=tuple(item.field_id for item in fields),
|
|
611
|
+
module_id=identity.module_id,
|
|
612
|
+
document_id=identity.document_id,
|
|
613
|
+
logical_name=identity.logical_name,
|
|
614
|
+
receiver_model="direct-addressed-initialization-receiver",
|
|
615
|
+
evaluation_order="field-declaration-order-left-to-right-once",
|
|
616
|
+
initialization_completeness="all-fields-exactly-once",
|
|
617
|
+
)
|
|
618
|
+
|
|
619
|
+
def _instances(
|
|
620
|
+
self,
|
|
621
|
+
by_resolution_name: Mapping[str, _RecordSpec],
|
|
622
|
+
by_class_binding: Mapping[str, _RecordSpec],
|
|
623
|
+
root_members: set[str],
|
|
624
|
+
) -> tuple[RecordInstanceFact, ...]:
|
|
625
|
+
recognized_constructor_names: set[str] = set()
|
|
626
|
+
instances: list[RecordInstanceFact] = []
|
|
627
|
+
used_bindings: dict[tuple[str, str], str] = {}
|
|
628
|
+
known_instance_fields: dict[tuple[str, str], dict[str, ValueCategory]] = {}
|
|
629
|
+
record_constructor_spellings = {
|
|
630
|
+
name
|
|
631
|
+
for item in by_resolution_name.values()
|
|
632
|
+
for name in (
|
|
633
|
+
item.definition.source_name,
|
|
634
|
+
item.definition.flattened_name,
|
|
635
|
+
)
|
|
636
|
+
}
|
|
637
|
+
for call in sorted(
|
|
638
|
+
(item for item in self._nodes() if item["kind"] == "Call"),
|
|
639
|
+
key=lambda item: self._ordinal(item["node_id"]),
|
|
640
|
+
):
|
|
641
|
+
self._check_canceled()
|
|
642
|
+
function = self.index.nodes.get(call["fields"].get("func"))
|
|
643
|
+
if function is None or function["kind"] != "Name":
|
|
644
|
+
continue
|
|
645
|
+
callee_binding = self.binding_by_occurrence.get(function["node_id"])
|
|
646
|
+
spec = (
|
|
647
|
+
by_class_binding.get(str(callee_binding.get("binding_id")))
|
|
648
|
+
if callee_binding is not None
|
|
649
|
+
else None
|
|
650
|
+
)
|
|
651
|
+
if spec is None:
|
|
652
|
+
if function["fields"].get("id") in record_constructor_spellings:
|
|
653
|
+
self._reject(
|
|
654
|
+
"PYC3606",
|
|
655
|
+
"Static-record constructor call does not resolve to its lexical class binding",
|
|
656
|
+
function,
|
|
657
|
+
)
|
|
658
|
+
continue
|
|
659
|
+
recognized_constructor_names.add(function["node_id"])
|
|
660
|
+
parent = self._one_parent(call["node_id"])
|
|
661
|
+
assignment = self.index.nodes.get(parent[0]) if parent else None
|
|
662
|
+
if (
|
|
663
|
+
assignment is None
|
|
664
|
+
or assignment["kind"] != "Assign"
|
|
665
|
+
or parent[1] != "value"
|
|
666
|
+
or len(tuple(assignment["fields"].get("targets", ()))) != 1
|
|
667
|
+
or assignment["fields"].get("type_comment") is not None
|
|
668
|
+
):
|
|
669
|
+
self._reject(
|
|
670
|
+
"PYC3605",
|
|
671
|
+
"Static-record construction must be the complete value of one direct local assignment",
|
|
672
|
+
call,
|
|
673
|
+
)
|
|
674
|
+
target = self.index.nodes.get(assignment["fields"]["targets"][0])
|
|
675
|
+
if target is None or target["kind"] != "Name":
|
|
676
|
+
self._reject(
|
|
677
|
+
"PYC3605", "Static-record construction target must be one fresh local name", assignment
|
|
678
|
+
)
|
|
679
|
+
owner_id = self._enclosing_function(assignment["node_id"])
|
|
680
|
+
owner = self.index.nodes.get(owner_id)
|
|
681
|
+
if (
|
|
682
|
+
owner is None
|
|
683
|
+
or owner_id not in root_members
|
|
684
|
+
or assignment["node_id"] not in tuple(owner["fields"].get("body", ()))
|
|
685
|
+
):
|
|
686
|
+
self._reject(
|
|
687
|
+
"PYC3605",
|
|
688
|
+
"Static-record construction is allowed only as a direct top-level-function-body statement",
|
|
689
|
+
assignment,
|
|
690
|
+
)
|
|
691
|
+
self._require_same_module(spec.definition, owner)
|
|
692
|
+
arguments = tuple(call["fields"].get("args", ()))
|
|
693
|
+
if call["fields"].get("keywords") or len(arguments) != len(spec.fields):
|
|
694
|
+
self._reject(
|
|
695
|
+
"PYC3605",
|
|
696
|
+
"Static-record construction uses exact positional arity and no keywords",
|
|
697
|
+
call,
|
|
698
|
+
)
|
|
699
|
+
for argument_id, field in zip(arguments, spec.fields):
|
|
700
|
+
self._check_canceled()
|
|
701
|
+
category = self._argument_category(
|
|
702
|
+
argument_id,
|
|
703
|
+
owner,
|
|
704
|
+
known_instance_fields,
|
|
705
|
+
)
|
|
706
|
+
if category is not field.category:
|
|
707
|
+
self._reject(
|
|
708
|
+
"PYC3605",
|
|
709
|
+
f"Constructor argument for {field.source_name!r} must be exactly {field.category.value}",
|
|
710
|
+
self.index.node(argument_id),
|
|
711
|
+
)
|
|
712
|
+
|
|
713
|
+
target_name = str(target["fields"].get("id", ""))
|
|
714
|
+
if target_name in self._parameter_names(owner):
|
|
715
|
+
self._reject(
|
|
716
|
+
"PYC3606", "A static-record instance cannot replace a function parameter", target
|
|
717
|
+
)
|
|
718
|
+
source_binding = self.binding_by_occurrence.get(target["node_id"])
|
|
719
|
+
if (
|
|
720
|
+
source_binding is None
|
|
721
|
+
or source_binding.get("binding_kind") != "local"
|
|
722
|
+
or source_binding.get("declaration_node_id") != target["node_id"]
|
|
723
|
+
or source_binding.get("source_name") != target_name
|
|
724
|
+
):
|
|
725
|
+
self._reject(
|
|
726
|
+
"PYC3606",
|
|
727
|
+
"A static-record instance requires one fresh ordinary function-local binding",
|
|
728
|
+
target,
|
|
729
|
+
)
|
|
730
|
+
conflict = self._owner_binding_conflict(
|
|
731
|
+
owner,
|
|
732
|
+
target_name,
|
|
733
|
+
target["node_id"],
|
|
734
|
+
)
|
|
735
|
+
if conflict is not None:
|
|
736
|
+
self._reject(
|
|
737
|
+
"PYC3606",
|
|
738
|
+
"A static-record instance binding cannot be redirected, rebound, or deleted",
|
|
739
|
+
conflict,
|
|
740
|
+
)
|
|
741
|
+
binding_id = str(
|
|
742
|
+
source_binding.get("binding_id")
|
|
743
|
+
)
|
|
744
|
+
binding_key = (owner_id, binding_id)
|
|
745
|
+
if binding_key in used_bindings:
|
|
746
|
+
self._reject(
|
|
747
|
+
"PYC3606",
|
|
748
|
+
"A static-record instance binding is single-assignment and cannot be rebound",
|
|
749
|
+
target,
|
|
750
|
+
)
|
|
751
|
+
instance_id = _stable_id(
|
|
752
|
+
"record-instance-", spec.definition.record_id, owner_id, call["node_id"], binding_id
|
|
753
|
+
)
|
|
754
|
+
used_bindings[binding_key] = instance_id
|
|
755
|
+
identity = self._identity(owner, self.function_records.get(owner_id, {}))
|
|
756
|
+
instances.append(
|
|
757
|
+
RecordInstanceFact(
|
|
758
|
+
instance_id=instance_id,
|
|
759
|
+
record_id=spec.definition.record_id,
|
|
760
|
+
class_node_id=spec.definition.class_node_id,
|
|
761
|
+
owner_function_node_id=owner_id,
|
|
762
|
+
construction_node_id=call["node_id"],
|
|
763
|
+
assignment_node_id=assignment["node_id"],
|
|
764
|
+
target_node_id=target["node_id"],
|
|
765
|
+
binding_id=binding_id,
|
|
766
|
+
source_name=target_name,
|
|
767
|
+
argument_node_ids=arguments,
|
|
768
|
+
module_id=identity.module_id,
|
|
769
|
+
document_id=identity.document_id,
|
|
770
|
+
logical_name=identity.logical_name,
|
|
771
|
+
category=RecordValueCategory.INSTANCE,
|
|
772
|
+
storage_model="automatic-inline-record",
|
|
773
|
+
ownership_model="unique-lexical-owner",
|
|
774
|
+
lifetime_model="enclosing-function-activation",
|
|
775
|
+
aliasing_model="forbidden",
|
|
776
|
+
cleanup_model="none",
|
|
777
|
+
nullability_model="non-null-by-construction",
|
|
778
|
+
allocation_model="none",
|
|
779
|
+
mutable=False,
|
|
780
|
+
)
|
|
781
|
+
)
|
|
782
|
+
known_instance_fields[(owner_id, binding_id)] = {
|
|
783
|
+
field.source_name: field.category for field in spec.fields
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
record_class_binding_ids = set(by_class_binding)
|
|
787
|
+
for node in sorted(
|
|
788
|
+
(
|
|
789
|
+
item
|
|
790
|
+
for item in self._nodes()
|
|
791
|
+
if item["kind"] == "Name"
|
|
792
|
+
and (
|
|
793
|
+
self.binding_by_occurrence.get(item["node_id"], {}).get("binding_id")
|
|
794
|
+
in record_class_binding_ids
|
|
795
|
+
)
|
|
796
|
+
),
|
|
797
|
+
key=lambda item: self._ordinal(item["node_id"]),
|
|
798
|
+
):
|
|
799
|
+
self._check_canceled()
|
|
800
|
+
if node["node_id"] not in recognized_constructor_names:
|
|
801
|
+
self._reject(
|
|
802
|
+
"PYC3606",
|
|
803
|
+
"Static-record class values, object annotations, and indirect constructors are unsupported",
|
|
804
|
+
node,
|
|
805
|
+
)
|
|
806
|
+
return tuple(instances)
|
|
807
|
+
|
|
808
|
+
def _validate_instance_uses(
|
|
809
|
+
self,
|
|
810
|
+
instance: RecordInstanceFact,
|
|
811
|
+
specs_by_record_id: Mapping[str, _RecordSpec],
|
|
812
|
+
) -> tuple[RecordInstanceBindingFact, tuple[RecordFieldAccessFact, ...]]:
|
|
813
|
+
self._check_canceled()
|
|
814
|
+
spec = specs_by_record_id.get(instance.record_id)
|
|
815
|
+
if spec is None:
|
|
816
|
+
self._reject(
|
|
817
|
+
"PYC3606",
|
|
818
|
+
"Static-record instance does not resolve to its proved definition",
|
|
819
|
+
self.index.node(instance.construction_node_id),
|
|
820
|
+
)
|
|
821
|
+
fields = {item.source_name: item for item in spec.fields}
|
|
822
|
+
source_binding = self.binding_by_occurrence.get(instance.target_node_id)
|
|
823
|
+
if source_binding is not None:
|
|
824
|
+
occurrence_ids = tuple(
|
|
825
|
+
item
|
|
826
|
+
for item in source_binding.get("occurrence_node_ids", ())
|
|
827
|
+
if item in self.index.nodes
|
|
828
|
+
)
|
|
829
|
+
else:
|
|
830
|
+
occurrence_ids = self._function_name_occurrences(
|
|
831
|
+
instance.owner_function_node_id, instance.source_name
|
|
832
|
+
)
|
|
833
|
+
if instance.target_node_id not in occurrence_ids:
|
|
834
|
+
occurrence_ids = (instance.target_node_id,) + occurrence_ids
|
|
835
|
+
occurrence_ids = tuple(
|
|
836
|
+
sorted(set(occurrence_ids), key=self._ordinal)
|
|
837
|
+
)
|
|
838
|
+
access_facts: list[RecordFieldAccessFact] = []
|
|
839
|
+
for occurrence_id in occurrence_ids:
|
|
840
|
+
self._check_canceled()
|
|
841
|
+
if occurrence_id == instance.target_node_id:
|
|
842
|
+
continue
|
|
843
|
+
occurrence = self.index.node(occurrence_id)
|
|
844
|
+
if (
|
|
845
|
+
self._enclosing_function(occurrence_id) != instance.owner_function_node_id
|
|
846
|
+
or self._crosses_nested_scope(occurrence_id, instance.owner_function_node_id)
|
|
847
|
+
):
|
|
848
|
+
self._reject(
|
|
849
|
+
"PYC3606",
|
|
850
|
+
"Static-record instances cannot be captured by a nested scope",
|
|
851
|
+
occurrence,
|
|
852
|
+
)
|
|
853
|
+
parent_slot = self._one_parent(occurrence_id)
|
|
854
|
+
parent = self.index.nodes.get(parent_slot[0]) if parent_slot else None
|
|
855
|
+
if (
|
|
856
|
+
parent is not None
|
|
857
|
+
and parent["kind"] == "Attribute"
|
|
858
|
+
and parent_slot[1] == "value"
|
|
859
|
+
):
|
|
860
|
+
occurrence_position = self._owner_body_position(
|
|
861
|
+
occurrence_id,
|
|
862
|
+
instance.owner_function_node_id,
|
|
863
|
+
)
|
|
864
|
+
construction_position = self._owner_body_position(
|
|
865
|
+
instance.assignment_node_id,
|
|
866
|
+
instance.owner_function_node_id,
|
|
867
|
+
)
|
|
868
|
+
if (
|
|
869
|
+
occurrence_position is None
|
|
870
|
+
or construction_position is None
|
|
871
|
+
or occurrence_position <= construction_position
|
|
872
|
+
):
|
|
873
|
+
self._reject(
|
|
874
|
+
"PYC3606",
|
|
875
|
+
"Static-record fields cannot be read before construction completes",
|
|
876
|
+
parent,
|
|
877
|
+
)
|
|
878
|
+
field_name = parent["fields"].get("attr")
|
|
879
|
+
field = fields.get(field_name)
|
|
880
|
+
if field is None:
|
|
881
|
+
self._reject(
|
|
882
|
+
"PYC3607",
|
|
883
|
+
f"Static-record field {field_name!r} is not declared",
|
|
884
|
+
parent,
|
|
885
|
+
)
|
|
886
|
+
if self._is_store_target(parent["node_id"]):
|
|
887
|
+
self._reject(
|
|
888
|
+
"PYC3607",
|
|
889
|
+
"Static-record fields are immutable after construction",
|
|
890
|
+
parent,
|
|
891
|
+
)
|
|
892
|
+
outer = self._one_parent(parent["node_id"])
|
|
893
|
+
if outer and self.index.node(outer[0])["kind"] == "Attribute":
|
|
894
|
+
self._reject(
|
|
895
|
+
"PYC3607", "Dynamic or chained attribute lookup is unsupported", self.index.node(outer[0])
|
|
896
|
+
)
|
|
897
|
+
identity = self._identity(
|
|
898
|
+
occurrence,
|
|
899
|
+
self.function_records.get(instance.owner_function_node_id, {}),
|
|
900
|
+
)
|
|
901
|
+
access_facts.append(
|
|
902
|
+
RecordFieldAccessFact(
|
|
903
|
+
access_node_id=parent["node_id"],
|
|
904
|
+
instance_id=instance.instance_id,
|
|
905
|
+
binding_id=instance.binding_id,
|
|
906
|
+
record_id=instance.record_id,
|
|
907
|
+
field_id=field.field_id,
|
|
908
|
+
field_name=field.source_name,
|
|
909
|
+
field_category=field.category,
|
|
910
|
+
owner_function_node_id=instance.owner_function_node_id,
|
|
911
|
+
module_id=identity.module_id,
|
|
912
|
+
document_id=identity.document_id,
|
|
913
|
+
logical_name=identity.logical_name,
|
|
914
|
+
access_mode="read",
|
|
915
|
+
statically_bound=True,
|
|
916
|
+
)
|
|
917
|
+
)
|
|
918
|
+
continue
|
|
919
|
+
if self._is_dynamic_attribute_use(occurrence_id):
|
|
920
|
+
self._reject(
|
|
921
|
+
"PYC3607", "Dynamic attribute access on static records is unsupported", occurrence
|
|
922
|
+
)
|
|
923
|
+
self._reject(
|
|
924
|
+
"PYC3606",
|
|
925
|
+
"Static-record instances cannot be aliased, rebound, escaped, compared by identity, tested for truth, passed, returned, or stored in containers",
|
|
926
|
+
occurrence,
|
|
927
|
+
)
|
|
928
|
+
identity = self._identity(
|
|
929
|
+
self.index.node(instance.owner_function_node_id),
|
|
930
|
+
self.function_records.get(instance.owner_function_node_id, {}),
|
|
931
|
+
)
|
|
932
|
+
binding = RecordInstanceBindingFact(
|
|
933
|
+
binding_id=instance.binding_id,
|
|
934
|
+
instance_id=instance.instance_id,
|
|
935
|
+
record_id=instance.record_id,
|
|
936
|
+
source_name=instance.source_name,
|
|
937
|
+
declaration_node_id=instance.target_node_id,
|
|
938
|
+
occurrence_node_ids=occurrence_ids,
|
|
939
|
+
allowed_field_access_node_ids=tuple(
|
|
940
|
+
item.access_node_id for item in access_facts
|
|
941
|
+
),
|
|
942
|
+
owner_function_node_id=instance.owner_function_node_id,
|
|
943
|
+
module_id=identity.module_id,
|
|
944
|
+
document_id=identity.document_id,
|
|
945
|
+
logical_name=identity.logical_name,
|
|
946
|
+
category=RecordValueCategory.INSTANCE,
|
|
947
|
+
single_assignment=True,
|
|
948
|
+
noalias=True,
|
|
949
|
+
escapes=False,
|
|
950
|
+
)
|
|
951
|
+
return binding, tuple(access_facts)
|
|
952
|
+
|
|
953
|
+
def _reject_cross_module_imports(self, specs: tuple[_RecordSpec, ...]) -> None:
|
|
954
|
+
by_source = defaultdict(list)
|
|
955
|
+
for spec in specs:
|
|
956
|
+
self._check_canceled()
|
|
957
|
+
by_source[spec.definition.source_name].append(spec)
|
|
958
|
+
for node in sorted(
|
|
959
|
+
(item for item in self._nodes() if item["kind"] == "ImportFrom"),
|
|
960
|
+
key=lambda item: self._ordinal(item["node_id"]),
|
|
961
|
+
):
|
|
962
|
+
self._check_canceled()
|
|
963
|
+
imported_module = node["fields"].get("module")
|
|
964
|
+
for alias_id in node["fields"].get("names", ()):
|
|
965
|
+
self._check_canceled()
|
|
966
|
+
alias = self.index.nodes.get(alias_id)
|
|
967
|
+
if alias is None:
|
|
968
|
+
continue
|
|
969
|
+
for spec in by_source.get(alias["fields"].get("name"), ()):
|
|
970
|
+
if imported_module == spec.definition.module_id:
|
|
971
|
+
self._reject(
|
|
972
|
+
"PYC3608",
|
|
973
|
+
"Cross-module static-record imports are deferred; records remain module-local",
|
|
974
|
+
alias,
|
|
975
|
+
)
|
|
976
|
+
|
|
977
|
+
def _reject_forward_object_annotations(self, specs: tuple[_RecordSpec, ...]) -> None:
|
|
978
|
+
names = {
|
|
979
|
+
name
|
|
980
|
+
for spec in specs
|
|
981
|
+
for name in (
|
|
982
|
+
spec.definition.source_name,
|
|
983
|
+
spec.definition.flattened_name,
|
|
984
|
+
)
|
|
985
|
+
}
|
|
986
|
+
for function in sorted(
|
|
987
|
+
(
|
|
988
|
+
item
|
|
989
|
+
for item in self._nodes()
|
|
990
|
+
if item["kind"] in {"FunctionDef", "AsyncFunctionDef"}
|
|
991
|
+
),
|
|
992
|
+
key=lambda item: self._ordinal(item["node_id"]),
|
|
993
|
+
):
|
|
994
|
+
self._check_canceled()
|
|
995
|
+
arguments = self.index.nodes.get(function["fields"].get("args"), {})
|
|
996
|
+
parameter_ids = tuple(arguments.get("fields", {}).get("posonlyargs", ())) + tuple(
|
|
997
|
+
arguments.get("fields", {}).get("args", ())
|
|
998
|
+
) + tuple(arguments.get("fields", {}).get("kwonlyargs", ()))
|
|
999
|
+
annotation_ids = [
|
|
1000
|
+
self.index.node(item)["fields"].get("annotation")
|
|
1001
|
+
for item in parameter_ids
|
|
1002
|
+
]
|
|
1003
|
+
annotation_ids.append(function["fields"].get("returns"))
|
|
1004
|
+
for annotation_id in annotation_ids:
|
|
1005
|
+
self._check_canceled()
|
|
1006
|
+
annotation = self.index.nodes.get(annotation_id)
|
|
1007
|
+
if annotation and (
|
|
1008
|
+
(
|
|
1009
|
+
annotation["kind"] == "Constant"
|
|
1010
|
+
and annotation["fields"].get("value") in names
|
|
1011
|
+
)
|
|
1012
|
+
or (
|
|
1013
|
+
annotation["kind"] == "Name"
|
|
1014
|
+
and annotation["fields"].get("id") in names
|
|
1015
|
+
)
|
|
1016
|
+
):
|
|
1017
|
+
self._reject(
|
|
1018
|
+
"PYC3606",
|
|
1019
|
+
"Static-record parameters and returns are unsupported, including forward annotations",
|
|
1020
|
+
annotation,
|
|
1021
|
+
)
|
|
1022
|
+
|
|
1023
|
+
def _require_same_module(
|
|
1024
|
+
self, definition: RecordDefinitionFact, owner: dict[str, Any]
|
|
1025
|
+
) -> None:
|
|
1026
|
+
metadata = self.function_records.get(owner["node_id"])
|
|
1027
|
+
if metadata and metadata.get("module_id") is not None:
|
|
1028
|
+
if str(metadata["module_id"]) != definition.module_id:
|
|
1029
|
+
self._reject(
|
|
1030
|
+
"PYC3608",
|
|
1031
|
+
"Static-record construction is restricted to the defining module",
|
|
1032
|
+
owner,
|
|
1033
|
+
)
|
|
1034
|
+
return
|
|
1035
|
+
owner_document = self._node_document_id(owner)
|
|
1036
|
+
if owner_document and definition.document_id and owner_document != definition.document_id:
|
|
1037
|
+
self._reject(
|
|
1038
|
+
"PYC3608",
|
|
1039
|
+
"Static-record construction is restricted to the defining source document",
|
|
1040
|
+
owner,
|
|
1041
|
+
)
|
|
1042
|
+
|
|
1043
|
+
def _argument_category(
|
|
1044
|
+
self,
|
|
1045
|
+
node_id: str,
|
|
1046
|
+
owner: dict[str, Any],
|
|
1047
|
+
known_instance_fields: Mapping[tuple[str, str], Mapping[str, ValueCategory]] | None = None,
|
|
1048
|
+
) -> ValueCategory:
|
|
1049
|
+
known = self.categories.get(node_id, ValueCategory.UNKNOWN)
|
|
1050
|
+
if known not in {ValueCategory.UNKNOWN, ValueCategory.CONTRADICTORY}:
|
|
1051
|
+
return known
|
|
1052
|
+
node = self.index.node(node_id)
|
|
1053
|
+
if node["kind"] == "Constant":
|
|
1054
|
+
value = node["fields"].get("value")
|
|
1055
|
+
if isinstance(value, bool):
|
|
1056
|
+
return ValueCategory.BOOLEAN
|
|
1057
|
+
if isinstance(value, int):
|
|
1058
|
+
return ValueCategory.INTEGER
|
|
1059
|
+
if isinstance(value, float):
|
|
1060
|
+
return ValueCategory.FLOAT
|
|
1061
|
+
return ValueCategory.UNKNOWN
|
|
1062
|
+
if node["kind"] == "Name":
|
|
1063
|
+
name = node["fields"].get("id")
|
|
1064
|
+
arguments = self.index.nodes.get(owner["fields"].get("args"), {})
|
|
1065
|
+
for argument_id in tuple(arguments.get("fields", {}).get("posonlyargs", ())) + tuple(
|
|
1066
|
+
arguments.get("fields", {}).get("args", ())
|
|
1067
|
+
) + tuple(arguments.get("fields", {}).get("kwonlyargs", ())):
|
|
1068
|
+
self._check_canceled()
|
|
1069
|
+
argument = self.index.node(argument_id)
|
|
1070
|
+
if argument["fields"].get("arg") != name:
|
|
1071
|
+
continue
|
|
1072
|
+
annotation = self.index.nodes.get(argument["fields"].get("annotation"))
|
|
1073
|
+
if annotation and annotation["kind"] == "Name":
|
|
1074
|
+
return _FIELD_CATEGORIES.get(
|
|
1075
|
+
annotation["fields"].get("id"), ValueCategory.UNKNOWN
|
|
1076
|
+
)
|
|
1077
|
+
if node["kind"] == "Attribute" and known_instance_fields:
|
|
1078
|
+
receiver = self.index.nodes.get(node["fields"].get("value"))
|
|
1079
|
+
source_binding = (
|
|
1080
|
+
self.binding_by_occurrence.get(receiver["node_id"])
|
|
1081
|
+
if receiver is not None and receiver["kind"] == "Name"
|
|
1082
|
+
else None
|
|
1083
|
+
)
|
|
1084
|
+
fields = known_instance_fields.get(
|
|
1085
|
+
(owner["node_id"], str(source_binding.get("binding_id")))
|
|
1086
|
+
) if source_binding else None
|
|
1087
|
+
if fields is not None:
|
|
1088
|
+
return fields.get(node["fields"].get("attr"), ValueCategory.UNKNOWN)
|
|
1089
|
+
if node["kind"] == "UnaryOp":
|
|
1090
|
+
return self._argument_category(
|
|
1091
|
+
node["fields"].get("operand"), owner, known_instance_fields
|
|
1092
|
+
)
|
|
1093
|
+
if node["kind"] == "BinOp":
|
|
1094
|
+
left = self._argument_category(
|
|
1095
|
+
node["fields"].get("left"), owner, known_instance_fields
|
|
1096
|
+
)
|
|
1097
|
+
right = self._argument_category(
|
|
1098
|
+
node["fields"].get("right"), owner, known_instance_fields
|
|
1099
|
+
)
|
|
1100
|
+
if left is right and left in {ValueCategory.INTEGER, ValueCategory.FLOAT}:
|
|
1101
|
+
operator = self.index.nodes.get(node["fields"].get("op"), {})
|
|
1102
|
+
if operator.get("kind") != "Div" or left is ValueCategory.FLOAT:
|
|
1103
|
+
return left
|
|
1104
|
+
return ValueCategory.UNKNOWN
|
|
1105
|
+
|
|
1106
|
+
def _parameter_names(self, function: dict[str, Any]) -> set[str]:
|
|
1107
|
+
arguments = self.index.nodes.get(function["fields"].get("args"), {})
|
|
1108
|
+
ids = tuple(arguments.get("fields", {}).get("posonlyargs", ())) + tuple(
|
|
1109
|
+
arguments.get("fields", {}).get("args", ())
|
|
1110
|
+
) + tuple(arguments.get("fields", {}).get("kwonlyargs", ()))
|
|
1111
|
+
return {
|
|
1112
|
+
str(self.index.node(item)["fields"].get("arg", "")) for item in ids
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
def _function_name_occurrences(
|
|
1116
|
+
self, function_node_id: str, source_name: str
|
|
1117
|
+
) -> tuple[str, ...]:
|
|
1118
|
+
found: list[str] = []
|
|
1119
|
+
stack = list(reversed(self.index.node(function_node_id)["fields"].get("body", ())))
|
|
1120
|
+
while stack:
|
|
1121
|
+
self._check_canceled()
|
|
1122
|
+
node_id = stack.pop()
|
|
1123
|
+
node = self.index.node(node_id)
|
|
1124
|
+
if node["kind"] == "Name" and node["fields"].get("id") == source_name:
|
|
1125
|
+
found.append(node_id)
|
|
1126
|
+
if node["kind"] in {"FunctionDef", "AsyncFunctionDef", "ClassDef"}:
|
|
1127
|
+
continue
|
|
1128
|
+
stack.extend(reversed(self.index.child_ids(node)))
|
|
1129
|
+
return tuple(found)
|
|
1130
|
+
|
|
1131
|
+
def _owner_binding_conflict(
|
|
1132
|
+
self,
|
|
1133
|
+
owner: dict[str, Any],
|
|
1134
|
+
source_name: str,
|
|
1135
|
+
declaration_node_id: str,
|
|
1136
|
+
) -> dict[str, Any] | None:
|
|
1137
|
+
"""Return any second owner-scope binding/unbinding form.
|
|
1138
|
+
|
|
1139
|
+
Python binds names through more syntax than ordinary assignments. The
|
|
1140
|
+
general symbol facts deliberately serve the cumulative language, so
|
|
1141
|
+
the record gate independently closes its stricter one-declaration
|
|
1142
|
+
proof over exception aliases, patterns, imports, nested definitions,
|
|
1143
|
+
scope directives, context-manager targets, and deletion as well.
|
|
1144
|
+
"""
|
|
1145
|
+
|
|
1146
|
+
owner_id = owner["node_id"]
|
|
1147
|
+
for node in sorted(self._nodes(), key=lambda item: self._ordinal(item["node_id"])):
|
|
1148
|
+
self._check_canceled()
|
|
1149
|
+
node_id = node["node_id"]
|
|
1150
|
+
if node_id == declaration_node_id:
|
|
1151
|
+
continue
|
|
1152
|
+
if self._enclosing_function(node_id) != owner_id:
|
|
1153
|
+
continue
|
|
1154
|
+
if self._crosses_class_or_lambda_scope(node_id, owner_id):
|
|
1155
|
+
continue
|
|
1156
|
+
kind = node["kind"]
|
|
1157
|
+
fields = node["fields"]
|
|
1158
|
+
if kind == "Name" and fields.get("id") == source_name:
|
|
1159
|
+
if self._is_comprehension_iteration_target(node_id):
|
|
1160
|
+
continue
|
|
1161
|
+
if self._is_store_target(node_id):
|
|
1162
|
+
return node
|
|
1163
|
+
continue
|
|
1164
|
+
if kind in {"Global", "Nonlocal"} and source_name in tuple(fields.get("names", ())):
|
|
1165
|
+
return node
|
|
1166
|
+
if kind == "ExceptHandler" and fields.get("name") == source_name:
|
|
1167
|
+
return node
|
|
1168
|
+
if kind in {"MatchAs", "MatchStar"} and fields.get("name") == source_name:
|
|
1169
|
+
return node
|
|
1170
|
+
if kind == "MatchMapping" and fields.get("rest") == source_name:
|
|
1171
|
+
return node
|
|
1172
|
+
if (
|
|
1173
|
+
kind in {"FunctionDef", "AsyncFunctionDef", "ClassDef"}
|
|
1174
|
+
and node_id != owner_id
|
|
1175
|
+
and fields.get("name") == source_name
|
|
1176
|
+
):
|
|
1177
|
+
return node
|
|
1178
|
+
if kind == "alias":
|
|
1179
|
+
imported = str(fields.get("name") or "")
|
|
1180
|
+
bound_name = str(fields.get("asname") or imported.split(".", 1)[0])
|
|
1181
|
+
if bound_name == source_name:
|
|
1182
|
+
return node
|
|
1183
|
+
return None
|
|
1184
|
+
|
|
1185
|
+
def _owner_body_position(
|
|
1186
|
+
self,
|
|
1187
|
+
node_id: str,
|
|
1188
|
+
owner_function_node_id: str,
|
|
1189
|
+
) -> int | None:
|
|
1190
|
+
owner = self.index.nodes.get(owner_function_node_id)
|
|
1191
|
+
if owner is None:
|
|
1192
|
+
return None
|
|
1193
|
+
body = tuple(owner["fields"].get("body", ()))
|
|
1194
|
+
current = node_id
|
|
1195
|
+
seen: set[str] = set()
|
|
1196
|
+
while current not in seen:
|
|
1197
|
+
self._check_canceled()
|
|
1198
|
+
seen.add(current)
|
|
1199
|
+
parent = self._one_parent(current)
|
|
1200
|
+
if not parent:
|
|
1201
|
+
return None
|
|
1202
|
+
if parent[0] == owner_function_node_id and parent[1] == "body":
|
|
1203
|
+
try:
|
|
1204
|
+
return body.index(current)
|
|
1205
|
+
except ValueError:
|
|
1206
|
+
return None
|
|
1207
|
+
current = parent[0]
|
|
1208
|
+
return None
|
|
1209
|
+
|
|
1210
|
+
def _crosses_class_or_lambda_scope(
|
|
1211
|
+
self,
|
|
1212
|
+
node_id: str,
|
|
1213
|
+
owner_function_node_id: str,
|
|
1214
|
+
) -> bool:
|
|
1215
|
+
current = node_id
|
|
1216
|
+
seen: set[str] = set()
|
|
1217
|
+
while current not in seen:
|
|
1218
|
+
self._check_canceled()
|
|
1219
|
+
seen.add(current)
|
|
1220
|
+
parent = self._one_parent(current)
|
|
1221
|
+
if not parent or parent[0] == owner_function_node_id:
|
|
1222
|
+
return False
|
|
1223
|
+
parent_node = self.index.node(parent[0])
|
|
1224
|
+
if parent_node["kind"] in {"ClassDef", "Lambda"}:
|
|
1225
|
+
return True
|
|
1226
|
+
current = parent[0]
|
|
1227
|
+
return True
|
|
1228
|
+
|
|
1229
|
+
def _is_comprehension_iteration_target(self, node_id: str) -> bool:
|
|
1230
|
+
current = node_id
|
|
1231
|
+
seen: set[str] = set()
|
|
1232
|
+
while current not in seen:
|
|
1233
|
+
self._check_canceled()
|
|
1234
|
+
seen.add(current)
|
|
1235
|
+
parent = self._one_parent(current)
|
|
1236
|
+
if not parent:
|
|
1237
|
+
return False
|
|
1238
|
+
parent_node = self.index.node(parent[0])
|
|
1239
|
+
if parent_node["kind"] == "comprehension":
|
|
1240
|
+
return parent[1] == "target"
|
|
1241
|
+
if parent_node["kind"] not in {"Tuple", "List", "Starred"}:
|
|
1242
|
+
return False
|
|
1243
|
+
current = parent[0]
|
|
1244
|
+
return False
|
|
1245
|
+
|
|
1246
|
+
def _is_dynamic_attribute_use(self, occurrence_id: str) -> bool:
|
|
1247
|
+
parent_slot = self._one_parent(occurrence_id)
|
|
1248
|
+
if not parent_slot:
|
|
1249
|
+
return False
|
|
1250
|
+
parent = self.index.node(parent_slot[0])
|
|
1251
|
+
if parent["kind"] != "Call" or parent_slot[1] not in {"args", "keywords"}:
|
|
1252
|
+
return False
|
|
1253
|
+
function = self.index.nodes.get(parent["fields"].get("func"))
|
|
1254
|
+
return bool(
|
|
1255
|
+
function
|
|
1256
|
+
and function["kind"] == "Name"
|
|
1257
|
+
and function["fields"].get("id") in _DYNAMIC_ATTRIBUTE_BUILTINS
|
|
1258
|
+
)
|
|
1259
|
+
|
|
1260
|
+
def _is_store_target(self, node_id: str) -> bool:
|
|
1261
|
+
current = node_id
|
|
1262
|
+
while True:
|
|
1263
|
+
self._check_canceled()
|
|
1264
|
+
parent_slot = self._one_parent(current)
|
|
1265
|
+
if not parent_slot:
|
|
1266
|
+
return False
|
|
1267
|
+
parent = self.index.node(parent_slot[0])
|
|
1268
|
+
if (parent["kind"], parent_slot[1]) in _STORE_PARENT_SLOTS:
|
|
1269
|
+
return True
|
|
1270
|
+
if parent["kind"] in {"Tuple", "List"} and parent_slot[1] == "elts":
|
|
1271
|
+
current = parent["node_id"]
|
|
1272
|
+
continue
|
|
1273
|
+
return False
|
|
1274
|
+
|
|
1275
|
+
def _enclosing_function(self, node_id: str) -> str | None:
|
|
1276
|
+
current = node_id
|
|
1277
|
+
seen: set[str] = set()
|
|
1278
|
+
while current not in seen:
|
|
1279
|
+
self._check_canceled()
|
|
1280
|
+
seen.add(current)
|
|
1281
|
+
parent = self._one_parent(current)
|
|
1282
|
+
if not parent:
|
|
1283
|
+
return None
|
|
1284
|
+
node = self.index.node(parent[0])
|
|
1285
|
+
if node["kind"] in {"FunctionDef", "AsyncFunctionDef"}:
|
|
1286
|
+
return node["node_id"]
|
|
1287
|
+
current = node["node_id"]
|
|
1288
|
+
return None
|
|
1289
|
+
|
|
1290
|
+
def _crosses_nested_scope(self, node_id: str, owner_function_node_id: str) -> bool:
|
|
1291
|
+
current = node_id
|
|
1292
|
+
seen: set[str] = set()
|
|
1293
|
+
nested_scope_kinds = {"Lambda", "ListComp", "SetComp", "DictComp", "GeneratorExp"}
|
|
1294
|
+
while current not in seen:
|
|
1295
|
+
self._check_canceled()
|
|
1296
|
+
seen.add(current)
|
|
1297
|
+
parent = self._one_parent(current)
|
|
1298
|
+
if not parent:
|
|
1299
|
+
return False
|
|
1300
|
+
parent_id = parent[0]
|
|
1301
|
+
if parent_id == owner_function_node_id:
|
|
1302
|
+
return False
|
|
1303
|
+
node = self.index.node(parent_id)
|
|
1304
|
+
if node["kind"] in nested_scope_kinds:
|
|
1305
|
+
return True
|
|
1306
|
+
current = parent_id
|
|
1307
|
+
return True
|
|
1308
|
+
|
|
1309
|
+
def _one_parent(self, node_id: str) -> tuple[str, str] | None:
|
|
1310
|
+
values = self.parents.get(node_id, ())
|
|
1311
|
+
if not values:
|
|
1312
|
+
return None
|
|
1313
|
+
return min(values, key=lambda item: (self._ordinal(item[0]), item[1]))
|
|
1314
|
+
|
|
1315
|
+
def _identity(
|
|
1316
|
+
self, node: dict[str, Any], metadata: Mapping[str, Any]
|
|
1317
|
+
) -> _Identity:
|
|
1318
|
+
node_document_id = self._node_document_id(node)
|
|
1319
|
+
document_identity = self.identity_by_document.get(node_document_id)
|
|
1320
|
+
document_id = str(
|
|
1321
|
+
metadata.get("document_id")
|
|
1322
|
+
or metadata.get("source_document_id")
|
|
1323
|
+
or node_document_id
|
|
1324
|
+
or self.module.get("document_id", "")
|
|
1325
|
+
)
|
|
1326
|
+
module_id = str(
|
|
1327
|
+
metadata.get("module_id")
|
|
1328
|
+
or (document_identity.module_id if document_identity else None)
|
|
1329
|
+
or self.default_module_id
|
|
1330
|
+
)
|
|
1331
|
+
logical_name = str(
|
|
1332
|
+
metadata.get("logical_name")
|
|
1333
|
+
or metadata.get("logical_source_name")
|
|
1334
|
+
or (document_identity.logical_name if document_identity else None)
|
|
1335
|
+
or self.default_logical_name
|
|
1336
|
+
)
|
|
1337
|
+
return _Identity(module_id, document_id, logical_name)
|
|
1338
|
+
|
|
1339
|
+
@staticmethod
|
|
1340
|
+
def _coerce_category(value: ValueCategory | str) -> ValueCategory:
|
|
1341
|
+
if isinstance(value, ValueCategory):
|
|
1342
|
+
return value
|
|
1343
|
+
try:
|
|
1344
|
+
return ValueCategory(value)
|
|
1345
|
+
except ValueError:
|
|
1346
|
+
return ValueCategory.UNKNOWN
|
|
1347
|
+
|
|
1348
|
+
def _is_none_annotation(self, node_id: Any) -> bool:
|
|
1349
|
+
node = self.index.nodes.get(node_id)
|
|
1350
|
+
return bool(
|
|
1351
|
+
node
|
|
1352
|
+
and node["kind"] == "Constant"
|
|
1353
|
+
and node["fields"].get("value") is None
|
|
1354
|
+
)
|
|
1355
|
+
|
|
1356
|
+
@staticmethod
|
|
1357
|
+
def _node_document_id(node: Mapping[str, Any]) -> str:
|
|
1358
|
+
span = node.get("provenance", {}).get("source_span") or {}
|
|
1359
|
+
return str(span.get("document_id") or "")
|
|
1360
|
+
|
|
1361
|
+
def _ordinal(self, node_id: str) -> int:
|
|
1362
|
+
return self.index.ordinals.get(node_id, 2**63 - 1)
|
|
1363
|
+
|
|
1364
|
+
def _nodes(self):
|
|
1365
|
+
"""Yield normalized nodes with a bounded cancellation safe point."""
|
|
1366
|
+
|
|
1367
|
+
for node in self.index.nodes.values():
|
|
1368
|
+
self._check_canceled()
|
|
1369
|
+
yield node
|
|
1370
|
+
|
|
1371
|
+
def _reject(
|
|
1372
|
+
self,
|
|
1373
|
+
code: str,
|
|
1374
|
+
message: str,
|
|
1375
|
+
node: dict[str, Any],
|
|
1376
|
+
identity: _Identity | None = None,
|
|
1377
|
+
) -> None:
|
|
1378
|
+
self._check_canceled()
|
|
1379
|
+
actual = identity or self._identity(node, {})
|
|
1380
|
+
raise RecordAnalysisError(
|
|
1381
|
+
code,
|
|
1382
|
+
message,
|
|
1383
|
+
node_id=node["node_id"],
|
|
1384
|
+
module_id=actual.module_id,
|
|
1385
|
+
document_id=actual.document_id,
|
|
1386
|
+
logical_name=actual.logical_name,
|
|
1387
|
+
source_span=node.get("provenance", {}).get("source_span"),
|
|
1388
|
+
)
|
|
1389
|
+
|
|
1390
|
+
def _check_canceled(self) -> None:
|
|
1391
|
+
if self.cancellation is not None and bool(
|
|
1392
|
+
getattr(self.cancellation, "is_canceled", False)
|
|
1393
|
+
):
|
|
1394
|
+
raise RecordAnalysisCanceled
|