coretrace-python-analyzer 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- coretrace_python/__init__.py +4 -0
- coretrace_python/__main__.py +4 -0
- coretrace_python/abstract/__init__.py +45 -0
- coretrace_python/abstract/constants.py +226 -0
- coretrace_python/abstract/heap.py +252 -0
- coretrace_python/abstract/ranges.py +285 -0
- coretrace_python/abstract/values.py +56 -0
- coretrace_python/analysis/__init__.py +31 -0
- coretrace_python/analysis/manager.py +165 -0
- coretrace_python/analysis/provider.py +73 -0
- coretrace_python/bundled/dependency/dependency_policy/dependency_policy.py +46 -0
- coretrace_python/bundled/dependency/dependency_policy/plugin.toml +9 -0
- coretrace_python/bundled/dependency/reachable_vulnerability/plugin.toml +9 -0
- coretrace_python/bundled/dependency/reachable_vulnerability/reachable_vulnerability.py +56 -0
- coretrace_python/bundled/dependency/sample_advisories/plugin.toml +9 -0
- coretrace_python/bundled/dependency/sample_advisories/sample_advisories.py +109 -0
- coretrace_python/bundled/dependency/vulnerable_dependency/plugin.toml +9 -0
- coretrace_python/bundled/dependency/vulnerable_dependency/vulnerable_dependency.py +43 -0
- coretrace_python/bundled/models/cli/cli_models.py +35 -0
- coretrace_python/bundled/models/cli/plugin.toml +9 -0
- coretrace_python/bundled/models/credentials/credential_models.py +43 -0
- coretrace_python/bundled/models/credentials/plugin.toml +9 -0
- coretrace_python/bundled/models/django/django_models.py +123 -0
- coretrace_python/bundled/models/django/plugin.toml +9 -0
- coretrace_python/bundled/models/fastapi/fastapi_models.py +29 -0
- coretrace_python/bundled/models/fastapi/plugin.toml +9 -0
- coretrace_python/bundled/models/flask/flask_models.py +53 -0
- coretrace_python/bundled/models/flask/plugin.toml +9 -0
- coretrace_python/bundled/models/http_clients/http_client_models.py +43 -0
- coretrace_python/bundled/models/http_clients/plugin.toml +9 -0
- coretrace_python/bundled/models/python_stdlib/plugin.toml +9 -0
- coretrace_python/bundled/models/python_stdlib/python_stdlib.py +68 -0
- coretrace_python/bundled/models/sqlalchemy/plugin.toml +9 -0
- coretrace_python/bundled/models/sqlalchemy/sqlalchemy_models.py +47 -0
- coretrace_python/bundled/secrets/config_secrets/config_secrets.py +38 -0
- coretrace_python/bundled/secrets/config_secrets/plugin.toml +9 -0
- coretrace_python/bundled/secrets/hardcoded_secrets/hardcoded_secrets.py +19 -0
- coretrace_python/bundled/secrets/hardcoded_secrets/plugin.toml +9 -0
- coretrace_python/bundled/security/command_injection/command_injection.py +17 -0
- coretrace_python/bundled/security/command_injection/plugin.toml +9 -0
- coretrace_python/bundled/security/insecure_deserialization/insecure_deserialization.py +17 -0
- coretrace_python/bundled/security/insecure_deserialization/plugin.toml +9 -0
- coretrace_python/bundled/security/open_redirect/open_redirect.py +17 -0
- coretrace_python/bundled/security/open_redirect/plugin.toml +9 -0
- coretrace_python/bundled/security/path_traversal/path_traversal.py +17 -0
- coretrace_python/bundled/security/path_traversal/plugin.toml +9 -0
- coretrace_python/bundled/security/plaintext_credentials/plaintext_credentials.py +21 -0
- coretrace_python/bundled/security/plaintext_credentials/plugin.toml +9 -0
- coretrace_python/bundled/security/sql_injection/plugin.toml +9 -0
- coretrace_python/bundled/security/sql_injection/sql_injection.py +17 -0
- coretrace_python/bundled/security/ssrf/plugin.toml +9 -0
- coretrace_python/bundled/security/ssrf/ssrf.py +17 -0
- coretrace_python/bundled/security/xss/plugin.toml +9 -0
- coretrace_python/bundled/security/xss/xss.py +17 -0
- coretrace_python/bundled/syntax/dangerous_eval/dangerous_eval.py +19 -0
- coretrace_python/bundled/syntax/dangerous_eval/plugin.toml +9 -0
- coretrace_python/bundled/syntax/flask_debug/flask_debug.py +63 -0
- coretrace_python/bundled/syntax/flask_debug/plugin.toml +9 -0
- coretrace_python/bundled/syntax/missing_timeout/missing_timeout.py +51 -0
- coretrace_python/bundled/syntax/missing_timeout/plugin.toml +9 -0
- coretrace_python/bundled/syntax/weak_crypto/plugin.toml +9 -0
- coretrace_python/bundled/syntax/weak_crypto/weak_crypto.py +19 -0
- coretrace_python/cache.py +310 -0
- coretrace_python/cfg/__init__.py +46 -0
- coretrace_python/cfg/builder.py +589 -0
- coretrace_python/cfg/dominance.py +183 -0
- coretrace_python/cfg/model.py +166 -0
- coretrace_python/cli.py +216 -0
- coretrace_python/dataflow/__init__.py +29 -0
- coretrace_python/dataflow/lattice.py +78 -0
- coretrace_python/dataflow/solver.py +96 -0
- coretrace_python/dependency/__init__.py +44 -0
- coretrace_python/dependency/advisories.py +168 -0
- coretrace_python/dependency/correlation.py +87 -0
- coretrace_python/dependency/graph.py +274 -0
- coretrace_python/dependency/policy.py +65 -0
- coretrace_python/dependency/sbom.py +65 -0
- coretrace_python/engine.py +688 -0
- coretrace_python/findings/__init__.py +13 -0
- coretrace_python/findings/coverage.py +45 -0
- coretrace_python/findings/model.py +49 -0
- coretrace_python/findings/refutation.py +427 -0
- coretrace_python/frontend/__init__.py +18 -0
- coretrace_python/frontend/ast_adapter.py +447 -0
- coretrace_python/frontend/parser.py +23 -0
- coretrace_python/hir/__init__.py +5 -0
- coretrace_python/hir/nodes.py +584 -0
- coretrace_python/hir/visitors.py +36 -0
- coretrace_python/interprocedural/__init__.py +49 -0
- coretrace_python/interprocedural/callgraph.py +291 -0
- coretrace_python/interprocedural/modulegraph.py +218 -0
- coretrace_python/interprocedural/summaries.py +463 -0
- coretrace_python/ir/__init__.py +5 -0
- coretrace_python/ir/defuse.py +74 -0
- coretrace_python/ir/lowering.py +575 -0
- coretrace_python/ir/model.py +481 -0
- coretrace_python/ir/printer.py +201 -0
- coretrace_python/ir/ssa.py +277 -0
- coretrace_python/plugins/__init__.py +58 -0
- coretrace_python/plugins/api.py +153 -0
- coretrace_python/plugins/detectors.py +114 -0
- coretrace_python/plugins/loader.py +84 -0
- coretrace_python/plugins/manifest.py +112 -0
- coretrace_python/plugins/registry.py +34 -0
- coretrace_python/plugins/secrets.py +330 -0
- coretrace_python/reporters/__init__.py +22 -0
- coretrace_python/reporters/json_format.py +48 -0
- coretrace_python/reporters/report.py +23 -0
- coretrace_python/reporters/sarif.py +70 -0
- coretrace_python/reporters/text.py +24 -0
- coretrace_python/semantic/__init__.py +9 -0
- coretrace_python/semantic/identity.py +39 -0
- coretrace_python/semantic/imports.py +131 -0
- coretrace_python/semantic/scopes.py +473 -0
- coretrace_python/semantic/symbols.py +87 -0
- coretrace_python/source/__init__.py +13 -0
- coretrace_python/source/manager.py +81 -0
- coretrace_python/source/model.py +57 -0
- coretrace_python/taint/__init__.py +55 -0
- coretrace_python/taint/engine.py +800 -0
- coretrace_python/taint/models.py +317 -0
- coretrace_python/taint/routes.py +99 -0
- coretrace_python_analyzer-0.1.0.dist-info/METADATA +74 -0
- coretrace_python_analyzer-0.1.0.dist-info/RECORD +126 -0
- coretrace_python_analyzer-0.1.0.dist-info/WHEEL +4 -0
- coretrace_python_analyzer-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Canonical symbol identities (architecture §4.3).
|
|
2
|
+
|
|
3
|
+
A ``SymbolId`` names a Python object independently of how a file spells it: every way
|
|
4
|
+
of importing ``os.system`` yields ``python.os.system``. Framework models may live in
|
|
5
|
+
their own namespaces, such as ``flask.request.args``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True, order=True)
|
|
14
|
+
class SymbolId:
|
|
15
|
+
"""A stable, import-style-independent, namespace-qualified dotted name."""
|
|
16
|
+
|
|
17
|
+
canonical_name: str
|
|
18
|
+
|
|
19
|
+
def __post_init__(self) -> None:
|
|
20
|
+
components = self.canonical_name.split(".")
|
|
21
|
+
if len(components) < 2:
|
|
22
|
+
raise ValueError("a canonical symbol needs a namespace and a path")
|
|
23
|
+
if not all(component.isidentifier() for component in components):
|
|
24
|
+
raise ValueError(f"invalid canonical symbol: {self.canonical_name!r}")
|
|
25
|
+
|
|
26
|
+
@classmethod
|
|
27
|
+
def from_python_path(cls, path: str) -> SymbolId:
|
|
28
|
+
normalized_path = path.removeprefix("python.")
|
|
29
|
+
if not normalized_path or normalized_path.startswith("."):
|
|
30
|
+
raise ValueError("a Python symbol path cannot be empty")
|
|
31
|
+
return cls(f"python.{normalized_path}")
|
|
32
|
+
|
|
33
|
+
def attribute(self, name: str) -> SymbolId:
|
|
34
|
+
if not name or "." in name:
|
|
35
|
+
raise ValueError("an attribute name must be one non-empty component")
|
|
36
|
+
return SymbolId(f"{self.canonical_name}.{name}")
|
|
37
|
+
|
|
38
|
+
def __str__(self) -> str:
|
|
39
|
+
return self.canonical_name
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""Import analysis: per-scope bindings from import statements (architecture §4.2).
|
|
2
|
+
|
|
3
|
+
No module is ever imported. Each binding maps the local name an import introduces to
|
|
4
|
+
the canonical path of the object it refers to, resolved statically from the statement
|
|
5
|
+
and, for relative imports, from the importing module's dotted name.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Mapping
|
|
11
|
+
from types import MappingProxyType
|
|
12
|
+
from typing import ClassVar
|
|
13
|
+
|
|
14
|
+
from coretrace_python.analysis import Analysis, AnalysisContext, AnyAnalysis
|
|
15
|
+
from coretrace_python.hir import nodes
|
|
16
|
+
from coretrace_python.semantic.identity import SymbolId
|
|
17
|
+
from coretrace_python.semantic.scopes import ScopeAnalysis, ScopeId, ScopeTable
|
|
18
|
+
|
|
19
|
+
_NO_BINDINGS: Mapping[str, SymbolId] = MappingProxyType({})
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ImportResolutionError(Exception):
|
|
23
|
+
"""A source-located import that cannot be resolved statically."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ImportTable:
|
|
27
|
+
"""Immutable import bindings and wildcard imports, keyed by scope."""
|
|
28
|
+
|
|
29
|
+
def __init__(
|
|
30
|
+
self,
|
|
31
|
+
bindings: Mapping[ScopeId, Mapping[str, SymbolId]],
|
|
32
|
+
wildcards: Mapping[ScopeId, tuple[SymbolId, ...]],
|
|
33
|
+
) -> None:
|
|
34
|
+
self._bindings = MappingProxyType(
|
|
35
|
+
{scope_id: MappingProxyType(dict(found)) for scope_id, found in bindings.items()}
|
|
36
|
+
)
|
|
37
|
+
self._wildcards = MappingProxyType(dict(wildcards))
|
|
38
|
+
|
|
39
|
+
def bindings(self, scope_id: ScopeId) -> Mapping[str, SymbolId]:
|
|
40
|
+
return self._bindings.get(scope_id, _NO_BINDINGS)
|
|
41
|
+
|
|
42
|
+
def wildcards(self, scope_id: ScopeId) -> tuple[SymbolId, ...]:
|
|
43
|
+
return self._wildcards.get(scope_id, ())
|
|
44
|
+
|
|
45
|
+
def all_symbols(self) -> tuple[SymbolId, ...]:
|
|
46
|
+
"""Every symbol any import in the module refers to, in every scope."""
|
|
47
|
+
|
|
48
|
+
found: list[SymbolId] = []
|
|
49
|
+
for bindings in self._bindings.values():
|
|
50
|
+
found.extend(bindings.values())
|
|
51
|
+
for wildcards in self._wildcards.values():
|
|
52
|
+
found.extend(wildcards)
|
|
53
|
+
return tuple(found)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class _Collector:
|
|
57
|
+
def __init__(self, module: nodes.Module, scopes: ScopeTable) -> None:
|
|
58
|
+
self.package = module.name if module.is_package else module.name.rpartition(".")[0]
|
|
59
|
+
self.scopes = scopes
|
|
60
|
+
self.bindings: dict[ScopeId, dict[str, SymbolId]] = {}
|
|
61
|
+
self.wildcards: dict[ScopeId, list[SymbolId]] = {}
|
|
62
|
+
self.body(module.body, scopes.module_scope.id)
|
|
63
|
+
|
|
64
|
+
def body(self, statements: tuple[nodes.Statement, ...], scope_id: ScopeId) -> None:
|
|
65
|
+
for statement in statements:
|
|
66
|
+
if isinstance(statement, nodes.Import):
|
|
67
|
+
for alias in statement.names:
|
|
68
|
+
# ``import a.b.c`` binds ``a``; ``import a.b.c as d`` binds the full path.
|
|
69
|
+
top_level = alias.name.partition(".")[0]
|
|
70
|
+
if alias.as_name is None:
|
|
71
|
+
self.bind(scope_id, top_level, top_level)
|
|
72
|
+
else:
|
|
73
|
+
self.bind(scope_id, alias.as_name, alias.name)
|
|
74
|
+
elif isinstance(statement, nodes.ImportFrom):
|
|
75
|
+
base = self.base_module(statement)
|
|
76
|
+
for alias in statement.names:
|
|
77
|
+
if alias.name == "*":
|
|
78
|
+
self.wildcards.setdefault(scope_id, []).append(
|
|
79
|
+
SymbolId.from_python_path(base)
|
|
80
|
+
)
|
|
81
|
+
else:
|
|
82
|
+
self.bind(scope_id, alias.as_name or alias.name, f"{base}.{alias.name}")
|
|
83
|
+
elif isinstance(statement, (nodes.Function, nodes.Class)):
|
|
84
|
+
self.body(statement.body, self.scopes.scope_for(statement).id)
|
|
85
|
+
elif isinstance(statement, nodes.If):
|
|
86
|
+
self.body(statement.body, scope_id)
|
|
87
|
+
self.body(statement.orelse, scope_id)
|
|
88
|
+
elif isinstance(statement, (nodes.While, nodes.For, nodes.With)):
|
|
89
|
+
self.body(statement.body, scope_id)
|
|
90
|
+
elif isinstance(statement, nodes.Try):
|
|
91
|
+
self.body(statement.body, scope_id)
|
|
92
|
+
for handler in statement.handlers:
|
|
93
|
+
self.body(handler.body, scope_id)
|
|
94
|
+
self.body(statement.orelse, scope_id)
|
|
95
|
+
self.body(statement.finalbody, scope_id)
|
|
96
|
+
|
|
97
|
+
def base_module(self, statement: nodes.ImportFrom) -> str:
|
|
98
|
+
if not statement.level:
|
|
99
|
+
assert statement.module is not None, "absolute import without a module"
|
|
100
|
+
return statement.module
|
|
101
|
+
parts = self.package.split(".") if self.package else []
|
|
102
|
+
kept = len(parts) - (statement.level - 1)
|
|
103
|
+
if kept < 1:
|
|
104
|
+
raise ImportResolutionError(
|
|
105
|
+
f"{statement.span.display()}: relative import beyond top-level package"
|
|
106
|
+
)
|
|
107
|
+
base = ".".join(parts[:kept])
|
|
108
|
+
return f"{base}.{statement.module}" if statement.module else base
|
|
109
|
+
|
|
110
|
+
def bind(self, scope_id: ScopeId, local_name: str, canonical_path: str) -> None:
|
|
111
|
+
symbol = SymbolId.from_python_path(canonical_path)
|
|
112
|
+
self.bindings.setdefault(scope_id, {})[local_name] = symbol
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def analyze_imports(module: nodes.Module, scopes: ScopeTable) -> ImportTable:
|
|
116
|
+
"""Collect every import binding of ``module`` without importing anything."""
|
|
117
|
+
|
|
118
|
+
collector = _Collector(module, scopes)
|
|
119
|
+
return ImportTable(
|
|
120
|
+
collector.bindings,
|
|
121
|
+
{scope_id: tuple(found) for scope_id, found in collector.wildcards.items()},
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class ImportAnalysis(Analysis[ImportTable]):
|
|
126
|
+
name: ClassVar[str] = "semantic.imports"
|
|
127
|
+
requires: ClassVar[frozenset[AnyAnalysis]] = frozenset({ScopeAnalysis})
|
|
128
|
+
|
|
129
|
+
@classmethod
|
|
130
|
+
def compute(cls, ctx: AnalysisContext) -> ImportTable:
|
|
131
|
+
return analyze_imports(ctx.module, ctx.get(ScopeAnalysis))
|
|
@@ -0,0 +1,473 @@
|
|
|
1
|
+
"""Lexical scope analysis over PyHIR (architecture §4.1).
|
|
2
|
+
|
|
3
|
+
Python decides where a name lives per scope, not per statement: any binding anywhere in
|
|
4
|
+
a function makes the name local to the whole function, ``global`` and ``nonlocal``
|
|
5
|
+
redirect a name to another scope, class bodies are invisible to the functions they
|
|
6
|
+
contain, and comprehensions get a scope of their own. The analysis therefore runs in
|
|
7
|
+
two passes: collect every binding of every scope, then resolve names against the
|
|
8
|
+
finished tree.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
from collections.abc import Mapping
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from enum import Enum
|
|
16
|
+
from types import MappingProxyType
|
|
17
|
+
from typing import ClassVar
|
|
18
|
+
|
|
19
|
+
from coretrace_python.analysis import Analysis, AnalysisContext
|
|
20
|
+
from coretrace_python.hir import nodes
|
|
21
|
+
from coretrace_python.source import SourceSpan
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ScopeKind(Enum):
|
|
25
|
+
MODULE = "module"
|
|
26
|
+
FUNCTION = "function"
|
|
27
|
+
CLASS = "class"
|
|
28
|
+
COMPREHENSION = "comprehension"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class BindingKind(Enum):
|
|
32
|
+
PARAMETER = "parameter"
|
|
33
|
+
LOCAL = "local"
|
|
34
|
+
IMPORT = "import"
|
|
35
|
+
FUNCTION = "function"
|
|
36
|
+
CLASS = "class"
|
|
37
|
+
GLOBAL = "global"
|
|
38
|
+
NONLOCAL = "nonlocal"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class ResolutionKind(Enum):
|
|
42
|
+
LOCAL = "local"
|
|
43
|
+
FREE = "free"
|
|
44
|
+
GLOBAL = "global"
|
|
45
|
+
UNBOUND = "unbound"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
_REDIRECTIONS = frozenset({BindingKind.GLOBAL, BindingKind.NONLOCAL})
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class ScopeError(Exception):
|
|
52
|
+
"""A source-located scoping rule violation, such as an invalid ``nonlocal``."""
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass(frozen=True, order=True)
|
|
56
|
+
class ScopeId:
|
|
57
|
+
"""Stable identity of a scope: source ID, then ``name#ordinal`` per nesting level."""
|
|
58
|
+
|
|
59
|
+
value: str
|
|
60
|
+
|
|
61
|
+
def child(self, name: str, ordinal: int) -> ScopeId:
|
|
62
|
+
return ScopeId(f"{self.value}::{name}#{ordinal}")
|
|
63
|
+
|
|
64
|
+
def __str__(self) -> str:
|
|
65
|
+
return self.value
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass(frozen=True)
|
|
69
|
+
class Binding:
|
|
70
|
+
name: str
|
|
71
|
+
kind: BindingKind
|
|
72
|
+
span: SourceSpan
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@dataclass(frozen=True)
|
|
76
|
+
class Scope:
|
|
77
|
+
id: ScopeId
|
|
78
|
+
kind: ScopeKind
|
|
79
|
+
name: str
|
|
80
|
+
parent: ScopeId | None
|
|
81
|
+
span: SourceSpan
|
|
82
|
+
bindings: Mapping[str, Binding]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@dataclass(frozen=True)
|
|
86
|
+
class Resolution:
|
|
87
|
+
kind: ResolutionKind
|
|
88
|
+
scope: ScopeId | None
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class ScopeTable:
|
|
92
|
+
"""Immutable scope tree with Python name resolution."""
|
|
93
|
+
|
|
94
|
+
def __init__(self, scopes: tuple[Scope, ...], spans: Mapping[SourceSpan, ScopeId]) -> None:
|
|
95
|
+
self._scopes: Mapping[ScopeId, Scope] = MappingProxyType({s.id: s for s in scopes})
|
|
96
|
+
children: dict[ScopeId, list[Scope]] = {scope.id: [] for scope in scopes}
|
|
97
|
+
for scope in scopes:
|
|
98
|
+
if scope.parent is not None:
|
|
99
|
+
children[scope.parent].append(scope)
|
|
100
|
+
self._children: Mapping[ScopeId, tuple[Scope, ...]] = MappingProxyType(
|
|
101
|
+
{scope_id: tuple(found) for scope_id, found in children.items()}
|
|
102
|
+
)
|
|
103
|
+
self._spans = MappingProxyType(dict(spans))
|
|
104
|
+
self._module = scopes[0]
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def module_scope(self) -> Scope:
|
|
108
|
+
return self._module
|
|
109
|
+
|
|
110
|
+
def scope(self, scope_id: ScopeId) -> Scope:
|
|
111
|
+
return self._scopes[scope_id]
|
|
112
|
+
|
|
113
|
+
def children(self, scope_id: ScopeId) -> tuple[Scope, ...]:
|
|
114
|
+
return self._children[scope_id]
|
|
115
|
+
|
|
116
|
+
def scope_for(self, node: nodes.Function | nodes.Class | nodes.Comprehension) -> Scope:
|
|
117
|
+
"""Return the scope introduced by a definition or comprehension node."""
|
|
118
|
+
|
|
119
|
+
return self._scopes[self._spans[node.span]]
|
|
120
|
+
|
|
121
|
+
def resolve(self, scope_id: ScopeId, name: str) -> Resolution:
|
|
122
|
+
scope = self.scope(scope_id)
|
|
123
|
+
binding = scope.bindings.get(name)
|
|
124
|
+
if binding is not None:
|
|
125
|
+
if binding.kind is BindingKind.GLOBAL:
|
|
126
|
+
return self._global(name)
|
|
127
|
+
if binding.kind is BindingKind.NONLOCAL:
|
|
128
|
+
target = self._enclosing_function_binding(scope, name)
|
|
129
|
+
assert target is not None, "validated at construction"
|
|
130
|
+
return Resolution(ResolutionKind.FREE, target)
|
|
131
|
+
if scope.kind is ScopeKind.MODULE:
|
|
132
|
+
return Resolution(ResolutionKind.GLOBAL, scope.id)
|
|
133
|
+
return Resolution(ResolutionKind.LOCAL, scope.id)
|
|
134
|
+
|
|
135
|
+
current = self._parent(scope)
|
|
136
|
+
while current is not None:
|
|
137
|
+
if current.kind is ScopeKind.MODULE:
|
|
138
|
+
return self._global(name)
|
|
139
|
+
if current.kind is not ScopeKind.CLASS:
|
|
140
|
+
enclosing = current.bindings.get(name)
|
|
141
|
+
if enclosing is not None and enclosing.kind is BindingKind.GLOBAL:
|
|
142
|
+
return self._global(name)
|
|
143
|
+
if enclosing is not None and enclosing.kind is not BindingKind.NONLOCAL:
|
|
144
|
+
return Resolution(ResolutionKind.FREE, current.id)
|
|
145
|
+
current = self._parent(current)
|
|
146
|
+
return Resolution(ResolutionKind.UNBOUND, None)
|
|
147
|
+
|
|
148
|
+
def _global(self, name: str) -> Resolution:
|
|
149
|
+
if name in self._module.bindings:
|
|
150
|
+
return Resolution(ResolutionKind.GLOBAL, self._module.id)
|
|
151
|
+
return Resolution(ResolutionKind.UNBOUND, None)
|
|
152
|
+
|
|
153
|
+
def _parent(self, scope: Scope) -> Scope | None:
|
|
154
|
+
return None if scope.parent is None else self._scopes[scope.parent]
|
|
155
|
+
|
|
156
|
+
def _enclosing_function_binding(self, scope: Scope, name: str) -> ScopeId | None:
|
|
157
|
+
current = self._parent(scope)
|
|
158
|
+
while current is not None and current.kind is not ScopeKind.MODULE:
|
|
159
|
+
if current.kind is ScopeKind.FUNCTION:
|
|
160
|
+
binding = current.bindings.get(name)
|
|
161
|
+
if binding is not None and binding.kind not in _REDIRECTIONS:
|
|
162
|
+
return current.id
|
|
163
|
+
current = self._parent(current)
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# --------------------------------------------------------------------------- construction
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class _ScopeBuilder:
|
|
171
|
+
def __init__(
|
|
172
|
+
self,
|
|
173
|
+
scope_id: ScopeId,
|
|
174
|
+
kind: ScopeKind,
|
|
175
|
+
name: str,
|
|
176
|
+
parent: _ScopeBuilder | None,
|
|
177
|
+
span: SourceSpan,
|
|
178
|
+
) -> None:
|
|
179
|
+
self.id = scope_id
|
|
180
|
+
self.kind = kind
|
|
181
|
+
self.name = name
|
|
182
|
+
self.parent = parent
|
|
183
|
+
self.span = span
|
|
184
|
+
self.bindings: dict[str, Binding] = {}
|
|
185
|
+
self.child_count = 0
|
|
186
|
+
self.declarations: list[tuple[nodes.Nonlocal, str]] = []
|
|
187
|
+
|
|
188
|
+
def bind(self, name: str, kind: BindingKind, span: SourceSpan) -> None:
|
|
189
|
+
existing = self.bindings.get(name)
|
|
190
|
+
if existing is None:
|
|
191
|
+
self.bindings[name] = Binding(name, kind, span)
|
|
192
|
+
elif kind in (BindingKind.GLOBAL, BindingKind.NONLOCAL):
|
|
193
|
+
# Declarations win over ordinary bindings regardless of statement order.
|
|
194
|
+
self.bindings[name] = Binding(name, kind, span)
|
|
195
|
+
|
|
196
|
+
def child(self, kind: ScopeKind, name: str, span: SourceSpan) -> _ScopeBuilder:
|
|
197
|
+
builder = _ScopeBuilder(self.id.child(name, self.child_count), kind, name, self, span)
|
|
198
|
+
self.child_count += 1
|
|
199
|
+
return builder
|
|
200
|
+
|
|
201
|
+
def freeze(self) -> Scope:
|
|
202
|
+
return Scope(
|
|
203
|
+
id=self.id,
|
|
204
|
+
kind=self.kind,
|
|
205
|
+
name=self.name,
|
|
206
|
+
parent=None if self.parent is None else self.parent.id,
|
|
207
|
+
span=self.span,
|
|
208
|
+
bindings=MappingProxyType(dict(self.bindings)),
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
_COMPREHENSION_NAMES = {"list": "<listcomp>", "set": "<setcomp>", "generator": "<genexpr>", "dict": "<dictcomp>"}
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class _Collector:
|
|
216
|
+
def __init__(self, module: nodes.Module) -> None:
|
|
217
|
+
self.module = _ScopeBuilder(
|
|
218
|
+
ScopeId(str(module.span.source_id)), ScopeKind.MODULE, "<module>", None, module.span
|
|
219
|
+
)
|
|
220
|
+
self.builders: list[_ScopeBuilder] = [self.module]
|
|
221
|
+
self.spans: dict[SourceSpan, ScopeId] = {}
|
|
222
|
+
self.body(module.body, self.module)
|
|
223
|
+
|
|
224
|
+
def body(self, statements: tuple[nodes.Statement, ...], scope: _ScopeBuilder) -> None:
|
|
225
|
+
for statement in statements:
|
|
226
|
+
self.statement(statement, scope)
|
|
227
|
+
|
|
228
|
+
def statement(self, node: nodes.Statement, scope: _ScopeBuilder) -> None:
|
|
229
|
+
if isinstance(node, nodes.Assign | nodes.AugAssign):
|
|
230
|
+
self.expression(node.value, scope)
|
|
231
|
+
self.target(node.target, scope)
|
|
232
|
+
elif isinstance(node, nodes.Assert):
|
|
233
|
+
self.expression(node.test, scope)
|
|
234
|
+
if node.message is not None:
|
|
235
|
+
self.expression(node.message, scope)
|
|
236
|
+
elif isinstance(node, nodes.With):
|
|
237
|
+
for item in node.items:
|
|
238
|
+
self.expression(item.context, scope)
|
|
239
|
+
if item.target is not None:
|
|
240
|
+
scope.bind(item.target.identifier, BindingKind.LOCAL, item.target.span)
|
|
241
|
+
self.body(node.body, scope)
|
|
242
|
+
elif isinstance(node, nodes.Try):
|
|
243
|
+
self.body(node.body, scope)
|
|
244
|
+
for handler in node.handlers:
|
|
245
|
+
if handler.type is not None:
|
|
246
|
+
self.expression(handler.type, scope)
|
|
247
|
+
if handler.name is not None:
|
|
248
|
+
scope.bind(handler.name, BindingKind.LOCAL, handler.span)
|
|
249
|
+
self.body(handler.body, scope)
|
|
250
|
+
self.body(node.orelse, scope)
|
|
251
|
+
self.body(node.finalbody, scope)
|
|
252
|
+
elif isinstance(node, nodes.EnterWith | nodes.ExitWith | nodes.EnterHandler):
|
|
253
|
+
pass
|
|
254
|
+
elif isinstance(node, nodes.Return):
|
|
255
|
+
if node.value is not None:
|
|
256
|
+
self.expression(node.value, scope)
|
|
257
|
+
elif isinstance(node, nodes.ExpressionStatement):
|
|
258
|
+
self.expression(node.expression, scope)
|
|
259
|
+
elif isinstance(node, nodes.Import):
|
|
260
|
+
for alias in node.names:
|
|
261
|
+
local_name = alias.as_name or alias.name.partition(".")[0]
|
|
262
|
+
scope.bind(local_name, BindingKind.IMPORT, alias.span)
|
|
263
|
+
elif isinstance(node, nodes.ImportFrom):
|
|
264
|
+
for alias in node.names:
|
|
265
|
+
if alias.name != "*":
|
|
266
|
+
scope.bind(alias.as_name or alias.name, BindingKind.IMPORT, alias.span)
|
|
267
|
+
elif isinstance(node, nodes.Global):
|
|
268
|
+
if scope.kind is not ScopeKind.MODULE:
|
|
269
|
+
for name in node.names:
|
|
270
|
+
scope.bind(name, BindingKind.GLOBAL, node.span)
|
|
271
|
+
self.module.bind(name, BindingKind.LOCAL, node.span)
|
|
272
|
+
elif isinstance(node, nodes.Nonlocal):
|
|
273
|
+
if scope.kind is ScopeKind.MODULE:
|
|
274
|
+
raise ScopeError(f"{node.span.display()}: nonlocal declaration at module level")
|
|
275
|
+
for name in node.names:
|
|
276
|
+
scope.bind(name, BindingKind.NONLOCAL, node.span)
|
|
277
|
+
scope.declarations.append((node, name))
|
|
278
|
+
elif isinstance(node, nodes.Function):
|
|
279
|
+
for decorator in node.decorators:
|
|
280
|
+
self.expression(decorator, scope)
|
|
281
|
+
for parameter in node.parameters:
|
|
282
|
+
if parameter.default is not None:
|
|
283
|
+
self.expression(parameter.default, scope)
|
|
284
|
+
scope.bind(node.name, BindingKind.FUNCTION, node.span)
|
|
285
|
+
function = self.open(scope, ScopeKind.FUNCTION, node.name, node.span)
|
|
286
|
+
for parameter in node.parameters:
|
|
287
|
+
function.bind(parameter.name, BindingKind.PARAMETER, parameter.span)
|
|
288
|
+
self.body(node.body, function)
|
|
289
|
+
elif isinstance(node, nodes.Class):
|
|
290
|
+
for decorator in node.decorators:
|
|
291
|
+
self.expression(decorator, scope)
|
|
292
|
+
for base in node.bases:
|
|
293
|
+
self.expression(base, scope)
|
|
294
|
+
scope.bind(node.name, BindingKind.CLASS, node.span)
|
|
295
|
+
self.body(node.body, self.open(scope, ScopeKind.CLASS, node.name, node.span))
|
|
296
|
+
elif isinstance(node, nodes.If | nodes.While):
|
|
297
|
+
self.expression(node.condition, scope)
|
|
298
|
+
self.body(node.body, scope)
|
|
299
|
+
self.body(node.orelse, scope)
|
|
300
|
+
elif isinstance(node, nodes.For):
|
|
301
|
+
self.expression(node.iterable, scope)
|
|
302
|
+
scope.bind(node.target.identifier, BindingKind.LOCAL, node.target.span)
|
|
303
|
+
self.body(node.body, scope)
|
|
304
|
+
self.body(node.orelse, scope)
|
|
305
|
+
elif isinstance(node, nodes.Match):
|
|
306
|
+
self.expression(node.subject, scope)
|
|
307
|
+
for case in node.cases:
|
|
308
|
+
self.pattern(case.pattern, scope)
|
|
309
|
+
if case.guard is not None:
|
|
310
|
+
self.expression(case.guard, scope)
|
|
311
|
+
self.body(case.body, scope)
|
|
312
|
+
elif isinstance(node, nodes.Raise):
|
|
313
|
+
if node.exception is not None:
|
|
314
|
+
self.expression(node.exception, scope)
|
|
315
|
+
if node.cause is not None:
|
|
316
|
+
self.expression(node.cause, scope)
|
|
317
|
+
elif isinstance(node, nodes.Delete):
|
|
318
|
+
for target in node.targets:
|
|
319
|
+
self.target(target, scope)
|
|
320
|
+
elif isinstance(node, nodes.Declaration):
|
|
321
|
+
scope.bind(node.name, BindingKind.LOCAL, node.span)
|
|
322
|
+
elif not isinstance(node, nodes.Pass | nodes.Break | nodes.Continue):
|
|
323
|
+
raise TypeError(f"unknown statement: {node!r}")
|
|
324
|
+
|
|
325
|
+
def expression(self, node: nodes.Expression, scope: _ScopeBuilder) -> None:
|
|
326
|
+
if isinstance(node, (nodes.Name, nodes.Constant)):
|
|
327
|
+
return
|
|
328
|
+
if isinstance(node, (nodes.BinaryOp, nodes.Compare)):
|
|
329
|
+
self.expression(node.left, scope)
|
|
330
|
+
self.expression(node.right, scope)
|
|
331
|
+
elif isinstance(node, nodes.UnaryOp):
|
|
332
|
+
self.expression(node.operand, scope)
|
|
333
|
+
elif isinstance(node, nodes.Attribute):
|
|
334
|
+
self.expression(node.value, scope)
|
|
335
|
+
elif isinstance(node, nodes.Subscript):
|
|
336
|
+
self.expression(node.value, scope)
|
|
337
|
+
self.expression(node.key, scope)
|
|
338
|
+
elif isinstance(node, nodes.Call):
|
|
339
|
+
self.expression(node.callee, scope)
|
|
340
|
+
for argument in node.arguments:
|
|
341
|
+
self.expression(argument, scope)
|
|
342
|
+
for keyword in node.keywords:
|
|
343
|
+
self.expression(keyword.value, scope)
|
|
344
|
+
elif isinstance(node, nodes.Comprehension):
|
|
345
|
+
self.comprehension(node, scope)
|
|
346
|
+
elif isinstance(node, nodes.BoolOp):
|
|
347
|
+
for value in node.values:
|
|
348
|
+
self.expression(value, scope)
|
|
349
|
+
elif isinstance(node, nodes.Await):
|
|
350
|
+
self.expression(node.value, scope)
|
|
351
|
+
elif isinstance(node, nodes.Yield):
|
|
352
|
+
if node.value is not None:
|
|
353
|
+
self.expression(node.value, scope)
|
|
354
|
+
elif isinstance(node, nodes.Tuple | nodes.List):
|
|
355
|
+
for element in node.elements:
|
|
356
|
+
self.expression(element, scope)
|
|
357
|
+
elif isinstance(node, nodes.Dict):
|
|
358
|
+
for key, value in node.items:
|
|
359
|
+
if key is not None:
|
|
360
|
+
self.expression(key, scope)
|
|
361
|
+
self.expression(value, scope)
|
|
362
|
+
elif isinstance(node, nodes.FormattedString):
|
|
363
|
+
for part in node.parts:
|
|
364
|
+
self.expression(part, scope)
|
|
365
|
+
elif isinstance(node, nodes.Slice):
|
|
366
|
+
for bound in (node.lower, node.upper, node.step):
|
|
367
|
+
if bound is not None:
|
|
368
|
+
self.expression(bound, scope)
|
|
369
|
+
elif isinstance(node, nodes.Starred):
|
|
370
|
+
self.expression(node.value, scope)
|
|
371
|
+
elif isinstance(node, nodes.Set):
|
|
372
|
+
for element in node.elements:
|
|
373
|
+
self.expression(element, scope)
|
|
374
|
+
elif isinstance(node, nodes.Conditional):
|
|
375
|
+
self.expression(node.test, scope)
|
|
376
|
+
self.expression(node.body, scope)
|
|
377
|
+
self.expression(node.orelse, scope)
|
|
378
|
+
elif isinstance(node, nodes.Lambda):
|
|
379
|
+
for parameter in node.parameters:
|
|
380
|
+
if parameter.default is not None:
|
|
381
|
+
self.expression(parameter.default, scope)
|
|
382
|
+
inner = self.open(scope, ScopeKind.FUNCTION, "<lambda>", node.span)
|
|
383
|
+
for parameter in node.parameters:
|
|
384
|
+
inner.bind(parameter.name, BindingKind.PARAMETER, parameter.span)
|
|
385
|
+
self.expression(node.body, inner)
|
|
386
|
+
else:
|
|
387
|
+
raise TypeError(f"unknown expression: {node!r}")
|
|
388
|
+
|
|
389
|
+
def pattern(self, node: nodes.Pattern, scope: _ScopeBuilder) -> None:
|
|
390
|
+
if isinstance(node, nodes.ValuePattern):
|
|
391
|
+
self.expression(node.value, scope)
|
|
392
|
+
elif isinstance(node, nodes.CapturePattern):
|
|
393
|
+
scope.bind(node.name, BindingKind.LOCAL, node.span)
|
|
394
|
+
if node.pattern is not None:
|
|
395
|
+
self.pattern(node.pattern, scope)
|
|
396
|
+
elif isinstance(node, nodes.OrPattern):
|
|
397
|
+
for alternative in node.alternatives:
|
|
398
|
+
self.pattern(alternative, scope)
|
|
399
|
+
elif isinstance(node, nodes.StarPattern):
|
|
400
|
+
if node.name is not None:
|
|
401
|
+
scope.bind(node.name, BindingKind.LOCAL, node.span)
|
|
402
|
+
elif isinstance(node, nodes.SequencePattern):
|
|
403
|
+
for sub in node.patterns:
|
|
404
|
+
self.pattern(sub, scope)
|
|
405
|
+
elif isinstance(node, nodes.MappingPattern):
|
|
406
|
+
for key in node.keys:
|
|
407
|
+
self.expression(key, scope)
|
|
408
|
+
for sub in node.patterns:
|
|
409
|
+
self.pattern(sub, scope)
|
|
410
|
+
if node.rest is not None:
|
|
411
|
+
scope.bind(node.rest, BindingKind.LOCAL, node.span)
|
|
412
|
+
elif isinstance(node, nodes.ClassPattern):
|
|
413
|
+
self.expression(node.cls, scope)
|
|
414
|
+
for sub in (*node.patterns, *node.keyword_patterns):
|
|
415
|
+
self.pattern(sub, scope)
|
|
416
|
+
|
|
417
|
+
def target(self, node: nodes.Target, scope: _ScopeBuilder) -> None:
|
|
418
|
+
if isinstance(node, nodes.Name):
|
|
419
|
+
scope.bind(node.identifier, BindingKind.LOCAL, node.span)
|
|
420
|
+
elif isinstance(node, nodes.Tuple):
|
|
421
|
+
for element in node.elements:
|
|
422
|
+
assert isinstance(element, nodes.Name | nodes.Attribute | nodes.Subscript | nodes.Tuple)
|
|
423
|
+
self.target(element, scope)
|
|
424
|
+
else:
|
|
425
|
+
self.expression(node, scope)
|
|
426
|
+
|
|
427
|
+
def comprehension(self, node: nodes.Comprehension, scope: _ScopeBuilder) -> None:
|
|
428
|
+
# The first iterable is evaluated in the enclosing scope; everything else runs
|
|
429
|
+
# inside the comprehension's own scope.
|
|
430
|
+
self.expression(node.generators[0].iterable, scope)
|
|
431
|
+
inner = self.open(scope, ScopeKind.COMPREHENSION, _COMPREHENSION_NAMES[node.kind], node.span)
|
|
432
|
+
for index, generator in enumerate(node.generators):
|
|
433
|
+
if index:
|
|
434
|
+
self.expression(generator.iterable, inner)
|
|
435
|
+
self.target(generator.target, inner)
|
|
436
|
+
for condition in generator.conditions:
|
|
437
|
+
self.expression(condition, inner)
|
|
438
|
+
if node.key is not None:
|
|
439
|
+
self.expression(node.key, inner)
|
|
440
|
+
self.expression(node.element, inner)
|
|
441
|
+
|
|
442
|
+
def open(
|
|
443
|
+
self, parent: _ScopeBuilder, kind: ScopeKind, name: str, span: SourceSpan
|
|
444
|
+
) -> _ScopeBuilder:
|
|
445
|
+
builder = parent.child(kind, name, span)
|
|
446
|
+
self.builders.append(builder)
|
|
447
|
+
self.spans[span] = builder.id
|
|
448
|
+
return builder
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def analyze_scopes(module: nodes.Module) -> ScopeTable:
|
|
452
|
+
"""Build the scope tree of ``module`` and validate its declarations."""
|
|
453
|
+
|
|
454
|
+
collector = _Collector(module)
|
|
455
|
+
analysis = ScopeTable(
|
|
456
|
+
tuple(builder.freeze() for builder in collector.builders), collector.spans
|
|
457
|
+
)
|
|
458
|
+
for builder in collector.builders:
|
|
459
|
+
for declaration, name in builder.declarations:
|
|
460
|
+
scope = analysis.scope(builder.id)
|
|
461
|
+
if analysis._enclosing_function_binding(scope, name) is None:
|
|
462
|
+
raise ScopeError(
|
|
463
|
+
f"{declaration.span.display()}: no binding for nonlocal {name!r}"
|
|
464
|
+
)
|
|
465
|
+
return analysis
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
class ScopeAnalysis(Analysis[ScopeTable]):
|
|
469
|
+
name: ClassVar[str] = "semantic.scopes"
|
|
470
|
+
|
|
471
|
+
@classmethod
|
|
472
|
+
def compute(cls, ctx: AnalysisContext) -> ScopeTable:
|
|
473
|
+
return analyze_scopes(ctx.module)
|