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,1250 @@
|
|
|
1
|
+
"""Closed-world analysis for explicit logical modules in a SourceBundle.
|
|
2
|
+
|
|
3
|
+
This module deliberately operates only on normalized in-memory Python IR. It
|
|
4
|
+
does not import, discover, open, or execute anything named by source imports.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import hashlib
|
|
10
|
+
import re
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from pycforge.converter.contracts.identifiers import (
|
|
15
|
+
C11_EXTERNAL_IDENTIFIERS,
|
|
16
|
+
C_KEYWORDS,
|
|
17
|
+
TARGET_RESERVED_NAMES,
|
|
18
|
+
)
|
|
19
|
+
from pycforge.converter.ir.python_ir import python_ir_reference_ids
|
|
20
|
+
|
|
21
|
+
from .model import (
|
|
22
|
+
ModuleAnalysisCanceled,
|
|
23
|
+
ModuleResolutionError,
|
|
24
|
+
ModuleResolutionProduct,
|
|
25
|
+
ResolvedImport,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
_DYNAMIC_IMPORT_ATTRIBUTES = frozenset(
|
|
30
|
+
{"find_spec", "import_module", "module_from_spec", "reload"}
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _stable_id(prefix: str, *parts: object) -> str:
|
|
35
|
+
seed = "\x1f".join(str(part) for part in parts).encode("utf-8")
|
|
36
|
+
return prefix + hashlib.sha256(seed).hexdigest()[:20]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _thaw(value: Any) -> Any:
|
|
40
|
+
if isinstance(value, dict):
|
|
41
|
+
return {key: _thaw(item) for key, item in value.items()}
|
|
42
|
+
if isinstance(value, (list, tuple)):
|
|
43
|
+
return [_thaw(item) for item in value]
|
|
44
|
+
return value
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _legacy_function_token(source_name: str) -> str:
|
|
48
|
+
base = re.sub(r"[^A-Za-z0-9_]", "_", source_name)
|
|
49
|
+
if not base or not (base[0].isalpha() or base[0] == "_"):
|
|
50
|
+
base = "py_" + base
|
|
51
|
+
if base.startswith("_"):
|
|
52
|
+
base = "py" + base
|
|
53
|
+
if (
|
|
54
|
+
base in C_KEYWORDS
|
|
55
|
+
or base in TARGET_RESERVED_NAMES
|
|
56
|
+
or base in C11_EXTERNAL_IDENTIFIERS
|
|
57
|
+
or base.startswith(("pycf_", "pycm_"))
|
|
58
|
+
):
|
|
59
|
+
base = "py_" + base
|
|
60
|
+
return base
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _module_escape(module_id: str) -> str:
|
|
64
|
+
return module_id.replace("_", "_u").replace(".", "_d")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _qualified_function_name(module_id: str, source_name: str) -> str:
|
|
68
|
+
binding_identity = f"{module_id}.{source_name}"
|
|
69
|
+
digest = hashlib.sha256(binding_identity.encode("utf-8")).hexdigest()
|
|
70
|
+
# Put collision entropy inside C11's guaranteed 31 significant initial
|
|
71
|
+
# characters for external identifiers. The readable suffix is diagnostic
|
|
72
|
+
# context only; portable identity does not depend on its tail.
|
|
73
|
+
return f"pycm_{digest}__{_module_escape(module_id)}__{_legacy_function_token(source_name)}"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(slots=True)
|
|
77
|
+
class _Module:
|
|
78
|
+
module_id: str
|
|
79
|
+
logical_name: str
|
|
80
|
+
bundle_ordinal: int
|
|
81
|
+
is_primary: bool
|
|
82
|
+
document_id: str
|
|
83
|
+
python_ir: dict[str, Any]
|
|
84
|
+
source: dict[str, Any]
|
|
85
|
+
nodes: dict[str, dict[str, Any]] = field(init=False)
|
|
86
|
+
root: dict[str, Any] = field(init=False)
|
|
87
|
+
body: tuple[str, ...] = field(init=False)
|
|
88
|
+
function_ids: tuple[str, ...] = ()
|
|
89
|
+
functions: dict[str, dict[str, Any]] = field(default_factory=dict)
|
|
90
|
+
record_ids: tuple[str, ...] = ()
|
|
91
|
+
records: dict[str, dict[str, Any]] = field(default_factory=dict)
|
|
92
|
+
ineligible_functions: dict[str, dict[str, Any]] = field(default_factory=dict)
|
|
93
|
+
deferred_top_level_node: dict[str, Any] | None = None
|
|
94
|
+
import_node_ids: tuple[str, ...] = ()
|
|
95
|
+
raw_imports: list[tuple[str, int, int, str, str, str]] = field(default_factory=list)
|
|
96
|
+
resolved_imports: list[ResolvedImport] = field(default_factory=list)
|
|
97
|
+
imported_bindings: dict[str, ResolvedImport] = field(default_factory=dict)
|
|
98
|
+
document_plan_node_id: str = ""
|
|
99
|
+
|
|
100
|
+
def __post_init__(self) -> None:
|
|
101
|
+
self.nodes = {node["node_id"]: node for node in self.python_ir["nodes"]}
|
|
102
|
+
self.root = self.nodes[self.python_ir["root_node_id"]]
|
|
103
|
+
self.body = tuple(self.root["fields"].get("body", ()))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class ExplicitModuleAnalyzer:
|
|
107
|
+
"""Resolve the approved absolute-from-import module profile."""
|
|
108
|
+
|
|
109
|
+
stage_id = "modules.resolve"
|
|
110
|
+
|
|
111
|
+
def __init__(
|
|
112
|
+
self,
|
|
113
|
+
bundle: dict[str, Any],
|
|
114
|
+
source_bundle: dict[str, Any],
|
|
115
|
+
*,
|
|
116
|
+
max_import_edges: int,
|
|
117
|
+
cancellation: Any = None,
|
|
118
|
+
invalidation_dependency: str,
|
|
119
|
+
allow_records: bool = False,
|
|
120
|
+
allow_required_keyword_only: bool = False,
|
|
121
|
+
) -> None:
|
|
122
|
+
self.bundle = bundle
|
|
123
|
+
self.source_bundle = source_bundle
|
|
124
|
+
self.max_import_edges = max_import_edges
|
|
125
|
+
self.cancellation = cancellation
|
|
126
|
+
self.invalidation_dependency = invalidation_dependency
|
|
127
|
+
self.allow_records = allow_records
|
|
128
|
+
self.allow_required_keyword_only = allow_required_keyword_only
|
|
129
|
+
sources = {
|
|
130
|
+
item["module_id"]: item for item in source_bundle.get("documents", ())
|
|
131
|
+
}
|
|
132
|
+
self.modules = tuple(
|
|
133
|
+
_Module(
|
|
134
|
+
module_id=item["module_id"],
|
|
135
|
+
logical_name=item["logical_name"],
|
|
136
|
+
bundle_ordinal=item["bundle_ordinal"],
|
|
137
|
+
is_primary=item["is_primary"],
|
|
138
|
+
document_id=item["document_id"],
|
|
139
|
+
python_ir=_thaw(item["python_ir"]),
|
|
140
|
+
source=_thaw(sources.get(item["module_id"], {})),
|
|
141
|
+
)
|
|
142
|
+
for item in bundle["documents"]
|
|
143
|
+
)
|
|
144
|
+
self.by_id = {item.module_id: item for item in self.modules}
|
|
145
|
+
|
|
146
|
+
def analyze(self) -> ModuleResolutionProduct:
|
|
147
|
+
for module in self.modules:
|
|
148
|
+
self._check_canceled()
|
|
149
|
+
self._analyze_document_shape(module)
|
|
150
|
+
import_count = sum(len(module.raw_imports) for module in self.modules)
|
|
151
|
+
if import_count > self.max_import_edges:
|
|
152
|
+
module = next(item for item in self.modules if item.raw_imports)
|
|
153
|
+
node = module.nodes[module.raw_imports[0][0]]
|
|
154
|
+
self._reject(
|
|
155
|
+
module,
|
|
156
|
+
"PYC3510",
|
|
157
|
+
"SourceBundle exceeds max_import_edges",
|
|
158
|
+
node,
|
|
159
|
+
)
|
|
160
|
+
for module in self.modules:
|
|
161
|
+
self._check_canceled()
|
|
162
|
+
self._resolve_imports(module)
|
|
163
|
+
for module in self.modules:
|
|
164
|
+
self._check_canceled()
|
|
165
|
+
self._validate_import_binding_uses(module)
|
|
166
|
+
for module in self.modules:
|
|
167
|
+
if module.deferred_top_level_node is not None:
|
|
168
|
+
self._reject(
|
|
169
|
+
module,
|
|
170
|
+
"PYC3509",
|
|
171
|
+
"Only eligible synchronous top-level functions may define a module namespace",
|
|
172
|
+
module.deferred_top_level_node,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
dependency_edges = tuple(
|
|
176
|
+
sorted(
|
|
177
|
+
{
|
|
178
|
+
(item.module_id, resolved.target_module_id)
|
|
179
|
+
for item in self.modules
|
|
180
|
+
for resolved in item.resolved_imports
|
|
181
|
+
}
|
|
182
|
+
)
|
|
183
|
+
)
|
|
184
|
+
module_order = self._dependency_first_order(dependency_edges)
|
|
185
|
+
return self._publish(module_order, dependency_edges)
|
|
186
|
+
|
|
187
|
+
def _analyze_document_shape(self, module: _Module) -> None:
|
|
188
|
+
if module.root["kind"] != "Module":
|
|
189
|
+
self._reject(module, "PYC3509", "Module document lacks a module root", module.root)
|
|
190
|
+
|
|
191
|
+
body_set = set(module.body)
|
|
192
|
+
for node in sorted(module.nodes.values(), key=self._source_order_key):
|
|
193
|
+
self._check_canceled()
|
|
194
|
+
if node["kind"] in {"Import", "ImportFrom"} and node["node_id"] not in body_set:
|
|
195
|
+
self._reject(
|
|
196
|
+
module,
|
|
197
|
+
"PYC3504",
|
|
198
|
+
"Local and conditional imports are unsupported",
|
|
199
|
+
node,
|
|
200
|
+
)
|
|
201
|
+
if self._is_dynamic_import_call(module, node):
|
|
202
|
+
self._reject(
|
|
203
|
+
module,
|
|
204
|
+
"PYC3504",
|
|
205
|
+
"Dynamic import behavior is unsupported",
|
|
206
|
+
node,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
imports: list[str] = []
|
|
210
|
+
functions: list[str] = []
|
|
211
|
+
records: list[str] = []
|
|
212
|
+
seen_function = False
|
|
213
|
+
function_names: set[str] = set()
|
|
214
|
+
raw_imports: list[tuple[str, int, int, str, str, str]] = []
|
|
215
|
+
import_item_ordinal = 0
|
|
216
|
+
for node_id in module.body:
|
|
217
|
+
self._check_canceled()
|
|
218
|
+
node = module.nodes[node_id]
|
|
219
|
+
kind = node["kind"]
|
|
220
|
+
if kind == "ImportFrom":
|
|
221
|
+
if seen_function:
|
|
222
|
+
self._reject(
|
|
223
|
+
module,
|
|
224
|
+
"PYC3504",
|
|
225
|
+
"Imports must form the module preamble before every function",
|
|
226
|
+
node,
|
|
227
|
+
)
|
|
228
|
+
imports.append(node_id)
|
|
229
|
+
target_module = node["fields"].get("module")
|
|
230
|
+
level = node["fields"].get("level")
|
|
231
|
+
aliases = node["fields"].get("names", ())
|
|
232
|
+
if level != 0 or not isinstance(target_module, str) or not target_module or not aliases:
|
|
233
|
+
self._reject(
|
|
234
|
+
module,
|
|
235
|
+
"PYC3504",
|
|
236
|
+
"Only nonempty absolute from-import declarations are supported",
|
|
237
|
+
node,
|
|
238
|
+
)
|
|
239
|
+
for alias_ordinal, alias_id in enumerate(aliases):
|
|
240
|
+
alias = module.nodes.get(alias_id)
|
|
241
|
+
if alias is None or alias.get("kind") != "alias":
|
|
242
|
+
self._reject(module, "PYC3504", "Import contains an invalid alias", node)
|
|
243
|
+
imported_name = alias["fields"].get("name")
|
|
244
|
+
asname = alias["fields"].get("asname")
|
|
245
|
+
if imported_name == "*" or not isinstance(imported_name, str) or not imported_name:
|
|
246
|
+
self._reject(
|
|
247
|
+
module,
|
|
248
|
+
"PYC3504",
|
|
249
|
+
"Star and malformed imports are unsupported",
|
|
250
|
+
alias,
|
|
251
|
+
)
|
|
252
|
+
if asname is not None and (not isinstance(asname, str) or not asname):
|
|
253
|
+
self._reject(module, "PYC3504", "Import alias is malformed", alias)
|
|
254
|
+
raw_imports.append(
|
|
255
|
+
(
|
|
256
|
+
node_id,
|
|
257
|
+
alias_ordinal,
|
|
258
|
+
import_item_ordinal,
|
|
259
|
+
target_module,
|
|
260
|
+
imported_name,
|
|
261
|
+
asname or imported_name,
|
|
262
|
+
)
|
|
263
|
+
)
|
|
264
|
+
import_item_ordinal += 1
|
|
265
|
+
elif kind == "FunctionDef":
|
|
266
|
+
seen_function = True
|
|
267
|
+
functions.append(node_id)
|
|
268
|
+
source_name = node["fields"].get("name")
|
|
269
|
+
if not isinstance(source_name, str) or not source_name:
|
|
270
|
+
self._reject(module, "PYC3506", "Function binding is malformed", node)
|
|
271
|
+
if source_name in function_names:
|
|
272
|
+
self._reject(
|
|
273
|
+
module,
|
|
274
|
+
"PYC3506",
|
|
275
|
+
f"Module function {source_name!r} is rebound",
|
|
276
|
+
node,
|
|
277
|
+
)
|
|
278
|
+
function_names.add(source_name)
|
|
279
|
+
module.functions[source_name] = node
|
|
280
|
+
elif kind == "ClassDef" and self.allow_records:
|
|
281
|
+
if seen_function:
|
|
282
|
+
self._reject(
|
|
283
|
+
module,
|
|
284
|
+
"PYC3601",
|
|
285
|
+
"Static record classes must precede every top-level function",
|
|
286
|
+
node,
|
|
287
|
+
)
|
|
288
|
+
source_name = node["fields"].get("name")
|
|
289
|
+
if not isinstance(source_name, str) or not source_name:
|
|
290
|
+
self._reject(module, "PYC3601", "Record class binding is malformed", node)
|
|
291
|
+
if source_name in function_names:
|
|
292
|
+
self._reject(
|
|
293
|
+
module,
|
|
294
|
+
"PYC3506",
|
|
295
|
+
f"Module binding {source_name!r} is rebound",
|
|
296
|
+
node,
|
|
297
|
+
)
|
|
298
|
+
function_names.add(source_name)
|
|
299
|
+
records.append(node_id)
|
|
300
|
+
module.records[source_name] = node
|
|
301
|
+
elif kind == "AsyncFunctionDef":
|
|
302
|
+
seen_function = True
|
|
303
|
+
source_name = node["fields"].get("name")
|
|
304
|
+
if not isinstance(source_name, str) or not source_name:
|
|
305
|
+
self._reject(module, "PYC3506", "Function binding is malformed", node)
|
|
306
|
+
if source_name in function_names:
|
|
307
|
+
self._reject(
|
|
308
|
+
module,
|
|
309
|
+
"PYC3506",
|
|
310
|
+
f"Module function {source_name!r} is rebound",
|
|
311
|
+
node,
|
|
312
|
+
)
|
|
313
|
+
function_names.add(source_name)
|
|
314
|
+
module.ineligible_functions[source_name] = node
|
|
315
|
+
module.deferred_top_level_node = module.deferred_top_level_node or node
|
|
316
|
+
elif kind == "Import":
|
|
317
|
+
self._reject(
|
|
318
|
+
module,
|
|
319
|
+
"PYC3504",
|
|
320
|
+
"Plain imports are unsupported",
|
|
321
|
+
node,
|
|
322
|
+
)
|
|
323
|
+
else:
|
|
324
|
+
imported_names = {item[5] for item in raw_imports}
|
|
325
|
+
rebound = sorted(imported_names & self._statement_bound_names(module, node))
|
|
326
|
+
if rebound:
|
|
327
|
+
self._reject(
|
|
328
|
+
module,
|
|
329
|
+
"PYC3506",
|
|
330
|
+
f"Imported binding {rebound[0]!r} is rebound at module scope",
|
|
331
|
+
node,
|
|
332
|
+
)
|
|
333
|
+
self._reject(
|
|
334
|
+
module,
|
|
335
|
+
"PYC3509",
|
|
336
|
+
"Executable module initialization and top-level state are unsupported",
|
|
337
|
+
node,
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
if not functions and not module.ineligible_functions:
|
|
341
|
+
self._reject(
|
|
342
|
+
module,
|
|
343
|
+
"PYC3509",
|
|
344
|
+
"Every module document must define at least one top-level function",
|
|
345
|
+
module.nodes[module.body[0]] if module.body else None,
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
module.function_ids = tuple(functions)
|
|
349
|
+
module.record_ids = tuple(records)
|
|
350
|
+
module.import_node_ids = tuple(imports)
|
|
351
|
+
module.raw_imports = raw_imports
|
|
352
|
+
|
|
353
|
+
def _statement_bound_names(
|
|
354
|
+
self,
|
|
355
|
+
module: _Module,
|
|
356
|
+
node: dict[str, Any],
|
|
357
|
+
) -> set[str]:
|
|
358
|
+
fields = node["fields"]
|
|
359
|
+
target_ids: list[str] = []
|
|
360
|
+
if node["kind"] in {"Assign", "Delete"}:
|
|
361
|
+
target_ids.extend(fields.get("targets", ()))
|
|
362
|
+
elif node["kind"] in {"AnnAssign", "AugAssign", "For", "NamedExpr"}:
|
|
363
|
+
target_id = fields.get("target")
|
|
364
|
+
if isinstance(target_id, str):
|
|
365
|
+
target_ids.append(target_id)
|
|
366
|
+
names: set[str] = set()
|
|
367
|
+
stack = list(target_ids)
|
|
368
|
+
while stack:
|
|
369
|
+
target = module.nodes.get(stack.pop())
|
|
370
|
+
if not target:
|
|
371
|
+
continue
|
|
372
|
+
if target["kind"] == "Name":
|
|
373
|
+
name = target["fields"].get("id")
|
|
374
|
+
if isinstance(name, str):
|
|
375
|
+
names.add(name)
|
|
376
|
+
for field_name, value in target["fields"].items():
|
|
377
|
+
stack.extend(
|
|
378
|
+
python_ir_reference_ids(
|
|
379
|
+
target["kind"], field_name, value, module.nodes
|
|
380
|
+
)
|
|
381
|
+
)
|
|
382
|
+
return names
|
|
383
|
+
|
|
384
|
+
def _is_dynamic_import_call(self, module: _Module, node: dict[str, Any]) -> bool:
|
|
385
|
+
if node["kind"] != "Call":
|
|
386
|
+
return False
|
|
387
|
+
callee = module.nodes.get(node["fields"].get("func"))
|
|
388
|
+
if not callee:
|
|
389
|
+
return False
|
|
390
|
+
if callee["kind"] == "Name":
|
|
391
|
+
return callee["fields"].get("id") == "__import__"
|
|
392
|
+
if callee["kind"] == "Attribute":
|
|
393
|
+
path = self._attribute_path(module, callee)
|
|
394
|
+
return bool(
|
|
395
|
+
(path and path[0] == "importlib" and path[-1] in _DYNAMIC_IMPORT_ATTRIBUTES)
|
|
396
|
+
or path in {("builtins", "__import__"), ("__builtins__", "__import__")}
|
|
397
|
+
)
|
|
398
|
+
return False
|
|
399
|
+
|
|
400
|
+
def _attribute_path(
|
|
401
|
+
self,
|
|
402
|
+
module: _Module,
|
|
403
|
+
node: dict[str, Any],
|
|
404
|
+
) -> tuple[str, ...] | None:
|
|
405
|
+
if node["kind"] == "Name":
|
|
406
|
+
name = node["fields"].get("id")
|
|
407
|
+
return (name,) if isinstance(name, str) else None
|
|
408
|
+
if node["kind"] != "Attribute":
|
|
409
|
+
return None
|
|
410
|
+
value = module.nodes.get(node["fields"].get("value"))
|
|
411
|
+
if value is None:
|
|
412
|
+
return None
|
|
413
|
+
prefix = self._attribute_path(module, value)
|
|
414
|
+
attribute = node["fields"].get("attr")
|
|
415
|
+
if prefix is None or not isinstance(attribute, str):
|
|
416
|
+
return None
|
|
417
|
+
return prefix + (attribute,)
|
|
418
|
+
|
|
419
|
+
@staticmethod
|
|
420
|
+
def _source_order_key(node: dict[str, Any]) -> tuple[int, str]:
|
|
421
|
+
span = node.get("provenance", {}).get("source_span") or {}
|
|
422
|
+
start = span.get("start", {}) if isinstance(span, dict) else {}
|
|
423
|
+
offset = start.get("offset") if isinstance(start, dict) else None
|
|
424
|
+
return (offset if isinstance(offset, int) else 2**63 - 1, node["node_id"])
|
|
425
|
+
|
|
426
|
+
def _resolve_imports(self, module: _Module) -> None:
|
|
427
|
+
occupied = set(module.functions) | set(module.records)
|
|
428
|
+
for node_id, alias_ordinal, source_ordinal, target_id, imported_name, local_name in module.raw_imports:
|
|
429
|
+
self._check_canceled()
|
|
430
|
+
import_node = module.nodes[node_id]
|
|
431
|
+
alias_id = import_node["fields"]["names"][alias_ordinal]
|
|
432
|
+
alias_node = module.nodes[alias_id]
|
|
433
|
+
target = self.by_id.get(target_id)
|
|
434
|
+
if target is None:
|
|
435
|
+
code = "PYC3508" if self._has_package_relation(target_id) else "PYC3503"
|
|
436
|
+
message = (
|
|
437
|
+
f"Import {target_id!r} requests unsupported package or implicit-parent behavior"
|
|
438
|
+
if code == "PYC3508"
|
|
439
|
+
else f"Import target module {target_id!r} is not present in SourceBundle"
|
|
440
|
+
)
|
|
441
|
+
self._reject(module, code, message, import_node)
|
|
442
|
+
if target_id == module.module_id:
|
|
443
|
+
self._reject(
|
|
444
|
+
module,
|
|
445
|
+
"PYC3507",
|
|
446
|
+
f"Module {module.module_id!r} imports itself",
|
|
447
|
+
import_node,
|
|
448
|
+
)
|
|
449
|
+
target_function = target.functions.get(imported_name)
|
|
450
|
+
if target_function is None:
|
|
451
|
+
if imported_name in target.records:
|
|
452
|
+
self._reject(
|
|
453
|
+
module,
|
|
454
|
+
"PYC3610",
|
|
455
|
+
f"Record class import {target_id}.{imported_name} is outside the module-private Phase 13 record boundary",
|
|
456
|
+
alias_node,
|
|
457
|
+
related=(self._span(target, target.records[imported_name]),),
|
|
458
|
+
)
|
|
459
|
+
if imported_name in target.ineligible_functions:
|
|
460
|
+
self._reject(
|
|
461
|
+
module,
|
|
462
|
+
"PYC3505",
|
|
463
|
+
f"Imported member {target_id}.{imported_name} is not an eligible synchronous function",
|
|
464
|
+
alias_node,
|
|
465
|
+
related=(
|
|
466
|
+
self._span(target, target.ineligible_functions[imported_name]),
|
|
467
|
+
),
|
|
468
|
+
)
|
|
469
|
+
if imported_name in {item[5] for item in target.raw_imports}:
|
|
470
|
+
self._reject(
|
|
471
|
+
module,
|
|
472
|
+
"PYC3506",
|
|
473
|
+
f"Imported member {target_id}.{imported_name} would be a re-export",
|
|
474
|
+
alias_node,
|
|
475
|
+
)
|
|
476
|
+
if f"{target_id}.{imported_name}" in self.by_id:
|
|
477
|
+
self._reject(
|
|
478
|
+
module,
|
|
479
|
+
"PYC3506",
|
|
480
|
+
f"Imported member {target_id}.{imported_name} would be a module object",
|
|
481
|
+
alias_node,
|
|
482
|
+
)
|
|
483
|
+
self._reject(
|
|
484
|
+
module,
|
|
485
|
+
"PYC3505",
|
|
486
|
+
f"Imported member {target_id}.{imported_name} is not a direct top-level function",
|
|
487
|
+
alias_node,
|
|
488
|
+
)
|
|
489
|
+
if not self._importable_signature(target, target_function):
|
|
490
|
+
self._reject(
|
|
491
|
+
module,
|
|
492
|
+
"PYC3505",
|
|
493
|
+
f"Imported member {target_id}.{imported_name} is not an eligible direct function",
|
|
494
|
+
alias_node,
|
|
495
|
+
related=(self._span(target, target_function),),
|
|
496
|
+
)
|
|
497
|
+
if local_name == "range" or local_name in occupied:
|
|
498
|
+
self._reject(
|
|
499
|
+
module,
|
|
500
|
+
"PYC3506",
|
|
501
|
+
f"Imported binding {local_name!r} collides with another module binding",
|
|
502
|
+
alias_node,
|
|
503
|
+
)
|
|
504
|
+
occupied.add(local_name)
|
|
505
|
+
resolved = ResolvedImport(
|
|
506
|
+
import_item_id=(
|
|
507
|
+
f"module-import-{module.module_id}-{source_ordinal:04d}-"
|
|
508
|
+
+ _stable_id(
|
|
509
|
+
"",
|
|
510
|
+
module.module_id,
|
|
511
|
+
node_id,
|
|
512
|
+
alias_ordinal,
|
|
513
|
+
target_id,
|
|
514
|
+
imported_name,
|
|
515
|
+
local_name,
|
|
516
|
+
)
|
|
517
|
+
),
|
|
518
|
+
import_node_id=node_id,
|
|
519
|
+
alias_node_id=alias_id,
|
|
520
|
+
importer_module_id=module.module_id,
|
|
521
|
+
target_module_id=target_id,
|
|
522
|
+
imported_name=imported_name,
|
|
523
|
+
local_name=local_name,
|
|
524
|
+
target_function_node_id=target_function["node_id"],
|
|
525
|
+
source_ordinal=source_ordinal,
|
|
526
|
+
)
|
|
527
|
+
module.resolved_imports.append(resolved)
|
|
528
|
+
module.imported_bindings[local_name] = resolved
|
|
529
|
+
|
|
530
|
+
def _has_package_relation(self, target_id: str) -> bool:
|
|
531
|
+
return any(
|
|
532
|
+
candidate.startswith(target_id + ".") or target_id.startswith(candidate + ".")
|
|
533
|
+
for candidate in self.by_id
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
def _importable_signature(self, module: _Module, function: dict[str, Any]) -> bool:
|
|
537
|
+
fields = function["fields"]
|
|
538
|
+
if fields.get("decorator_list") or fields.get("type_params"):
|
|
539
|
+
return False
|
|
540
|
+
arguments = module.nodes.get(fields.get("args"), {})
|
|
541
|
+
if arguments.get("kind") != "arguments":
|
|
542
|
+
return False
|
|
543
|
+
args = arguments["fields"]
|
|
544
|
+
keyword_only_ids = tuple(args.get("kwonlyargs", ()))
|
|
545
|
+
keyword_defaults = tuple(args.get("kw_defaults", ()))
|
|
546
|
+
if (
|
|
547
|
+
args.get("vararg") is not None
|
|
548
|
+
or args.get("kwarg") is not None
|
|
549
|
+
or args.get("defaults")
|
|
550
|
+
or (
|
|
551
|
+
bool(keyword_only_ids)
|
|
552
|
+
and (
|
|
553
|
+
not self.allow_required_keyword_only
|
|
554
|
+
or len(keyword_defaults) != len(keyword_only_ids)
|
|
555
|
+
or any(item is not None for item in keyword_defaults)
|
|
556
|
+
)
|
|
557
|
+
)
|
|
558
|
+
):
|
|
559
|
+
return False
|
|
560
|
+
parameter_ids = (
|
|
561
|
+
tuple(args.get("posonlyargs", ()))
|
|
562
|
+
+ tuple(args.get("args", ()))
|
|
563
|
+
+ keyword_only_ids
|
|
564
|
+
)
|
|
565
|
+
annotation_ids = [
|
|
566
|
+
module.nodes[parameter_id]["fields"].get("annotation")
|
|
567
|
+
for parameter_id in parameter_ids
|
|
568
|
+
]
|
|
569
|
+
annotation_ids.append(fields.get("returns"))
|
|
570
|
+
for annotation_id in annotation_ids:
|
|
571
|
+
annotation = module.nodes.get(annotation_id)
|
|
572
|
+
if (
|
|
573
|
+
not annotation
|
|
574
|
+
or annotation["kind"] != "Name"
|
|
575
|
+
or annotation["fields"].get("id") not in {"bool", "float", "int", "str"}
|
|
576
|
+
):
|
|
577
|
+
return False
|
|
578
|
+
rejected_body_kinds = {
|
|
579
|
+
"AsyncFunctionDef",
|
|
580
|
+
"Await",
|
|
581
|
+
"ClassDef",
|
|
582
|
+
"Global",
|
|
583
|
+
"Lambda",
|
|
584
|
+
"Nonlocal",
|
|
585
|
+
"Yield",
|
|
586
|
+
"YieldFrom",
|
|
587
|
+
}
|
|
588
|
+
stack = list(function["fields"].get("body", ()))
|
|
589
|
+
while stack:
|
|
590
|
+
node_id = stack.pop()
|
|
591
|
+
node = module.nodes[node_id]
|
|
592
|
+
if node["kind"] in rejected_body_kinds or node["kind"] == "FunctionDef":
|
|
593
|
+
return False
|
|
594
|
+
for field_name, value in node["fields"].items():
|
|
595
|
+
stack.extend(
|
|
596
|
+
python_ir_reference_ids(
|
|
597
|
+
node["kind"],
|
|
598
|
+
field_name,
|
|
599
|
+
value,
|
|
600
|
+
module.nodes,
|
|
601
|
+
)
|
|
602
|
+
)
|
|
603
|
+
return True
|
|
604
|
+
|
|
605
|
+
def _validate_import_binding_uses(self, module: _Module) -> None:
|
|
606
|
+
parents = self._parent_slots(module)
|
|
607
|
+
imported_names = set(module.imported_bindings)
|
|
608
|
+
owners = self._function_owners(module, parents)
|
|
609
|
+
shadowed = self._function_shadow_names(module)
|
|
610
|
+
foreign_function_names = {
|
|
611
|
+
source_name
|
|
612
|
+
for other in self.modules
|
|
613
|
+
if other.module_id != module.module_id
|
|
614
|
+
for source_name in set(other.functions) | set(other.ineligible_functions)
|
|
615
|
+
}
|
|
616
|
+
module_id_parts = {
|
|
617
|
+
tuple(item.module_id.split(".")) for item in self.modules
|
|
618
|
+
}
|
|
619
|
+
for node in module.nodes.values():
|
|
620
|
+
self._check_canceled()
|
|
621
|
+
fields = node["fields"]
|
|
622
|
+
if node["kind"] == "arg" and fields.get("arg") in imported_names:
|
|
623
|
+
self._reject(
|
|
624
|
+
module,
|
|
625
|
+
"PYC3506",
|
|
626
|
+
f"Imported binding {fields['arg']!r} is rebound by a parameter",
|
|
627
|
+
node,
|
|
628
|
+
)
|
|
629
|
+
if node["kind"] in {"ExceptHandler", "MatchAs", "MatchStar"}:
|
|
630
|
+
name = fields.get("name")
|
|
631
|
+
if name in imported_names:
|
|
632
|
+
self._reject(
|
|
633
|
+
module,
|
|
634
|
+
"PYC3506",
|
|
635
|
+
f"Imported binding {name!r} is rebound by a local declaration",
|
|
636
|
+
node,
|
|
637
|
+
)
|
|
638
|
+
if node["kind"] in {"FunctionDef", "AsyncFunctionDef"}:
|
|
639
|
+
name = fields.get("name")
|
|
640
|
+
if node["node_id"] not in module.function_ids and name in imported_names:
|
|
641
|
+
self._reject(
|
|
642
|
+
module,
|
|
643
|
+
"PYC3506",
|
|
644
|
+
f"Imported binding {name!r} is rebound by a nested definition",
|
|
645
|
+
node,
|
|
646
|
+
)
|
|
647
|
+
if node["kind"] == "Attribute":
|
|
648
|
+
parent_id, field_name = parents.get(node["node_id"], (None, None))
|
|
649
|
+
parent = module.nodes.get(parent_id)
|
|
650
|
+
if parent and parent["kind"] == "Attribute" and field_name == "value":
|
|
651
|
+
continue
|
|
652
|
+
path = self._attribute_path(module, node)
|
|
653
|
+
owner = owners.get(node["node_id"])
|
|
654
|
+
if (
|
|
655
|
+
path
|
|
656
|
+
and path[0] not in shadowed.get(owner, set())
|
|
657
|
+
and any(path[: len(parts)] == parts for parts in module_id_parts)
|
|
658
|
+
):
|
|
659
|
+
self._reject(
|
|
660
|
+
module,
|
|
661
|
+
"PYC3506",
|
|
662
|
+
f"Logical module {'.'.join(path)!r} cannot be used as a runtime value",
|
|
663
|
+
node,
|
|
664
|
+
)
|
|
665
|
+
if node["kind"] != "Name" or fields.get("id") not in imported_names:
|
|
666
|
+
if node["kind"] != "Name":
|
|
667
|
+
continue
|
|
668
|
+
source_name = fields.get("id")
|
|
669
|
+
parent_id, field_name = parents.get(node["node_id"], (None, None))
|
|
670
|
+
parent = module.nodes.get(parent_id)
|
|
671
|
+
owner = owners.get(node["node_id"])
|
|
672
|
+
if (
|
|
673
|
+
(source_name,) in module_id_parts
|
|
674
|
+
and source_name not in module.functions
|
|
675
|
+
and source_name not in shadowed.get(owner, set())
|
|
676
|
+
):
|
|
677
|
+
self._reject(
|
|
678
|
+
module,
|
|
679
|
+
"PYC3506",
|
|
680
|
+
f"Logical module {source_name!r} cannot be used as a runtime value",
|
|
681
|
+
node,
|
|
682
|
+
)
|
|
683
|
+
if (
|
|
684
|
+
parent
|
|
685
|
+
and parent["kind"] == "Call"
|
|
686
|
+
and field_name == "func"
|
|
687
|
+
and source_name in foreign_function_names
|
|
688
|
+
and source_name not in module.functions
|
|
689
|
+
and source_name not in imported_names
|
|
690
|
+
and source_name != "range"
|
|
691
|
+
and source_name not in shadowed.get(owner, set())
|
|
692
|
+
):
|
|
693
|
+
self._reject(
|
|
694
|
+
module,
|
|
695
|
+
"PYC3506",
|
|
696
|
+
f"Foreign function {source_name!r} is used without an explicit import",
|
|
697
|
+
node,
|
|
698
|
+
)
|
|
699
|
+
continue
|
|
700
|
+
parent_id, field_name = parents.get(node["node_id"], (None, None))
|
|
701
|
+
parent = module.nodes.get(parent_id)
|
|
702
|
+
if not parent or parent["kind"] != "Call" or field_name != "func":
|
|
703
|
+
self._reject(
|
|
704
|
+
module,
|
|
705
|
+
"PYC3506",
|
|
706
|
+
f"Imported binding {fields['id']!r} may only be used as a direct call target",
|
|
707
|
+
node,
|
|
708
|
+
)
|
|
709
|
+
|
|
710
|
+
def _parent_slots(self, module: _Module) -> dict[str, tuple[str, str]]:
|
|
711
|
+
result: dict[str, tuple[str, str]] = {}
|
|
712
|
+
known = set(module.nodes)
|
|
713
|
+
for parent in module.nodes.values():
|
|
714
|
+
for field_name, value in parent["fields"].items():
|
|
715
|
+
for child_id in python_ir_reference_ids(parent["kind"], field_name, value, known):
|
|
716
|
+
result.setdefault(child_id, (parent["node_id"], field_name))
|
|
717
|
+
return result
|
|
718
|
+
|
|
719
|
+
def _dependency_first_order(
|
|
720
|
+
self, dependency_edges: tuple[tuple[str, str], ...]
|
|
721
|
+
) -> tuple[str, ...]:
|
|
722
|
+
dependencies = {module_id: set() for module_id in self.by_id}
|
|
723
|
+
dependents = {module_id: set() for module_id in self.by_id}
|
|
724
|
+
for importer, target in dependency_edges:
|
|
725
|
+
dependencies[importer].add(target)
|
|
726
|
+
dependents[target].add(importer)
|
|
727
|
+
ready = sorted(module_id for module_id, values in dependencies.items() if not values)
|
|
728
|
+
order: list[str] = []
|
|
729
|
+
while ready:
|
|
730
|
+
self._check_canceled()
|
|
731
|
+
current = ready.pop(0)
|
|
732
|
+
order.append(current)
|
|
733
|
+
for dependent in sorted(dependents[current]):
|
|
734
|
+
dependencies[dependent].discard(current)
|
|
735
|
+
if not dependencies[dependent] and dependent not in ready and dependent not in order:
|
|
736
|
+
ready.append(dependent)
|
|
737
|
+
ready.sort()
|
|
738
|
+
if len(order) != len(self.by_id):
|
|
739
|
+
component = self._first_cyclic_component(dependency_edges)
|
|
740
|
+
cycle_items = sorted(
|
|
741
|
+
(
|
|
742
|
+
resolved.importer_module_id,
|
|
743
|
+
resolved.source_ordinal,
|
|
744
|
+
resolved.target_module_id,
|
|
745
|
+
module,
|
|
746
|
+
resolved,
|
|
747
|
+
)
|
|
748
|
+
for module in self.modules
|
|
749
|
+
for resolved in module.resolved_imports
|
|
750
|
+
if resolved.importer_module_id in component
|
|
751
|
+
and resolved.target_module_id in component
|
|
752
|
+
)
|
|
753
|
+
_, _, _, module, resolved = cycle_items[0]
|
|
754
|
+
related = tuple(
|
|
755
|
+
self._span(item_module, item_module.nodes[item.import_node_id])
|
|
756
|
+
for _, _, _, item_module, item in cycle_items[1:]
|
|
757
|
+
)
|
|
758
|
+
self._reject(
|
|
759
|
+
module,
|
|
760
|
+
"PYC3507",
|
|
761
|
+
"Module dependency graph contains a rejected import cycle",
|
|
762
|
+
module.nodes[resolved.import_node_id],
|
|
763
|
+
related=related,
|
|
764
|
+
)
|
|
765
|
+
return tuple(order)
|
|
766
|
+
|
|
767
|
+
def _first_cyclic_component(
|
|
768
|
+
self,
|
|
769
|
+
dependency_edges: tuple[tuple[str, str], ...],
|
|
770
|
+
) -> frozenset[str]:
|
|
771
|
+
graph = {module_id: set() for module_id in self.by_id}
|
|
772
|
+
reverse = {module_id: set() for module_id in self.by_id}
|
|
773
|
+
for importer, target in dependency_edges:
|
|
774
|
+
graph[importer].add(target)
|
|
775
|
+
reverse[target].add(importer)
|
|
776
|
+
|
|
777
|
+
visited: set[str] = set()
|
|
778
|
+
finish: list[str] = []
|
|
779
|
+
|
|
780
|
+
def visit(node: str) -> None:
|
|
781
|
+
self._check_canceled()
|
|
782
|
+
visited.add(node)
|
|
783
|
+
for child in sorted(graph[node]):
|
|
784
|
+
if child not in visited:
|
|
785
|
+
visit(child)
|
|
786
|
+
finish.append(node)
|
|
787
|
+
|
|
788
|
+
for module_id in sorted(graph):
|
|
789
|
+
if module_id not in visited:
|
|
790
|
+
visit(module_id)
|
|
791
|
+
|
|
792
|
+
assigned: set[str] = set()
|
|
793
|
+
cyclic: list[frozenset[str]] = []
|
|
794
|
+
|
|
795
|
+
def collect(node: str, component: set[str]) -> None:
|
|
796
|
+
self._check_canceled()
|
|
797
|
+
assigned.add(node)
|
|
798
|
+
component.add(node)
|
|
799
|
+
for parent in sorted(reverse[node]):
|
|
800
|
+
if parent not in assigned:
|
|
801
|
+
collect(parent, component)
|
|
802
|
+
|
|
803
|
+
for module_id in reversed(finish):
|
|
804
|
+
if module_id in assigned:
|
|
805
|
+
continue
|
|
806
|
+
component: set[str] = set()
|
|
807
|
+
collect(module_id, component)
|
|
808
|
+
if len(component) > 1 or any(item in graph[item] for item in component):
|
|
809
|
+
cyclic.append(frozenset(component))
|
|
810
|
+
if not cyclic:
|
|
811
|
+
raise RuntimeError("topological ordering failed without a cyclic component")
|
|
812
|
+
return min(cyclic, key=lambda item: tuple(sorted(item)))
|
|
813
|
+
|
|
814
|
+
def _publish(
|
|
815
|
+
self,
|
|
816
|
+
module_order: tuple[str, ...],
|
|
817
|
+
dependency_edges: tuple[tuple[str, str], ...],
|
|
818
|
+
) -> ModuleResolutionProduct:
|
|
819
|
+
qualify_names = len(self.modules) > 1
|
|
820
|
+
function_facts: dict[str, dict[str, Any]] = {}
|
|
821
|
+
record_facts: dict[str, dict[str, Any]] = {}
|
|
822
|
+
generated_by_node: dict[str, str] = {}
|
|
823
|
+
generated_record_by_node: dict[str, str] = {}
|
|
824
|
+
function_ordinal = 0
|
|
825
|
+
for module_id in module_order:
|
|
826
|
+
module = self.by_id[module_id]
|
|
827
|
+
for function_id in module.function_ids:
|
|
828
|
+
function = module.nodes[function_id]
|
|
829
|
+
source_name = function["fields"]["name"]
|
|
830
|
+
flattened_name = (
|
|
831
|
+
_qualified_function_name(module_id, source_name)
|
|
832
|
+
if qualify_names
|
|
833
|
+
else source_name
|
|
834
|
+
)
|
|
835
|
+
generated_by_node[function_id] = flattened_name
|
|
836
|
+
function_facts[function_id] = {
|
|
837
|
+
"function_node_id": function_id,
|
|
838
|
+
"module_id": module_id,
|
|
839
|
+
"document_id": module.document_id,
|
|
840
|
+
"source_name": source_name,
|
|
841
|
+
"flattened_name": flattened_name,
|
|
842
|
+
"bundle_function_ordinal": function_ordinal,
|
|
843
|
+
"linkage": "external",
|
|
844
|
+
"module_generated_name": qualify_names,
|
|
845
|
+
}
|
|
846
|
+
function_ordinal += 1
|
|
847
|
+
for record_ordinal, record_id in enumerate(module.record_ids):
|
|
848
|
+
record = module.nodes[record_id]
|
|
849
|
+
source_name = record["fields"]["name"]
|
|
850
|
+
flattened_name = (
|
|
851
|
+
_qualified_function_name(module_id, source_name)
|
|
852
|
+
if qualify_names
|
|
853
|
+
else source_name
|
|
854
|
+
)
|
|
855
|
+
generated_record_by_node[record_id] = flattened_name
|
|
856
|
+
record_facts[record_id] = {
|
|
857
|
+
"class_node_id": record_id,
|
|
858
|
+
"module_id": module_id,
|
|
859
|
+
"document_id": module.document_id,
|
|
860
|
+
"source_name": source_name,
|
|
861
|
+
"flattened_name": flattened_name,
|
|
862
|
+
"module_record_ordinal": record_ordinal,
|
|
863
|
+
"module_generated_name": qualify_names,
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
rewritten_nodes: dict[str, dict[str, Any]] = {}
|
|
867
|
+
flattened_body: list[str] = []
|
|
868
|
+
for module_id in module_order:
|
|
869
|
+
self._check_canceled()
|
|
870
|
+
module = self.by_id[module_id]
|
|
871
|
+
parents = self._parent_slots(module)
|
|
872
|
+
shadowed = self._function_shadow_names(module)
|
|
873
|
+
owner_by_node = self._function_owners(module, parents)
|
|
874
|
+
for original in module.python_ir["nodes"]:
|
|
875
|
+
if original["node_id"] == module.python_ir["root_node_id"]:
|
|
876
|
+
continue
|
|
877
|
+
node = _thaw(original)
|
|
878
|
+
node_id = node["node_id"]
|
|
879
|
+
if node["kind"] == "FunctionDef" and node_id in generated_by_node:
|
|
880
|
+
node["fields"]["name"] = generated_by_node[node_id]
|
|
881
|
+
elif node["kind"] == "ClassDef" and node_id in generated_record_by_node:
|
|
882
|
+
node["fields"]["name"] = generated_record_by_node[node_id]
|
|
883
|
+
elif node["kind"] == "Name":
|
|
884
|
+
parent_id, field_name = parents.get(node_id, (None, None))
|
|
885
|
+
parent = module.nodes.get(parent_id)
|
|
886
|
+
source_name = node["fields"].get("id")
|
|
887
|
+
owner = owner_by_node.get(node_id)
|
|
888
|
+
if (
|
|
889
|
+
source_name in module.records
|
|
890
|
+
and owner is not None
|
|
891
|
+
and source_name not in shadowed.get(owner, set())
|
|
892
|
+
):
|
|
893
|
+
node["fields"]["id"] = generated_record_by_node[
|
|
894
|
+
module.records[source_name]["node_id"]
|
|
895
|
+
]
|
|
896
|
+
if parent and parent["kind"] == "Call" and field_name == "func":
|
|
897
|
+
source_name = node["fields"].get("id")
|
|
898
|
+
if source_name in module.imported_bindings:
|
|
899
|
+
target_id = module.imported_bindings[source_name].target_function_node_id
|
|
900
|
+
node["fields"]["id"] = generated_by_node[target_id]
|
|
901
|
+
elif source_name in module.functions:
|
|
902
|
+
owner = owner_by_node.get(node_id)
|
|
903
|
+
if source_name not in shadowed.get(owner, set()):
|
|
904
|
+
node["fields"]["id"] = generated_by_node[
|
|
905
|
+
module.functions[source_name]["node_id"]
|
|
906
|
+
]
|
|
907
|
+
rewritten_nodes[node_id] = node
|
|
908
|
+
flattened_body.extend(module.body)
|
|
909
|
+
|
|
910
|
+
document_nodes: list[dict[str, Any]] = []
|
|
911
|
+
for module in self.modules:
|
|
912
|
+
document_node_id = _stable_id(
|
|
913
|
+
"syn-", "ModuleDocument", module.module_id, module.document_id
|
|
914
|
+
)
|
|
915
|
+
module.document_plan_node_id = document_node_id
|
|
916
|
+
document_nodes.append(
|
|
917
|
+
self._synthetic_node(
|
|
918
|
+
document_node_id,
|
|
919
|
+
"ModuleDocument",
|
|
920
|
+
{
|
|
921
|
+
"module_id": module.module_id,
|
|
922
|
+
"bundle_ordinal": module.bundle_ordinal,
|
|
923
|
+
"is_primary": module.is_primary,
|
|
924
|
+
"import_node_ids": list(module.import_node_ids),
|
|
925
|
+
"function_node_ids": list(module.function_ids),
|
|
926
|
+
},
|
|
927
|
+
tuple(module.import_node_ids + module.record_ids + module.function_ids),
|
|
928
|
+
)
|
|
929
|
+
)
|
|
930
|
+
|
|
931
|
+
initialization_node_id = _stable_id(
|
|
932
|
+
"syn-", "ModuleInitialization", *module_order, *dependency_edges
|
|
933
|
+
)
|
|
934
|
+
initialization_node = self._synthetic_node(
|
|
935
|
+
initialization_node_id,
|
|
936
|
+
"ModuleInitialization",
|
|
937
|
+
{
|
|
938
|
+
"module_order": list(module_order),
|
|
939
|
+
"dependency_edges": [
|
|
940
|
+
{"importer_module_id": importer, "target_module_id": target}
|
|
941
|
+
for importer, target in dependency_edges
|
|
942
|
+
],
|
|
943
|
+
"cycle_policy": "reject-all-cycles",
|
|
944
|
+
"runtime_initialization": "none",
|
|
945
|
+
},
|
|
946
|
+
tuple(module.document_plan_node_id for module in self.modules),
|
|
947
|
+
)
|
|
948
|
+
assembly_node_id = _stable_id(
|
|
949
|
+
"syn-", "ModuleBundleAssembly", initialization_node_id, *module_order
|
|
950
|
+
)
|
|
951
|
+
assembly_node = self._synthetic_node(
|
|
952
|
+
assembly_node_id,
|
|
953
|
+
"ModuleBundleAssembly",
|
|
954
|
+
{
|
|
955
|
+
"module_order": list(module_order),
|
|
956
|
+
"translation_unit_count": 1,
|
|
957
|
+
"runtime_initializers": 0,
|
|
958
|
+
},
|
|
959
|
+
(initialization_node_id,),
|
|
960
|
+
)
|
|
961
|
+
bundle_document_id = _stable_id(
|
|
962
|
+
"bundle-", self.bundle["primary_module_id"], *(
|
|
963
|
+
module.document_id for module in self.modules
|
|
964
|
+
)
|
|
965
|
+
)
|
|
966
|
+
root_node_id = _stable_id("syn-", "Module", bundle_document_id, *flattened_body)
|
|
967
|
+
root_node = self._synthetic_node(
|
|
968
|
+
root_node_id,
|
|
969
|
+
"Module",
|
|
970
|
+
{"body": flattened_body, "type_ignores": []},
|
|
971
|
+
tuple(module.document_plan_node_id for module in self.modules),
|
|
972
|
+
)
|
|
973
|
+
python_ir = {
|
|
974
|
+
"schema_version": "python-ir/0.4",
|
|
975
|
+
"document_id": bundle_document_id,
|
|
976
|
+
"root_node_id": root_node_id,
|
|
977
|
+
"nodes": [
|
|
978
|
+
root_node,
|
|
979
|
+
*(
|
|
980
|
+
rewritten_nodes[node["node_id"]]
|
|
981
|
+
for module_id in module_order
|
|
982
|
+
for node in self.by_id[module_id].python_ir["nodes"]
|
|
983
|
+
if node["node_id"] != self.by_id[module_id].python_ir["root_node_id"]
|
|
984
|
+
),
|
|
985
|
+
*document_nodes,
|
|
986
|
+
initialization_node,
|
|
987
|
+
assembly_node,
|
|
988
|
+
],
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
import_facts = [
|
|
992
|
+
resolved.to_fact()
|
|
993
|
+
for module_id in module_order
|
|
994
|
+
for resolved in self.by_id[module_id].resolved_imports
|
|
995
|
+
]
|
|
996
|
+
identity_facts = [
|
|
997
|
+
{
|
|
998
|
+
"module_id": module.module_id,
|
|
999
|
+
"document_id": module.document_id,
|
|
1000
|
+
"logical_name": module.logical_name,
|
|
1001
|
+
"bundle_ordinal": module.bundle_ordinal,
|
|
1002
|
+
"is_primary": module.is_primary,
|
|
1003
|
+
"document_plan_node_id": module.document_plan_node_id,
|
|
1004
|
+
"import_node_ids": list(module.import_node_ids),
|
|
1005
|
+
"function_node_ids": list(module.function_ids),
|
|
1006
|
+
}
|
|
1007
|
+
for module in self.modules
|
|
1008
|
+
]
|
|
1009
|
+
source_facts = [self._source_fact(module) for module in self.modules]
|
|
1010
|
+
namespace_facts = [
|
|
1011
|
+
{
|
|
1012
|
+
"module_id": module.module_id,
|
|
1013
|
+
"document_plan_node_id": module.document_plan_node_id,
|
|
1014
|
+
"import_node_ids": list(module.import_node_ids),
|
|
1015
|
+
"function_node_ids": list(module.function_ids),
|
|
1016
|
+
"local_function_names": [
|
|
1017
|
+
module.nodes[node_id]["fields"]["name"] for node_id in module.function_ids
|
|
1018
|
+
],
|
|
1019
|
+
"imported_bindings": [
|
|
1020
|
+
{
|
|
1021
|
+
"local_name": item.local_name,
|
|
1022
|
+
"target_module_id": item.target_module_id,
|
|
1023
|
+
"target_function_node_id": item.target_function_node_id,
|
|
1024
|
+
}
|
|
1025
|
+
for item in module.resolved_imports
|
|
1026
|
+
],
|
|
1027
|
+
"generated_function_names": [
|
|
1028
|
+
generated_by_node[node_id] for node_id in module.function_ids
|
|
1029
|
+
],
|
|
1030
|
+
}
|
|
1031
|
+
for module in self.modules
|
|
1032
|
+
]
|
|
1033
|
+
initialization_fact = {
|
|
1034
|
+
"initialization_node_id": initialization_node_id,
|
|
1035
|
+
"module_order": list(module_order),
|
|
1036
|
+
"dependency_edges": [
|
|
1037
|
+
{"importer_module_id": importer, "target_module_id": target}
|
|
1038
|
+
for importer, target in dependency_edges
|
|
1039
|
+
],
|
|
1040
|
+
"cycle_policy": "reject-all-cycles",
|
|
1041
|
+
"runtime_initialization": "none",
|
|
1042
|
+
}
|
|
1043
|
+
tables = (
|
|
1044
|
+
self._fact_table("module-identity-facts", "module-id", identity_facts, "module_id"),
|
|
1045
|
+
self._fact_table("module-import-facts", "module-import-item-id", import_facts, "import_item_id"),
|
|
1046
|
+
self._fact_table("module-function-facts", "function-node-id", list(function_facts.values()), "function_node_id"),
|
|
1047
|
+
self._fact_table("module-initialization-facts", "initialization-node-id", [initialization_fact], "initialization_node_id"),
|
|
1048
|
+
self._fact_table("module-namespace-facts", "module-id", namespace_facts, "module_id"),
|
|
1049
|
+
self._fact_table("module-source-facts", "module-id", source_facts, "module_id"),
|
|
1050
|
+
)
|
|
1051
|
+
resolution = {
|
|
1052
|
+
"schema_version": "module-resolution/0.12",
|
|
1053
|
+
"primary_module_id": self.bundle["primary_module_id"],
|
|
1054
|
+
"source_module_order": [module.module_id for module in self.modules],
|
|
1055
|
+
"module_order": list(module_order),
|
|
1056
|
+
"dependency_edges": initialization_fact["dependency_edges"],
|
|
1057
|
+
"import_item_count": len(import_facts),
|
|
1058
|
+
"flattened_root_node_id": root_node_id,
|
|
1059
|
+
"bundle_document_id": bundle_document_id,
|
|
1060
|
+
"function_renames": [
|
|
1061
|
+
{
|
|
1062
|
+
"function_node_id": node_id,
|
|
1063
|
+
"source_name": fact["source_name"],
|
|
1064
|
+
"flattened_name": fact["flattened_name"],
|
|
1065
|
+
}
|
|
1066
|
+
for node_id, fact in sorted(
|
|
1067
|
+
function_facts.items(), key=lambda item: item[1]["bundle_function_ordinal"]
|
|
1068
|
+
)
|
|
1069
|
+
],
|
|
1070
|
+
"initialization_node_id": initialization_node_id,
|
|
1071
|
+
"module_bundle_assembly_node_id": assembly_node_id,
|
|
1072
|
+
}
|
|
1073
|
+
return ModuleResolutionProduct(
|
|
1074
|
+
python_ir=python_ir,
|
|
1075
|
+
module_bundle=_thaw(self.bundle),
|
|
1076
|
+
module_resolution=resolution,
|
|
1077
|
+
module_fact_tables=tables,
|
|
1078
|
+
module_import_node_ids=tuple(
|
|
1079
|
+
node_id
|
|
1080
|
+
for module_id in module_order
|
|
1081
|
+
for node_id in self.by_id[module_id].import_node_ids
|
|
1082
|
+
),
|
|
1083
|
+
module_function_by_node={
|
|
1084
|
+
key: value for key, value in sorted(function_facts.items())
|
|
1085
|
+
},
|
|
1086
|
+
module_record_by_node={
|
|
1087
|
+
key: value for key, value in sorted(record_facts.items())
|
|
1088
|
+
},
|
|
1089
|
+
module_bundle_assembly_node_id=assembly_node_id,
|
|
1090
|
+
)
|
|
1091
|
+
|
|
1092
|
+
def _function_shadow_names(self, module: _Module) -> dict[str, set[str]]:
|
|
1093
|
+
result: dict[str, set[str]] = {node_id: set() for node_id in module.function_ids}
|
|
1094
|
+
owners = self._function_owners(module, self._parent_slots(module))
|
|
1095
|
+
store_parent_fields = {"targets", "target"}
|
|
1096
|
+
parents = self._parent_slots(module)
|
|
1097
|
+
for node in module.nodes.values():
|
|
1098
|
+
owner = owners.get(node["node_id"])
|
|
1099
|
+
if owner not in result:
|
|
1100
|
+
continue
|
|
1101
|
+
if node["kind"] == "arg":
|
|
1102
|
+
result[owner].add(node["fields"].get("arg", ""))
|
|
1103
|
+
elif node["kind"] == "Name":
|
|
1104
|
+
_, field_name = parents.get(node["node_id"], (None, None))
|
|
1105
|
+
if field_name in store_parent_fields:
|
|
1106
|
+
result[owner].add(node["fields"].get("id", ""))
|
|
1107
|
+
elif node["kind"] in {"FunctionDef", "AsyncFunctionDef"} and node["node_id"] != owner:
|
|
1108
|
+
result[owner].add(node["fields"].get("name", ""))
|
|
1109
|
+
return result
|
|
1110
|
+
|
|
1111
|
+
def _function_owners(
|
|
1112
|
+
self,
|
|
1113
|
+
module: _Module,
|
|
1114
|
+
parents: dict[str, tuple[str, str]],
|
|
1115
|
+
) -> dict[str, str]:
|
|
1116
|
+
functions = set(module.function_ids)
|
|
1117
|
+
result: dict[str, str] = {}
|
|
1118
|
+
for node_id in module.nodes:
|
|
1119
|
+
current = node_id
|
|
1120
|
+
seen: set[str] = set()
|
|
1121
|
+
while current not in seen:
|
|
1122
|
+
seen.add(current)
|
|
1123
|
+
if current in functions:
|
|
1124
|
+
result[node_id] = current
|
|
1125
|
+
break
|
|
1126
|
+
parent_id, _ = parents.get(current, (None, None))
|
|
1127
|
+
if parent_id is None:
|
|
1128
|
+
break
|
|
1129
|
+
current = parent_id
|
|
1130
|
+
return result
|
|
1131
|
+
|
|
1132
|
+
def _source_fact(self, module: _Module) -> dict[str, Any]:
|
|
1133
|
+
source_document = module.source.get("source_document", {})
|
|
1134
|
+
return {
|
|
1135
|
+
"module_id": module.module_id,
|
|
1136
|
+
"source_document_id": module.document_id,
|
|
1137
|
+
"logical_source_name": module.logical_name,
|
|
1138
|
+
"document_id": module.document_id,
|
|
1139
|
+
"logical_name": module.logical_name,
|
|
1140
|
+
"bundle_ordinal": module.bundle_ordinal,
|
|
1141
|
+
"is_primary": module.is_primary,
|
|
1142
|
+
"document_plan_node_id": module.document_plan_node_id,
|
|
1143
|
+
"import_node_ids": list(module.import_node_ids),
|
|
1144
|
+
"function_node_ids": list(module.function_ids),
|
|
1145
|
+
"content_fingerprint": source_document.get("utf8_sha256"),
|
|
1146
|
+
"eligible": True,
|
|
1147
|
+
"diagnostic_code": None,
|
|
1148
|
+
"reason": None,
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
def _fact_table(
|
|
1152
|
+
self,
|
|
1153
|
+
table_id: str,
|
|
1154
|
+
key_domain: str,
|
|
1155
|
+
values: list[dict[str, Any]],
|
|
1156
|
+
key_field: str,
|
|
1157
|
+
) -> dict[str, Any]:
|
|
1158
|
+
records = []
|
|
1159
|
+
for value in sorted(values, key=lambda item: item[key_field]):
|
|
1160
|
+
source_ids: list[str] = []
|
|
1161
|
+
for field_name in (
|
|
1162
|
+
"document_plan_node_id",
|
|
1163
|
+
"import_node_id",
|
|
1164
|
+
"alias_node_id",
|
|
1165
|
+
"target_function_node_id",
|
|
1166
|
+
"function_node_id",
|
|
1167
|
+
"initialization_node_id",
|
|
1168
|
+
):
|
|
1169
|
+
item = value.get(field_name)
|
|
1170
|
+
if isinstance(item, str):
|
|
1171
|
+
source_ids.append(item)
|
|
1172
|
+
for field_name in ("import_node_ids", "function_node_ids"):
|
|
1173
|
+
source_ids.extend(
|
|
1174
|
+
item for item in value.get(field_name, ()) if isinstance(item, str)
|
|
1175
|
+
)
|
|
1176
|
+
for binding in value.get("imported_bindings", ()):
|
|
1177
|
+
target = binding.get("target_function_node_id") if isinstance(binding, dict) else None
|
|
1178
|
+
if isinstance(target, str):
|
|
1179
|
+
source_ids.append(target)
|
|
1180
|
+
source_ids = list(dict.fromkeys(source_ids))
|
|
1181
|
+
records.append(
|
|
1182
|
+
{
|
|
1183
|
+
"key": value[key_field],
|
|
1184
|
+
"value": value,
|
|
1185
|
+
"provenance": {
|
|
1186
|
+
"source_node_ids": source_ids,
|
|
1187
|
+
"evidence": ["explicit-sourcebundle-closed-world", "deterministic-module-resolution"],
|
|
1188
|
+
},
|
|
1189
|
+
}
|
|
1190
|
+
)
|
|
1191
|
+
return {
|
|
1192
|
+
"schema_version": "fact-table/0.12",
|
|
1193
|
+
"table_id": table_id,
|
|
1194
|
+
"producer_stage": self.stage_id,
|
|
1195
|
+
"key_domain": key_domain,
|
|
1196
|
+
"completeness": "complete",
|
|
1197
|
+
"invalidation_dependencies": [self.invalidation_dependency],
|
|
1198
|
+
"records": records,
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
@staticmethod
|
|
1202
|
+
def _synthetic_node(
|
|
1203
|
+
node_id: str,
|
|
1204
|
+
kind: str,
|
|
1205
|
+
fields: dict[str, Any],
|
|
1206
|
+
origins: tuple[str, ...],
|
|
1207
|
+
) -> dict[str, Any]:
|
|
1208
|
+
return {
|
|
1209
|
+
"node_id": node_id,
|
|
1210
|
+
"kind": kind,
|
|
1211
|
+
"fields": fields,
|
|
1212
|
+
"provenance": {
|
|
1213
|
+
"origin_kind": "synthetic",
|
|
1214
|
+
"source_span": None,
|
|
1215
|
+
"origin_node_ids": list(origins),
|
|
1216
|
+
},
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
def _span(self, module: _Module, node: dict[str, Any] | None) -> dict[str, Any]:
|
|
1220
|
+
if node is not None:
|
|
1221
|
+
span = node.get("provenance", {}).get("source_span")
|
|
1222
|
+
if isinstance(span, dict):
|
|
1223
|
+
return _thaw(span)
|
|
1224
|
+
return {
|
|
1225
|
+
"document_id": module.document_id,
|
|
1226
|
+
"start": {"line": 1, "column": 0, "offset": 0},
|
|
1227
|
+
"end": {"line": 1, "column": 0, "offset": 0},
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
def _reject(
|
|
1231
|
+
self,
|
|
1232
|
+
module: _Module,
|
|
1233
|
+
code: str,
|
|
1234
|
+
message: str,
|
|
1235
|
+
node: dict[str, Any] | None,
|
|
1236
|
+
*,
|
|
1237
|
+
related: tuple[dict[str, Any], ...] = (),
|
|
1238
|
+
) -> None:
|
|
1239
|
+
raise ModuleResolutionError(
|
|
1240
|
+
code,
|
|
1241
|
+
message,
|
|
1242
|
+
module_id=module.module_id,
|
|
1243
|
+
logical_name=module.logical_name,
|
|
1244
|
+
source_span=self._span(module, node),
|
|
1245
|
+
related_spans=related,
|
|
1246
|
+
)
|
|
1247
|
+
|
|
1248
|
+
def _check_canceled(self) -> None:
|
|
1249
|
+
if self.cancellation is not None and self.cancellation.is_canceled:
|
|
1250
|
+
raise ModuleAnalysisCanceled
|