CPG слой: интеграция Joern¶
Документ фиксирует, что именно проверено про Joern применительно к нашему корпусу, и какие выводы из этого следуют для конвейера. Составлен по первоисточникам (исходники joernio/joern, официальная документация), а не по памяти: каждый факт помечен уровнем достоверности.
1. Ключевое открытие, меняющее предусловия¶
php2cpg не содержит собственного парсера PHP. Он вызывает внешний php с бандленным php-parser.phar.
Подтверждено по исходникам:
joern-cli/frontends/php2cpg/README.md: «A PHP to CPG converter based onphp-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) |