cg-code-graph 0.10.1__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.
- cg_code_graph-0.10.1.dist-info/METADATA +678 -0
- cg_code_graph-0.10.1.dist-info/RECORD +174 -0
- cg_code_graph-0.10.1.dist-info/WHEEL +5 -0
- cg_code_graph-0.10.1.dist-info/entry_points.txt +3 -0
- cg_code_graph-0.10.1.dist-info/licenses/LICENSE +21 -0
- cg_code_graph-0.10.1.dist-info/top_level.txt +1 -0
- codegraph/__init__.py +2 -0
- codegraph/aitools.py +129 -0
- codegraph/apps.py +76 -0
- codegraph/blindspots.py +428 -0
- codegraph/bridges.py +1701 -0
- codegraph/cli.py +725 -0
- codegraph/concepts.py +362 -0
- codegraph/config.py +559 -0
- codegraph/core/__init__.py +0 -0
- codegraph/core/cache.py +375 -0
- codegraph/core/detect.py +80 -0
- codegraph/core/extractors.py +187 -0
- codegraph/core/fsutil.py +61 -0
- codegraph/core/generated.py +575 -0
- codegraph/core/model.py +174 -0
- codegraph/core/paths.py +175 -0
- codegraph/core/plugin.py +160 -0
- codegraph/core/store.py +80 -0
- codegraph/core/syntax_errors.py +132 -0
- codegraph/coverage.py +928 -0
- codegraph/doctor.py +453 -0
- codegraph/external.py +613 -0
- codegraph/indexer.py +336 -0
- codegraph/link.py +434 -0
- codegraph/lint_async.py +524 -0
- codegraph/mcp_server.py +1303 -0
- codegraph/parity.py +473 -0
- codegraph/parity_structure.py +307 -0
- codegraph/payload.py +321 -0
- codegraph/plans.py +1285 -0
- codegraph/platform_scan.py +643 -0
- codegraph/platforms.py +1369 -0
- codegraph/plugins/__init__.py +0 -0
- codegraph/plugins/cfamily/__init__.py +0 -0
- codegraph/plugins/cfamily/plugin.py +930 -0
- codegraph/plugins/cfamily/syntax.py +881 -0
- codegraph/plugins/dart/__init__.py +0 -0
- codegraph/plugins/dart/bridges.py +345 -0
- codegraph/plugins/dart/extractor/bin/extract.dart +717 -0
- codegraph/plugins/dart/extractor/pubspec.lock +149 -0
- codegraph/plugins/dart/extractor/pubspec.yaml +7 -0
- codegraph/plugins/dart/http.py +904 -0
- codegraph/plugins/dart/models.py +308 -0
- codegraph/plugins/dart/plugin.py +625 -0
- codegraph/plugins/dart/program.py +907 -0
- codegraph/plugins/django/__init__.py +0 -0
- codegraph/plugins/django/extras.py +378 -0
- codegraph/plugins/django/models.py +508 -0
- codegraph/plugins/django/plugin.py +728 -0
- codegraph/plugins/django/schemas.py +339 -0
- codegraph/plugins/django/shapes.py +216 -0
- codegraph/plugins/django/urls.py +603 -0
- codegraph/plugins/express/__init__.py +0 -0
- codegraph/plugins/express/plugin.py +428 -0
- codegraph/plugins/flutter/__init__.py +0 -0
- codegraph/plugins/flutter/plugin.py +538 -0
- codegraph/plugins/kotlin/__init__.py +0 -0
- codegraph/plugins/kotlin/exact.py +457 -0
- codegraph/plugins/kotlin/plugin.py +1961 -0
- codegraph/plugins/kotlin/reparse.py +234 -0
- codegraph/plugins/laravel/__init__.py +0 -0
- codegraph/plugins/laravel/broadcast.py +351 -0
- codegraph/plugins/laravel/plugin.py +863 -0
- codegraph/plugins/laravel/tests.py +262 -0
- codegraph/plugins/laravel/values.py +728 -0
- codegraph/plugins/native/__init__.py +0 -0
- codegraph/plugins/native/gates.py +286 -0
- codegraph/plugins/native/runner.py +183 -0
- codegraph/plugins/native/scipread.py +194 -0
- codegraph/plugins/native/ts.py +54 -0
- codegraph/plugins/nest/__init__.py +0 -0
- codegraph/plugins/nest/plugin.py +654 -0
- codegraph/plugins/nextjs/__init__.py +0 -0
- codegraph/plugins/nextjs/plugin.py +336 -0
- codegraph/plugins/nuxt/__init__.py +0 -0
- codegraph/plugins/nuxt/plugin.py +308 -0
- codegraph/plugins/php/__init__.py +0 -0
- codegraph/plugins/php/extractor/composer.json +5 -0
- codegraph/plugins/php/extractor/composer.lock +76 -0
- codegraph/plugins/php/extractor/extract.php +743 -0
- codegraph/plugins/php/gating.py +573 -0
- codegraph/plugins/php/plugin.py +668 -0
- codegraph/plugins/php/strings.py +197 -0
- codegraph/plugins/python/__init__.py +0 -0
- codegraph/plugins/python/aitools.py +664 -0
- codegraph/plugins/python/external.py +245 -0
- codegraph/plugins/python/fields.py +107 -0
- codegraph/plugins/python/plugin.py +1733 -0
- codegraph/plugins/python/refs.py +485 -0
- codegraph/plugins/python/roots.py +412 -0
- codegraph/plugins/python/socketio.py +210 -0
- codegraph/plugins/python/subproc.py +864 -0
- codegraph/plugins/python/tests.py +1040 -0
- codegraph/plugins/python/values.py +179 -0
- codegraph/plugins/pyweb/__init__.py +0 -0
- codegraph/plugins/pyweb/plugin.py +1334 -0
- codegraph/plugins/pyweb/values.py +68 -0
- codegraph/plugins/rust/__init__.py +0 -0
- codegraph/plugins/rust/cargo.py +226 -0
- codegraph/plugins/rust/plugin.py +980 -0
- codegraph/plugins/rust/syntax.py +678 -0
- codegraph/plugins/scip/__init__.py +0 -0
- codegraph/plugins/scip/importer.py +129 -0
- codegraph/plugins/scip/scip.proto +962 -0
- codegraph/plugins/scip/scip_pb2.py +97 -0
- codegraph/plugins/stubs/__init__.py +0 -0
- codegraph/plugins/stubs/plugins.py +38 -0
- codegraph/plugins/swift/__init__.py +0 -0
- codegraph/plugins/swift/baseurl.py +109 -0
- codegraph/plugins/swift/exact.py +415 -0
- codegraph/plugins/swift/indexstore.py +209 -0
- codegraph/plugins/swift/packages.py +174 -0
- codegraph/plugins/swift/plugin.py +2890 -0
- codegraph/plugins/ts/__init__.py +0 -0
- codegraph/plugins/ts/baseurl.py +185 -0
- codegraph/plugins/ts/extractor/extract.mjs +2652 -0
- codegraph/plugins/ts/extractor/fw.mjs +685 -0
- codegraph/plugins/ts/extractor/package-lock.json +205 -0
- codegraph/plugins/ts/extractor/package.json +9 -0
- codegraph/plugins/ts/plugin.py +480 -0
- codegraph/plugins/tsweb/__init__.py +0 -0
- codegraph/plugins/tsweb/common.py +290 -0
- codegraph/plugins/tsweb/data.py +276 -0
- codegraph/presets/__init__.py +146 -0
- codegraph/presets/c_cpp.yaml +9 -0
- codegraph/presets/common.yaml +66 -0
- codegraph/presets/dart.yaml +9 -0
- codegraph/presets/django-ninja.yaml +15 -0
- codegraph/presets/django.yaml +25 -0
- codegraph/presets/djangorestframework.yaml +17 -0
- codegraph/presets/express.yaml +17 -0
- codegraph/presets/kotlin.yaml +11 -0
- codegraph/presets/laravel.yaml +40 -0
- codegraph/presets/nest.yaml +11 -0
- codegraph/presets/nextjs.yaml +15 -0
- codegraph/presets/nuxt.yaml +9 -0
- codegraph/presets/php.yaml +5 -0
- codegraph/presets/python.yaml +10 -0
- codegraph/presets/rust.yaml +5 -0
- codegraph/presets/swift.yaml +10 -0
- codegraph/presets/typescript.yaml +13 -0
- codegraph/process_runs.py +328 -0
- codegraph/protocols/__init__.py +299 -0
- codegraph/protocols/builtin.py +67 -0
- codegraph/protocols/matchers.py +144 -0
- codegraph/protocols/view.py +334 -0
- codegraph/query.py +2089 -0
- codegraph/realtime.py +260 -0
- codegraph/roundtrip.py +346 -0
- codegraph/routes.py +442 -0
- codegraph/starters.py +218 -0
- codegraph/tests_index.py +117 -0
- codegraph/viz/__init__.py +0 -0
- codegraph/viz/graph.py +369 -0
- codegraph/viz/server.py +198 -0
- codegraph/viz/static/app.css +148 -0
- codegraph/viz/static/app.js +1082 -0
- codegraph/viz/static/index.html +81 -0
- codegraph/viz/static/layered.js +237 -0
- codegraph/viz/static/vendor/VERSIONS.txt +4 -0
- codegraph/viz/static/vendor/cose-base.js +3214 -0
- codegraph/viz/static/vendor/cytoscape-fcose.js +1549 -0
- codegraph/viz/static/vendor/cytoscape.min.js +31 -0
- codegraph/viz/static/vendor/layout-base.js +5230 -0
- codegraph/viz/tools/package-lock.json +303 -0
- codegraph/viz/tools/package.json +7 -0
- codegraph/viz/tools/shoot.mjs +165 -0
- codegraph/xcode.py +251 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""Python settings -> external-system facts for codegraph/external.py (#40).
|
|
2
|
+
|
|
3
|
+
Module-level settings dicts (Django `DATABASES` / `CACHES`, NetBox-style `DATABASE` / `REDIS`, and nested aliases
|
|
4
|
+
`{"default": {...}}`) with a HOST / LOCATION, and URL settings (`DATABASE_URL`, `CELERY_BROKER_URL`, `BROKER_URL`,
|
|
5
|
+
`REDIS_URL`, `CACHE_URL`, `EMAIL_HOST`): literal values, `os.environ.get("K", default)` / `os.getenv` / `env("K")`
|
|
6
|
+
reads and names of other module dicts. Client constructors with an address in their arguments (#77):
|
|
7
|
+
`psycopg.connect(host=...)`, `redis.Redis(host=)` / `redis.from_url(url)`, `smtplib.SMTP(host, port)`,
|
|
8
|
+
`pymongo.MongoClient(url)`, `ldap3.Server(host)`, `boto3.client("s3", endpoint_url=)` ... Only facts are recorded
|
|
9
|
+
(builder.external_facts); passwords are kept as the env key or the location of a literal, never the value."""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import ast
|
|
13
|
+
|
|
14
|
+
DICT_VARS = {"DATABASES": None, "DATABASE": None, "CACHES": None, "REDIS": "redis", "CACHE": None}
|
|
15
|
+
URL_VARS = {"DATABASE_URL": None, "CELERY_BROKER_URL": None, "BROKER_URL": None, "REDIS_URL": "redis", "CACHE_URL": None,
|
|
16
|
+
"CELERY_RESULT_BACKEND": None, "EMAIL_HOST": "smtp", "MONGO_URL": "mongodb", "MONGODB_URI": "mongodb"}
|
|
17
|
+
ENGINES = (("postgis", "postgres"), ("postgresql", "postgres"), ("psycopg", "postgres"), ("mysql", "mysql"),
|
|
18
|
+
("oracle", "oracle"), ("mssql", "mssql"), ("sql_server", "mssql"), ("redis", "redis"), ("memcache", "memcached"),
|
|
19
|
+
("sqlite", None), ("locmem", None), ("dummy", None), ("filebased", None), ("db.DatabaseCache", None))
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _value(prog, m, e, depth=0):
|
|
23
|
+
"""('lit', str) | ('env', key, default) | ('dict', ast.Dict, module) | None"""
|
|
24
|
+
if e is None or depth > 3:
|
|
25
|
+
return None
|
|
26
|
+
if isinstance(e, ast.Constant) and isinstance(e.value, (str, int)) and not isinstance(e.value, bool):
|
|
27
|
+
return ("lit", str(e.value))
|
|
28
|
+
if isinstance(e, ast.Dict):
|
|
29
|
+
return ("dict", e, m)
|
|
30
|
+
if isinstance(e, ast.Call):
|
|
31
|
+
fn = e.func
|
|
32
|
+
name = fn.attr if isinstance(fn, ast.Attribute) else fn.id if isinstance(fn, ast.Name) else ""
|
|
33
|
+
recv = fn.value if isinstance(fn, ast.Attribute) else None
|
|
34
|
+
rname = recv.attr if isinstance(recv, ast.Attribute) else recv.id if isinstance(recv, ast.Name) else None
|
|
35
|
+
envish = (isinstance(fn, ast.Name) and name in ("getenv", "env")) or (name == "get" and rname == "environ") \
|
|
36
|
+
or (name == "getenv" and rname == "os") or (rname in ("env", "environ") and name in ("str", "int", "url", "db", "cache_url", "db_url"))
|
|
37
|
+
if envish and e.args \
|
|
38
|
+
and isinstance(e.args[0], ast.Constant) and isinstance(e.args[0].value, str) and e.args[0].value.isupper():
|
|
39
|
+
d = e.args[1] if len(e.args) > 1 else next((k.value for k in e.keywords if k.arg == "default"), None)
|
|
40
|
+
dv = d.value if isinstance(d, ast.Constant) and isinstance(d.value, (str, int)) else None
|
|
41
|
+
return ("env", e.args[0].value, None if dv is None else str(dv))
|
|
42
|
+
if name in ("int", "str") and e.args:
|
|
43
|
+
return _value(prog, m, e.args[0], depth + 1)
|
|
44
|
+
if isinstance(e, ast.Subscript) and isinstance(e.slice, ast.Constant) and isinstance(e.slice.value, str) \
|
|
45
|
+
and (getattr(e.value, "attr", None) == "environ" or getattr(e.value, "id", None) == "environ"):
|
|
46
|
+
return ("env", e.slice.value, None)
|
|
47
|
+
if isinstance(e, ast.Name):
|
|
48
|
+
r = prog.resolve_name(m, e.id)
|
|
49
|
+
if r and r[0] == "var":
|
|
50
|
+
for v, _l, _a in r[1].vars.get(r[2], [])[-1:]:
|
|
51
|
+
return _value(prog, r[1], v, depth + 1)
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _proto(engine: str | None, default):
|
|
56
|
+
if engine:
|
|
57
|
+
for k, p in ENGINES:
|
|
58
|
+
if k in engine.lower():
|
|
59
|
+
return p, True
|
|
60
|
+
return default, False
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def index(prog, b, walk_body=None) -> int:
|
|
64
|
+
facts = []
|
|
65
|
+
for m in prog.modules.values():
|
|
66
|
+
if not any(s in m.name.rsplit(".", 1)[-1].lower() for s in ("settings", "config", "configuration")):
|
|
67
|
+
continue
|
|
68
|
+
for name, vals in m.vars.items():
|
|
69
|
+
if name not in DICT_VARS and name not in URL_VARS:
|
|
70
|
+
continue
|
|
71
|
+
v, line, _a = vals[-1]
|
|
72
|
+
if name in URL_VARS:
|
|
73
|
+
x = _value(prog, m, v)
|
|
74
|
+
if x and x[0] in ("lit", "env"):
|
|
75
|
+
facts.append({"var": name, "module": m.name, "protocol": URL_VARS[name], "url": x, "src": f"module:{m.name}", "file": m.file, "line": line,
|
|
76
|
+
"host_only": name == "EMAIL_HOST"})
|
|
77
|
+
continue
|
|
78
|
+
x = _value(prog, m, v)
|
|
79
|
+
if not x or x[0] != "dict":
|
|
80
|
+
continue
|
|
81
|
+
d, dm = x[1], x[2]
|
|
82
|
+
entries = []
|
|
83
|
+
keys = {k.value: val for k, val in zip(d.keys, d.values) if isinstance(k, ast.Constant) and isinstance(k.value, str)}
|
|
84
|
+
if any(k in keys for k in ("HOST", "LOCATION", "ENGINE", "BACKEND", "URL")):
|
|
85
|
+
entries.append((None, keys, dm))
|
|
86
|
+
else:
|
|
87
|
+
for alias, val in keys.items():
|
|
88
|
+
y = _value(prog, dm, val)
|
|
89
|
+
if y and y[0] == "dict":
|
|
90
|
+
sub = {k.value: vv for k, vv in zip(y[1].keys, y[1].values) if isinstance(k, ast.Constant) and isinstance(k.value, str)}
|
|
91
|
+
entries.append((alias, sub, y[2]))
|
|
92
|
+
for alias, kv, em in entries:
|
|
93
|
+
eng = _value(prog, em, kv.get("ENGINE") or kv.get("BACKEND"))
|
|
94
|
+
proto, known = _proto(eng[1] if eng and eng[0] == "lit" else None, DICT_VARS[name])
|
|
95
|
+
if (eng and eng[0] == "lit") and known and proto is None:
|
|
96
|
+
continue # sqlite / local-memory cache: no network system
|
|
97
|
+
loc = _value(prog, em, kv.get("LOCATION") or kv.get("URL"))
|
|
98
|
+
nm = _value(prog, em, kv.get("NAME"))
|
|
99
|
+
f = {"var": name + (f"[{alias}]" if alias else ""), "module": m.name, "protocol": proto, "src": f"module:{m.name}", "file": m.file, "line": line,
|
|
100
|
+
"resource": nm[1] if nm and nm[0] == "lit" and nm[1] else None}
|
|
101
|
+
if loc is not None and (loc[0] == "env" or (loc[0] == "lit" and "://" in loc[1])):
|
|
102
|
+
f["url"] = loc
|
|
103
|
+
else:
|
|
104
|
+
f["host"] = _value(prog, em, kv.get("HOST"))
|
|
105
|
+
f["port"] = _value(prog, em, kv.get("PORT"))
|
|
106
|
+
if f["host"] is None:
|
|
107
|
+
continue
|
|
108
|
+
pw = kv.get("PASSWORD")
|
|
109
|
+
if pw is not None:
|
|
110
|
+
pv = _value(prog, em, pw)
|
|
111
|
+
f["password"] = ("env", pv[1]) if pv and pv[0] == "env" else ("literal", f"{em.file}:{pw.lineno}") \
|
|
112
|
+
if pv and pv[0] == "lit" and pv[1] else None
|
|
113
|
+
facts.append(f)
|
|
114
|
+
if walk_body is not None:
|
|
115
|
+
facts += client_facts(prog, b, walk_body)
|
|
116
|
+
if facts:
|
|
117
|
+
b.external_facts = getattr(b, "external_facts", []) + facts
|
|
118
|
+
return len(facts)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# top-level package -> (protocol, constructor / function names, default port override)
|
|
122
|
+
CLIENTS = {
|
|
123
|
+
"psycopg": ("postgres", {"connect", "Connection", "AsyncConnection", "ConnectionPool", "AsyncConnectionPool"}),
|
|
124
|
+
"psycopg2": ("postgres", {"connect", "SimpleConnectionPool", "ThreadedConnectionPool"}),
|
|
125
|
+
"psycopg_pool": ("postgres", {"ConnectionPool", "AsyncConnectionPool"}),
|
|
126
|
+
"asyncpg": ("postgres", {"connect", "create_pool"}),
|
|
127
|
+
"pg8000": ("postgres", {"connect", "Connection"}),
|
|
128
|
+
"pymysql": ("mysql", {"connect", "Connection"}),
|
|
129
|
+
"MySQLdb": ("mysql", {"connect", "Connection"}),
|
|
130
|
+
"mysql": ("mysql", {"connect", "MySQLConnection"}),
|
|
131
|
+
"aiomysql": ("mysql", {"connect", "create_pool"}),
|
|
132
|
+
"redis": ("redis", {"Redis", "StrictRedis", "from_url", "ConnectionPool"}),
|
|
133
|
+
"aioredis": ("redis", {"Redis", "from_url", "create_redis_pool"}),
|
|
134
|
+
"pymongo": ("mongodb", {"MongoClient"}),
|
|
135
|
+
"motor": ("mongodb", {"AsyncIOMotorClient", "MotorClient"}),
|
|
136
|
+
"smtplib": ("smtp", {"SMTP", "SMTP_SSL"}),
|
|
137
|
+
"aiosmtplib": ("smtp", {"SMTP", "send"}),
|
|
138
|
+
"ftplib": ("ftp", {"FTP", "FTP_TLS"}),
|
|
139
|
+
"ldap3": ("ldap", {"Server"}),
|
|
140
|
+
"ldap": ("ldap", {"initialize"}),
|
|
141
|
+
"pika": ("amqp", {"URLParameters", "ConnectionParameters"}),
|
|
142
|
+
"aio_pika": ("amqp", {"connect", "connect_robust"}),
|
|
143
|
+
"kombu": ("amqp", {"Connection"}),
|
|
144
|
+
"elasticsearch": ("elasticsearch", {"Elasticsearch", "AsyncElasticsearch"}),
|
|
145
|
+
"pymemcache": ("memcached", {"Client", "PooledClient"}),
|
|
146
|
+
"boto3": ("s3", {"client", "resource"}),
|
|
147
|
+
}
|
|
148
|
+
TLS_CTORS = {"SMTP_SSL": "465", "FTP_TLS": None}
|
|
149
|
+
URL_KW = ("dsn", "url", "conninfo", "host_url", "endpoint_url", "hosts")
|
|
150
|
+
HOST_KW = ("host", "hostname", "server")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _dotted(m, e):
|
|
154
|
+
"""`redis.asyncio.Redis` / `Redis` (from redis import Redis) -> 'redis.asyncio.Redis', through the imports."""
|
|
155
|
+
parts = []
|
|
156
|
+
while isinstance(e, ast.Attribute):
|
|
157
|
+
parts.append(e.attr)
|
|
158
|
+
e = e.value
|
|
159
|
+
if not isinstance(e, ast.Name):
|
|
160
|
+
return None
|
|
161
|
+
imp = m.imports.get(e.id)
|
|
162
|
+
if not isinstance(imp, tuple) or len(imp) < 2 or not isinstance(imp[1], str):
|
|
163
|
+
return None
|
|
164
|
+
base = imp[1] + (f".{imp[2]}" if len(imp) > 2 and imp[0] == "sym" and isinstance(imp[2], str) else "")
|
|
165
|
+
return ".".join([base, *reversed(parts)])
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _kwarg(call, names):
|
|
169
|
+
for k in call.keywords:
|
|
170
|
+
if k.arg in names:
|
|
171
|
+
return k.value
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def client_facts(prog, b, walk_body) -> list:
|
|
176
|
+
tops = set(CLIENTS)
|
|
177
|
+
facts = []
|
|
178
|
+
for m in prog.modules.values():
|
|
179
|
+
if not any(isinstance(v, tuple) and len(v) > 1 and isinstance(v[1], str) and v[1].split(".")[0] in tops
|
|
180
|
+
for v in m.imports.values()):
|
|
181
|
+
continue
|
|
182
|
+
scopes = [(f.id, walk_body(f.node)) for f in m.funcs.values()]
|
|
183
|
+
scopes += [(f.id, walk_body(f.node)) for c in m.classes.values() for f in c.methods.values()]
|
|
184
|
+
scopes.append((f"module:{m.name}", (n for st in m.tree.body if not isinstance(st, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef))
|
|
185
|
+
for n in ast.walk(st))))
|
|
186
|
+
for src, nodes in scopes:
|
|
187
|
+
if src not in b.nodes:
|
|
188
|
+
continue
|
|
189
|
+
for c in nodes:
|
|
190
|
+
if not isinstance(c, ast.Call):
|
|
191
|
+
continue
|
|
192
|
+
dn = _dotted(m, c.func)
|
|
193
|
+
if not dn:
|
|
194
|
+
continue
|
|
195
|
+
top, last = dn.split(".")[0], dn.rsplit(".", 1)[-1]
|
|
196
|
+
if top not in CLIENTS or last not in CLIENTS[top][1]:
|
|
197
|
+
continue
|
|
198
|
+
proto = CLIENTS[top][0]
|
|
199
|
+
if top == "boto3":
|
|
200
|
+
if not (c.args and isinstance(c.args[0], ast.Constant) and c.args[0].value == "s3"):
|
|
201
|
+
continue
|
|
202
|
+
if _kwarg(c, ("endpoint_url",)) is None:
|
|
203
|
+
continue
|
|
204
|
+
url = _kwarg(c, URL_KW)
|
|
205
|
+
host = _kwarg(c, HOST_KW)
|
|
206
|
+
pos = c.args[0] if c.args and top != "boto3" else None
|
|
207
|
+
if url is None and host is None and pos is not None:
|
|
208
|
+
pv = _value(prog, m, pos)
|
|
209
|
+
if pv and pv[0] == "lit" and "://" in pv[1] or last in ("from_url", "URLParameters") or top in ("pymongo", "motor", "asyncpg", "aio_pika") and pv and pv[0] == "env":
|
|
210
|
+
url = pos
|
|
211
|
+
elif proto in ("smtp", "ftp", "ldap", "elasticsearch", "memcached", "amqp") or (top == "redis" and last != "from_url"):
|
|
212
|
+
host = pos
|
|
213
|
+
port = _kwarg(c, ("port",)) or (c.args[1] if len(c.args) > 1 and proto in ("smtp", "ftp", "redis") else None)
|
|
214
|
+
f = {"var": f"{dn}()", "module": m.name, "protocol": proto, "src": src, "file": m.file, "line": c.lineno,
|
|
215
|
+
"client": top, "resource": None}
|
|
216
|
+
if url is not None:
|
|
217
|
+
u = _value(prog, m, url)
|
|
218
|
+
if not u or u[0] not in ("lit", "env") or (u[0] == "lit" and "://" not in u[1] and proto != "ldap"):
|
|
219
|
+
continue
|
|
220
|
+
if u[0] == "lit" and "://" not in u[1]: # ldap3.Server("ldap.example.org")
|
|
221
|
+
f["host"], f["port"] = u, None
|
|
222
|
+
else:
|
|
223
|
+
f["url"] = u
|
|
224
|
+
elif host is not None:
|
|
225
|
+
h = _value(prog, m, host)
|
|
226
|
+
if not h or h[0] not in ("lit", "env"):
|
|
227
|
+
continue
|
|
228
|
+
if h[0] == "lit" and "://" in h[1]:
|
|
229
|
+
f["url"] = h
|
|
230
|
+
else:
|
|
231
|
+
f["host"] = h
|
|
232
|
+
pv = _value(prog, m, port) if port is not None else None
|
|
233
|
+
f["port"] = pv if pv and pv[0] in ("lit", "env") else (("lit", TLS_CTORS[last]) if TLS_CTORS.get(last) else None)
|
|
234
|
+
else:
|
|
235
|
+
continue
|
|
236
|
+
db = _kwarg(c, ("dbname", "database", "db"))
|
|
237
|
+
dv = _value(prog, m, db) if db is not None else None
|
|
238
|
+
f["resource"] = dv[1] if dv and dv[0] == "lit" else None
|
|
239
|
+
pw = _kwarg(c, ("password", "passwd"))
|
|
240
|
+
if pw is not None:
|
|
241
|
+
pv = _value(prog, m, pw)
|
|
242
|
+
f["password"] = ("env", pv[1]) if pv and pv[0] == "env" else ("literal", f"{m.file}:{pw.lineno}") \
|
|
243
|
+
if pv and pv[0] == "lit" and pv[1] else None
|
|
244
|
+
facts.append(f)
|
|
245
|
+
return facts
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"""Python stored attributes as field nodes (#88 phase 1).
|
|
2
|
+
|
|
3
|
+
A class's instance attributes (`self.x = ...` in any of its methods) and its annotated class-level attributes
|
|
4
|
+
(`x: int = 0` in a dataclass / pydantic model / attrs class; not `ClassVar[...]`) are `field:<Class>.<attr>` nodes,
|
|
5
|
+
attrs `property: stored`, `declared: self | class`. Plain class-level assignments stay out: they are constants or
|
|
6
|
+
framework descriptors (Django model fields are the Django plugin's own `field:` nodes), and methods, properties and
|
|
7
|
+
nested classes are never fields.
|
|
8
|
+
|
|
9
|
+
Every `self.x` / `v.x` attribute access whose receiver type the inference knows (`self`, a parameter annotation, a
|
|
10
|
+
local `v = Cart()`, a typed attribute) and whose class (or a base) declares the field is a READS_PROP / WRITES_PROP
|
|
11
|
+
edge (`resolved`, attr `receiver`). A write is an assignment / augmented assignment / `del` target, or the receiver
|
|
12
|
+
of an in-place collection method (`self.items.append(x)`, `via: mutating`), or an item assignment / deletion
|
|
13
|
+
(`self.cache[k] = v`, `via: item`). An unknown receiver binds nothing. Test
|
|
14
|
+
code's accesses become TEST_USES (tests_index.py).
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import ast
|
|
19
|
+
|
|
20
|
+
from ...core.model import RESOLVED
|
|
21
|
+
|
|
22
|
+
MUTATING = frozenset({
|
|
23
|
+
"append", "extend", "insert", "remove", "pop", "clear", "sort", "reverse", "add", "discard", "update",
|
|
24
|
+
"setdefault", "popitem", "difference_update", "intersection_update", "symmetric_difference_update", "appendleft",
|
|
25
|
+
"extendleft", "popleft", "rotate", "put", "put_nowait"})
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _classvar(ann) -> bool:
|
|
29
|
+
t = ast.unparse(ann) if ann is not None else ""
|
|
30
|
+
return t.startswith(("ClassVar", "typing.ClassVar", "Final", "typing.Final", "InitVar", "dataclasses.InitVar"))
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def index(prog, b, Ctx, walk_body) -> dict:
|
|
34
|
+
fields: dict[str, dict[str, str]] = {}
|
|
35
|
+
names: set[str] = set()
|
|
36
|
+
n_nodes = 0
|
|
37
|
+
for c in prog.classes.values():
|
|
38
|
+
if c.id not in b.nodes:
|
|
39
|
+
continue
|
|
40
|
+
own: dict[str, tuple[int, str]] = {}
|
|
41
|
+
for nm, (val, line, ann) in c.attrs.items():
|
|
42
|
+
if ann is not None and not _classvar(ann):
|
|
43
|
+
own[nm] = (line, "class")
|
|
44
|
+
for nm, line in c.self_attr_lines.items():
|
|
45
|
+
own.setdefault(nm, (line, "self"))
|
|
46
|
+
inner = getattr(c, "inner_infos", {})
|
|
47
|
+
for nm, (line, how) in own.items():
|
|
48
|
+
if nm in c.methods or nm in inner:
|
|
49
|
+
continue
|
|
50
|
+
fid = b.add_node("field", f"{c.qual}.{nm}", name=nm, fqn=f"{c.qual}.{nm}", file=c.file, line=line,
|
|
51
|
+
module=b.nodes[c.id].module, lang="python",
|
|
52
|
+
attrs={"property": "stored", "declared": how})
|
|
53
|
+
b.add_edge(c.id, fid, "CONTAINS", c.file, line, RESOLVED)
|
|
54
|
+
fields.setdefault(c.qual, {})[nm] = fid
|
|
55
|
+
names.add(nm)
|
|
56
|
+
n_nodes += 1
|
|
57
|
+
if not names:
|
|
58
|
+
return {}
|
|
59
|
+
|
|
60
|
+
def lookup(cls, attr):
|
|
61
|
+
if cls.qual in fields and attr in fields[cls.qual]:
|
|
62
|
+
return fields[cls.qual][attr]
|
|
63
|
+
for bt in prog.mro(cls):
|
|
64
|
+
if bt[0] == "type" and bt[1].qual in fields and attr in fields[bt[1].qual]:
|
|
65
|
+
return fields[bt[1].qual][attr]
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
reads = writes = mutating = unresolved = 0
|
|
69
|
+
for f in list(prog.funcs.values()):
|
|
70
|
+
ctx = Ctx(f.module, f, f.cls)
|
|
71
|
+
parent: dict[int, ast.AST] = {}
|
|
72
|
+
attrs = []
|
|
73
|
+
for n in walk_body(f.node):
|
|
74
|
+
for ch in ast.iter_child_nodes(n):
|
|
75
|
+
parent[id(ch)] = n
|
|
76
|
+
if isinstance(n, ast.Attribute) and n.attr in names:
|
|
77
|
+
attrs.append(n)
|
|
78
|
+
for a in attrs:
|
|
79
|
+
p = parent.get(id(a))
|
|
80
|
+
if isinstance(p, ast.Call) and p.func is a:
|
|
81
|
+
continue # `self.save()`: a call, not an attribute access
|
|
82
|
+
try:
|
|
83
|
+
t = prog.infer(a.value, ctx)
|
|
84
|
+
except RecursionError:
|
|
85
|
+
t = None
|
|
86
|
+
fid = lookup(t[1], a.attr) if t is not None and t[0] == "inst" else None
|
|
87
|
+
if fid is None:
|
|
88
|
+
unresolved += 1
|
|
89
|
+
continue
|
|
90
|
+
via = None
|
|
91
|
+
write = isinstance(a.ctx, (ast.Store, ast.Del))
|
|
92
|
+
if not write and isinstance(p, ast.Subscript) and p.value is a and isinstance(p.ctx, (ast.Store, ast.Del)):
|
|
93
|
+
write, via = True, "item" # `self.cache[k] = v` / `del self.cache[k]`
|
|
94
|
+
elif not write and isinstance(p, ast.Attribute) and p.value is a and p.attr in MUTATING:
|
|
95
|
+
gp = parent.get(id(p))
|
|
96
|
+
if isinstance(gp, ast.Call) and gp.func is p:
|
|
97
|
+
write, via = True, "mutating"
|
|
98
|
+
recv = ast.unparse(a.value)[:40] if not (isinstance(a.value, ast.Name) and a.value.id == "self") else "self"
|
|
99
|
+
b.add_edge(f.id, fid, "WRITES_PROP" if write else "READS_PROP", f.file, a.lineno, RESOLVED,
|
|
100
|
+
receiver=recv, **({"via": via} if via else {}))
|
|
101
|
+
if write:
|
|
102
|
+
writes += 1
|
|
103
|
+
mutating += via == "mutating"
|
|
104
|
+
else:
|
|
105
|
+
reads += 1
|
|
106
|
+
return {"stored_attribute_nodes": n_nodes, "stored_attribute_reads": reads, "stored_attribute_writes": writes,
|
|
107
|
+
"stored_attribute_writes_mutating": mutating, "stored_attribute_refs_unresolved": unresolved}
|