# CPG слой: интеграция Joern

Документ фиксирует, **что именно проверено** про Joern применительно к нашему корпусу, и какие выводы из этого следуют для конвейера. Составлен по первоисточникам (исходники `joernio/joern`, официальная документация), а не по памяти: каждый факт помечен уровнем достоверности.

---

## 1. Ключевое открытие, меняющее предусловия

**php2cpg не содержит собственного парсера PHP.** Он вызывает внешний `php` с бандленным `php-parser.phar`.

Подтверждено по исходникам:

- `joern-cli/frontends/php2cpg/README.md`: *«A PHP to CPG converter based on `php-parse`. Requirements: PHP (>=7.0)»*;
- `PhpParser.scala`, метод `phpParseCommand`: `Seq("php", "--php-ini", phpIniPath, phpParserPath) ++ Seq("--with-recovery", "--resolve-names", "--json-dump") ++ filenames`;
- `Php2Cpg.createCpg` → `isPhpVersionSupported` запускает `ExternalCommand.run(Seq("php", "--version"))` и требует **≥ 7.1.0**; иначе `Failure("php not found or version not supported")`.

**Следствие для конвейера:** без `php` в `PATH` фронтенд не деградирует, а **отказывает**. Поэтому в `cpg.py` появилась отдельная проверка `find_php()`, а окружение процесса собирается так, чтобы каталог с `php` был первым в `PATH` — фронтенд вызывает буквально `php`, полный путь к `php.exe` не подходит.

На этой машине: **PHP 8.5 найден** (`C:\tools\php85\php.exe`).

---

## 2. Проверенные факты о структуре и запуске

| Факт | Источник | Уровень |
|---|---|---|
| Фронтенды и их скрипты лежат в `joern-cli/frontends/<имя>/` | `joern-cli/build.sbt`: `frontendMappings("php2cpg", …)` → `ref -> s"frontends/$frontendName/$name"` | подтверждено |
| Утилиты `joern`, `joern-parse`, `joern-export`, `joern-slice` — в `joern-cli/bin/` | механика sbt-native-packager (`JavaAppPackaging`) | вывод, не цитата |
| Язык задаётся `--language PHP` (регистр не важен) | таблица frontends; `buildLanguageList` — «Available languages (case insensitive)» | подтверждено |
| `--frontend-args` передаёт аргументы фронтенду дословно | `CpgBasedTool.scala`: `ARGS_DELIMITER = "--frontend-args"` | подтверждено |
| `--exclude <путь>` исключает файлы (путь относительно входа) | docs.joern.io/frontends/x2cpg | подтверждено |
| Скриптовый режим требует `@main def exec(...)` и `--param k=v` | docs.joern.io/interpreter | подтверждено |
| Результат запроса пишется оператором `#>` в файл | docs.joern.io/shell | подтверждено |
| `PHP_PARSER_BIN` переопределяет путь к парсеру | `PhpParser.scala`: `private val PhpParserBinEnvVar = "PHP_PARSER_BIN"` | подтверждено |
| Фронтенд всегда передаёт `--with-recovery` | `phpParseCommand` | подтверждено |

**Не проверено (требует запуска на распакованном дистрибутиве):**
- точный состав `joern-cli/bin` и наличие `.bat`-двойников;
- наличие флага одиночной команды у `joern` (`-c`/`--command`) — в документации его нет, в конвейер не закладывается;
- поведение `-J-Xmx` именно у `joern-parse.bat`.

---

## 3. Как извлекать данные (проверено по коду экспортёра)

`cpg.bin` — бинарник flatgraph, **из Python напрямую не читается**. Нужен экспорт.

| Что нужно | `--repr` | Совместимые форматы |
|---|---|---|
| CFG / DDG / CDG / PDG | `cfg`/`ddg`/`cdg`/`pdg` | **только `dot`** |
| Весь граф со всеми типами рёбер | `all` | `graphml`, `graphson`, `neo4jcsv`, `dot` |
| Подграф на метод | `cpg` | `graphml`, `graphson`, `neo4jcsv`, `dot` |

Из `JoernExport.scala`: в `exportWithFlatgraphFormat` ветки есть ровно для `All` и `Cpg`, для прочих — `throw new NotImplementedError(...)`. То есть **комбинация `--repr=pdg --format=graphml` не работает**.

**Принятое решение:** основной путь — **CPGQL-скрипты** с выводом в JSON. Это даёт точный контроль над тем, что именно мы извлекаем (нам нужны вызовы и методы, а не весь граф), и не требует парсинга GraphML/DOT. Скрипты находятся в `src/ragraf/cpg.py` (`QUERY_CALLS`, `QUERY_METHODS`, `QUERY_TYPES`).

Дополнительно: `joern-export` **сам** достраивает оверлеи данных (`addDataFlowOverlayIfNonExistent` вызывает `OssDataFlow`), поэтому отдельный прогон оверлея для PDG не нужен.

---

## 4. Ограничения php2cpg, критичные для нашего корпуса

### 4.1. Синтаксис ограничен PHP 8.2

Бандленный парсер — `nikic/PHP-Parser` ветки 4.x:

- `MODULE.bazel`: `PHP_PARSER_VERSION = "4.15.10"`;
- README форка: *«This is a PHP 5.2 to PHP 8.2 parser written in PHP»*.

Пилот — **PHP 8.4**. Новые конструкции (property hooks, асимметричная видимость) в грамматике 4.x отсутствуют → синтаксические ошибки.

**Смягчающий фактор:** фронтенд всегда передаёт `--with-recovery`, поэтому ожидается неполный AST, а не падение. Насколько неполный — проверяется на прогоне.

**Что делать:** считать это в метрике полноты. В отчёте появится отдельная строка о файлах, которые php2cpg не разобрал (в логах это `Failed to process '<файл>'` — предупреждение, при котором файл молча выпадает; такое надо ловить).

### 4.2. Нет composer.json → неполное разрешение имён

`buildFiles` ищет **только** `composer.json`: `SourceFiles.determine(config.inputPath, Set(".json"), …).filter(_.endsWith("composer.json"))`. У пилота composer.json нет (PHP без фреймворка, автозагрузка через `spl_autoload_register`).

**Смягчающий фактор:** фронтенд проходит код **дважды** — `SymbolSummaryPass` собирает символы по всем пространствам имён, затем `AstCreationPass` строит AST с этой сводкой (*«parses the code twice… This helps resolve symbols during the latter parse»*). Связи внутри проекта частично разрешатся.

### 4.3. JDK 21 вне протестированного диапазона

docs.joern.io/installation: *«JDK 19 … newer JDK versions might work, but have not been properly tested»*. У нас JDK 21.

Отдельное предупреждение в документации срезов: *«Class path issues related to Circe are known to rear its head on Java versions later than 17»* — касается именно `joern-slice`.

**Что делать:** если `joern-slice` упадёт на JDK 21, альтернатива — срезы нашими средствами через CPGQL (обход CFG/DDG), либо установка JDK 19. Это выяснится на первом прогоне.

### 4.4. Инкрементального обновления CPG нет

Ни `joern-parse`, ни `joern-export`, ни документация не описывают инкрементальную пересборку; `joern-scan --overwrite` описан как полная регенерация.

**Следствие:** CPG пересобирается целиком при каждой переиндексации. Это согласуется с принципами проекта (индекс иммутабелен в пределах ревизии, переиндексация — отдельный акт), но означает, что стадия CPG — самая дорогая в прогоне, и её время надо измерять.

---

## 5. Связь CPG с нашим графом

| Узел CPG | Ключевые свойства | Ключ склейки с нашими узлами |
|---|---|---|
| `FILE` | `name` | путь исходника |
| `METHOD` | `name`, `fullName`, `filename`, `lineNumber`, `lineNumberEnd`, `isExternal` | `(filename, lineNumber, name)` |
| `TYPE_DECL` | `name`, `fullName`, `filename`, `lineNumber` | классы, интерфейсы, traits, enum |
| `CALL` | `name`, `code`, `methodFullName`, `dispatchType`, `lineNumber`, `columnNumber` | **`methodFullName` — то, чего нет в tree-sitter индексе** |
| `IDENTIFIER`, `LITERAL`, `LOCAL`, `PARAMETER` | `name`, `code`, `lineNumber`, `typeFullName` | переменные и значения |

`dispatchType` различает `STATIC_DISPATCH` и `DYNAMIC_DISPATCH` — это готовый признак для разделения доказанных и эвристических связей.

**Реализованное разрешение имён** (`Indexer._make_fqn_resolver`): точное совпадение полного имени → совпадение имени метода и файла → **отказ**. Третий исход принципиален: в PHP без фреймворка короткие имена (`get`, `run`) есть у десятков классов, и привязка «по имени» дала бы ровно те выдуманные связи, от которых защищает принцип P3 архитектуры.

**Не проверено:** база нумерации строк у php-parser (1-based или 0-based). Проверяется сверкой на первом прогоне; при расхождении на 1 нужна поправка в ключе склейки.

---

## 6. Что осталось выяснить запуском

| Вопрос | Как проверяется |
|---|---|
| Наличие и точные имена исполняемых файлов в дистрибутиве | `scripts/install-joern.ps1` (делает это автоматически) |
| Работает ли `joern-parse --language PHP` на нашем корпусе | первый прогон стадии CPG |
| Сколько файлов php2cpg не разберёт из-за PHP 8.4 | сверка с логом Joern и с результатом tree-sitter (у нас 0 ошибок разбора) |
| Время сборки CPG на 112 тыс. строк PHP | замер стадии `cpg` |
| Потребление памяти и нужный `-Xmx` | замер + наблюдение |
| Совпадает ли `lineNumber` CPG с нашими строками | сверка 20 методов вручную |
| Работает ли `joern-slice` на JDK 21 | отдельный прогон |

---

## 7. Состояние на момент написания

| Компонент | Состояние |
|---|---|
| JDK 21 (Temurin 21.0.12.1) | ✅ установлен в `.tools/jdk`, проверен запуском |
| PHP CLI 8.5 | ✅ найден (`C:\tools\php85\php.exe`) — предусловие php2cpg выполнено |
| Joern 4.0.99 | ✅ установлен в `.tools/joern-cli` (1633 МБ), `php` подтверждён в `--list-languages` |
| Слой CPG в конвейере (`src/ragraf/cpg.py`) | ✅ работает: `capability=full` на пилоте |
| Разбор экспорта (`src/ragraf/joern_export.py`) | ✅ neo4jcsv через ijson |
| Стадия `cpg` в индексаторе | ✅ слияние подтверждённых вызовов с нашим графом |
| Скрипт установки | ✅ `scripts/install-joern.ps1` |

---

## 8. Что выяснилось только запуском (факты, которых нет в документации)

Это самая ценная часть: документация Joern не описывает ни одного из этих пунктов, а без них слой CPG не работает.

| № | Обнаружено | Как проявлялось | Решение |
|---|---|---|---|
| 1 | `php2cpg` **требует внешний `php` в PATH** (в нём нет собственного парсера) | без PHP фронтенд не стартует вообще | `find_php()` + подстановка каталога с php первым в PATH |
| 2 | **php2cpg не принимает `--exclude`** | `Unknown option --exclude` → `RuntimeException: unable to parse XTypeStubsParserConfig` | отсев служебных путей ПОСЛЕ экспорта (`is_noise_file`) |
| 3 | **Скриптовый режим (`--script`) на JDK 21 не работает** | ошибка компиляции: нет predef-скрипта (`importCpg`, `cpg` не найдены) | полностью перешли на `joern-export` |
| 4 | **Экспорт GraphSON падает по памяти даже на 18 ГБ** | `OutOfMemoryError: Required array length 2147483640 + 8 is too large` | формат `neo4jcsv`: потоковые CSV, 223 МБ, 27 с |
| 5 | **`--repr=cpg` создаёт файл на каждый метод и падает** | 731 файл, 739 МБ, `FileSystemException` | `--repr=all` + фильтрация при разборе |
| 6 | **`JAVA_OPTS` не доходит до JVM** | `OutOfMemoryError` при любом значении переменной | лимит передаётся аргументом `-J-Xmx18g` (форма без `=`) |
| 7 | **Пути Joern не совпадают с нашими** | ведущая точка и обратные слеши; сопоставление всегда давало отказ | `normalize_joern_path` |
| 8 | **Методы Joern пишутся через `->`, а не `::`** | `App\Core\Config->get` не сопоставлялось ни с чем | `normalize_joern_name`; результат: 411 связей по полному имени |
| 9 | **AST-обход без границы метода захватывает всё** | заглушка `<global>` есть в каждом файле и содержит весь файл: 179 330 из 183 357 вызовов получили её владельцем | остановка обхода на вложенных METHOD |
| 10 | **В экспорте Joern массив вершин называется `vertices`, а не `nodes`** | разбор давал 0 узлов при 83 542 разобранных рёбрах | ключ `@value.vertices.item`; свойства — в `properties` под именами в ВЕРХНЕМ РЕГИСТРЕ |
| 11 | **Большинство CALL — операторы языка** | 50 тыс. из 183 тыс. — `<operator>.assignment` | `_is_noise_call`; осталось 17 822 настоящих вызова |
| 12 | **Встроенные функции PHP надо классифицировать отдельно** | 13 008 из 17 822 «не разрешено» были `array_push`, `is_array`, `Iterator.next` | `_is_builtin_target`: 7 819 распознаны как норма, честный остаток — 4 985 |

**Урок для проекта.** Из 12 пунктов 7 приводили к **молчаливой** потере данных: код не падал, но результат оказывался нулевым или искажённым. Такие дефекты ловятся только сверкой независимых путей — ровно как было с CPGQL (работал в теории, не работал на практике) и с фильтром `<global>` (формально обход корректен, фактически почти все данные у одного владельца).

---

## 9. Результат на пилоте

| Показатель | Значение |
|---|---|
| `capability` | **full** (CPG подключена) |
| Время полной индексации с CPG | 71 с (из них CPG — 64 с) |
| Сборка CPG | 18.6 МБ, ~50 с на 323 файла |
| Экспорт | 223.8 МБ, 132 файла, ~27 с |
| Значимых вершин извлечено | 193 442 |
| Рёбер извлечено | 3 348 516 (в т.ч. 444 464 CFG, 930 567 потоков данных) |
| Вызовов после фильтрации | 17 822 из 183 357 |
| **Связей подтверждено в графе** | **603** (411 по полному имени, 192 по имени и файлу) |
| Встроенных функций PHP распознано | 7 819 |
| Честно не разрешено | 4 985 |
| `slice` | **работает** (был недоступен в режиме degraded) |
| **Влияние на M2** | **recall 0,771 → 0,858**; медиана 1,000; нулевых случаев 0 (было 1); полная связность у 13 из 20 (было 11) |
