codeanalyzer-python 1.0.1__tar.gz → 1.0.3__tar.gz
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.
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/PKG-INFO +2 -1
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/core.py +161 -8
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/neo4j/project.py +29 -14
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/pyproject.toml +4 -1
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/.gitignore +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/LICENSE +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/NOTICE +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/README.md +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/__init__.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/__main__.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/config/__init__.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/config/config.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/dataflow/__init__.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/dataflow/access_paths.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/dataflow/alias.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/dataflow/builder.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/dataflow/cfg.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/dataflow/defuse.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/dataflow/dominance.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/dataflow/identity.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/dataflow/pdg.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/dataflow/scalpel_oracle.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/dataflow/scc.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/dataflow/sdg.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/dataflow/slicing.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/dataflow/summaries.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/dataflow/syntactic.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/jedi/__init__.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/jedi/jedi.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/neo4j/__init__.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/neo4j/bolt.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/neo4j/cypher.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/neo4j/emit.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/neo4j/rows.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/neo4j/schema.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/options/__init__.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/options/options.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/provenance.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/py.typed +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/schema/__init__.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/schema/assign_ids.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/schema/call_graph_ids.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/schema/ids.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/schema/l1_body.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/schema/l2_callees.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/schema/py_schema.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/semantic_analysis/__init__.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/semantic_analysis/call_graph.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/semantic_analysis/pycg/__init__.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/semantic_analysis/pycg/pycg_analysis.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/semantic_analysis/pycg/pycg_exceptions.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/semantic_analysis/pycg/shard_planner.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/syntactic_analysis/__init__.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/syntactic_analysis/exceptions.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/syntactic_analysis/import_resolver.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/syntactic_analysis/symbol_table_builder.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/utils/__init__.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/utils/logging.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/utils/progress_bar.py +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/docs/handoff/README.md +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/docs/handoff/schema.neo4j.json +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/schema-uml.drawio +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/schema.neo4j.json +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/test/fixtures/whole_applications/flask/README.md +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/test/fixtures/whole_applications/flask/examples/celery/README.md +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/test/fixtures/whole_applications/flask/requirements-skip/README.md +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/test/fixtures/whole_applications/flask/src/flask/sansio/README.md +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/test/fixtures/whole_applications/requests/LICENSE +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/test/fixtures/whole_applications/requests/NOTICE +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/test/fixtures/whole_applications/requests/README.md +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/test/fixtures/whole_applications/requests/docs/_themes/LICENSE +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/test/fixtures/whole_applications/requests/ext/LICENSE +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/test/fixtures/whole_applications/xarray/LICENSE +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/test/fixtures/whole_applications/xarray/README.md +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/test/fixtures/whole_applications/xarray/properties/README.md +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/test/fixtures/whole_applications/xarray/xarray/datatree_/LICENSE +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/test/fixtures/whole_applications/xarray/xarray/datatree_/README.md +0 -0
- {codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/test/fixtures/whole_applications/xarray/xarray/datatree_/docs/README.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codeanalyzer-python
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.3
|
|
4
4
|
Summary: Static analysis for Python — canonical schema v2 (symbol table, call graph, and native CFG/PDG/SDG dataflow) as analysis.json or a Neo4j property graph.
|
|
5
5
|
Author-email: Rahul Krishna <i.m.ralk@gmail.com>
|
|
6
6
|
License-File: LICENSE
|
|
@@ -18,6 +18,7 @@ Requires-Dist: numpy<2.0.0,>=1.26.0; python_version >= '3.12'
|
|
|
18
18
|
Requires-Dist: packaging>=25.0
|
|
19
19
|
Requires-Dist: pandas<2.0.0,>=1.3.0; python_version < '3.11'
|
|
20
20
|
Requires-Dist: pandas<3.0.0,>=2.0.0; python_version >= '3.11'
|
|
21
|
+
Requires-Dist: parso>=0.8.5
|
|
21
22
|
Requires-Dist: pycg>=0.0.6
|
|
22
23
|
Requires-Dist: pydantic<2.0.0,>=1.8.0; python_version < '3.11'
|
|
23
24
|
Requires-Dist: pydantic<3.0.0,>=2.0.0; python_version >= '3.11'
|
|
@@ -163,8 +163,159 @@ class Codeanalyzer:
|
|
|
163
163
|
stderr=None,
|
|
164
164
|
)
|
|
165
165
|
|
|
166
|
+
@classmethod
|
|
167
|
+
def _get_base_interpreter(cls) -> Path:
|
|
168
|
+
"""The interpreter used to provision the analysis virtualenv.
|
|
169
|
+
|
|
170
|
+
jedi parses the *analysis environment's* Python version with parso,
|
|
171
|
+
which ships one hardcoded grammar file per minor version — an
|
|
172
|
+
environment newer than the newest shipped grammar makes every file
|
|
173
|
+
fail with "Python version X.Y is currently not supported" while the
|
|
174
|
+
run still exits 0 (#107). So the default choice is gated on the
|
|
175
|
+
installed parso's ceiling: a too-new default is swapped for the
|
|
176
|
+
newest supported interpreter found on the host, falling back to the
|
|
177
|
+
default (loudly) only when none exists. An explicit ``SYSTEM_PYTHON``
|
|
178
|
+
always wins, with a warning when parso cannot parse its version.
|
|
179
|
+
"""
|
|
180
|
+
# An explicit SYSTEM_PYTHON override wins (consulted only when running
|
|
181
|
+
# inside a virtualenv, matching the historical behavior).
|
|
182
|
+
if sys.prefix != sys.base_prefix:
|
|
183
|
+
system_python = os.getenv("SYSTEM_PYTHON")
|
|
184
|
+
if system_python:
|
|
185
|
+
system_python_path = Path(system_python)
|
|
186
|
+
if system_python_path.exists() and system_python_path.is_file():
|
|
187
|
+
ceiling = cls._parso_supported_ceiling()
|
|
188
|
+
version = cls._interpreter_version(system_python_path)
|
|
189
|
+
if ceiling is not None and version is not None and version > ceiling:
|
|
190
|
+
logger.warning(
|
|
191
|
+
f"SYSTEM_PYTHON={system_python} is Python "
|
|
192
|
+
f"{version[0]}.{version[1]}, newer than the newest grammar "
|
|
193
|
+
f"the installed parso ships ({ceiling[0]}.{ceiling[1]}). "
|
|
194
|
+
"jedi will likely reject every file in the analysis "
|
|
195
|
+
"environment (#107); honoring the explicit override anyway."
|
|
196
|
+
)
|
|
197
|
+
return system_python_path
|
|
198
|
+
|
|
199
|
+
candidate = cls._default_base_interpreter()
|
|
200
|
+
ceiling = cls._parso_supported_ceiling()
|
|
201
|
+
if ceiling is None:
|
|
202
|
+
return candidate
|
|
203
|
+
version = cls._interpreter_version(candidate)
|
|
204
|
+
if version is None or version <= ceiling:
|
|
205
|
+
return candidate
|
|
206
|
+
logger.warning(
|
|
207
|
+
f"Default interpreter {candidate} is Python {version[0]}.{version[1]}, "
|
|
208
|
+
f"newer than the newest grammar the installed parso ships "
|
|
209
|
+
f"({ceiling[0]}.{ceiling[1]}) — looking for a supported interpreter "
|
|
210
|
+
"for the analysis environment (#107)."
|
|
211
|
+
)
|
|
212
|
+
supported = cls._find_supported_interpreter(ceiling)
|
|
213
|
+
if supported is not None:
|
|
214
|
+
logger.info(f"Provisioning the analysis environment with {supported}.")
|
|
215
|
+
return supported
|
|
216
|
+
logger.warning(
|
|
217
|
+
f"No interpreter <= {ceiling[0]}.{ceiling[1]} found on this host; "
|
|
218
|
+
f"falling back to {candidate}. jedi/parso will likely reject every "
|
|
219
|
+
"file — install a supported Python or upgrade parso."
|
|
220
|
+
)
|
|
221
|
+
return candidate
|
|
222
|
+
|
|
223
|
+
@staticmethod
|
|
224
|
+
def _versions_from_grammar_stems(stems: List[str]) -> List[tuple]:
|
|
225
|
+
"""``grammar313`` → ``(3, 13)``, sorted ascending; malformed stems dropped."""
|
|
226
|
+
versions = []
|
|
227
|
+
for stem in stems:
|
|
228
|
+
digits = stem[len("grammar"):]
|
|
229
|
+
if len(digits) >= 2 and digits.isdigit():
|
|
230
|
+
versions.append((int(digits[0]), int(digits[1:])))
|
|
231
|
+
return sorted(versions)
|
|
232
|
+
|
|
233
|
+
@classmethod
|
|
234
|
+
def _parso_supported_ceiling(cls) -> Optional[tuple]:
|
|
235
|
+
"""Newest ``(major, minor)`` the installed parso ships a grammar for,
|
|
236
|
+
derived from its ``python/grammar*.txt`` files so the ceiling moves
|
|
237
|
+
automatically when parso adds a version. ``None`` if undeterminable."""
|
|
238
|
+
try:
|
|
239
|
+
import parso
|
|
240
|
+
|
|
241
|
+
stems = [
|
|
242
|
+
p.stem
|
|
243
|
+
for p in (Path(parso.__file__).parent / "python").glob("grammar*.txt")
|
|
244
|
+
]
|
|
245
|
+
versions = cls._versions_from_grammar_stems(stems)
|
|
246
|
+
return versions[-1] if versions else None
|
|
247
|
+
except Exception:
|
|
248
|
+
return None
|
|
249
|
+
|
|
250
|
+
@staticmethod
|
|
251
|
+
def _interpreter_version(interpreter: Path) -> Optional[tuple]:
|
|
252
|
+
"""``(major, minor)`` of an interpreter, or ``None`` if it can't run."""
|
|
253
|
+
try:
|
|
254
|
+
result = subprocess.run(
|
|
255
|
+
[
|
|
256
|
+
str(interpreter),
|
|
257
|
+
"-c",
|
|
258
|
+
"import sys; print('%d.%d' % sys.version_info[:2])",
|
|
259
|
+
],
|
|
260
|
+
capture_output=True,
|
|
261
|
+
text=True,
|
|
262
|
+
timeout=5,
|
|
263
|
+
)
|
|
264
|
+
if result.returncode == 0:
|
|
265
|
+
major, minor = result.stdout.strip().split(".")
|
|
266
|
+
return (int(major), int(minor))
|
|
267
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, PermissionError, ValueError):
|
|
268
|
+
pass
|
|
269
|
+
return None
|
|
270
|
+
|
|
271
|
+
@staticmethod
|
|
272
|
+
def _pick_supported_interpreter(
|
|
273
|
+
candidates: List[tuple], ceiling: tuple
|
|
274
|
+
) -> Optional[Path]:
|
|
275
|
+
"""Newest candidate whose version is within the ceiling.
|
|
276
|
+
|
|
277
|
+
``candidates`` is ``[(path, (major, minor) | None), ...]``."""
|
|
278
|
+
supported = [
|
|
279
|
+
(version, path)
|
|
280
|
+
for path, version in candidates
|
|
281
|
+
if version is not None and version <= ceiling
|
|
282
|
+
]
|
|
283
|
+
return max(supported)[1] if supported else None
|
|
284
|
+
|
|
285
|
+
@classmethod
|
|
286
|
+
def _find_supported_interpreter(cls, ceiling: tuple) -> Optional[Path]:
|
|
287
|
+
"""Search the host for the newest interpreter within the parso ceiling:
|
|
288
|
+
versioned names on PATH (``python3.13``, ``python3.12``, ...) first,
|
|
289
|
+
then pyenv installs."""
|
|
290
|
+
paths: List[Path] = []
|
|
291
|
+
for minor in range(ceiling[1], 7, -1):
|
|
292
|
+
which = shutil.which(f"python{ceiling[0]}.{minor}")
|
|
293
|
+
# Skip the current virtualenv's own interpreter (same rule as
|
|
294
|
+
# _default_base_interpreter): the analysis env must come from a
|
|
295
|
+
# base installation.
|
|
296
|
+
if which and not which.startswith(sys.prefix):
|
|
297
|
+
paths.append(Path(which))
|
|
298
|
+
for pyenv_root in (os.getenv("PYENV_ROOT"), str(Path.home() / ".pyenv")):
|
|
299
|
+
if not pyenv_root:
|
|
300
|
+
continue
|
|
301
|
+
versions_dir = Path(pyenv_root) / "versions"
|
|
302
|
+
if versions_dir.is_dir():
|
|
303
|
+
for install in sorted(versions_dir.iterdir(), reverse=True):
|
|
304
|
+
exe = install / "bin" / "python3"
|
|
305
|
+
if exe.exists():
|
|
306
|
+
paths.append(exe)
|
|
307
|
+
seen = set()
|
|
308
|
+
candidates = []
|
|
309
|
+
for path in paths:
|
|
310
|
+
key = str(path)
|
|
311
|
+
if key in seen:
|
|
312
|
+
continue
|
|
313
|
+
seen.add(key)
|
|
314
|
+
candidates.append((path, cls._interpreter_version(path)))
|
|
315
|
+
return cls._pick_supported_interpreter(candidates, ceiling)
|
|
316
|
+
|
|
166
317
|
@staticmethod
|
|
167
|
-
def
|
|
318
|
+
def _default_base_interpreter() -> Path:
|
|
168
319
|
"""Get the base Python interpreter path.
|
|
169
320
|
|
|
170
321
|
This method finds a suitable base Python interpreter that can be used
|
|
@@ -183,13 +334,6 @@ class Codeanalyzer:
|
|
|
183
334
|
|
|
184
335
|
# We're inside a virtual environment; need to find the base interpreter
|
|
185
336
|
|
|
186
|
-
# First, check if user explicitly set SYSTEM_PYTHON
|
|
187
|
-
system_python = os.getenv("SYSTEM_PYTHON")
|
|
188
|
-
if system_python:
|
|
189
|
-
system_python_path = Path(system_python)
|
|
190
|
-
if system_python_path.exists() and system_python_path.is_file():
|
|
191
|
-
return system_python_path
|
|
192
|
-
|
|
193
337
|
# Try to get the base interpreter from sys.base_executable (Python 3.3+)
|
|
194
338
|
if hasattr(sys, "base_executable") and sys.base_executable:
|
|
195
339
|
base_exec = Path(sys.base_executable)
|
|
@@ -778,6 +922,15 @@ class Codeanalyzer:
|
|
|
778
922
|
if files_from_cache > 0:
|
|
779
923
|
logger.info(f"Reused {files_from_cache} files from cache, processed {files_processed} new/changed files")
|
|
780
924
|
|
|
925
|
+
if py_files and not symbol_table:
|
|
926
|
+
logger.error(
|
|
927
|
+
"Every one of the %d discovered Python files failed to process — "
|
|
928
|
+
"the symbol table is empty. This usually means the analysis "
|
|
929
|
+
"environment's interpreter is newer than the installed jedi/parso "
|
|
930
|
+
"stack supports (#107); check the per-file errors above.",
|
|
931
|
+
len(py_files),
|
|
932
|
+
)
|
|
933
|
+
|
|
781
934
|
logger.info(
|
|
782
935
|
"✅ Symbol table: %d modules in %.1fs",
|
|
783
936
|
len(symbol_table), time.perf_counter() - t0_st,
|
|
@@ -290,9 +290,11 @@ def _project_module_body(
|
|
|
290
290
|
externals: dict, sig_to_id: dict, module_id_by_key: dict,
|
|
291
291
|
) -> None:
|
|
292
292
|
for fn in (mod.functions or {}).values():
|
|
293
|
-
_project_callable(b, file_key, mod_ref, "PY_DECLARES", fn, externals, sig_to_id
|
|
293
|
+
_project_callable(b, file_key, mod_ref, "PY_DECLARES", fn, externals, sig_to_id,
|
|
294
|
+
mod.source)
|
|
294
295
|
for cl in (mod.types or {}).values():
|
|
295
|
-
_project_class(b, file_key, mod_ref, "PY_DECLARES", cl, externals, sig_to_id
|
|
296
|
+
_project_class(b, file_key, mod_ref, "PY_DECLARES", cl, externals, sig_to_id,
|
|
297
|
+
mod.source)
|
|
296
298
|
for v in mod.variables or []:
|
|
297
299
|
_project_variable(b, file_key, mod_ref, file_key, v)
|
|
298
300
|
_project_imports(b, mod_ref, mod, module_id_by_key)
|
|
@@ -360,10 +362,10 @@ def _project_imports(b: RowBuilder, mod_ref: NodeRef, mod: PyModule,
|
|
|
360
362
|
|
|
361
363
|
def _project_class(
|
|
362
364
|
b: RowBuilder, file_key: str, parent: NodeRef, parent_rel: str, cl: PyClass,
|
|
363
|
-
externals: dict, sig_to_id: dict,
|
|
365
|
+
externals: dict, sig_to_id: dict, source: str,
|
|
364
366
|
) -> None:
|
|
365
367
|
ref = b.node(
|
|
366
|
-
["PySymbol", "PyClass"], "id", cl.id, _class_props(cl, file_key)
|
|
368
|
+
["PySymbol", "PyClass"], "id", cl.id, _class_props(cl, file_key, source)
|
|
367
369
|
)
|
|
368
370
|
b.edge(parent_rel, parent, ref)
|
|
369
371
|
|
|
@@ -372,22 +374,23 @@ def _project_class(
|
|
|
372
374
|
b.edge_to_symbol("PY_EXTENDS", ref, _symbol_ref(base, externals, sig_to_id))
|
|
373
375
|
|
|
374
376
|
for m in (cl.callables or {}).values():
|
|
375
|
-
_project_callable(b, file_key, ref, "PY_HAS_METHOD", m, externals, sig_to_id
|
|
377
|
+
_project_callable(b, file_key, ref, "PY_HAS_METHOD", m, externals, sig_to_id,
|
|
378
|
+
source)
|
|
376
379
|
for a in (cl.attributes or {}).values():
|
|
377
380
|
_project_attribute(b, file_key, ref, cl.signature, a)
|
|
378
381
|
for ic in (cl.types or {}).values():
|
|
379
|
-
_project_class(b, file_key, ref, "PY_DECLARES", ic, externals, sig_to_id)
|
|
382
|
+
_project_class(b, file_key, ref, "PY_DECLARES", ic, externals, sig_to_id, source)
|
|
380
383
|
|
|
381
384
|
|
|
382
385
|
def _project_callable(
|
|
383
386
|
b: RowBuilder, file_key: str, owner: NodeRef, owner_rel: str, c: PyCallable,
|
|
384
|
-
externals: dict, sig_to_id: dict,
|
|
387
|
+
externals: dict, sig_to_id: dict, source: str,
|
|
385
388
|
) -> None:
|
|
386
389
|
ref = b.node(
|
|
387
390
|
["PySymbol", "PyCallable"],
|
|
388
391
|
"id",
|
|
389
392
|
c.id,
|
|
390
|
-
_callable_props(c, file_key),
|
|
393
|
+
_callable_props(c, file_key, source),
|
|
391
394
|
)
|
|
392
395
|
b.edge(owner_rel, owner, ref)
|
|
393
396
|
|
|
@@ -410,9 +413,10 @@ def _project_callable(
|
|
|
410
413
|
for v in c.local_variables or []:
|
|
411
414
|
_project_variable(b, file_key, ref, c.signature, v)
|
|
412
415
|
for ic in (c.callables or {}).values():
|
|
413
|
-
_project_callable(b, file_key, ref, "PY_DECLARES", ic, externals, sig_to_id
|
|
416
|
+
_project_callable(b, file_key, ref, "PY_DECLARES", ic, externals, sig_to_id,
|
|
417
|
+
source)
|
|
414
418
|
for cl in (c.types or {}).values():
|
|
415
|
-
_project_class(b, file_key, ref, "PY_DECLARES", cl, externals, sig_to_id)
|
|
419
|
+
_project_class(b, file_key, ref, "PY_DECLARES", cl, externals, sig_to_id, source)
|
|
416
420
|
|
|
417
421
|
|
|
418
422
|
def _project_attribute(
|
|
@@ -459,13 +463,24 @@ def _module_props(mod: PyModule, file_key: str) -> Props:
|
|
|
459
463
|
)
|
|
460
464
|
|
|
461
465
|
|
|
462
|
-
def
|
|
466
|
+
def _span_code(source: str, span) -> str | None:
|
|
467
|
+
"""A declaration's text: the owning module's ``source`` sliced by the node's
|
|
468
|
+
utf-8 byte span. Schema v2 stores source once per module, so the graph's
|
|
469
|
+
``code`` property (declared on :PyClass/:PyCallable and indexed by
|
|
470
|
+
``py_code_fts``) is derived here at projection time (#104)."""
|
|
471
|
+
if span is None or not source:
|
|
472
|
+
return None
|
|
473
|
+
lo, hi = span.bytes
|
|
474
|
+
return source.encode("utf-8")[lo:hi].decode("utf-8")
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _class_props(cl: PyClass, file_key: str, source: str) -> Props:
|
|
463
478
|
return prune(
|
|
464
479
|
{
|
|
465
480
|
"id": cl.id,
|
|
466
481
|
"signature": cl.signature,
|
|
467
482
|
"name": cl.name,
|
|
468
|
-
"code":
|
|
483
|
+
"code": _span_code(source, cl.span),
|
|
469
484
|
"base_classes": list(cl.base_classes or []),
|
|
470
485
|
"docstring": _docstring_of(cl.comments),
|
|
471
486
|
"start_line": cl.start_line,
|
|
@@ -475,7 +490,7 @@ def _class_props(cl: PyClass, file_key: str) -> Props:
|
|
|
475
490
|
)
|
|
476
491
|
|
|
477
492
|
|
|
478
|
-
def _callable_props(c: PyCallable, file_key: str) -> Props:
|
|
493
|
+
def _callable_props(c: PyCallable, file_key: str, source: str) -> Props:
|
|
479
494
|
return prune(
|
|
480
495
|
{
|
|
481
496
|
"id": c.id,
|
|
@@ -484,7 +499,7 @@ def _callable_props(c: PyCallable, file_key: str) -> Props:
|
|
|
484
499
|
"path": c.path,
|
|
485
500
|
"return_type": c.return_type,
|
|
486
501
|
"cyclomatic_complexity": c.cyclomatic_complexity,
|
|
487
|
-
"code":
|
|
502
|
+
"code": _span_code(source, c.span),
|
|
488
503
|
"code_start_line": c.code_start_line,
|
|
489
504
|
"start_line": c.start_line,
|
|
490
505
|
"end_line": c.end_line,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "codeanalyzer-python"
|
|
3
|
-
version = "1.0.
|
|
3
|
+
version = "1.0.3"
|
|
4
4
|
description = "Static analysis for Python — canonical schema v2 (symbol table, call graph, and native CFG/PDG/SDG dataflow) as analysis.json or a Neo4j property graph."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
authors = [
|
|
@@ -12,6 +12,9 @@ dependencies = [
|
|
|
12
12
|
# jedi
|
|
13
13
|
"jedi>=0.18.0,<0.20.0; python_version < '3.11'",
|
|
14
14
|
"jedi<=0.19.2; python_version >= '3.11'",
|
|
15
|
+
# parso 0.8.5 is the first release shipping the Python 3.14 grammar; older
|
|
16
|
+
# resolutions make jedi reject every file in a 3.14 analysis env (#107)
|
|
17
|
+
"parso>=0.8.5",
|
|
15
18
|
# msgpack
|
|
16
19
|
"msgpack>=1.0.0,<1.0.7; python_version < '3.11'",
|
|
17
20
|
"msgpack>=1.0.7,<2.0.0; python_version >= '3.11'",
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/dataflow/access_paths.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/dataflow/scalpel_oracle.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/schema/call_graph_ids.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/semantic_analysis/__init__.py
RENAMED
|
File without changes
|
{codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/semantic_analysis/call_graph.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{codeanalyzer_python-1.0.1 → codeanalyzer_python-1.0.3}/codeanalyzer/syntactic_analysis/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|