graphitect 0.2.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.
- graphify/__init__.py +30 -0
- graphify/__main__.py +757 -0
- graphify/_minhash.py +107 -0
- graphify/affected.py +318 -0
- graphify/always_on/agents-md.md +12 -0
- graphify/always_on/antigravity-rules.md +14 -0
- graphify/always_on/claude-md.md +9 -0
- graphify/always_on/gemini-md.md +9 -0
- graphify/always_on/kiro-steering.md +5 -0
- graphify/always_on/vscode-instructions.md +17 -0
- graphify/analyze.py +769 -0
- graphify/benchmark.py +152 -0
- graphify/build.py +2300 -0
- graphify/cache.py +1746 -0
- graphify/callflow_html.py +2051 -0
- graphify/cargo_introspect.py +109 -0
- graphify/cli.py +4745 -0
- graphify/cluster.py +409 -0
- graphify/command-kilo.md +15 -0
- graphify/cross_repo_calls.py +216 -0
- graphify/cross_repo_types.py +75 -0
- graphify/csharp_dispatch.py +154 -0
- graphify/dedup.py +1213 -0
- graphify/detect.py +2566 -0
- graphify/diagnostics.py +406 -0
- graphify/export.py +1349 -0
- graphify/exporters/__init__.py +1 -0
- graphify/exporters/base.py +14 -0
- graphify/exporters/graphdb.py +173 -0
- graphify/exporters/html.py +637 -0
- graphify/extract.py +7856 -0
- graphify/extractors/MIGRATION.md +107 -0
- graphify/extractors/__init__.py +66 -0
- graphify/extractors/apex.py +215 -0
- graphify/extractors/base.py +85 -0
- graphify/extractors/bash.py +579 -0
- graphify/extractors/blade.py +53 -0
- graphify/extractors/commonlisp.py +540 -0
- graphify/extractors/csharp.py +448 -0
- graphify/extractors/dart.py +564 -0
- graphify/extractors/dm.py +494 -0
- graphify/extractors/elixir.py +241 -0
- graphify/extractors/engine.py +6509 -0
- graphify/extractors/fortran.py +311 -0
- graphify/extractors/go.py +527 -0
- graphify/extractors/json_config.py +240 -0
- graphify/extractors/julia.py +289 -0
- graphify/extractors/markdown.py +408 -0
- graphify/extractors/models.py +131 -0
- graphify/extractors/objc.py +566 -0
- graphify/extractors/ocaml.py +289 -0
- graphify/extractors/pascal.py +688 -0
- graphify/extractors/pascal_forms.py +196 -0
- graphify/extractors/powershell.py +522 -0
- graphify/extractors/razor.py +192 -0
- graphify/extractors/resolution.py +3584 -0
- graphify/extractors/robot.py +296 -0
- graphify/extractors/rust.py +470 -0
- graphify/extractors/sln.py +92 -0
- graphify/extractors/sql.py +720 -0
- graphify/extractors/terraform.py +181 -0
- graphify/extractors/verilog.py +329 -0
- graphify/extractors/zig.py +181 -0
- graphify/file_slice.py +246 -0
- graphify/global_graph.py +194 -0
- graphify/google_workspace.py +237 -0
- graphify/hooks.py +933 -0
- graphify/ids.py +93 -0
- graphify/ingest.py +358 -0
- graphify/install.py +2366 -0
- graphify/llm.py +3544 -0
- graphify/manifest.py +4 -0
- graphify/manifest_ingest.py +311 -0
- graphify/mcp_ingest.py +386 -0
- graphify/multigraph_compat.py +212 -0
- graphify/pascal_resolution.py +129 -0
- graphify/paths.py +436 -0
- graphify/pg_introspect.py +165 -0
- graphify/prs.py +770 -0
- graphify/querylog.py +80 -0
- graphify/reflect.py +882 -0
- graphify/report.py +346 -0
- graphify/resolver_registry.py +85 -0
- graphify/ruby_resolution.py +242 -0
- graphify/scip_ingest.py +363 -0
- graphify/security.py +460 -0
- graphify/semantic_cleanup.py +336 -0
- graphify/serve.py +2608 -0
- graphify/skill-agents.md +710 -0
- graphify/skill-aider.md +1283 -0
- graphify/skill-amp.md +710 -0
- graphify/skill-claw.md +713 -0
- graphify/skill-codex.md +710 -0
- graphify/skill-copilot.md +713 -0
- graphify/skill-devin.md +1410 -0
- graphify/skill-droid.md +710 -0
- graphify/skill-kilo.md +722 -0
- graphify/skill-kiro.md +713 -0
- graphify/skill-opencode.md +705 -0
- graphify/skill-pi.md +713 -0
- graphify/skill-trae.md +711 -0
- graphify/skill-vscode.md +709 -0
- graphify/skill-windows.md +755 -0
- graphify/skill.md +713 -0
- graphify/skills/agents/references/add-watch.md +56 -0
- graphify/skills/agents/references/exports.md +87 -0
- graphify/skills/agents/references/extraction-spec.md +70 -0
- graphify/skills/agents/references/github-and-merge.md +46 -0
- graphify/skills/agents/references/hooks.md +33 -0
- graphify/skills/agents/references/query.md +311 -0
- graphify/skills/agents/references/transcribe.md +52 -0
- graphify/skills/agents/references/update.md +210 -0
- graphify/skills/amp/references/add-watch.md +56 -0
- graphify/skills/amp/references/exports.md +87 -0
- graphify/skills/amp/references/extraction-spec.md +70 -0
- graphify/skills/amp/references/github-and-merge.md +46 -0
- graphify/skills/amp/references/hooks.md +33 -0
- graphify/skills/amp/references/query.md +311 -0
- graphify/skills/amp/references/transcribe.md +52 -0
- graphify/skills/amp/references/update.md +210 -0
- graphify/skills/claude/references/add-watch.md +56 -0
- graphify/skills/claude/references/exports.md +87 -0
- graphify/skills/claude/references/extraction-spec.md +70 -0
- graphify/skills/claude/references/github-and-merge.md +46 -0
- graphify/skills/claude/references/hooks.md +33 -0
- graphify/skills/claude/references/query.md +311 -0
- graphify/skills/claude/references/transcribe.md +52 -0
- graphify/skills/claude/references/update.md +210 -0
- graphify/skills/claw/references/add-watch.md +56 -0
- graphify/skills/claw/references/exports.md +87 -0
- graphify/skills/claw/references/extraction-spec.md +31 -0
- graphify/skills/claw/references/github-and-merge.md +46 -0
- graphify/skills/claw/references/hooks.md +33 -0
- graphify/skills/claw/references/query.md +311 -0
- graphify/skills/claw/references/transcribe.md +52 -0
- graphify/skills/claw/references/update.md +210 -0
- graphify/skills/codex/references/add-watch.md +56 -0
- graphify/skills/codex/references/exports.md +87 -0
- graphify/skills/codex/references/extraction-spec.md +31 -0
- graphify/skills/codex/references/github-and-merge.md +46 -0
- graphify/skills/codex/references/hooks.md +33 -0
- graphify/skills/codex/references/query.md +311 -0
- graphify/skills/codex/references/transcribe.md +52 -0
- graphify/skills/codex/references/update.md +210 -0
- graphify/skills/copilot/references/add-watch.md +56 -0
- graphify/skills/copilot/references/exports.md +87 -0
- graphify/skills/copilot/references/extraction-spec.md +70 -0
- graphify/skills/copilot/references/github-and-merge.md +46 -0
- graphify/skills/copilot/references/hooks.md +33 -0
- graphify/skills/copilot/references/query.md +311 -0
- graphify/skills/copilot/references/transcribe.md +52 -0
- graphify/skills/copilot/references/update.md +210 -0
- graphify/skills/droid/references/add-watch.md +56 -0
- graphify/skills/droid/references/exports.md +87 -0
- graphify/skills/droid/references/extraction-spec.md +70 -0
- graphify/skills/droid/references/github-and-merge.md +46 -0
- graphify/skills/droid/references/hooks.md +33 -0
- graphify/skills/droid/references/query.md +311 -0
- graphify/skills/droid/references/transcribe.md +52 -0
- graphify/skills/droid/references/update.md +210 -0
- graphify/skills/kilo/references/add-watch.md +56 -0
- graphify/skills/kilo/references/exports.md +87 -0
- graphify/skills/kilo/references/extraction-spec.md +70 -0
- graphify/skills/kilo/references/github-and-merge.md +46 -0
- graphify/skills/kilo/references/hooks.md +33 -0
- graphify/skills/kilo/references/query.md +311 -0
- graphify/skills/kilo/references/transcribe.md +52 -0
- graphify/skills/kilo/references/update.md +210 -0
- graphify/skills/kiro/references/add-watch.md +56 -0
- graphify/skills/kiro/references/exports.md +87 -0
- graphify/skills/kiro/references/extraction-spec.md +31 -0
- graphify/skills/kiro/references/github-and-merge.md +46 -0
- graphify/skills/kiro/references/hooks.md +33 -0
- graphify/skills/kiro/references/query.md +311 -0
- graphify/skills/kiro/references/transcribe.md +52 -0
- graphify/skills/kiro/references/update.md +210 -0
- graphify/skills/opencode/references/add-watch.md +56 -0
- graphify/skills/opencode/references/exports.md +87 -0
- graphify/skills/opencode/references/extraction-spec.md +70 -0
- graphify/skills/opencode/references/github-and-merge.md +46 -0
- graphify/skills/opencode/references/hooks.md +33 -0
- graphify/skills/opencode/references/query.md +311 -0
- graphify/skills/opencode/references/transcribe.md +52 -0
- graphify/skills/opencode/references/update.md +210 -0
- graphify/skills/pi/references/add-watch.md +56 -0
- graphify/skills/pi/references/exports.md +87 -0
- graphify/skills/pi/references/extraction-spec.md +31 -0
- graphify/skills/pi/references/github-and-merge.md +46 -0
- graphify/skills/pi/references/hooks.md +33 -0
- graphify/skills/pi/references/query.md +311 -0
- graphify/skills/pi/references/transcribe.md +52 -0
- graphify/skills/pi/references/update.md +210 -0
- graphify/skills/trae/references/add-watch.md +56 -0
- graphify/skills/trae/references/exports.md +87 -0
- graphify/skills/trae/references/extraction-spec.md +70 -0
- graphify/skills/trae/references/github-and-merge.md +46 -0
- graphify/skills/trae/references/hooks.md +35 -0
- graphify/skills/trae/references/query.md +311 -0
- graphify/skills/trae/references/transcribe.md +52 -0
- graphify/skills/trae/references/update.md +210 -0
- graphify/skills/vscode/references/add-watch.md +56 -0
- graphify/skills/vscode/references/exports.md +87 -0
- graphify/skills/vscode/references/extraction-spec.md +70 -0
- graphify/skills/vscode/references/github-and-merge.md +46 -0
- graphify/skills/vscode/references/hooks.md +33 -0
- graphify/skills/vscode/references/query.md +311 -0
- graphify/skills/vscode/references/transcribe.md +52 -0
- graphify/skills/vscode/references/update.md +210 -0
- graphify/skills/windows/references/add-watch.md +56 -0
- graphify/skills/windows/references/exports.md +87 -0
- graphify/skills/windows/references/extraction-spec.md +70 -0
- graphify/skills/windows/references/github-and-merge.md +46 -0
- graphify/skills/windows/references/hooks.md +33 -0
- graphify/skills/windows/references/query.md +311 -0
- graphify/skills/windows/references/transcribe.md +52 -0
- graphify/skills/windows/references/update.md +210 -0
- graphify/symbol_resolution.py +556 -0
- graphify/transcribe.py +186 -0
- graphify/tree_html.py +603 -0
- graphify/validate.py +95 -0
- graphify/watch.py +2280 -0
- graphify/wiki.py +405 -0
- graphitect/__init__.py +28 -0
- graphitect/__main__.py +4 -0
- graphitect/_vendor/__init__.py +2 -0
- graphitect/_vendor/archify/LICENSE +22 -0
- graphitect/_vendor/archify/SKILL.md +137 -0
- graphitect/_vendor/archify/THIRD_PARTY_NOTICES.md +69 -0
- graphitect/_vendor/archify/assets/JetBrainsMono-OFL.txt +93 -0
- graphitect/_vendor/archify/assets/template.html +14935 -0
- graphitect/_vendor/archify/bin/archify.mjs +2091 -0
- graphitect/_vendor/archify/bin/open-artifact.mjs +86 -0
- graphitect/_vendor/archify/bin/preview.mjs +653 -0
- graphitect/_vendor/archify/bin/visual-check.mjs +829 -0
- graphitect/_vendor/archify/brand-marks/README.md +31 -0
- graphitect/_vendor/archify/brand-marks/catalog.json +131 -0
- graphitect/_vendor/archify/delta/architecture-delta.mjs +1221 -0
- graphitect/_vendor/archify/examples/agent-run.lifecycle.json +60 -0
- graphitect/_vendor/archify/examples/agent-tool-call.workflow.json +94 -0
- graphitect/_vendor/archify/examples/async-job-roundtrip.sequence.json +61 -0
- graphitect/_vendor/archify/examples/brand-aware-delivery.architecture.json +47 -0
- graphitect/_vendor/archify/examples/cache-miss-request.sequence.json +82 -0
- graphitect/_vendor/archify/examples/checkout-platform.base.architecture.json +31 -0
- graphitect/_vendor/archify/examples/checkout-platform.head.architecture.json +31 -0
- graphitect/_vendor/archify/examples/dataflow-product-analytics.html +15045 -0
- graphitect/_vendor/archify/examples/deployment-release.lifecycle.json +49 -0
- graphitect/_vendor/archify/examples/event-stream.dataflow.json +57 -0
- graphitect/_vendor/archify/examples/incident-response.workflow.json +64 -0
- graphitect/_vendor/archify/examples/lifecycle-agent-run.html +14980 -0
- graphitect/_vendor/archify/examples/product-analytics.dataflow.json +76 -0
- graphitect/_vendor/archify/examples/production-deployment.architecture.json +71 -0
- graphitect/_vendor/archify/examples/release-delivery.workflow.json +62 -0
- graphitect/_vendor/archify/examples/sequence-cache-miss-request.html +15060 -0
- graphitect/_vendor/archify/examples/web-app-rendered.html +15009 -0
- graphitect/_vendor/archify/examples/web-app.architecture.json +46 -0
- graphitect/_vendor/archify/examples/workflow-agent-tool-call-rendered.html +15051 -0
- graphitect/_vendor/archify/migrations/workflow-v2.mjs +279 -0
- graphitect/_vendor/archify/package-lock.json +149 -0
- graphitect/_vendor/archify/package.json +39 -0
- graphitect/_vendor/archify/recipes/scenarios.mjs +391 -0
- graphitect/_vendor/archify/references/authoring-contract.md +243 -0
- graphitect/_vendor/archify/references/brand-marks.md +65 -0
- graphitect/_vendor/archify/references/delivery-contract.md +120 -0
- graphitect/_vendor/archify/references/viewer-runtime.md +45 -0
- graphitect/_vendor/archify/renderers/architecture/grid.mjs +62 -0
- graphitect/_vendor/archify/renderers/architecture/render-architecture.mjs +1078 -0
- graphitect/_vendor/archify/renderers/dataflow/README.md +104 -0
- graphitect/_vendor/archify/renderers/dataflow/render-dataflow.mjs +483 -0
- graphitect/_vendor/archify/renderers/lifecycle/README.md +115 -0
- graphitect/_vendor/archify/renderers/lifecycle/render-lifecycle.mjs +561 -0
- graphitect/_vendor/archify/renderers/sequence/README.md +114 -0
- graphitect/_vendor/archify/renderers/sequence/render-sequence.mjs +464 -0
- graphitect/_vendor/archify/renderers/shared/brand-marks.mjs +563 -0
- graphitect/_vendor/archify/renderers/shared/cli.mjs +218 -0
- graphitect/_vendor/archify/renderers/shared/desktop-readability.mjs +26 -0
- graphitect/_vendor/archify/renderers/shared/diagnostics.mjs +127 -0
- graphitect/_vendor/archify/renderers/shared/engineering-profiles.mjs +157 -0
- graphitect/_vendor/archify/renderers/shared/generated-brand-marks.mjs +2003 -0
- graphitect/_vendor/archify/renderers/shared/generated-validators.mjs +13 -0
- graphitect/_vendor/archify/renderers/shared/geometry.mjs +1423 -0
- graphitect/_vendor/archify/renderers/shared/i18n.mjs +595 -0
- graphitect/_vendor/archify/renderers/shared/layout-report.mjs +40 -0
- graphitect/_vendor/archify/renderers/shared/legend.mjs +217 -0
- graphitect/_vendor/archify/renderers/shared/output-path.mjs +340 -0
- graphitect/_vendor/archify/renderers/shared/repository-evidence.mjs +238 -0
- graphitect/_vendor/archify/renderers/shared/repository-location.mjs +58 -0
- graphitect/_vendor/archify/renderers/shared/text-fit.mjs +49 -0
- graphitect/_vendor/archify/renderers/shared/utils.mjs +232 -0
- graphitect/_vendor/archify/renderers/shared/validator.mjs +86 -0
- graphitect/_vendor/archify/renderers/workflow/README.md +223 -0
- graphitect/_vendor/archify/renderers/workflow/render-workflow.mjs +35 -0
- graphitect/_vendor/archify/renderers/workflow/workflow-compiler.mjs +4400 -0
- graphitect/_vendor/archify/renderers/workflow/workflow-migration-geometry.mjs +144 -0
- graphitect/_vendor/archify/schemas/README.md +211 -0
- graphitect/_vendor/archify/schemas/architecture.schema.json +178 -0
- graphitect/_vendor/archify/schemas/common.schema.json +115 -0
- graphitect/_vendor/archify/schemas/dataflow.schema.json +243 -0
- graphitect/_vendor/archify/schemas/lifecycle.schema.json +266 -0
- graphitect/_vendor/archify/schemas/sequence.schema.json +223 -0
- graphitect/_vendor/archify/schemas/workflow.schema.json +428 -0
- graphitect/_vendor/archify/scripts/check-render-output.mjs +836 -0
- graphitect/_vendor/archify/scripts/check-update.mjs +1667 -0
- graphitect/_vendor/archify/scripts/generate-brand-marks.mjs +141 -0
- graphitect/_vendor/archify/scripts/generate-validators.mjs +66 -0
- graphitect/_vendor/archify/scripts/render-examples.mjs +26 -0
- graphitect/_vendor/archify/scripts/update-contract.mjs +182 -0
- graphitect/_vendor/archify/skill-release.json +10 -0
- graphitect/cli.py +981 -0
- graphitect/deliver/__init__.py +5 -0
- graphitect/deliver/archify_adapter.py +1877 -0
- graphitect/deliver/archify_ir.py +160 -0
- graphitect/deliver/archify_repair.py +135 -0
- graphitect/deliver/doc_compiler.py +916 -0
- graphitect/ground/__init__.py +5 -0
- graphitect/ground/describe_source.py +27 -0
- graphitect/ground/fullread_source.py +56 -0
- graphitect/ground/graphify_source.py +107 -0
- graphitect/models.py +118 -0
- graphitect/skill/SKILL.md +80 -0
- graphitect/skill/agents/openai.yaml +4 -0
- graphitect/synthesize/__init__.py +5 -0
- graphitect/synthesize/engine.py +281 -0
- graphitect/synthesize/llm_backend.py +331 -0
- graphitect/synthesize/questions.py +139 -0
- graphitect/synthesize/rubric.py +104 -0
- graphitect-0.2.0.dist-info/METADATA +284 -0
- graphitect-0.2.0.dist-info/RECORD +336 -0
- graphitect-0.2.0.dist-info/WHEEL +5 -0
- graphitect-0.2.0.dist-info/entry_points.txt +2 -0
- graphitect-0.2.0.dist-info/licenses/LICENSE +21 -0
- graphitect-0.2.0.dist-info/licenses/LICENSE-ARCHIFY-MIT +22 -0
- graphitect-0.2.0.dist-info/licenses/LICENSE-GRAPHIFY-APACHE-2.0 +202 -0
- graphitect-0.2.0.dist-info/licenses/LICENSE-GRAPHIFY-MIT +21 -0
- graphitect-0.2.0.dist-info/licenses/NOTICE-ARCHIFY-THIRD-PARTY.md +69 -0
- graphitect-0.2.0.dist-info/licenses/NOTICE-GRAPHIFY +8 -0
- graphitect-0.2.0.dist-info/top_level.txt +2 -0
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
"""objc — moved verbatim from graphify/extract.py."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from graphify.extractors.base import _file_stem, _make_id, _read_text
|
|
5
|
+
from graphify.extractors.engine import _cpp_declarator_name, _semantic_reference_edge
|
|
6
|
+
from graphify.extractors.resolution import _resolve_c_include_path
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
# A category stem splits only when both halves look like plain ObjC identifiers, so
|
|
12
|
+
# `C++Bridge.h` and `Foo+.h` are left intact.
|
|
13
|
+
_OBJC_STEM_PART = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _objc_category_base_stem(stem: str) -> str:
|
|
17
|
+
"""Strip an ObjC category/extension suffix from a file stem (``Foo+Cat`` -> ``Foo``).
|
|
18
|
+
|
|
19
|
+
A category (``@interface Foo (Cat)``) or class extension (``@interface Foo ()``)
|
|
20
|
+
declares members of an EXISTING class, so its class node must key to the same id
|
|
21
|
+
as ``Foo.h``'s ``@interface Foo`` instead of minting a second class node (#1556).
|
|
22
|
+
Only the final path segment is considered, and only a well-formed
|
|
23
|
+
``Name+Suffix`` pair is split.
|
|
24
|
+
|
|
25
|
+
Mirrors ``_decldef_class_stem``, which already compares sibling header/impl files
|
|
26
|
+
by the stem before ``+``.
|
|
27
|
+
"""
|
|
28
|
+
head, sep, tail = stem.rpartition("/")
|
|
29
|
+
base, plus, suffix = tail.partition("+")
|
|
30
|
+
if not plus or not _OBJC_STEM_PART.fullmatch(base) or not _OBJC_STEM_PART.fullmatch(suffix):
|
|
31
|
+
return stem
|
|
32
|
+
return f"{head}{sep}{base}"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _objc_is_category(node) -> bool:
|
|
36
|
+
"""True for ``@interface/@implementation Foo (Cat)`` and ``Foo ()``.
|
|
37
|
+
|
|
38
|
+
The grammar emits the parentheses as anonymous children only for a category or
|
|
39
|
+
class extension; a generic class (``@interface Box<T>``) uses
|
|
40
|
+
``parameterized_arguments`` instead, so it is not matched.
|
|
41
|
+
"""
|
|
42
|
+
return any(c.type == "(" for c in node.children)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _objc_local_var_types(body_node, source: bytes, table: dict[str, str]) -> None:
|
|
46
|
+
"""Collect ``var -> ClassName`` from ObjC local declarations (``Foo *f = ...;``)
|
|
47
|
+
in a method body, for receiver typing in the cross-file message-send pass
|
|
48
|
+
(#1556). Only a capitalized ``type_identifier`` with a single named declarator
|
|
49
|
+
is recorded; a built-in/lower-cased type or an un-nameable declarator is skipped
|
|
50
|
+
(precision over recall). Reuses the C++ declarator unwrapper (identical grammar).
|
|
51
|
+
"""
|
|
52
|
+
stack = [body_node]
|
|
53
|
+
while stack:
|
|
54
|
+
n = stack.pop()
|
|
55
|
+
if n.type == "method_definition" and n is not body_node:
|
|
56
|
+
continue
|
|
57
|
+
if n.type == "declaration":
|
|
58
|
+
type_node = n.child_by_field_name("type")
|
|
59
|
+
if type_node is None:
|
|
60
|
+
for c in n.children:
|
|
61
|
+
if c.type == "type_identifier":
|
|
62
|
+
type_node = c
|
|
63
|
+
break
|
|
64
|
+
if type_node is not None and type_node.type == "type_identifier":
|
|
65
|
+
type_name = _read_text(type_node, source).strip()
|
|
66
|
+
declarators = [
|
|
67
|
+
c for c in n.children
|
|
68
|
+
if c.type in ("identifier", "pointer_declarator", "init_declarator")
|
|
69
|
+
]
|
|
70
|
+
if type_name and type_name[:1].isupper() and len(declarators) == 1:
|
|
71
|
+
var = _cpp_declarator_name(declarators[0], source)
|
|
72
|
+
if var and var not in table:
|
|
73
|
+
table[var] = type_name
|
|
74
|
+
for c in n.children:
|
|
75
|
+
stack.append(c)
|
|
76
|
+
|
|
77
|
+
def extract_objc(path: Path) -> dict:
|
|
78
|
+
"""Extract interfaces, implementations, protocols, methods, and imports from .m/.mm/.h files."""
|
|
79
|
+
try:
|
|
80
|
+
import tree_sitter_objc as tsobjc
|
|
81
|
+
from tree_sitter import Language, Parser
|
|
82
|
+
except ImportError:
|
|
83
|
+
return {"nodes": [], "edges": [], "error": "tree_sitter_objc not installed"}
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
language = Language(tsobjc.language())
|
|
87
|
+
parser = Parser(language)
|
|
88
|
+
source = path.read_bytes()
|
|
89
|
+
# tree-sitter-objc cannot expand these argument-less annotation macros (no
|
|
90
|
+
# trailing ';'), and their presence before @interface makes the parser fail to
|
|
91
|
+
# emit a class_interface node (#1475). Blank them to equal-length spaces so byte
|
|
92
|
+
# offsets / line numbers are preserved and the interface parses.
|
|
93
|
+
_OBJC_BLANK_MACROS = (b"NS_ASSUME_NONNULL_BEGIN", b"NS_ASSUME_NONNULL_END")
|
|
94
|
+
for _m in _OBJC_BLANK_MACROS:
|
|
95
|
+
source = source.replace(_m, b" " * len(_m))
|
|
96
|
+
tree = parser.parse(source)
|
|
97
|
+
root = tree.root_node
|
|
98
|
+
except Exception as e:
|
|
99
|
+
return {"nodes": [], "edges": [], "error": str(e)}
|
|
100
|
+
|
|
101
|
+
stem = _file_stem(path)
|
|
102
|
+
str_path = str(path)
|
|
103
|
+
nodes: list[dict] = []
|
|
104
|
+
edges: list[dict] = []
|
|
105
|
+
seen_ids: set[str] = set()
|
|
106
|
+
method_bodies: list[tuple[str, Any, str]] = []
|
|
107
|
+
# #1556: unresolved message sends saved for the cross-file ObjC resolver, plus a
|
|
108
|
+
# per-file `var -> ClassName` table from `Foo *f = ...;` local declarations.
|
|
109
|
+
raw_calls: list[dict] = []
|
|
110
|
+
objc_type_table: dict[str, str] = {}
|
|
111
|
+
# #1556: per-CLASS `field -> ClassName` tables from `@property Bar *bar;` and
|
|
112
|
+
# ivar `Bar *_bar;` declarations, keyed by the class nid the .h/.m pair share
|
|
113
|
+
# (preserved by _merge_decl_def_classes), so the cross-file resolver can type a
|
|
114
|
+
# `[self.bar doIt]` / `[_bar doIt]` receiver. A conflicting redeclaration of the
|
|
115
|
+
# same (class, field) tombstones the entry (None) — drop, don't guess.
|
|
116
|
+
objc_field_types: dict[str, dict[str, str | None]] = {}
|
|
117
|
+
|
|
118
|
+
def add_node(nid: str, label: str, line: int) -> None:
|
|
119
|
+
if nid not in seen_ids:
|
|
120
|
+
seen_ids.add(nid)
|
|
121
|
+
nodes.append({"id": nid, "label": label, "file_type": "code",
|
|
122
|
+
"source_file": str_path, "source_location": f"L{line}"})
|
|
123
|
+
|
|
124
|
+
def add_edge(src: str, tgt: str, relation: str, line: int,
|
|
125
|
+
confidence: str = "EXTRACTED", weight: float = 1.0,
|
|
126
|
+
context: str | None = None) -> None:
|
|
127
|
+
edge = {"source": src, "target": tgt, "relation": relation,
|
|
128
|
+
"confidence": confidence, "source_file": str_path,
|
|
129
|
+
"source_location": f"L{line}", "weight": weight}
|
|
130
|
+
if context:
|
|
131
|
+
edge["context"] = context
|
|
132
|
+
edges.append(edge)
|
|
133
|
+
|
|
134
|
+
file_nid = _make_id(str(path))
|
|
135
|
+
add_node(file_nid, path.name, 1)
|
|
136
|
+
|
|
137
|
+
def _read(node) -> str:
|
|
138
|
+
return source[node.start_byte:node.end_byte].decode("utf-8", errors="replace")
|
|
139
|
+
|
|
140
|
+
def _get_name(node, field: str) -> str | None:
|
|
141
|
+
n = node.child_by_field_name(field)
|
|
142
|
+
return _read(n) if n else None
|
|
143
|
+
|
|
144
|
+
def _type_identifiers(node):
|
|
145
|
+
"""Yield every type_identifier under a property's type node, descending
|
|
146
|
+
through generic_specifier/type_name so NSArray<Product *> yields both
|
|
147
|
+
NSArray and the element type Product (the generic case was invisible
|
|
148
|
+
because the type was wrapped in a generic_specifier, not a bare
|
|
149
|
+
type_identifier child) (#1475)."""
|
|
150
|
+
if node.type == "type_identifier":
|
|
151
|
+
yield node
|
|
152
|
+
return
|
|
153
|
+
for c in node.children:
|
|
154
|
+
yield from _type_identifiers(c)
|
|
155
|
+
|
|
156
|
+
def ensure_named_node(name: str, line: int) -> str:
|
|
157
|
+
nid = _make_id(stem, name)
|
|
158
|
+
if nid in seen_ids:
|
|
159
|
+
return nid
|
|
160
|
+
nid = _make_id(name)
|
|
161
|
+
if nid not in seen_ids:
|
|
162
|
+
# The name isn't defined in this file, so this is a cross-file reference
|
|
163
|
+
# (e.g. a `Thing` type annotation imported from another module). Emit a
|
|
164
|
+
# SOURCELESS stub — like the inheritance-base path below — so the
|
|
165
|
+
# corpus-level rewire can collapse it onto the real definition. A sourced
|
|
166
|
+
# stub here makes _disambiguate_colliding_node_ids bake the referencing
|
|
167
|
+
# file's path (with extension) into the id and blocks the rewire, which is
|
|
168
|
+
# the phantom-duplicate-node bug (#1402).
|
|
169
|
+
seen_ids.add(nid)
|
|
170
|
+
nodes.append({
|
|
171
|
+
"id": nid,
|
|
172
|
+
"label": name,
|
|
173
|
+
"file_type": "code",
|
|
174
|
+
"source_file": "",
|
|
175
|
+
"source_location": "",
|
|
176
|
+
"origin_file": str_path,
|
|
177
|
+
})
|
|
178
|
+
return nid
|
|
179
|
+
|
|
180
|
+
def _field_decl_entry(sd) -> tuple[str, str] | None:
|
|
181
|
+
"""``(field, TypeName)`` from a property/ivar ``struct_declaration``, else None.
|
|
182
|
+
|
|
183
|
+
Precision gates (#1556): the type must be a BARE capitalized
|
|
184
|
+
``type_identifier`` DIRECTLY under the struct_declaration — a
|
|
185
|
+
``generic_specifier`` (``NSArray<Bar *>``) or ``typedefed_specifier``
|
|
186
|
+
(``id<P>``) wraps its type_identifier, so both are naturally excluded and
|
|
187
|
+
never type a receiver — and there must be exactly one ``struct_declarator``
|
|
188
|
+
whose name unwraps via the C++ declarator unwrapper (identical grammar).
|
|
189
|
+
"""
|
|
190
|
+
type_ids = [s for s in sd.children if s.type == "type_identifier"]
|
|
191
|
+
declarators = [s for s in sd.children if s.type == "struct_declarator"]
|
|
192
|
+
if len(type_ids) != 1 or len(declarators) != 1:
|
|
193
|
+
return None
|
|
194
|
+
type_name = _read(type_ids[0]).strip()
|
|
195
|
+
if not type_name or not type_name[:1].isupper():
|
|
196
|
+
return None
|
|
197
|
+
# struct_declarator wraps ONE pointer_declarator (`*bar`) or identifier
|
|
198
|
+
# (`bar`); anything else (bitfields, arrays) has more children -> bail.
|
|
199
|
+
inner = declarators[0].children
|
|
200
|
+
if len(inner) != 1:
|
|
201
|
+
return None
|
|
202
|
+
field = _cpp_declarator_name(inner[0], source)
|
|
203
|
+
if not field:
|
|
204
|
+
return None
|
|
205
|
+
return field, type_name
|
|
206
|
+
|
|
207
|
+
def _record_field_type(cls_nid: str, field: str, type_name: str) -> None:
|
|
208
|
+
table = objc_field_types.setdefault(cls_nid, {})
|
|
209
|
+
if field in table:
|
|
210
|
+
if table[field] != type_name:
|
|
211
|
+
table[field] = None # conflicting redeclaration -> drop, don't guess
|
|
212
|
+
else:
|
|
213
|
+
table[field] = type_name
|
|
214
|
+
|
|
215
|
+
def _collect_instance_variables(ivars_node, cls_nid: str) -> None:
|
|
216
|
+
"""Record field types from an ``instance_variables`` block (``{ Bar *_b; }``)
|
|
217
|
+
under an @interface or @implementation. No nodes/edges are emitted here;
|
|
218
|
+
the table only feeds receiver typing in the cross-file resolver (#1556).
|
|
219
|
+
"""
|
|
220
|
+
for iv in ivars_node.children:
|
|
221
|
+
if iv.type != "instance_variable":
|
|
222
|
+
continue
|
|
223
|
+
for sd in iv.children:
|
|
224
|
+
if sd.type == "struct_declaration":
|
|
225
|
+
entry = _field_decl_entry(sd)
|
|
226
|
+
if entry is not None:
|
|
227
|
+
_record_field_type(cls_nid, *entry)
|
|
228
|
+
|
|
229
|
+
def walk(node, parent_nid: str | None = None) -> None:
|
|
230
|
+
t = node.type
|
|
231
|
+
line = node.start_point[0] + 1
|
|
232
|
+
|
|
233
|
+
if t == "preproc_include":
|
|
234
|
+
# #import <Foundation/Foundation.h> or #import "MyClass.h"
|
|
235
|
+
for child in node.children:
|
|
236
|
+
if child.type == "system_lib_string":
|
|
237
|
+
raw = _read(child).strip("<>")
|
|
238
|
+
module = raw.split("/")[-1].replace(".h", "")
|
|
239
|
+
if module:
|
|
240
|
+
tgt_nid = _make_id(module)
|
|
241
|
+
add_edge(file_nid, tgt_nid, "imports", line, context="import")
|
|
242
|
+
elif child.type == "string_literal":
|
|
243
|
+
# recurse into string_literal to find string_content
|
|
244
|
+
for sub in child.children:
|
|
245
|
+
if sub.type == "string_content":
|
|
246
|
+
raw = _read(sub)
|
|
247
|
+
# Resolve the quoted include to a real file so the target id
|
|
248
|
+
# matches the (possibly disambiguated) node id _make_id gives
|
|
249
|
+
# that file; the bare-stem id never survives
|
|
250
|
+
# _disambiguate_colliding_node_ids when a .h/.m pair exists,
|
|
251
|
+
# so the edge dangled and was dropped (#1475).
|
|
252
|
+
resolved = _resolve_c_include_path(raw, str_path)
|
|
253
|
+
if resolved is not None:
|
|
254
|
+
add_edge(file_nid, _make_id(str(resolved)), "imports", line, context="import")
|
|
255
|
+
else:
|
|
256
|
+
module = raw.split("/")[-1].replace(".h", "")
|
|
257
|
+
if module:
|
|
258
|
+
add_edge(file_nid, _make_id(module), "imports", line, context="import")
|
|
259
|
+
return
|
|
260
|
+
|
|
261
|
+
if t == "module_import":
|
|
262
|
+
# @import Foundation; / @import Foundation.NSString;
|
|
263
|
+
path_node = node.child_by_field_name("path")
|
|
264
|
+
if path_node is not None:
|
|
265
|
+
module = _read(path_node).split(".")[0].strip()
|
|
266
|
+
if module:
|
|
267
|
+
add_edge(file_nid, _make_id(module), "imports", line, context="import")
|
|
268
|
+
return
|
|
269
|
+
|
|
270
|
+
if t == "class_interface":
|
|
271
|
+
# @interface ClassName : SuperClass <Protocols>
|
|
272
|
+
# children: @interface, identifier(name), ':', identifier(super), parameterized_arguments, ...
|
|
273
|
+
identifiers = [c for c in node.children if c.type == "identifier"]
|
|
274
|
+
if not identifiers:
|
|
275
|
+
for child in node.children:
|
|
276
|
+
walk(child, parent_nid)
|
|
277
|
+
return
|
|
278
|
+
name = _read(identifiers[0])
|
|
279
|
+
# A category / class extension extends an existing class, so key its
|
|
280
|
+
# class node off the BASE stem (`Foo+Cat.h` -> `Foo`). Without this,
|
|
281
|
+
# `Foo+Cat.h` minted a SECOND node labelled `Foo`, which made every
|
|
282
|
+
# `[Foo ...]` receiver ambiguous and tripped the resolver's
|
|
283
|
+
# single-definition god-node guard — destroying edges the same corpus
|
|
284
|
+
# produced fine when the members lived in `Foo.h` (#1556).
|
|
285
|
+
cls_stem = _objc_category_base_stem(stem) if _objc_is_category(node) else stem
|
|
286
|
+
cls_nid = _make_id(cls_stem, name)
|
|
287
|
+
add_node(cls_nid, name, line)
|
|
288
|
+
add_edge(file_nid, cls_nid, "contains", line)
|
|
289
|
+
# superclass is second identifier after ':'
|
|
290
|
+
colon_seen = False
|
|
291
|
+
for child in node.children:
|
|
292
|
+
if child.type == ":":
|
|
293
|
+
colon_seen = True
|
|
294
|
+
elif colon_seen and child.type == "identifier":
|
|
295
|
+
super_nid = ensure_named_node(_read(child), line)
|
|
296
|
+
add_edge(cls_nid, super_nid, "inherits", line)
|
|
297
|
+
colon_seen = False
|
|
298
|
+
elif child.type == "parameterized_arguments":
|
|
299
|
+
# protocols adopted: @interface Foo : Bar <Proto1, Proto2>
|
|
300
|
+
for sub in child.children:
|
|
301
|
+
if sub.type == "type_name":
|
|
302
|
+
for s in sub.children:
|
|
303
|
+
if s.type == "type_identifier":
|
|
304
|
+
proto_nid = ensure_named_node(_read(s), line)
|
|
305
|
+
add_edge(cls_nid, proto_nid, "implements", line)
|
|
306
|
+
elif child.type == "property_declaration":
|
|
307
|
+
prop_line = child.start_point[0] + 1
|
|
308
|
+
for sub in child.children:
|
|
309
|
+
if sub.type == "struct_declaration":
|
|
310
|
+
# The type is either a direct type_identifier
|
|
311
|
+
# (NSString *x) or wrapped in a generic_specifier
|
|
312
|
+
# (NSArray<Product *> *xs). Walk every type name in the
|
|
313
|
+
# type portion, skipping the declarator (the *field
|
|
314
|
+
# name), so generic collections are no longer invisible.
|
|
315
|
+
seen_types: set[str] = set()
|
|
316
|
+
for s in sub.children:
|
|
317
|
+
if s.type in ("struct_declarator", ";"):
|
|
318
|
+
continue
|
|
319
|
+
for ti in _type_identifiers(s):
|
|
320
|
+
tname = _read(ti)
|
|
321
|
+
if tname in seen_types:
|
|
322
|
+
continue
|
|
323
|
+
seen_types.add(tname)
|
|
324
|
+
type_nid = ensure_named_node(tname, prop_line)
|
|
325
|
+
edges.append(_semantic_reference_edge(
|
|
326
|
+
cls_nid, type_nid, "field", str_path, prop_line))
|
|
327
|
+
# #1556: a bare capitalized property type also types the
|
|
328
|
+
# `[self.field msg]` receiver in the cross-file resolver.
|
|
329
|
+
entry = _field_decl_entry(sub)
|
|
330
|
+
if entry is not None:
|
|
331
|
+
_record_field_type(cls_nid, *entry)
|
|
332
|
+
elif child.type == "instance_variables":
|
|
333
|
+
_collect_instance_variables(child, cls_nid)
|
|
334
|
+
elif child.type == "method_declaration":
|
|
335
|
+
walk(child, cls_nid)
|
|
336
|
+
return
|
|
337
|
+
|
|
338
|
+
if t == "class_implementation":
|
|
339
|
+
# @implementation ClassName
|
|
340
|
+
name = None
|
|
341
|
+
for child in node.children:
|
|
342
|
+
if child.type == "identifier":
|
|
343
|
+
name = _read(child)
|
|
344
|
+
break
|
|
345
|
+
if not name:
|
|
346
|
+
for child in node.children:
|
|
347
|
+
walk(child, parent_nid)
|
|
348
|
+
return
|
|
349
|
+
impl_stem = _objc_category_base_stem(stem) if _objc_is_category(node) else stem
|
|
350
|
+
impl_nid = _make_id(impl_stem, name)
|
|
351
|
+
if impl_nid not in seen_ids:
|
|
352
|
+
add_node(impl_nid, name, line)
|
|
353
|
+
add_edge(file_nid, impl_nid, "contains", line)
|
|
354
|
+
for child in node.children:
|
|
355
|
+
if child.type == "instance_variables":
|
|
356
|
+
_collect_instance_variables(child, impl_nid)
|
|
357
|
+
elif child.type == "implementation_definition":
|
|
358
|
+
for sub in child.children:
|
|
359
|
+
walk(sub, impl_nid)
|
|
360
|
+
return
|
|
361
|
+
|
|
362
|
+
if t == "protocol_declaration":
|
|
363
|
+
name = None
|
|
364
|
+
for child in node.children:
|
|
365
|
+
if child.type == "identifier":
|
|
366
|
+
name = _read(child)
|
|
367
|
+
break
|
|
368
|
+
if name:
|
|
369
|
+
proto_nid = _make_id(stem, name)
|
|
370
|
+
add_node(proto_nid, f"<{name}>", line)
|
|
371
|
+
add_edge(file_nid, proto_nid, "contains", line)
|
|
372
|
+
# Adopted protocols: `@protocol Derived <Base, Other>`. These
|
|
373
|
+
# nest under a protocol_reference_list node (distinct from the
|
|
374
|
+
# parameterized_arguments node used by @interface adoption), so
|
|
375
|
+
# they were never emitted. Emit an `implements` edge for each,
|
|
376
|
+
# matching how @interface protocol adoption is handled.
|
|
377
|
+
for child in node.children:
|
|
378
|
+
if child.type == "protocol_reference_list":
|
|
379
|
+
for sub in child.children:
|
|
380
|
+
if sub.type == "identifier":
|
|
381
|
+
base_nid = ensure_named_node(_read(sub), line)
|
|
382
|
+
if base_nid != proto_nid:
|
|
383
|
+
add_edge(proto_nid, base_nid, "implements", line)
|
|
384
|
+
for child in node.children:
|
|
385
|
+
walk(child, proto_nid)
|
|
386
|
+
return
|
|
387
|
+
|
|
388
|
+
if t in ("method_declaration", "method_definition"):
|
|
389
|
+
container = parent_nid or file_nid
|
|
390
|
+
# Class methods start with '+', instance methods with '-' (the grammar
|
|
391
|
+
# emits the sigil as the first child). The selector is the concatenation
|
|
392
|
+
# of the direct identifier children: one for a simple selector (-go),
|
|
393
|
+
# several for a compound one (-tableView:numberOfRowsInSection: ->
|
|
394
|
+
# "tableViewnumberOfRowsInSection"); method_parameter holds the arg
|
|
395
|
+
# types/names, not selector keywords, so it is correctly skipped.
|
|
396
|
+
prefix = "-"
|
|
397
|
+
for child in node.children:
|
|
398
|
+
if child.type in ("+", "-"):
|
|
399
|
+
prefix = child.type
|
|
400
|
+
break
|
|
401
|
+
parts = [_read(c) for c in node.children if c.type == "identifier"]
|
|
402
|
+
method_name = "".join(parts) if parts else None
|
|
403
|
+
if method_name:
|
|
404
|
+
method_nid = _make_id(container, method_name)
|
|
405
|
+
add_node(method_nid, f"{prefix}{method_name}", line)
|
|
406
|
+
add_edge(container, method_nid, "method", line)
|
|
407
|
+
if t == "method_definition":
|
|
408
|
+
method_bodies.append((method_nid, node, container))
|
|
409
|
+
return
|
|
410
|
+
|
|
411
|
+
for child in node.children:
|
|
412
|
+
walk(child, parent_nid)
|
|
413
|
+
|
|
414
|
+
walk(root)
|
|
415
|
+
|
|
416
|
+
# Second pass: resolve calls inside method bodies
|
|
417
|
+
all_method_nids = {n["id"] for n in nodes if n["id"] != file_nid}
|
|
418
|
+
class_method_nids: dict[str, set[str]] = {}
|
|
419
|
+
for m_nid, _, container_nid in method_bodies:
|
|
420
|
+
class_method_nids.setdefault(container_nid, set()).add(m_nid)
|
|
421
|
+
seen_calls: set[tuple[str, str]] = set()
|
|
422
|
+
# #1556: per-file `var -> ClassName` table from local declarations in every
|
|
423
|
+
# method body, so the cross-file resolver can type a `[f doThing]` receiver.
|
|
424
|
+
for _m_nid, body_node, _container in method_bodies:
|
|
425
|
+
_objc_local_var_types(body_node, source, objc_type_table)
|
|
426
|
+
|
|
427
|
+
for caller_nid, body_node, container_nid in method_bodies:
|
|
428
|
+
sibling_nids = class_method_nids.get(container_nid, set())
|
|
429
|
+
|
|
430
|
+
def walk_calls(n) -> None:
|
|
431
|
+
if n.type == "message_expression":
|
|
432
|
+
# `[[Foo alloc] init]` is a message_expression whose method is the
|
|
433
|
+
# identifier `alloc` and whose receiver is the bare class identifier
|
|
434
|
+
# `Foo`; resolve that class name and emit a `references` edge so the
|
|
435
|
+
# allocating method links to the allocated type. ensure_named_node
|
|
436
|
+
# emits a sourceless stub for unknown names, which the corpus rewire
|
|
437
|
+
# collapses ONLY when exactly one real class of that name exists, so an
|
|
438
|
+
# unknown/ambiguous class produces no false resolved edge (#1475).
|
|
439
|
+
meth = n.child_by_field_name("method")
|
|
440
|
+
recv = n.child_by_field_name("receiver")
|
|
441
|
+
if (meth is not None and meth.type == "identifier" and _read(meth) == "alloc"
|
|
442
|
+
and recv is not None and recv.type == "identifier"):
|
|
443
|
+
tname = _read(recv)
|
|
444
|
+
ref_line = n.start_point[0] + 1
|
|
445
|
+
type_nid = ensure_named_node(tname, ref_line)
|
|
446
|
+
if type_nid != caller_nid:
|
|
447
|
+
edges.append(_semantic_reference_edge(
|
|
448
|
+
caller_nid, type_nid, "type", str_path, ref_line))
|
|
449
|
+
# [receiver sel] and [receiver kw1:a kw2:b] both parse to a
|
|
450
|
+
# message_expression whose selector parts carry the field name
|
|
451
|
+
# "method" (one for a simple selector, several for a compound one);
|
|
452
|
+
# the receiver carries field name "receiver". Reconstruct the
|
|
453
|
+
# selector from every "method" child so self/super/ClassName
|
|
454
|
+
# receivers are never mistaken for a selector, and compound sends
|
|
455
|
+
# resolve too (the whole second pass was previously dead code for
|
|
456
|
+
# ObjC because the grammar emits these as `identifier`, not
|
|
457
|
+
# `selector`/`keyword_argument_list`) (#1475).
|
|
458
|
+
sel_parts = [
|
|
459
|
+
_read(child)
|
|
460
|
+
for i, child in enumerate(n.children)
|
|
461
|
+
if n.field_name_for_child(i) == "method" and child.type == "identifier"
|
|
462
|
+
]
|
|
463
|
+
method_name = "".join(sel_parts)
|
|
464
|
+
if method_name:
|
|
465
|
+
needle = _make_id("", method_name).lstrip("_")
|
|
466
|
+
for candidate in all_method_nids:
|
|
467
|
+
if candidate.endswith(needle):
|
|
468
|
+
pair = (caller_nid, candidate)
|
|
469
|
+
if pair not in seen_calls and caller_nid != candidate:
|
|
470
|
+
seen_calls.add(pair)
|
|
471
|
+
add_edge(caller_nid, candidate, "calls", n.start_point[0] + 1,
|
|
472
|
+
confidence="EXTRACTED", weight=1.0, context="call")
|
|
473
|
+
# #1556: also emit a raw_call so the cross-file resolver can type
|
|
474
|
+
# the receiver and link to a method in ANOTHER file. A bare
|
|
475
|
+
# identifier receiver (`f`, `self`, `Foo`) is captured; a nested
|
|
476
|
+
# message send (`[[Foo alloc] init]`) has no simple receiver name
|
|
477
|
+
# to type, so it is left to the alloc/init `references` edge above.
|
|
478
|
+
if recv is not None and recv.type == "identifier":
|
|
479
|
+
raw_calls.append({
|
|
480
|
+
"caller_nid": caller_nid,
|
|
481
|
+
"callee": method_name,
|
|
482
|
+
"is_member_call": True,
|
|
483
|
+
"source_file": str_path,
|
|
484
|
+
"source_location": f"L{n.start_point[0] + 1}",
|
|
485
|
+
"receiver": _read(recv),
|
|
486
|
+
"lang": "objc",
|
|
487
|
+
})
|
|
488
|
+
elif recv is not None and recv.type == "field_expression":
|
|
489
|
+
# `[self.bar doIt]`: capture ONLY the exact `self.<field>`
|
|
490
|
+
# shape and stamp the BARE field name, typed via the class's
|
|
491
|
+
# property/ivar table. Anything else (`obj.prop`, chains,
|
|
492
|
+
# `Foo.shared`) stays dropped: passing the dotted text
|
|
493
|
+
# through would let a capitalized `Foo.shared` enter the
|
|
494
|
+
# explicit-class arm, where _key strips the dot and collides
|
|
495
|
+
# with a real class `FooShared` — a fabricated edge (#1556).
|
|
496
|
+
kids = recv.children
|
|
497
|
+
if (len(kids) == 3 and kids[0].type == "identifier"
|
|
498
|
+
and _read(kids[0]) == "self" and kids[1].type == "."
|
|
499
|
+
and kids[2].type == "field_identifier"):
|
|
500
|
+
raw_calls.append({
|
|
501
|
+
"caller_nid": caller_nid,
|
|
502
|
+
"callee": method_name,
|
|
503
|
+
"is_member_call": True,
|
|
504
|
+
"source_file": str_path,
|
|
505
|
+
"source_location": f"L{n.start_point[0] + 1}",
|
|
506
|
+
"receiver": _read(kids[2]),
|
|
507
|
+
"receiver_kind": "self_field",
|
|
508
|
+
"lang": "objc",
|
|
509
|
+
})
|
|
510
|
+
elif n.type == "field_expression":
|
|
511
|
+
# self.name / self.product.name — dot-syntax sugar for [self name].
|
|
512
|
+
# Resolve to a sibling method of the SAME class, matched by EXACT
|
|
513
|
+
# node id (a method id is _make_id(container, name)). A suffix
|
|
514
|
+
# substring match would mis-resolve self.name -> -surname and would
|
|
515
|
+
# let a substring-colliding sibling (-surname) suppress the real
|
|
516
|
+
# -name edge, so it must be an exact match (#1475).
|
|
517
|
+
for child in n.children:
|
|
518
|
+
if child.type == "field_identifier":
|
|
519
|
+
field_name = _read(child)
|
|
520
|
+
target = _make_id(container_nid, field_name)
|
|
521
|
+
if target in sibling_nids and target != caller_nid:
|
|
522
|
+
pair = (caller_nid, target)
|
|
523
|
+
if pair not in seen_calls:
|
|
524
|
+
seen_calls.add(pair)
|
|
525
|
+
add_edge(caller_nid, target, "accesses",
|
|
526
|
+
n.start_point[0] + 1,
|
|
527
|
+
confidence="EXTRACTED", weight=1.0)
|
|
528
|
+
elif n.type == "selector_expression":
|
|
529
|
+
# @selector(doSomething:withParam:) — compile-time method ref.
|
|
530
|
+
# Match the selector name EXACTLY (a method id is
|
|
531
|
+
# _make_id(container, name)) against every class's methods, and emit
|
|
532
|
+
# only when exactly one method matches, to avoid ambiguous fan-out.
|
|
533
|
+
# Exact match (not a suffix) keeps -doThing distinct from
|
|
534
|
+
# -reallyDoThing (#1475).
|
|
535
|
+
sel_parts = [_read(c) for c in n.children if c.type == "identifier"]
|
|
536
|
+
sel_name = "".join(sel_parts)
|
|
537
|
+
if sel_name:
|
|
538
|
+
matches = sorted({
|
|
539
|
+
m for m, _, cont in method_bodies
|
|
540
|
+
if m == _make_id(cont, sel_name) and m != caller_nid
|
|
541
|
+
})
|
|
542
|
+
if len(matches) == 1:
|
|
543
|
+
pair = (caller_nid, matches[0])
|
|
544
|
+
if pair not in seen_calls:
|
|
545
|
+
seen_calls.add(pair)
|
|
546
|
+
add_edge(caller_nid, matches[0], "calls",
|
|
547
|
+
n.start_point[0] + 1,
|
|
548
|
+
confidence="EXTRACTED", weight=1.0,
|
|
549
|
+
context="call")
|
|
550
|
+
for child in n.children:
|
|
551
|
+
walk_calls(child)
|
|
552
|
+
walk_calls(body_node)
|
|
553
|
+
|
|
554
|
+
result = {"nodes": nodes, "edges": edges, "raw_calls": raw_calls,
|
|
555
|
+
"input_tokens": 0, "output_tokens": 0}
|
|
556
|
+
if objc_type_table:
|
|
557
|
+
result["objc_type_table"] = {"path": str_path, "table": objc_type_table}
|
|
558
|
+
# Drop tombstoned (conflicting) entries and empty tables before export.
|
|
559
|
+
field_tables = {
|
|
560
|
+
cls: {f: t for f, t in tbl.items() if t}
|
|
561
|
+
for cls, tbl in objc_field_types.items()
|
|
562
|
+
}
|
|
563
|
+
field_tables = {cls: tbl for cls, tbl in field_tables.items() if tbl}
|
|
564
|
+
if field_tables:
|
|
565
|
+
result["objc_field_types"] = {"path": str_path, "tables": field_tables}
|
|
566
|
+
return result
|