DOI: 10.15514/ISPRAS-2026-38(3)-24
Д.С. Дроздов, ORCID: 0000-0003-0381-1012 <ds-drozdov@yandex.ru>
С.С. Михалкович, ORCID: 0000-0003-0373-3886 <miks@sfedu.ru>
Южный федеральный университет,
Россия, 344006, г. Ростов-на-Дону, ул. Б. Садовая, д. 105/42.
Аннотация. В статье рассматривается новый архитектурный подход к быстрой навигации между связанными элементами программных проектов, написанных на разных языках. Приведен пример конкретной реализации архитектуры для языковой пары GraphQL и Go. Проблема навигации между фрагментами кода на разных языках решается за счет создания разметки проекта и автоматических переходов между соответствующими точками в коде. Подход ориентирован на повседневную деятельность разработчиков и сохраняет устойчивость по мере изменения кода. Метод основан на использовании легковесных грамматик и их синтаксических анализаторов для создания разметки проекта и установления связей между ее элементами. Введено определение нетривиального обработчика GraphQL-поля в Go как функции, содержащей вызовы или управляющие конструкции. Такие функции составляют основу разметки проекта на Go. Разработаны три специализированные легковесные грамматики, которые выделяют конструкции, однозначно характеризующие функцию на Go как нетривиальный обработчик поля. На этой базе построен алгоритм, который автоматически устанавливает соответствия между полями схемы языка запросов GraphQL и методами на Go. Алгоритм учитывает совокупность факторов: от соглашения об именовании методов до их синтаксических особенностей. Алгоритм при установке соответствия решает задачу ранжирования кандидатов на пару для данной точки. Рассчитаны метрики ранжирования MRR и hit@1. Показано, что новый подход обеспечивает медианное ускорение навигации по проекту примерно в 8 раз по сравнению с ручным поиском, а также полностью устраняет повторные переходы.
Ключевые слова: быстрая навигация; кросс-языковая навигация; многоязыковые проекты; разметка проектов; легковесные грамматики.
Для цитирования: Дроздов Д.С., Михалкович С.С. Архитектурный подход к быстрой кросс-языковой навигации по связанным сущностям в программных проектах. Труды ИСП РАН, том 38, вып. 3, часть 2, 2026 г., стр. 111–132. DOI: 10.15514/ISPRAS-2026-38(3)-24.
Современное промышленное программирование требует работы с большими проектами, написанными на нескольких языках. В статье [1] отмечается, что профессиональный разработчик в среднем для каждого проекта использует семь языков и три связанные языковые пары. В многоязыковых проектах одна и та же функциональность может быть реализована с использованием кода на нескольких языках. Отсюда возникает задача навигации между связанными сущностями, написанными на разных языках. Основываясь на научных публикациях [2-8] и собственном опыте разработки, нетрудно выделить основные недостатки традиционной ручной навигации, сводящейся к поиску сущностей по именам и ключевым словам:
В данном исследовании рассматривается новый архитектурный подход к быстрой кросс-языковой навигации по проектам, позволяющий решить описанные выше проблемы. В качестве платформы для экспериментов выбрана языковая связка GraphQL и Go. Основой архитектуры служит набор легковесных синтаксических анализаторов, общая теория которых описана в [9], а также разработанный авторами набор правил установки соответствия между синтаксическими деревьями разных языков. По сравнению с предыдущими работами [10-12] разметка создается не вручную, а автоматически с помощью предлагаемого авторами алгоритма, и учитывает сразу несколько языков.
В данной статье описываются преимущества использования легковесных синтаксических анализаторов по сравнению с полными в контексте межъязыковой навигации. Легковесные анализаторы для GraphQL и Go взяты из предыдущих работ авторов [11,13]. Однако для точной разметки потребовалось ввести ряд новых определений и разработать дополнительные специализированные грамматики для Go. Использование нескольких небольших грамматик вместо единой упрощает разбор текста и является новым результатом по сравнению с публикациями [9,13-15].
В работе получены следующие результаты:
Практическим результатом работы является ускорение навигации по многоязыковому проекту в 7,89 раз, а также существенное снижение когнитивной нагрузки на программистов за счет повышения точности навигации до 100% и устранения повторных посещений одних и тех же сущностей.
Архитектурный подход, реализованный и подробно описанный для языковой пары GraphQL – Go, также применен для пары GraphQL – TypeScript. В статье приводится ссылка на соответствующую грамматику TypeScript и алгоритм разметки и связи точек в проекте.
Определение 2.1.1. Пусть – многоязыковой программный проект, состоящий из множества файлов, написанных на языках из множества . Для каждого языка задан легковесный синтаксический анализатор , который за линейное время по количеству токенов в каждом файле , написанном на языке , строит синтаксическое дерево . При этом множество узлов дерева состоит из двух частей:
Пусть на основе задано множество точек привязки . Тогда разметкой кода проекта будем называть объединение точек привязки по всем файлам проекта и кратко записывать .
Определение 2.1.2. Быстрой кросс-языковой навигацией будем называть метод поддержки разработчика, основанный на построении и использовании семантического графа связей между сущностями проекта, написанными на разных языках. Данный метод позволяет формализовать и автоматизировать процесс установления и последующего использования семантических связей между сущностями проекта, обеспечивая мгновенный переход между ними непосредственно в среде разработки.
Пусть задан программный проект с разметкой . Тогда введем его формальную модель в виде -дольного графа семантических связей , где
Представим реализацию быстрой кросс-языковой навигации в виде трех фаз. Первая фаза выполняется один раз при запуске инструмента, вторая и третья – каждый раз при инициализации навигации.
Предварительная фаза (автоматическое установление связей). Система выполняет статический анализ проекта и формирует множество связей . Для связей, которые не могут быть установлены однозначно, система сохраняет множество кандидатов . Конкретная реализация данной фазы для языковой пары GraphQL – Go представлена в подразделе 2.7 данной работы.
Фаза выбора (ручное уточнение связей). При инициации навигации из точки на одном языке в точку на другом система проверяет наличие связи. Если на предыдущем шаге связь была установлена однозначно, то система показывает одну целевую точку. В противном случае предлагает пользователю ранжированный список кандидатов и затем фиксирует его выбор, переводя соответствующее ребро из в .
Фаза перехода (устойчивая навигация). После выбора точки среди кандидатов (или при однозначном соответствии) осуществляется немедленный переход к целевой сущности. Установленная связь используется для всех последующих переходов.
Дополнительно отметим, что код, соответствующий точкам привязки из разметки, может изменяться. Устойчивость разметки (соответственно, и связей между ее точками) обеспечивается с помощью алгоритмической перепривязки, описанной в статьях [12,14]. В данной работе акцент делается только на задачах создания разметки и связей между точками привязки.
Для программной реализации зафиксируем два языка программирования: GraphQL и Go. Согласно спецификации [16], схема GraphQL содержит перечень типов и их полей, каждому полю соответствует в основном языке метод, называемый обработчиком поля (в англоязычной литературе – resolver), который должен вернуть значение клиенту. В работе [10] подчеркивается, что данные сущности образуют ключевые функциональности проекта, поэтому их выделение является первоочередной задачей при создании разметки. В таком случае следует реализовать алгоритм, который установит связи между точками на GraphQL и Go. В рамках данного исследования будем рассматривать только случаи корректных проектов, где для каждого поля GraphQL существует обработчик (возможно, еще не полностью реализованный).
Опишем, какое множество точек на GraphQL и Go нас будет интересовать при решении задачи разметки. В данном исследовании будем рассматривать типы GraphQL, каждый тип имеет набор полей, а поле – набор аргументов. С точки зрения транспортного уровня GraphQL не выделяет самостоятельной сущности «функция» или «метод» как в других языках программирования [17]. Каждому полю GraphQL соответствует метод-обработчик, который содержит логику и отвечает за реализацию функциональности [18]. Связь между полем и его обработчиком и будет лежать в основе навигации. На рис. 1 представлено соответствие между полями GraphQL и их обработчиками.
Опишем, как реализован каждый из представленных выше обработчиков (рис. 1):

Рис. 1. Соответствие между полями GraphQL (слева) и их обработчиками (справа).
С точки зрения разметки кода нас интересуют только обработчики 1 и 2, поскольку именно в них совершаются значимые действия (вызовы других функций). Кроме этого, в разметку разумно включить методы-обработчики, которые содержат условные операторы, циклы или операторы выбора, поскольку данные конструкции несут смысловую нагрузку. Исходя из этого определим понятие нетривиального обработчика.
Определение 2.2.1. Метод-обработчик языка Go для поля схемы GraphQL будем называть нетривиальным, если он содержит хотя бы один вызов функции или метода Go, либо один из следующих операторов: if, for, select, switch. Иначе такой метод-обработчик будем называть тривиальным.
В таком случае методы 1 и 2 являются нетривиальными обработчиками, а 3 и 4 – тривиальными.
Мы определили множества интересующих точек на GraphQL и Go. При этом в разметку включаем только нетривиальные обработчики полей Go и сопоставленные им поля GraphQL.
Возникает вопрос, как определить набор связанных сущностей, который позволит переходить между точками. Очевидное решение сравнивать точки по имени является неполным по двум причинам. Во-первых, имя метода Go может не совпадать с именем поля GraphQL [19]. Во-вторых, методов с одинаковым названием может быть порядка десяти [20].
Помимо этого, необходимо исключить из разметки точки, относящиеся к тривиальным обработчикам. Как видно из примеров выше (рис. 1), по синтаксису поля GraphQL невозможно определить тип его обработчика. Действительно, нельзя опираться на количество аргументов, или на наличие круглых скобок, или на сигнатуру методов.
Для решения данной проблемы в настоящей работе разработаны легковесные синтаксические анализаторы, которые позволяют найти в теле обработчика те синтаксические конструкции, которые однозначно классифицируют его по определению 2.2.1. После этого в разметку кода включаются только нетривиальные обработчики.
Рассмотрим имеющиеся подходы к созданию разметки проекта и связей между ее точками. В работе [21] делается акцент на сигнатурах методов и на их наименовании. Анализ имен используется и в данной работе, однако он не работает в случае наличия одноименных функций, поэтому не может быть единственным инструментом. В статьях [22-23] предлагается опираться на специальные комментарии и аннотации, оставляемые разработчиками. Такой подход не используется в данном исследовании принципиально, потому что возлагает дополнительную работу на программистов и засоряет кодовую базу.
Существуют также способы отыскания функциональностей с помощью динамического анализа [24-25]. Однако зачастую такой подход достаточно трудозатратный. Например, в случае установки связи между Go и GraphQL придется проводить трассировку для каждого способа интеграции GraphQL отдельно, поскольку каждая библиотека, используемая разработчиками Go, по-разному взаимодействует со схемой GraphQL.
В статье [26] предлагается выделять функциональности с помощью машинного обучения с опорой на абстрактные синтаксические деревья. Однако анализаторы, используемые в статье, более не поддерживаются, и неясно, как обобщить результаты на другие языки.
В работе [27] рассматривается подход к созданию межъязыковой разметки и связи кода на Java/JavaScript и документацией, а также трекером задач. Однако поскольку связь осуществляется не между двумя языками, а между одним языком и сторонними ресурсами, то это можно рассматривать как разметку одного языка с дополнительной информацией.
В статьях [28-29] описываются способы создания разметки с помощью отслеживания движения глаз и позиции курсора у опытных разработчиков. Такой подход позволяет связывать точки на разных языках, однако для создания полноценной разметки требуется объемная работа по отслеживанию активности, а также дорогостоящее оборудование.
Задачу понимания и навигации по проекту можно решать и без создания явной разметки. Например, в статье [30] авторами разработан плагин для VS Code, который по выделенному фрагменту дает подсказки и объяснения пользователю, в числе которых может содержаться информация, необходимая для навигации. Тем не менее, плагин больше рассчитан на изучение кода, чем на перемещение по нему.
В работе [31] авторы применяют LLM для семантического поиска по коду. Модель, разработанная авторами статьи, работает с несколькими языками. Однако в работе отмечается невысокое качество для проектов на Go, что делает ее нежелательной для применения в настоящем исследовании. В статье [32] также применяются большие языковые модели для навигации по проекту. Плагин, разработанный авторами, предсказывает, куда дальше перешел бы разработчик. Это делается с помощью модели, обученной на датасете, хранящем информацию о движении глаз и курсора многих разработчиков. Основным недостатком является возможность перемещаться только внутри одного файла.
В последнее время разработчиками активно используется GitHub Copilot для работы с большими и незнакомыми проектами [33-34]. Для навигации по проекту можно задать запрос на установление связи между точками. Однако для эффективного поиска требуется задать хотя бы один опорный файл. Кроме того, известно, что Copilot может выдавать ссылки на несуществующие файлы или позиции в них или давать существенно разные результаты при незначительном изменении запроса или исходного кода [35-36].
Поскольку в данной работе кросс-языковая навигация рассматривается на примере связки GraphQL и парного языка, то следует рассмотреть инструменты для GraphQL. В открытом доступе опубликован плагин GraphQL: Language Feature Support [37] для VS Code. Исходя из описания, он позволяет перемещаться от определения в схеме GraphQL к реализации на языке TypeScript. Однако в замечаниях к расширению профессиональным сообществом отмечается, что функция навигации не работает.
Другой плагин GraphQL [38] обеспечивает перемещение между GraphQL и TypeScript за счет специальных аннотаций в коде TypeScript. В плагине отсутствует поддержка Go.
Еще одно расширение Apollo GraphQL [39] обеспечивает навигацию внутри файлов GraphQL и поддерживает федерацию – объединение множества файлов со схемами. Однако с его помощью нельзя переместиться в соответствующий код на Go.
Для связки GraphQL и Go существует популярный инструмент gqlgen [40], который по схеме GraphQL генерирует код на Go. Поскольку кодогенерация стандартизирована, можно встроиться в ее процесс и создавать разметку одновременно с генерацией. Кроме того, можно воспользоваться информацией о том, в какие директории и файлы gqlgen помещает код. Однако данный подход неудобен тем, что для других языков программирования нужны иные инструменты, следовательно, такое построение разметки плохо масштабируется на другие системы. Помимо этого, несмотря на распространенность библиотеки gqlgen, некоторые проекты принципиально пишутся без кодогенерации или на других библиотеках.
Для навигации внутри кода, написанного на Go и GraphQL, традиционно используются языковые сервера: gopls [41] и GraphQL LSP [42] соответственно. Однако в настоящем исследовании мы разрабатываем архитектурный подход к кросс-языковой навигации. Данная проблема не может быть решена простым использованием двух языковых серверов, нужен некий контроллер над ними. Кроме того, применение полноценных языковых серверов является непростой задачей, усложняющейся с ростом количества языков.
Решение, представленное в данной работе, фактически реализует ту часть некоторого универсального многоязыкового сервера, которая отвечает за переход между связанными сущностями.
В данной работе для навигации по многоязыковому проекту создана архитектура, состоящая из следующих компонентов:
В качестве набора легковесных синтаксических анализаторов для языковой пары GraphQL – Go используются анализаторы из работ [11,13], а также три дополнительных анализатора, разработанных в данной статье (подраздел 2.6) для классификации обработчиков полей. Алгоритм автоматического создания разметки и правил навигации является новым результатом, представленным в подразделе 2.7 настоящей работы. Данный алгоритм интегрирован в программный комплекс LanD Explorer, который обеспечивает отображение разметки на специальной панели, ее автоматическое обновление и техническую часть навигации по связным сущностям [12]. Устойчивость созданных связей и разметки обеспечивается благодаря алгоритмической перепривязке, реализованной в предыдущих работах [12,14].
Согласно определению 2.1.1 для разметки проекта требуется синтаксический анализатор, строящий синтаксическое дерево по коду программы на соответствующем языке. В данном исследовании принято решение использовать легковесные анализаторы на основе легковесных грамматик со специальным символом Any. В работах [9,13,15] убедительно показаны следующие преимущества легковесных анализаторов над полными:
Дополнительно отметим, что использование легковесных грамматик подразумевает написание только необходимых правил, что позволяет фокусировать внимание на нужных конструкциях и разрабатывать грамматики по внешнему виду описываемых конструкций.
Отдельно следует указать на то, что применение полного синтаксического анализатора зачастую не покрывает все случаи. Например, для GraphQL в открытом доступе можно найти три известных анализатора, каждый из которых имеет собственные ограничения: graphql-go-tools [45] игнорирует некорневые типы GraphQL; graphql-go [46] не поддерживает федеративный способ хранения схем GraphQL; gqlparser [47] некорректно разбирает поля с пустым списком аргументов.
Чтобы реализовать разметку в общем виде, требуется анализатор, который будет разбирать любые виды схем GraphQL. С этой задачей успешно справляется легковесный анализатор, ранее разработанный авторами [11].
Для создания легковесного синтаксического анализатора необходимо воспользоваться генератором LanD и написать для него легковесную грамматику соответствующего языка [12]. Такие грамматики являются разновидностью LR(1) грамматик и основываются на использовании специального символа Any, обозначающего часть кода, которую нужно пропустить при разборе [9].
Кроме того, легковесные грамматики являются подвидом островных грамматик [43]. Например, когда мы разбираем условные операторы, тело оператора является «водой» и обозначается символом Any, а ключевые слова и скобки – «островами». На рис. 2 приведен пример такой разметки. Сплошной заливкой выделены «острова», а штриховкой – «вода». Справа представлен соответствующий фрагмент легковесной грамматики.

Рис. 2. «Вода» и «острова» в разметке метода. Фрагмент грамматики с Any.
Последовательность символов, пропущенная при разборе, сохраняется в виде текста в соответствующем узле Any, что позволяет в дальнейшем разобрать его, например более детальным анализатором.
Как отмечено в подразделе 2.2.1, для языковой пары GraphQL – Go в разметку принято решение включать только нетривиальные обработчики полей. Согласно определению 2.2.1 такие методы-обработчики должны содержать как минимум один вызов метода или функции, либо управляющий оператор if, for, switch, select. Тогда для создания разметки необходимо не просто найти методы на языке Go, но и отфильтровать их в зависимости от содержимого. Для этого рассмотрим три новые легковесные грамматики Go: (1) обычных вызовов, (2) анонимных вызовов и (3) управляющих операторов. Синтаксические анализаторы, сгенерированные по этим грамматикам, будем применять последовательно для того, чтобы установить факт наличия конструкций, характеризующих обработчик поля как нетривиальный (определение 2.2.1).
В данной работе впервые по сравнению с предыдущими публикациями [9,12-15] применяется подход создания нескольких небольших грамматик вместо единой грамматики. Такой подход имеет ряд преимуществ. Во-первых, каждая грамматика фокусируется на одной семантической конструкции. Во-вторых, можно легко включать и исключать новые грамматики, поскольку они не зависят друг от друга. Наконец, в каждой грамматике снижается сложность правил за счет уменьшения числа рекурсии в них, что упрощает создание легковесных грамматик для малознакомых языков.
Далее опишем ключевые аспекты создания трех новых легковесных грамматик языка Go. Полный текст грамматик доступен в репозитории [48].
Рассмотрим различные варианты вызовов в языке Go:
При этом возможны и комбинации таких вариантов, например data.u[0].(*User).Login("123")(). Ключевая проблема состоит в том, что символ «.» может использоваться в двух случаях: обращение к функциям пакетов и к полям и методам структуры; приведение типа.
Во втором случае тип, к которому выполняется приведение, заключается в круглые скобки, что похоже на вызов функции. Учитывая эти особенности, сформулируем правила, описывающие вызовы.
call = fun ('(' entity* ')')+ fun = ID ('[' entity* ']')* ('.' fun2)?
fun2 = fun | '(' Any ')' ('[' entity* ']')* ('.' fun2)?
В данной грамматике терминал ID означает любой допустимый идентификатор в Go.
В языке Go есть 18 стандартных функций, таких как new, len, close и так далее, которые требуется исключить из разбора, потому что они не отвечают за функциональность. Для этого в грамматике достаточно определить терминалы new, len, close и прочие. В таком случае вызовы стандартных функций не будут подходить под описанное выше правило fun и будут пропущены. Кроме того, в грамматике определены терминалы для всех 27 стандартных типов Go и их синонимов, чтобы инициализаторы вида int(0) также игнорировались.
В языке Go среди всех вызовов выделяются вызовы анонимных функций, поскольку они имеют особую форму записи. Наиболее очевидным способом записать правило для анонимного вызова в Go является следующий:
anon_f_call = 'func' '(' Any ')' Any '{' Any '}' '(' Any ')'
Однако в качестве возвращаемого значения на позиции Any может оказаться структура или интерфейс Go, чья запись содержит фигурные скобки, которые будут конфликтовать со скобками, отвечающими за тело функции. Для этого заменим Any на более детальный фрагмент Any|struct|interface и для удобства разобьем правила на подправила. Получим следующую грамматику:
content = entity* entity = Any | anon_f_call
anon_f_call = anon_f '(' Any ')' anon_f = anon_f_title '{' Any '}'
anon_f_title = 'func' '(' Any ')' (Any|struct|interface)
struct = 'struct' '{' Any '}' interface = 'interface' '{' Any '}'
Рассмотрим процесс создания грамматики для следующих операторов Go: if, for, switch, select. Каждый из них имеет обязательную часть – тело, которое всегда обрамлено фигурными скобками. Определим для этого правило block = '{' Any '}'. Тогда правила для условного оператора будут следующими:
content = entity* if = 'if' Any block else?
entity = if | block | Any else = 'else' (if | block)
block = '{' entity* '}'
При детальном тестировании можно обнаружить, что данная грамматика некорректно описывает те единичные случаи, когда условие оператора содержит конструкции с фигурными скобками. Проблема возникает из-за того, что по спецификации Go в большинстве случаев условие можно не заключать в круглые скобки, поэтому трудно отследить, где начинается тело оператора [49]. Например, для кода if a == struct{int}{3} { print(a) } в качестве условного оператора будет определена подчеркнутая часть, а в качестве условия – только a == struct{int}.
Согласно грамматике, фигурные скобки в условии встречаются в следующих случаях и их комбинациях:
Примечательно, что оригинальный синтаксический анализатор Go для случая 3 требует, чтобы такая конструкция обрамлялась круглыми скобками [50], поэтому для нее не требуется отдельного правила. Для сценариев 1 и 2 анализатор учитывает, что фигурные скобки относятся к инициализатору, а не являются границами тела оператора. Поступим аналогичным образом и добавим в грамматику правила, которые описывают инициализаторы 1 и 2.
init = anon '{' Any '}' | ('[' Any ']')+ Any ('{' Any '}')?
anon = ('struct'|'interface') '{' Any '}'
Далее правила для тела и вызова анонимной функции (случаи 4–6) заимствуются из подраздела 2.6.4. Теперь мы можем описать правило для условия и скорректировать для if:
cond = Any | init | anon_f | anon_f_call | anon_f_title
if = 'if' cond* block else?
Полученные результаты нетрудно распространить на другие операторы: for, switch и select.
Для проверки корректности грамматик используется доработанная версия алгоритма валидации из работы [51]. Сначала подбирается набор тестовых репозиториев из числа проектов, опубликованных на GitHub. Затем полный синтаксический анализатор Go проходится по файлам из набора, находит функции и методы и разбирает их тела; результатом разбора является синтаксическое дерево операторов и вызовов. Дополнительно сохраняется текст функции или метода, который затем разбирает легковесный синтаксический анализатор. Наконец, сравниваются синтаксические деревья, построенные полным и легковесным анализаторами.
В качестве репозиториев для проверки выбрано пять проектов, их состав и результаты распознавания представлены в табл. 1. В первом столбце приведена ссылка на GitHub из списка литературы в конце статьи.
Табл. 1. Результаты работы легковесных анализаторов.
| Репозиторий | Всего вызовов | Распознано вызовов | Всего анонимных вызовов | Распознано анонимных вызовов | Всего операторов | Распознано операторов |
|---|---|---|---|---|---|---|
| [52] | 311857 | 311857 (100%) | 28 | 28 (100%) | 148429 | 148429 (100%) |
| [53] | 655748 | 655748 (100%) | 3470 | 3470 (100%) | 259428 | 259428 (100%) |
| [54] | 228761 | 228761 (100%) | 1331 | 1331 (100%) | 139167 | 139167 (100%) |
| [55] | 203397 | 203397 (100%) | 1127 | 1127 (100%) | 59310 | 59310 (100%) |
| [56] | 164904 | 164904 (100%) | 488 | 488 (100%) | 39837 | 39837 (100%) |
По результатам эксперимента установлено, что все три грамматики верно разбирают вызовы и операторы Go, ложноположительные и ложноотрицательные срабатывания отсутствуют.
Рассмотрим новый алгоритм создания разметки для языковой связи GraphQL – Go, а также связей между точками привязки.
Алгоритм состоит из нескольких частей:
Рассмотрим части 1–3. Вначале мы получаем список всех методов Go и полей GraphQL с помощью легковесных синтаксических анализаторов из предыдущих работ [10,12]. Далее отфильтровываются лишние методы Go, которые удовлетворяют хотя бы одному из следующих условий:
Затем для каждого метода Go мы просматриваем все поля GraphQL, совпадающие по имени, и добавляем в словарь новое значение, если количество аргументов метода Go соответствует одному из способов их перечисления в обработчиках полей. В качестве ключа словаря выступает поле GraphQL, а значения – список методов-кандидатов вместе с величиной score, отражающей, насколько вероятно данный метод является искомым обработчиком. Величина score складывается из следующих частей (соответствующие пункты подписаны в Алгоритме 2.7.1 с указанием конкретных значений):
Ключевые моменты программной реализации приведенного алгоритма представлены в репозитории [48].
procedure MakeNavEdges(D):
e_fixed ← ∅; e_ambiguous ← ∅
foreach gqlField, candidates in D:
if len(candidates) = 1:
e_fixed ← append(e_fixed, {gqlField, candidates[0]})
continue
foreach cand in candidates:
score ← 0; method ← cand.method
if ReceiverName(method) contains “resolver”: |
score ← score + 1 |
if ReceiverName(method) = ParentName(gqlField): | (1)
score ← score + 1 |
else if ReceiverName(method) contains ParentName(gqlField): |
score ← score + 0.75
score ← score + (1 - MockCallsRatio(cand)) | (2)
score ← score + (1 – CVForStruct(method)) * 0.25 + | (3)
+ (1 – CVForFile(method)) * 0.75
cand.score ← score
bestCandidate, ambiguousCands ← SortCandidates(candidates)
if bestCandidate != null:
e_fixed ← append(e_fixed, {gqlField, bestCandidate})
else foreach ambCand in ambiguousCands:
e_ambiguous ← append(e_ambiguous, {gqlField, ambCand})
return e_fixed, e_ambiguous
Алгоритм 2.7.1. Установка соответствия между точками привязки на GraphQL и Go.
Algorithm 2.7.1. Establishing correspondence between GraphQL and Go binding points.
В результате работы общего алгоритма мы получаем следующее:
Таким образом, на примере языковой связи GraphQL – Go показан алгоритм построения разметки и установки соответствия между фрагментами кода на разных языках.
Рассмотрим схему быстрой навигации и ручного поиска на примере пары GraphQL – Go. На рис. 3 изображено несколько сценариев навигации.
Сценарий I описывает первичный переход от точки на GraphQL к точке на Go. В случае быстрой навигации пользователю предлагается несколько точек-кандидатов в виде ранжированного списка (зачастую список и вовсе состоит из одного искомого элемента); при ручном поиске множество кандидатов неупорядоченно и содержит больше элементов.
Сценарий II демонстрирует повторный переход между точками. Инструмент быстрой навигации уже хранит эту связь, поэтому переход выполняется мгновенно, в отличие от ручного поиска, который требуется выполнить снова.
Сценарий III предполагает переход от найденной точки к первоначальной. Благодаря наличию сохраненной связи, инструмент быстрой навигации выполняет это мгновенно.
Таким образом, инструмент быстрой кросс-языковой навигации существенно упрощает работу с проектом и переход между языками.

Рис. 3. Быстрая кросс-языковая навигация GraphQL – Go в сравнении с ручным поиском.
Для оценки качества работы алгоритма навигации в пределах языковой пары GraphQL – Go обратимся к репозиториям с открытым исходным кодом, разметим проект и установим связи вручную и с помощью алгоритма, далее сверим результаты. Результаты анализа приведены в табл. 2, ссылки на репозитории даны в конце статьи. Во втором столбце указано количество эталонных пар, установленных вручную. В третьем столбце приведено количество пар, однозначно установленных алгоритмом. В четвертом столбце указано количество потенциальных связей, для которых требуется ручная верификация пользователем. Формат записи следующий: «количество точек в GraphQL x количество кандидатов в ранжированном списке». Из полученных данных следует, что ручные действия пользователя требуются только в 2,5% случаев, в остальных ситуациях мы определяем подходящего кандидата самостоятельно.
Поскольку при построении связей между точками мы находим одного или нескольких кандидатов и упорядочиваем их по величине, описанной в подразделе 2.7, то имеет место задача ранжирования. Для оценки качества решения такой задачи рассчитаем общепринятые метрики, приведенные в столбцах 5–6. Величина hit@1 показывает, как часто искомый элемент попадает в топ-1 кандидатов. Метрика MRR (Mean reciprocal rank) показывает, насколько высоко в среднем нужный кандидат стоит в ранжированном списке. По результатам экспериментов установлено, что алгоритм всегда ставит на первое место искомого кандидата, поэтому значения метрик следующие: hit@1 = 100%, MRR = 1.
Табл. 2. Результаты оценки качества алгоритма кросс-языковой навигации в паре GraphQL – Go.
| (1) | (2) | (3) | (4) | (5) | (6) |
|---|---|---|---|---|---|
| Репозиторий | Ручная разметка | Автоматическая разметка | Метрики ранжирования для автоматической разметки | ||
| [57] | 24 | 24 | 0 | 100% | 1 |
| [58] | 70 | 69 | 1 точка x 3 канд. | 100% | 1 |
| [59] | 91 | 91 | 0 | 100% | 1 |
| [60] | 6 | 6 | 0 | 100% | 1 |
| [61] | 17 | 16 | 1 точка x 2 канд. | 100% | 1 |
| [62] | 8 | 8 | 0 | 100% | 1 |
| [63] | 134 | 127 | 7 точек x 2 канд. | 100% | 1 |
| [64] | 12 | 12 | 0 | 100% | 1 |
При навигации между связанными сущностями, реализованными на разных языках, разработчик должен найти на втором языке точку, соответствующую выбранному элементу на первом языке. При ручном поиске это достигается за счет использования глобального поиска по имени и анализа найденных кандидатов. В литературе подобная деятельность описывается линейной моделью: общее время растет пропорционально количеству вариантов, которые необходимо просмотреть [65-66].
В предлагаемом инструменте быстрой навигации ситуация иная. Алгоритм заранее вычисляет связи между элементами и формирует ранжированный список кандидатов, состоящий, как правило, из одного-трех элементов. Правильная точка всегда присутствует среди кандидатов и по данным эксперимента (раздел 3) располагается на первой позиции. Таким образом, время автоматической навигации должно быть существенно меньше, чем время ручной навигации. Подтвердим предположение с помощью эксперимента.
Определение 4.1.1. Пусть – суммарное время, затраченное разработчиком на навигацию по проекту с помощью ручного поиска, а – с помощью нового инструмента. Тогда коэффициентом ускорения навигации будем называть величину .
Выберем в каждом репозитории по 10 точек на GraphQL и предложим трем разработчикам найти для них соответствующую пару на Go с помощью ручного поиска в IDE и с помощью инструмента для автоматической навигации. Вычислим коэффициент ускорения навигации и точность, с которой выполнено сопоставление. Дополнительно будем отслеживать процент повторных посещений одних и тех же точек в процессе ручного поиска. Как отмечается в статье [67], чем выше такой процент, тем выше стресс у программиста.
Поскольку объём выборки в проведенном эксперименте ограничен, а отдельные измерения времени обладают естественной вариативностью, в качестве обобщающих показателей используются медиана и диапазон наблюдаемых значений, которые считаются устойчивыми метриками при малых выборках [66,68].
В табл. 3 приведены результаты наблюдений для первого разработчика. На данных репозиториях коэффициент ускорения принимал значения в диапазоне 3,75–14 с медианой 7,31. Кроме того, точность сопоставления при ручном поиске составила 80–100% против 100% при автоматической навигации. Доля повторных посещений варьировалась от 9% до 38% для ручного поиска.
В табл. 3 и далее не приводятся точность сопоставления и процент повторных посещений при использовании инструмента, поскольку они составляют 100% и 0% соответственно. Это объясняется тем, что в инструменте предлагается либо одна точка для навигации, либо несколько в виде ранжированного списка с метрикой hit@1 = 100%. В последнем случае испытуемые переходили в первую в списке точку и уверенно выбирали ее как искомую.
Проведем аналогичные наблюдения с двумя другими испытуемыми. Результаты представлены в табл. 4-5.
По результатам серии экспериментов значение коэффициента варьировалось от 2,02 до 14 с медианой наблюдаемых значений 7,89. Кроме того, процент повторных посещений снизился с медианных 23% до 0%, а точность сопоставления возросла с 90% до 100%.
Табл. 3. Результаты эксперимента по оценке быстрой навигации. Испытуемый – Разработчик 1.
| [57] | [58] | [59] | [61] | [63] | |
|---|---|---|---|---|---|
| Время (инструмент), , сек. | 17 | 32 | 16 | 16 | 20 |
| Время (ручной поиск), , сек. | 109 | 120 | 129 | 117 | 280 |
| Точность сопоставления (ручной поиск) | 80% | 80% | 100% | 100% | 100% |
| Процент повторных посещений (ручной поиск) | 9% | 17% | 38% | 9% | 33% |
Табл. 4. Результаты эксперимента по оценке быстрой навигации. Испытуемый – Разработчик 2.
| [57] | [58] | [59] | [61] | [63] | |
|---|---|---|---|---|---|
| Время (инструмент), , сек. | 11 | 37 | 19 | 14 | 23 |
| Время (ручной поиск), , сек. | 123 | 131 | 150 | 103 | 294 |
| Точность сопоставления (ручной поиск) | 90% | 90% | 100% | 90% | 100% |
| Процент повторных посещений (ручной поиск) | 9% | 9% | 44% | 29% | 23% |
Табл. 5. Результаты эксперимента по оценке быстрой навигации. Испытуемый – Разработчик 3.
| [57] | [58] | [59] | [61] | [63] | |
|---|---|---|---|---|---|
| Время (инструмент), , сек. | 12 | 61 | 22 | 11 | 31 |
| Время (ручной поиск), , сек. | 114 | 123 | 124 | 98 | 245 |
| Точность сопоставления (ручной поиск) | 70% | 80% | 90% | 100% | 90% |
| Процент повторных посещений (ручной поиск) | 17% | 23% | 38% | 29% | 44% |
Отметим, что эксперимент проводился на профессиональных разработчиках. Для начинающих программистов провести ручной поиск сложнее, зачастую им требуется консультация старшего специалиста, чтобы выбрать нужную сущность. Использование инструмента для автоматической навигации позволяет самостоятельно разобраться с проектом разработчику любого уровня.
В текущем исследовании рассмотрена реализация архитектурного подхода для GraphQL и Go. Данная языковая пара является нетривиальной из-за описанных в статье особенностей грамматик Go (см. подразделы 2.6.3–2.6.5), а также из-за традиционного наличия большого количества одноименных методов.
Действуя аналогичным образом, нетрудно определить грамматику методов и стрелочных функций языка TypeScript, использующихся в качестве обработчиков полей, а также алгоритм связи между точками на TypeScript и GraphQL. Данные результаты представлены в репозитории.
Наконец, в работе [9] представлена легковесная грамматика C#, которая в числе прочего описывает методы языка (полный текст грамматики также приведен в нашем репозитории). Пользуясь правилами из этой грамматики и имеющейся грамматикой GraphQL, можно построить разметку проекта и реализовать алгоритм связи для языковой пары GraphQL – C#.
Использование легковесных синтаксических анализаторов как основы предлагаемого архитектурного подхода предъявляет требования к разработке и валидации соответствующих легковесных грамматик. Для языков со специфической грамматикой может потребоваться детальный разбор отдельных конструкций, как например анализ условного оператора в Go (см. подраздел 2.6.5).
Реализация подхода интегрирована в инструмент LanD Explorer, обеспечивающий графический интерфейс и сопряжение с IDE Microsoft Visual Studio. Интеграция с Visual Studio Code в текущей версии не выполнена и требует дополнительных доработок.
Текущий подход не поддерживает кросс-языковую навигацию в тех случаях, когда текст на одном языке встроен в текст на другом языке. В ряде языков программирования схемы GraphQL иногда записываются в виде строковых литералов непосредственно в исходном коде, а не выделяются в отдельные файлы. Для обработки таких проектов необходим механизм автоматизированного обнаружения и извлечения схем из программного текста.
В данной работе предложен архитектурный подход к быстрой кросс-языковой навигации по связанным сущностям программного проекта и показана его применимость на языковой связке GraphQL – Go. Ключевая идея состоит в создании явной разметки проекта и связей между ее частями. Для этого разработан и экспериментально валидирован набор легковесных анализаторов и грамматик, ориентированных на обнаружение тех синтаксических конструкций, которые критичны для навигации и классификации методов-обработчиков Go. Созданы и валидированы три специализированные грамматики Go: для обычных вызовов, для анонимных вызовов и для управляющих операторов. Показано, что их совместное применение корректно выделяет нетривиальные обработчики полей согласно предложенному определению и образует разметку проекта.
Кроме того, разработан алгоритм автоматического установления соответствий между полями схемы GraphQL и методами-обработчиками на Go, который использует соглашения об именовании, структуру кода и статические признаки для ранжирования кандидатов. По результатам экспериментов на ряде открытых репозиториев вычислены метрики ранжирования кандидатов hit@1 и MRR, которые равны 100% и 1 соответственно. Помимо этого, установлено, что число случаев, требующих ручного анализа, не превышает 2,5%.
Экспериментально рассчитан коэффициент ускорения навигации. Инструмент быстрой навигации дает ускорение в 7,89 раз по медиане по сравнению со стандартным поиском в среде разработки, дополнительно увеличивая точность навигации до 100% и устраняя повторные переходы по сущностям проекта.
Архитектурный подход, подробно описанный для пары GraphQL – Go, распространен на пару GraphQL – TypeScript: соответствующая легковесная грамматика TypeScript, а также алгоритм разметки и установления связей между точками опубликованы в репозитории [48].
Дмитрий Сергеевич ДРОЗДОВ – аспирант Южного федерального университета по научной специальности «Математическое и программное обеспечение вычислительных систем, комплексов и компьютерных сетей». В 2023 году получил степень магистра по направлению подготовки «Фундаментальная информатика и информационные технологии». Область интересов: языки программирования, компиляторы, грамматики.
Станислав Станиславович МИХАЛКОВИЧ – кандидат физ.-мат. наук, доцент, заведующий кафедрой информатики и вычислительного эксперимента Южного федерального университета, руководитель проекта PascalABC.NET. Основные научные интересы: разработка компиляторов, управление прорезающей функциональностью в программах, теория типов.