RE of PSI IntelliJ IDEA

C4-модель Архитектуры PSI в JetBrains IDE #

C4-модель — это иерархический подход к визуализации архитектуры программного обеспечения, состоящий из четырех уровней:

  1. Диаграмма контекста системы (System Context Diagram)

  2. Диаграмма контейнеров (Container Diagram)

  3. Диаграмма компонентов (Component Diagram)

  4. Диаграмма кода (Code Diagram)

Мы будем создавать диаграммы на каждом уровне, чтобы детально рассмотреть архитектуру PSI.

1. Диаграмма контекста системы #

Цель: Показать PSI в контексте всей экосистемы JetBrains IDE и взаимодействие с внешними факторами.

PSI


[\@startuml\
\' Подключаем библиотеку для контекстной диаграммы\
!include
https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Context.puml\
\
title Диаграмма контекста системы: PSI в экосистеме JetBrains IDE\
\
Person(Developer, \"Разработчик\")\
\
System_Boundary(IDE_Boundary, \"JetBrains IDE\") {\
System(PSI, \"Program Structure Interface\", \"Предоставляет структурное представление
кода\")\
System(Editor, \"Code Editor\", \"Позволяет писать и редактировать код\")\
System_Ext(Plugins, \"Plugins\", \"Расширяют функциональность IDE\")\
System_Ext(Analyzers, \"Code Analyzers\", \"Выполняют статический анализ кода\")\
}\
\
Rel(Developer, Editor, \"Пишет код\")\
Rel(Editor, PSI, \"Использует для представления кода\")\
Rel(Plugins, PSI, \"Используют PSI API\")\
Rel(Analyzers, PSI, \"Анализируют код через PSI\")\
\
\@enduml]{.mark}


2. Диаграмма контейнеров #

Цель: Показать основные контейнеры внутри IDE и как они взаимодействуют с PSI.

{width=“9.270833333333334in” height=“6.822916666666667in”}

Объяснение:

  • PSI является центральным контейнером, с которым взаимодействуют другие компоненты.

  • Редактор, Плагины, Анализаторы и Инструменты рефакторинга используют PSI для выполнения своих задач.


    [\@startuml\
    \' Подключаем библиотеку для диаграммы контейнеров\
    !include
    https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Container.puml\
    \
    title Диаграмма контейнеров: Взаимодействие PSI с другими контейнерами\
    \
    Boundary(IDE, \"JetBrains IDE\") {\
    Container(Editor, \"Code Editor\", \"Java/Kotlin\", \"Редактирует исходный код\")\
    Container(PSI, \"Program Structure Interface\", \"Java/Kotlin\", \"Представляет структуру
    кода\")\
    Container(Indexing, \"Indexing Service\", \"Java/Kotlin\", \"Индексирует код для быстрого
    поиска\")\
    Container(Plugins, \"Plugin System\", \"Java/Kotlin\", \"Управляет плагинами\")\
    Container(Analyzers, \"Code Analyzers\", \"Java/Kotlin\", \"Проводят статический
    анализ\")\
    Container(Refactoring, \"Refactoring Tools\", \"Java/Kotlin\", \"Обеспечивает рефакторинг
    кода\")\
    }\
    \
    Rel(Editor, PSI, \"Использует\")\
    Rel(PSI, Indexing, \"Предоставляет данные\")\
    Rel(Plugins, PSI, \"Взаимодействуют через PSI API\")\
    Rel(Analyzers, PSI, \"Анализируют код\")\
    Rel(Refactoring, PSI, \"Модифицирует код через\")\
    \
    \@enduml]{.mark}
    


3. Диаграмма компонентов #

Цель: Детализировать внутренние компоненты PSI и их взаимодействие

Объяснение:

  • Lexer и Parser работают вместе для создания AST.

  • AST оборачивается в PSI Tree, который содержит PsiElements.

  • PsiVisitor используется для обхода PSI дерева.

{width=“4.677083333333333in” height=“9.239583333333334in”}


[\@startuml\
\' Подключаем библиотеку для диаграммы компонентов\
!includeurl
https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Component.puml\
\
title Диаграмма компонентов: Внутренняя структура PSI\
\
\' Используем Boundary вместо ComponentBoundary\
Boundary(PSI_Boundary, \"Program Structure Interface\") {\
Component(Lexer, \"Lexer\", \"\", \"Разбивает код на токены\")\
Component(Parser, \"Parser\", \"\", \"Создает AST из токенов\")\
Component(AST_Node, \"Abstract Syntax Tree\", \"\", \"Дерево синтаксического анализа\")\
Component(PSI_Tree, \"PSI Tree\", \"\", \"Оборачивает AST с дополнительными
возможностями\")\
Component(PsiElements, \"PsiElements\", \"\", \"Элементы PSI дерева\")\
Component(PsiVisitor, \"PsiVisitor\", \"\", \"Обходит PSI дерево\")\
}\
\
Rel(Lexer, Parser, \"Передает токены\")\
Rel(Parser, AST_Node, \"Создает\")\
Rel(AST_Node, PSI_Tree, \"Оборачивается в\")\
Rel(PSI_Tree, PsiElements, \"Содержит\")\
Rel(PsiVisitor, PsiElements, \"Обходит\")\
\
\@enduml]{.mark}


Описание диаграммы: #

Цель диаграммы: Показать, как различные компоненты PSI взаимодействуют между собой при обработке исходного кода в IDE JetBrains.

Структура диаграммы:

  • Boundary (Граница): Обозначает область Program Structure Interface, внутри которой расположены основные компоненты.

  • Components (Компоненты): Представляют основные части системы, участвующие в процессе обработки кода.

  • Relations (Отношения): Показывают, как компоненты взаимодействуют между собой.

Основные компоненты: #

  1. Lexer (Лексер):

    • Описание: Разбивает исходный код на токены.

    • Функциональность:

      • Анализирует исходный текст программы.

      • Выделяет лексемы (токены), такие как ключевые слова, идентификаторы, операторы, литералы и т.д.

    • Роль: Подготавливает данные для синтаксического анализатора.

  2. Parser (Парсер):

    • Описание: Создает AST (Abstract Syntax Tree) из токенов.

    • Функциональность:

      • Получает последовательность токенов от лексера.

      • Построит абстрактное синтаксическое дерево, представляющее структурную организацию кода.

    • Роль: Преобразует линейную последовательность токенов в иерархическую структуру.

  3. AST_Node (Абстрактное синтаксическое дерево):

    • Описание: Дерево синтаксического анализа.

    • Функциональность:

      • Представляет синтаксическую структуру программы.

      • Узлы дерева соответствуют различным конструкциям языка программирования (например, выражения, операторы, функции).

    • Роль: Служит основой для дальнейшего анализа и преобразования кода.

  4. PSI_Tree (Дерево PSI):

    • Описание: Оборачивает AST с дополнительными возможностями.

    • Функциональность:

      • Добавляет семантическую информацию к узлам AST.

      • Предоставляет расширенный API для взаимодействия с кодом.

    • Роль: Обеспечивает более высокий уровень абстракции для работы с кодом в IDE.

  5. PsiElements (Элементы PSI):

    • Описание: Элементы PSI дерева.

    • Функциональность:

      • Представляют конкретные элементы кода (переменные, функции, классы, пакеты и т.д.).

      • Предоставляют методы для доступа и модификации этих элементов.

    • Роль: Позволяют инструментам IDE взаимодействовать с кодом на уровне отдельных элементов.

  6. PsiVisitor (Посетитель PSI):

    • Описание: Обходит PSI дерево.

    • Функциональность:

      • Реализует паттерн "Посетитель" для обхода PSI дерева.

      • Позволяет выполнять определенные действия над узлами дерева при их посещении.

    • Роль: Используется для анализа кода, поиска паттернов, рефакторинга и других операций.

Взаимодействия между компонентами: #

  1. Lexer -> Parser:

    • Отношение: "Передает токены"

    • Описание: Лексер передает полученные токены парсеру для дальнейшей обработки.

  2. Parser -> AST_Node:

    • Отношение: "Создает"

    • Описание: Парсер на основе последовательности токенов строит абстрактное синтаксическое дерево.

  3. AST_Node -> PSI_Tree:

    • Отношение: "Оборачивается в"

    • Описание: AST оборачивается в PSI дерево, добавляя семантическую информацию и дополнительные возможности для анализа и модификации кода.

  4. PSI_Tree -> PsiElements:

    • Отношение: "Содержит"

    • Описание: PSI дерево состоит из множества элементов PSI, каждый из которых представляет конкретный элемент кода.

  5. PsiVisitor -> PsiElements:

    • Отношение: "Обходит"

    • Описание: PsiVisitor обходит элементы PSI дерева для выполнения операций анализа, рефакторинга или других действий.

Подробное объяснение процесса: #

  1. Лексический анализ (Lexer):

    • Что происходит:

      • Исходный код, введенный разработчиком, поступает в лексер.

      • Лексер сканирует текст и разделяет его на токены.

    • Зачем это нужно:

      • Токены являются минимальными единицами синтаксиса, которые будут использоваться парсером.
  2. Синтаксический анализ (Parser):

    • Что происходит:

      • Парсер получает токены от лексера.

      • На основе грамматики языка строит AST.

    • Зачем это нужно:

      • AST представляет структуру программы в виде дерева, что позволяет понять взаимосвязи между различными элементами кода.
  3. Преобразование AST в PSI_Tree:

    • Что происходит:

      • AST оборачивается в PSI дерево.

      • Добавляются дополнительные данные, такие как ссылки между элементами, типы данных, контекст и т.д.

    • Зачем это нужно:

      • PSI предоставляет более богатый API для взаимодействия с кодом, что упрощает задачи анализа и модификации.
  4. Работа с PsiElements:

    • Что происходит:

      • PSI дерево состоит из PsiElements.

      • Каждый PsiElement представляет конкретный элемент кода и предоставляет методы для работы с ним.

    • Зачем это нужно:

      • Позволяет инструментам IDE выполнять точечные операции на уровне отдельных элементов кода (например, переименование переменной).
  5. Обход PSI дерева с помощью PsiVisitor:

    • Что происходит:

      • PsiVisitor рекурсивно обходит PSI дерево.

      • При посещении каждого узла выполняются определенные действия (например, проверка на соответствие стилю кодирования).

    • Зачем это нужно:

      • Обеспечивает возможность анализа кода, поиска паттернов, обнаружения ошибок и т.д.

Применение компонентов в практике разработки: #

  • Навигация по коду:

    • Используя PSI, IDE предоставляет функции перехода к определению, поиска использования переменных и т.д.
  • Статический анализ и инспекции:

    • Анализаторы кода обходят PSI дерево с помощью PsiVisitor для обнаружения потенциальных ошибок или проблем.
  • Рефакторинг:

    • Инструменты рефакторинга используют PSI для безопасного и корректного изменения кода, например, при переименовании методов или изменении сигнатур функций.
  • Подсветка синтаксиса и автодополнение:

    • PSI предоставляет информацию о типах данных, контексте и других аспектах, необходимых для интеллектуальной подсветки и автодополнения кода.

Важность каждого компонента: #

  • Lexer и Parser:

    • Основополагающие компоненты, преобразующие текстовый код в структуру данных, пригодную для анализа.
  • AST_Node:

    • Представляет синтаксическую структуру программы без семантической информации.
  • PSI_Tree:

    • Обогащает AST семантикой и предоставляет унифицированный интерфейс для взаимодействия с кодом.
  • PsiElements:

    • Позволяют работать с конкретными элементами кода, предоставляя методы для доступа и изменения.
  • PsiVisitor:

    • Удобный инструмент для обхода PSI дерева и выполнения операций над узлами.

Дополнительные детали: #

  • Унифицированный API:

    • PSI обеспечивает единый интерфейс для работы с кодом на разных языках программирования, что упрощает разработку инструментов и плагинов.
  • Расширяемость:

    • Благодаря структуре PSI, разработчики могут создавать собственные инструменты анализа и рефакторинга, взаимодействуя с PSI деревом.
  • Производительность:

    • Использование PSI позволяет оптимизировать работу IDE, обеспечивая быстрый доступ к структуре кода и возможность кэширования результатов анализа.

Заключение: #

Диаграмма демонстрирует, как исходный код преобразуется и обрабатывается внутри PSI, обеспечивая богатые возможности для анализа, навигации и модификации кода в средах разработки JetBrains. Понимание этой архитектуры позволяет разработчикам эффективно использовать возможности IDE и создавать собственные инструменты для улучшения процесса разработки.

4. Диаграмма кода #

Цель: Рассмотреть детали реализации одного из компонентов PSI.

Пример: Структура GoFunctionDeclaration и связанные с ней классы.

{width=“7.291666666666667in” height=“6.447916666666667in”}

Объяснение:

  • GoFunctionDeclaration представляет объявление функции в Go.

  • Он связан с параметрами, возвращаемым типом и телом функции.

  • Диаграмма показывает связи между классами и их методы.


  [\@startuml\
  title Диаграмма кода: Классы для *GoFunctionDeclaration*\
  \
  class GoFunctionDeclaration {\
  +getName(): *String*\
  +getParameters(): *GoParameterList*\
  +getReturnType(): *GoType*\
  +getBody(): *GoBlock*\
  +accept(visitor: *PsiElementVisitor*): void\
  }\
  \
  class GoParameterList {\
  +getParameters(): *List*\<*GoParameter*\>\
  }\
  \
  class GoParameter {\
  +getName(): *String*\
  +getType(): *GoType*\
  }\
  \
  class GoType {\
  +getName(): *String*\
  }\
  \
  class GoBlock {\
  +getStatements(): *List*\<*GoStatement*\>\
  }\
  \
  *GoFunctionDeclaration* \--\> *GoParameterList* : содержит\
  *GoFunctionDeclaration* \--\> *GoType* : возвращает\
  *GoFunctionDeclaration* \--\> *GoBlock* : содержит\
  *GoParameterList* \--\> *GoParameter* : содержит список\
  *GoParameter* \--\> *GoType* : имеет тип\
  \
  \@enduml]{.mark}


Подробное объяснение диаграммы: #

Цель диаграммы: Показать, как объявление функции в языке Go представляется внутри PSI, включая его составные части и взаимосвязи между ними. Это помогает понять, как IDE JetBrains, такие как GoLand, работают с кодом на уровне внутреннего представления.

Основные классы и их методы: #

  1. GoFunctionDeclaration

    • Описание: Представляет объявление функции в Go-коде.

    • Методы:

      • getName(): String — возвращает имя функции.

      • getParameters(): GoParameterList — возвращает список параметров функции.

      • getReturnType(): GoType — возвращает тип возвращаемого значения функции.

      • getBody(): GoBlock — возвращает тело функции.

      • accept(visitor: PsiElementVisitor): void — принимает посетителя для обхода PSI-дерева (паттерн "Посетитель").

  2. GoParameterList

    • Описание: Содержит список параметров функции.

    • Методы:

      • getParameters(): List<GoParameter> — возвращает список объектов GoParameter.
  3. GoParameter

    • Описание: Представляет отдельный параметр функции.

    • Методы:

      • getName(): String — возвращает имя параметра.

      • getType(): GoType — возвращает тип параметра.

  4. GoType

    • Описание: Представляет тип данных в Go.

    • Методы:

      • getName(): String — возвращает имя типа (например, int, string, MyCustomType).
  5. GoBlock

    • Описание: Представляет тело функции или блок кода.

    • Методы:

      • getStatements(): List<GoStatement> — возвращает список операторов внутри блока.

Взаимосвязи между классами: #

  • GoFunctionDeclaration имеет связи с:

    • GoParameterList: содержит список параметров.

    • GoType: имеет тип возвращаемого значения.

    • GoBlock: содержит тело функции.

  • GoParameterList связана с:

    • GoParameter: содержит список параметров.
  • GoParameter связана с:

    • GoType: имеет тип параметра.

Объяснение связей на диаграмме: #

  • Стрелки с надписью "содержит" обозначают, что один класс включает в себя другой как часть своей структуры.

  • Стрелка от GoFunctionDeclaration к GoParameterList показывает, что функция имеет список параметров.

  • Стрелка от GoFunctionDeclaration к GoType с надписью "возвращает" указывает на тип возвращаемого значения функции.

  • Стрелка от GoFunctionDeclaration к GoBlock означает, что функция содержит тело, состоящее из блоков кода.

  • Стрелка от GoParameterList к GoParameter показывает, что список параметров содержит один или несколько параметров.

  • Стрелка от GoParameter к GoType указывает, что каждый параметр имеет определенный тип.

Практический пример использования: #

Предположим, у нас есть функция в Go:


[func Add(a int, b int) int {\
return a + b\
}]{.mark}


В PSI это будет представлено следующим образом:

  • GoFunctionDeclaration:

    • getName() вернет "Add".

    • getParameters() вернет GoParameterList, содержащий два параметра.

    • getReturnType().getName() вернет "int".

    • getBody() вернет GoBlock, содержащий операторы тела функции.

  • GoParameterList:

    • getParameters() вернет список из двух GoParameter.
  • GoParameter (для каждого параметра):

    • getName() вернет "a" и "b".

    • getType().getName() вернет "int" для обоих параметров.

  • GoBlock:

    • getStatements() вернет список операторов, в данном случае один оператор return a + b.

Использование в IDE: #

  • Навигация по коду:

    • Разработчик может перейти к определению функции, параметра или типа благодаря методам, предоставляемым этими классами.
  • Автодополнение и подсказки:

    • IDE использует информацию о параметрах и возвращаемом типе для предоставления автодополнения и подсказок при вызове функции.
  • Рефакторинг:

    • При переименовании функции или параметра PSI обеспечивает обновление всех ссылок благодаря методу accept(visitor) и паттерну "Посетитель".
  • Статический анализ:

    • Анализаторы кода могут обходить PSI-дерево, используя PsiElementVisitor, чтобы обнаруживать потенциальные ошибки или несоответствия в коде.

Паттерн "Посетитель": #

  • accept(visitor: PsiElementVisitor): void:

    • Этот метод позволяет применять определенные действия к каждому узлу PSI-дерева.

    • Полезен для обхода структуры кода без необходимости писать рекурсивные обходы для каждого типа узла.

Взаимодействие с другими элементами PSI: #

  • Типы данных (GoType):

    • Позволяют получать информацию о типах, что важно для типизации и проверки корректности кода.
  • Операторы (GoStatement):

    • Через GoBlock можно анализировать и модифицировать тело функции.

Значимость этой структуры: #

  • Унифицированное представление кода:

    • PSI предоставляет единый интерфейс для работы с различными элементами кода, упрощая разработку инструментов для анализа и рефакторинга.
  • Расширяемость и модификация:

    • Разработчики плагинов могут использовать эти классы для создания новых функций в IDE.
  • Повышение производительности:

    • Благодаря четкой структуре и методам, IDE может эффективно обрабатывать большие проекты и предоставлять интерактивные возможности в реальном времени.

Дополнительные аспекты: #

  • Согласованность между языками:

    • Хотя этот пример относится к Go, аналогичная структура применяется для других языков в JetBrains IDE, обеспечивая унифицированный подход.
  • Интеграция с другими системами:

    • Информация из PSI может быть использована для генерации документации, диаграмм или экспорта кода в другие форматы.

Заключение: #

Диаграмма и подробное объяснение демонстрируют, как внутренние классы и их взаимосвязи в PSI позволяют IDE эффективно работать с кодом. Понимание этой структуры важно для разработчиков, интересующихся тем, как IDE предоставляет такие мощные инструменты для навигации, анализа и модификации кода.

5. Взаимодействие компонентов при рефакторинге

Цель: Показать процесс переименования функции с использованием PSI.

{width=“11.427083333333334in” height=“4.395833333333333in”}

Объяснение:

  • Разработчик инициирует переименование функции.

  • Инструмент рефакторинга использует PSI Tree для поиска и обновления функции и ее ссылок.

  • Код форматируется и отображается в Редакторе


  [\@startuml\
  \' Для диаграммы последовательности можно использовать стандартные
  функции PlantUML\
  \' или подключить дополнительные библиотеки, если требуется\
  \
  title Процесс переименования функции в PSI\
  \
  actor Developer as Dev\
  \
  participant RefactoringTool\
  participant PSI_Tree\
  participant ReferencesSearch\
  participant CodeFormatter\
  participant Editor\
  \
  Dev -\> RefactoringTool : Инициирует переименование функции\
  RefactoringTool -\> PSI_Tree : Находит PsiElement функции\
  PSI_Tree -\> ReferencesSearch : Ищет все ссылки на функцию\
  ReferencesSearch \--\> PSI_Tree : Возвращает список ссылок\
  RefactoringTool -\> PSI_Tree : Обновляет имя функции и ссылки\
  PSI_Tree -\> CodeFormatter : Форматирует измененный код\
  PSI_Tree -\> Editor : Отображает обновленный код\
  \
  \@enduml]{.mark}


6. Архитектура поддержки нескольких языков #

Цель: Показать, как PSI обеспечивает поддержку разных языков программирования.

Цель: Показать, как PSI (Program Structure Interface) обеспечивает поддержку разных языков программирования, предоставляя унифицированный подход к работе с кодом на различных языках в средах разработки JetBrains.

Общее объяснение: #

  • PSI Core предоставляет общие интерфейсы и базовые классы для всех языков программирования.

  • Языковые плагины реализуют свои собственные версии этих интерфейсов и классов, адаптированные под конкретный язык.

  • Это обеспечивает унифицированный API для работы с кодом, независимо от языка, что упрощает разработку инструментов анализа, рефакторинга и других функций IDE

Детальное объяснение: #

  1. PSI Core:

    • PsiElement: Базовый интерфейс для всех элементов PSI. Любой элемент синтаксического дерева (например, выражение, оператор, декларация) является наследником этого интерфейса.

    • PsiFile: Интерфейс, представляющий файл исходного кода. Наследуется от PsiElement и предоставляет методы для работы с содержимым файла.

    • PsiNamedElement: Интерфейс для элементов, которые имеют имя (например, классы, функции, переменные). Наследуется от PsiElement.

  2. Языковая поддержка:

    • Для каждого поддерживаемого языка (например, Go, Java, Python) реализуется свой набор классов, наследующихся от базовых интерфейсов PSI.

    • Эти классы адаптированы к синтаксису и особенностям конкретного языка.

    • Примеры:

      • GoPsiElement, GoFile: Реализации для языка Go.

      • JavaPsiElement, JavaFile: Реализации для языка Java.

      • PythonPsiElement, PythonFile: Реализации для языка Python.

  3. Унифицированный подход:

    • Благодаря тому, что все языковые реализации наследуются от общих интерфейсов, инструменты IDE (такие как рефакторинг, анализаторы кода) могут работать с кодом на разных языках, используя один и тот же API.

    • Это значительно упрощает расширение функциональности IDE и разработку плагинов, поскольку разработчикам нужно работать только с общими интерфейсами PSI

{width=“13.53125in” height=“3.2916666666666665in”}

Объяснение:

  • PSI Core предоставляет общие интерфейсы для всех языков.

  • Каждая языковая поддержка реализует свои версии PsiElement и PsiFile.

  • Это обеспечивает унифицированный подход к работе с разными языками.

Объяснение диаграммы: #

  1. PSI Core (Обозначен как контейнер):

    • PsiElement:

      • Базовый интерфейс для всех элементов PSI.

      • Является корневым классом для всех элементов синтаксического дерева.

    • PsiFile:

      • Интерфейс для представления файла исходного кода.

      • Наследуется от PsiElement.

    • PsiNamedElement:

      • Интерфейс для элементов с именами (например, функций, классов, переменных).

      • Позволяет получать и устанавливать имя элемента.

  2. Языковая поддержка (Обозначена как отдельный контейнер):

    • Содержит компоненты для каждого поддерживаемого языка:

      • GoLanguage

      • JavaLanguage

      • PythonLanguage

  3. Реализация базовых интерфейсов для каждого языка:

    • GoPsiElement, JavaPsiElement, PythonPsiElement:

      • Реализуют интерфейс PsiElement для конкретного языка.

      • Добавляют языковые особенности и дополнительные методы.

    • GoFile, JavaFile, PythonFile:

      • Реализуют интерфейс PsiFile для соответствующих языков.

      • Позволяют работать с файлами исходного кода на конкретном языке.

  4. Отношения реализации (наследования):

    • Стрелки с треугольником -|> обозначают наследование или реализацию интерфейса.

    • GoPsiElement, JavaPsiElement, PythonPsiElement наследуются от PsiElement.

    • GoFile, JavaFile, PythonFile наследуются от PsiFile.

  5. Отношения использования:

    • Стрелки --> обозначают отношение использования.

    • GoLanguage использует GoPsiElement.

    • JavaLanguage использует JavaPsiElement.

    • PythonLanguage использует PythonPsiElement.

    • Это показывает, что языковые плагины работают со своими реализациями PSI для предоставления функциональности, специфичной для языка.

Визуальное представление диаграммы: #

  • PSI Core находится в левой части диаграммы и представляет общие интерфейсы.

  • Языковая поддержка находится в правой части и содержит компоненты для каждого языка.

  • Элементы реализации расположены между PSI Core и языковой поддержкой.

  • Отношения наследования направлены от языковых реализаций к базовым интерфейсам в PSI Core.

  • Отношения использования направлены от языковой поддержки к соответствующим языковым реализациям PSI.

Практический пример: #

Предположим, разработчик создает инструмент рефакторинга, который должен работать с кодом на разных языках.

  • Благодаря PSI Core:

    • Инструмент может работать с интерфейсами PsiElement и PsiFile, не завися от конкретного языка.

    • Это позволяет обрабатывать код на Go, Java, Python и других языках, используя общий код.

  • Языковые плагины:

    • Обеспечивают конкретные реализации интерфейсов, учитывая особенности синтаксиса и семантики языка.

    • Предоставляют дополнительные методы и свойства, специфичные для языка, если это необходимо.

Преимущества такого подхода: #

  • Унификация: Один API для работы с разными языками.

  • Расширяемость: Легко добавить поддержку нового языка, реализовав его версии PSI.

  • Поддержка инструментов: Инструменты анализа и рефакторинга могут работать с кодом на любом поддерживаемом языке без значительных изменений.

  • Снижение сложности: Разработчикам инструментов не нужно разбираться в деталях каждого языка — достаточно работать с общими интерфейсами.


    [\@startuml\
    \' Подключаем библиотеку для диаграммы компонентов\
    !include
    https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Component.puml\
    \
    title Поддержка разных языков в PSI\
    \
    Container_Boundary(PSI_Core, \"PSI Core\") {\
    Component(PsiElement, \"PsiElement\", \"Базовый интерфейс\")\
    Component(PsiFile, \"PsiFile\", \"Интерфейс файла\")\
    Component(PsiNamedElement, \"PsiNamedElement\", \"Именованный элемент\")\
    }\
    \
    Container_Boundary(Language_Support, \"Языковая поддержка\") {\
    Component(GoLanguage, \"Go Language\")\
    Component(JavaLanguage, \"Java Language\")\
    Component(PythonLanguage, \"Python Language\")\
    }\
    \
    \' Определяем элементы, которые реализуют PsiElement и PsiFile\
    Component(GoPsiElement, \"GoPsiElement\")\
    Component(JavaPsiElement, \"JavaPsiElement\")\
    Component(PythonPsiElement, \"PythonPsiElement\")\
    \
    Component(GoFile, \"GoFile\")\
    Component(JavaFile, \"JavaFile\")\
    Component(PythonFile, \"PythonFile\")\
    \
    \' Отношения реализации (наследования)\
    GoPsiElement -up-\|\> PsiElement : реализует\
    JavaPsiElement -up-\|\> PsiElement : реализует\
    PythonPsiElement -up-\|\> PsiElement : реализует\
    \
    GoFile -up-\|\> PsiFile : реализует\
    JavaFile -up-\|\> PsiFile : реализует\
    PythonFile -up-\|\> PsiFile : реализует\
    \
    \' Отношения использования\
    GoLanguage \--\> GoPsiElement : использует\
    JavaLanguage \--\> JavaPsiElement : использует\
    PythonLanguage \--\> PythonPsiElement : использует\
    \
    \@enduml]{.mark}
    


8. Интеграция PSI с плагинами #

Цель: Показать, как плагины взаимодействуют с PSI для расширения функциональности.

{width=“2.9583333333333335in” height=“6.53125in”}

Объяснение:

  • Плагины используют PSI API для взаимодействия с кодом.

  • PSI API предоставляет доступ к PSI Tree, позволяя плагинам анализировать и изменять код.


    [\@startuml\
    \' Подключаем библиотеку для диаграммы компонентов\
    !include
    https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Component.puml\
    \
    title Взаимодействие плагинов с PSI\
    \
    Container_Boundary(IDE_Plugin_System, \"Система плагинов\") {\
    Component(CustomPlugin, \"Пользовательский плагин\")\
    Component(PsiAPI, \"PSI API\")\
    }\
    \
    Component(PSI_Tree, \"PSI Tree\")\
    \
    Rel(CustomPlugin, PsiAPI, \"Использует для анализа и модификации кода\")\
    Rel(PsiAPI, PSI_Tree, \"Доступ к\")\
    Rel(PSI_Tree, CodeBase, \"Представляет исходный код\")\
    \
    \@enduml]{.mark}
    


9. Кэширование и индексирование в PSI #

Цель: Показать, как кэширование и индексирование улучшают производительность.

{width=“3.5625in” height=“5.479166666666667in”}

Объяснение:

  • Cache Manager кэширует данные для быстрого доступа.

  • Indexing Service использует кэшированные данные для создания индексов.

  • Это обеспечивает быструю навигацию и поиск по коду.


    [\@startuml\
    \' Подключаем библиотеку для диаграммы компонентов\
    !include
    https://raw.githubusercontent.com/plantuml-stdlib/C4-PlantUML/master/C4_Component.puml\
    \
    title Кэширование и индексирование в PSI\
    \
    Component(PSI_Tree, \"PSI Tree\")\
    Component(CacheManager, \"Cache Manager\")\
    Component(IndexingService, \"Indexing Service\")\
    Component(FileSystem, \"File System\")\
    \
    Rel(PSI_Tree, CacheManager, \"Кэширует результаты анализа\")\
    Rel(CacheManager, IndexingService, \"Предоставляет данные\")\
    Rel(IndexingService, FileSystem, \"Обновляет индексы файлов\")\
    Rel(PSI_Tree, IndexingService, \"Сообщает об изменениях\")\
    \
    \@enduml]{.mark}
    


10. Поток данных при работе с PSI #

Цель: Показать общий поток данных от редактирования кода до отображения результатов анализа.

{width=“19.59375in” height=“1.9270833333333333in”}

Объяснение:

  • Разработчик вносит изменения в код.

  • Lexer и Parser обновляют PSI Tree.

  • Анализатор использует PSI Tree для поиска ошибок.

  • Редактор отображает результаты анализа.


    [\@startuml\
    title Поток данных в PSI\
    \
    actor \"Разработчик\" as Dev\
    component \"Editor\" as Editor\
    component \"Lexer\" as Lexer\
    component \"Parser\" as Parser\
    component \"PSI Tree\" as PSITree\
    component \"Code Analyzer\" as Analyzer\
    component \"Error Highlighter\" as Highlighter\
    \
    Dev -\> Editor : Редактирует код\
    Editor -\> Lexer : Передает изменения\
    Lexer -\> Parser : Токены\
    Parser -\> PSITree : Обновляет PSI дерево\
    PSITree -\> Analyzer : Выполняет анализ\
    Analyzer -\> PSITree : Обновляет узлы с ошибками\
    PSITree -\> Highlighter : Предоставляет данные об ошибках\
    Highlighter -\> Editor : Подсвечивает ошибки\
    \
    \@enduml]{.mark}
    


Заключение #

Используя C4-модель для создания диаграмм в формате PlantUML, мы рассмотрели архитектуру PSI на разных уровнях абстракции:

  • Диаграмма контекста системы показала PSI в контексте всей IDE.

  • Диаграмма контейнеров детализировала основные компоненты и их взаимодействие.

  • Диаграмма компонентов раскрыла внутреннюю структуру PSI.

  • Диаграмма кода предоставила детали реализации ключевых классов.

  • Дополнительные диаграммы помогли понять процессы рефакторинга, обработки ошибок, кэширования и интеграции с плагинами.

Как использовать диаграммы:

  • Скопируйте код диаграмм и вставьте его в онлайн-редактор PlantUML, чтобы визуализировать их.

  • Изучите каждую диаграмму, обращая внимание на связи между компонентами и потоки данных.

  • Используйте диаграммы в качестве справочного материала при изучении или разработке на базе PSI.

Архитектуры PSI в JetBrains IDE #

PSI (Program Structure Interface) — это внутреннее представление кода в IDE JetBrains, которое позволяет инструментам и плагинам эффективно работать с исходным кодом на уровне языка. PSI предоставляет абстрактный слой над синтаксическим деревом (AST), обеспечивая дополнительные возможности для анализа и модификации кода.

Основные компоненты:

  1. Лексический анализатор (Lexer): Разбивает исходный код на токены.

  2. Парсер (Parser): Создает AST на основе токенов.

  3. PSI дерево (PSI Tree): Оборачивает AST и предоставляет дополнительные возможности.

  4. Проверяющий анализатор (Annotator/Inspector): Выполняет статический анализ кода.

  5. Рефакторинг (Refactoring): Предоставляет инструменты для изменения кода с сохранением его корректности.

1. Процесс преобразования исходного кода в AST и PSI #

{width=“4.791666666666667in” height=“7.8125in”}

Объяснение:

  • Lexer разбивает исходный код на токены.

  • Parser создает AST на основе этих токенов.

  • AST представляет синтаксическую структуру кода.

  • PSI Tree оборачивает AST и предоставляет более богатый API для работы с кодом.

  • Inspector и Refactoring Tools используют PSI для анализа и модификации кода.

Диаграмма компонентов #


[\@startuml\
title Процесс преобразования исходного кода в *AST* и *PSI*\
\
package \"JetBrains IDE\" {\
component \"Lexer\" as *Lexer*\
component \"Parser\" as *Parser*\
component \"AST\" as *AST*\
component \"PSI Tree\" as *PSITree*\
component \"Annotator/Inspector\" as *Inspector*\
component \"Refactoring Tools\" as *Refactoring*\
}\
\
\[Исходный код\] \--\> *Lexer* : Лексический анализ\
*Lexer* \--\> *Parser* : Токены\
*Parser* \--\> *AST* : Создание *AST*\
*AST* \--\> *PSITree* : Оборачивание в *PSI*\
*PSITree* \--\> *Inspector* : Анализ кода\
*PSITree* \--\> *Refactoring* : Модификация кода\
\
\@enduml]{.mark}


2. Подробный процесс работы с PSI в GoLand #

Шаг 1: Лексический анализ #

  • Lexer читает исходный код и преобразует его в последовательность токенов.

  • Токены могут быть ключевыми словами, идентификаторами, операторами, литералами и т.д.

Шаг 2: Синтаксический анализ #

  • Parser принимает токены от Lexer и строит AST.

  • AST представляет собой древовидную структуру, отражающую синтаксис программы.

Шаг 3: Создание PSI дерева #

  • PSI Tree оборачивает узлы AST, добавляя дополнительную информацию и функциональность.

  • PSI предоставляет удобные методы для навигации, поиска и изменения кода.

Шаг 4: Анализ и модификация кода #

  • Annotator/Inspector использует PSI для выполнения статического анализа.

  • Refactoring Tools используют PSI для безопасного изменения кода.

Диаграмма последовательности

{width=“11.447916666666666in” height=“4.395833333333333in”}


[\@startuml\
title Последовательность преобразования кода в PSI в GoLand\
\
actor \"Разработчик\" as Dev\
\
participant \"Lexer\" as Lexer\
participant \"Parser\" as Parser\
participant \"AST\"\
participant \"PSI Builder\" as PSIBuilder\
participant \"PSI Tree\" as PSITree\
participant \"Inspector\"\
participant \"Refactoring Tools\" as Refactoring\
\
Dev -\> Lexer : Пишет исходный код\
Lexer -\> Parser : Передает токены\
Parser -\> AST : Создает AST\
AST -\> PSIBuilder : Передает AST\
PSIBuilder -\> PSITree : Создает PSI дерево\
PSITree -\> Inspector : Выполняет анализ\
PSITree -\> Refactoring : Модифицирует код\
\
\@enduml]{.mark}


3. Пример использования PSI в плагинах JetBrains #

Цель: Создать плагин, который находит все функции с определенным именем в Go-коде.

Пример кода на Kotlin для плагина #


[import com.intellij.psi.PsiElement\
import com.intellij.psi.PsiFile\
import com.intellij.psi.search.PsiElementVisitor\
import com.intellij.psi.util.PsiTreeUtil\
import org.jetbrains.plugins.golang.psi.GoFunctionDeclaration\
\
fun findFunctionsByName(psiFile: *PsiFile*, functionName: *String*):
List\<GoFunctionDeclaration\> {\
val functions = mutableListOf\<GoFunctionDeclaration\>()\
PsiTreeUtil.processElements(psiFile) { element -\>\
if (element is GoFunctionDeclaration && element.name == functionName)
{\
functions.add(element)\
}\
true\
}\
return functions\
}]{.mark}


Объяснение:

  • PsiFile представляет собой PSI дерево для файла.

  • PsiTreeUtil предоставляет утилиты для обхода PSI дерева.

  • GoFunctionDeclaration — узел PSI, представляющий объявление функции в Go.

4. Структура PSI дерева #

Диаграмма классов PSI #

{width=“14.5in” height=“3.3333333333333335in”}

Объяснение:

  • PsiElement — базовый интерфейс для всех элементов PSI.

  • GoFile, GoFunctionDeclaration, GoParameterList, GoBlock — конкретные реализации для Go.

  • Каждый элемент предоставляет методы для доступа к своим дочерним элементам и свойствам.


    [\@startuml\
    title Классы PSI для Go\
    \
    interface PsiElement {\
    +getText(): String\
    +getParent(): PsiElement\
    +getChildren(): Array\<PsiElement\>\
    +accept(visitor: PsiElementVisitor)\
    }\
    \
    class GoFile implements PsiElement {\
    +getPackage(): GoPackageDeclaration\
    +getImports(): List\<GoImportDeclaration\>\
    +getFunctions(): List\<GoFunctionDeclaration\>\
    }\
    \
    class GoFunctionDeclaration implements PsiElement {\
    +getName(): String\
    +getParameters(): GoParameterList\
    +getBody(): GoBlock\
    }\
    \
    class GoParameterList implements PsiElement {\
    +getParameters(): List\<GoParameterDeclaration\>\
    }\
    \
    class GoBlock implements PsiElement {\
    +getStatements(): List\<GoStatement\>\
    }\
    \
    PsiElement \<\|\-- GoFile\
    PsiElement \<\|\-- GoFunctionDeclaration\
    PsiElement \<\|\-- GoParameterList\
    PsiElement \<\|\-- GoBlock\
    \
    \@enduml]{.mark}
    


5. Как GoLand работает под капотом #

Ваше понимание верно. GoLand использует следующую последовательность действий для обработки кода:

  1. Лексический анализ (Lexer): Разбивает исходный код на токены.

  2. Синтаксический анализ (Parser): Создает AST на основе токенов.

  3. Создание PSI дерева: Оборачивает AST в PSI, предоставляя более богатый API.

  4. Анализ и рефакторинг: Использует PSI для анализа, навигации и модификации кода.

6. Преимущества использования PSI #

  • Унифицированный API: PSI предоставляет общий интерфейс для работы с кодом на разных языках.

  • Расширяемость: Разработчики могут создавать плагины, использующие PSI для анализа и изменения кода.

  • Интеграция функций IDE: Навигация, автодополнение, рефакторинг и другие функции основаны на PSI.

  • Высокая производительность: Оптимизированное представление кода для быстрого доступа и модификации.

7. Связь между AST и PSI #

  • AST фокусируется на синтаксической структуре кода.

  • PSI добавляет семантическую информацию и предоставляет удобные методы для работы с кодом.

  • PSI оборачивает узлы AST, добавляя возможности для навигации и анализа.

8. Работа с PSI: практические примеры #

Пример: Переименование функции #

  • Шаг 1: Найти узел GoFunctionDeclaration с заданным именем.

  • Шаг 2: Использовать метод setName(newName) для изменения имени функции.

  • Шаг 3: PSI автоматически обновит все ссылки на эту функцию.

Пример кода: #


[fun renameFunction(function: *GoFunctionDeclaration*, newName:
*String*) {\
function.setName(newName)\
}]{.mark}


9. Использование PSI для статического анализа #

  • Annotator/Inspector использует PSI для обнаружения ошибок, предупреждений и подсказок.

  • Можно создавать собственные инспекции, используя PSI для поиска определенных паттернов кода.

Пример: Поиск неиспользуемых переменных #


[class UnusedVariableInspection : *LocalInspectionTool*() {\
override fun buildVisitor(holder: *ProblemsHolder*, isOnTheFly:
*Boolean*): PsiElementVisitor {\
return object : GoRecursiveElementVisitor() {\
override fun visitVarDefinition(o: *GoVarDefinition*) {\
if (!isVariableUsed(o)) {\
holder.registerProblem(o, \"Переменная не используется\")\
}\
}\
}\
}\
\
private fun isVariableUsed(variable: *GoVarDefinition*): Boolean {\
// Логика проверки использования переменной\
return true\
}\
}]{.mark}


10. Ресурсы для изучения PSI #

Заключение #

  • PSI является ключевым компонентом архитектуры JetBrains IDE, включая GoLand.

  • Он обеспечивает мощный и гибкий способ работы с кодом на уровне IDE.

  • Ваше понимание процесса преобразования исходного кода в AST, а затем в PSI, правильно.

  • Использование PSI позволяет создавать расширенные инструменты для анализа, навигации и рефакторинга кода.

1. Общая структура PSI #

Диаграмма классов основных компонентов PSI

{width=“9.020833333333334in” height=“3.1666666666666665in”}

Объяснение:

  • PsiElement: Базовый интерфейс для всех элементов PSI. Любой узел PSI дерева является PsiElement.

  • PsiFile: Представляет файл исходного кода. Наследуется от PsiElement.

  • PsiDirectory: Представляет директорию в проекте. Может содержать файлы и поддиректории.

  • PsiNamedElement: Интерфейс для элементов, которые имеют имя (например, классы, функции, переменные).


    [\@startuml\
    title Основные компоненты *PSI*\
    \
    interface *PsiElement* {\
    +getText(): *String*\
    +getParent(): *PsiElement*\
    +getChildren(): *PsiElement*\[\]\
    +accept(visitor: *PsiElementVisitor*): void\
    }\
    \
    abstract class PsiFile implements PsiElement {\
    +getVirtualFile(): *VirtualFile*\
    +getLanguage(): *Language*\
    }\
    \
    class PsiDirectory implements PsiElement {\
    +getFiles(): *PsiFile*\[\]\
    +getSubdirectories(): *PsiDirectory*\[\]\
    }\
    \
    interface *PsiNamedElement* extends *PsiElement* {\
    +getName(): *String*\
    +setName(newName: *String*): *PsiElement*\
    }\
    \
    *PsiElement* \<\|\-- *PsiFile*\
    *PsiElement* \<\|\-- *PsiDirectory*\
    *PsiElement* \<\|\-- *PsiNamedElement*\
    \
    \@enduml]{.mark}
    


2. Структура PSI для языка Go #

Диаграмма классов PSI для Go:

{width=“16.666666666666668in” height=“2.9895833333333335in”}

Объяснение:

  • GoFile: Специализированный класс для файлов Go. Предоставляет методы для доступа к пакетам, импортам, функциям и другим элементам.

  • GoPackageDeclaration: Представляет объявление пакета в файле Go.

  • GoImportDeclaration: Представляет импортируемый пакет.

  • GoFunctionDeclaration: Представляет объявление функции, включая параметры, возвращаемые значения и тело функции.

  • GoTypeDeclaration и GoVarDeclaration: Представляют объявления типов и переменных соответственно.


    [\@startuml\
    title *PSI* для языка *Go*\
    \
    class GoFile implements PsiFile {\
    +getPackage(): *GoPackageDeclaration*\
    +getImports(): *GoImportDeclaration*\[\]\
    +getFunctions(): *GoFunctionDeclaration*\[\]\
    +getTypes(): *GoTypeDeclaration*\[\]\
    +getVariables(): *GoVarDeclaration*\[\]\
    }\
    \
    class GoPackageDeclaration implements PsiNamedElement {\
    +getName(): *String*\
    }\
    \
    class GoImportDeclaration implements PsiElement {\
    +getImportedPackageName(): *String*\
    +getAlias(): *String*\
    }\
    \
    class GoFunctionDeclaration implements PsiNamedElement {\
    +getParameters(): *GoParameterList*\
    +getResult(): *GoResult*\
    +getBody(): *GoBlock*\
    }\
    \
    class GoTypeDeclaration implements PsiNamedElement {\
    +getType(): *GoType*\
    }\
    \
    class GoVarDeclaration implements PsiNamedElement {\
    +getType(): *GoType*\
    +getValue(): *GoExpression*\
    }\
    \
    *PsiFile* \<\|\-- *GoFile*\
    *PsiNamedElement* \<\|\-- *GoPackageDeclaration*\
    *PsiElement* \<\|\-- *GoImportDeclaration*\
    *PsiNamedElement* \<\|\-- *GoFunctionDeclaration*\
    *PsiNamedElement* \<\|\-- *GoTypeDeclaration*\
    *PsiNamedElement* \<\|\-- *GoVarDeclaration*\
    \
    \@enduml]{.mark}
    


3. Процесс рефакторинга с использованием PSI #

Диаграмма последовательности рефакторинга кода:

{width=“13.666666666666666in” height=“4.395833333333333in”}

Объяснение:

  • Разработчик инициирует рефакторинг (например, переименование функции).

  • IDE использует PSI Tree для поиска соответствующего элемента кода.

  • Refactoring Engine вносит изменения в PSI дерево.

  • Code Formatter применяет форматирование к измененному коду.

  • Обновленный код отображается разработчику.


    [\@startuml\
    title Процесс рефакторинга с использованием PSI\
    \
    actor \"Разработчик\" as Dev\
    participant \"IDE\" as IDE\
    participant \"PSI Tree\" as PSITree\
    participant \"Refactoring Engine\" as Refactoring\
    participant \"Code Formatter\" as Formatter\
    \
    Dev -\> IDE : Инициирует рефакторинг (например, переименование)\
    IDE -\> PSITree : Находит соответствующий PsiElement\
    PSITree -\> Refactoring : Передает элемент для изменения\
    Refactoring -\> PSITree : Вносит изменения в PSI дерево\
    Refactoring -\> Formatter : Форматирует измененный код\
    Formatter -\> PSITree : Обновляет PSI дерево\
    IDE -\> Dev : Отображает обновленный код\
    \
    \@enduml]{.mark}
    


4. Обход PSI дерева с использованием посетителя (Visitor) #

Диаграмма классов посетителя PSI:

{width=“4.208333333333333in” height=“3.1666666666666665in”}

Объяснение:

  • PsiElementVisitor: Паттерн "Посетитель" для обхода PSI дерева.

  • GoRecursiveElementVisitor: Реализация посетителя для языка Go, который рекурсивно обходит элементы PSI дерева.


    [\@startuml\
    title Посетитель *PSI*\
    \
    interface *PsiElementVisitor* {\
    +visitElement(element: *PsiElement*): void\
    }\
    \
    class GoRecursiveElementVisitor implements PsiElementVisitor {\
    +visitElement(element: *PsiElement*): void\
    +visitFile(file: *GoFile*): void\
    +visitFunction(function: *GoFunctionDeclaration*): void\
    +visitType(type: *GoTypeDeclaration*): void\
    +visitVariable(variable: *GoVarDeclaration*): void\
    }\
    \
    *PsiElementVisitor* \<\|\-- *GoRecursiveElementVisitor*\
    \
    \@enduml]{.mark}
    


Пример использования посетителя:


[val visitor = object : *GoRecursiveElementVisitor*() {\
override fun visitFunction(function: *GoFunctionDeclaration*) {\
println(\"Function found: \${function.name}\")\
super.visitFunction(function)\
}\
}\
\
psiFile.accept(visitor)]{.mark}


5. Работа с PSI для анализа кода #

Пример: Поиск всех вызовов определенной функции

Диаграмма последовательности:

{width=“5.770833333333333in” height=“2.5416666666666665in”}


[\@startuml\
title Поиск вызовов функции с использованием PSI\
\
participant \"IDE\" as IDE\
participant \"PSI Tree\" as PSITree\
participant \"Reference Resolver\" as Resolver\
\
IDE -\> PSITree : Обходит PSI дерево\
PSITree -\> Resolver : Проверяет ссылки на функцию\
Resolver -\> PSITree : Возвращает места вызова\
IDE -\> Dev : Отображает результаты поиска\
\
\@enduml]{.mark}


Код для поиска вызовов функции:

  -----------------------------------------------------------------------
[fun findFunctionUsages(project: *Project*, functionName: *String*):
*List*\<*PsiReference*\> {\
val searchScope = *GlobalSearchScope*.projectScope(project)\
val function =
*GoPsiFacade*.getInstance(project).findFunction(functionName,
searchScope) ?: return emptyList()\
return *ReferencesSearch*.search(function,
searchScope).findAll().toList()\
}]{.mark}


6. Расширение PSI: Добавление поддержки нового языка #

Диаграмма компонентов для поддержки нового языка:

{width=“7.5in” height=“4.3125in”}

Объяснение:

  • Для добавления поддержки нового языка необходимо реализовать:

    • Lexer: Лексический анализатор для разбивки исходного кода на токены.

    • Parser: Синтаксический анализатор для создания AST.

    • PSI Elements: Узлы PSI дерева для нового языка.

Дополнительные компоненты для подсветки синтаксиса, автодополнения и рефакторинга.


[\@startuml\
title Добавление поддержки нового языка в PSI\
\
component \"Lexer\" as NewLexer\
component \"Parser\" as NewParser\
component \"PSI Elements\" as NewPSIElements\
component \"PsiFile\" as NewPsiFile\
component \"Syntax Highlighting\" as SyntaxHighlighting\
component \"Code Completion\" as CodeCompletion\
component \"Refactoring Support\" as RefactoringSupport\
\
NewLexer \--\> NewParser\
NewParser \--\> NewPSIElements\
NewPSIElements \--\> NewPsiFile\
NewPSIElements \--\> SyntaxHighlighting\
NewPSIElements \--\> CodeCompletion\
NewPSIElements \--\> RefactoringSupport\
\
\@enduml]{.mark}


7. Взаимодействие PSI с другими компонентами IDE #

Диаграмма взаимодействия:

{width=“10.010416666666666in” height=“2.09375in”}

Объяснение:

  • PSI Tree является центральным компонентом, используемым различными инструментами IDE.

  • Компоненты для навигации, анализа кода, рефакторинга и другие взаимодействуют с PSI для предоставления своих функций.


    [\@startuml\
    title Взаимодействие PSI с другими компонентами IDE\
    \
    component \"PSI Tree\" as PSITree\
    component \"Code Editor\" as Editor\
    component \"Navigation\" as Navigation\
    component \"Code Analysis\" as Analysis\
    component \"Refactoring\" as Refactoring\
    component \"Code Completion\" as Completion\
    component \"Debugging\" as Debugging\
    \
    Editor \--\> PSITree\
    Navigation \--\> PSITree\
    Analysis \--\> PSITree\
    Refactoring \--\> PSITree\
    Completion \--\> PSITree\
    Debugging \--\> PSITree\
    \
    \@enduml]{.mark}
    


8. Модификация кода с использованием PSI #

Пример: Добавление новой функции в код

Код для добавления функции:


[fun addNewFunction(psiFile: *GoFile*, functionName: *String*,
parameters: *String*, body: *String*) {\
val factory = GoElementFactory.createFunction(psiFile.project, \"func
\$functionName(\$parameters) {\\n\$body\\n}\")\
psiFile.add(factory)\
}]{.mark}


Объяснение:

  • Используя GoElementFactory, можно создавать новые элементы кода.

  • Метод add добавляет новый элемент в PSI дерево, которое затем обновляет исходный код в редакторе.

9. Обработка ошибок и восстановление PSI дерева #

Диаграмма обработки ошибок:

{width=“8.520833333333334in” height=“2.84375in”}

Объяснение:

  • При парсинге кода могут возникать синтаксические ошибки.

  • Error Recovery позволяет парсеру продолжить работу и создать PSI дерево, несмотря на ошибки.

  • IDE может подсветить ошибки и предоставить рекомендации по их исправлению.


    [\@startuml\
    title Обработка ошибок при парсинге кода\
    \
    participant \"Lexer\" as Lexer\
    participant \"Parser\" as Parser\
    participant \"Error Recovery\" as ErrorRecovery\
    participant \"PSI Tree\" as PSITree\
    \
    Lexer -\> Parser : Передает токены\
    Parser -\> ErrorRecovery : Обнаруживает синтаксическую ошибку\
    ErrorRecovery -\> Parser : Применяет стратегию восстановления\
    Parser -\> PSITree : Создает PSI дерево с ошибками\
    PSITree -\> IDE : Отображает ошибки в коде\
    \
    \@enduml]{.mark}
    


10. Кэширование и производительность PSI #

Диаграмма кэширования PSI:

{width=“9.3125in” height=“4.96875in”}

Объяснение:

  • PSI Tree использует кэширование для хранения результатов анализа и ускорения доступа.

  • Indexing Service индексирует кэшированные данные для быстрого поиска.

  • Кэширование важно для производительности IDE, особенно при работе с большими проектами.


    [\@startuml\
    title Кэширование в PSI для повышения производительности\
    \
    component \"PSI Tree\" as PSITree\
    component \"File Manager\" as FileManager\
    component \"Indexing Service\" as Indexing\
    component \"Caches\" as Caches\
    \
    FileManager \--\> PSITree : Загружает файлы\
    PSITree \--\> Caches : Кэширует результаты анализа\
    Indexing \--\> Caches : Использует кэшированные данные\
    PSITree \--\> Indexing : Обновляет индексы при изменениях\
    IDE \--\> Caches : Получает данные из кэша для ускорения работы\
    \
    \@enduml]{.mark}
    


Заключение #

  • PSI является мощным инструментом внутри IDE JetBrains, обеспечивающим глубокое понимание и взаимодействие с исходным кодом.

  • Используя PSI, разработчики могут создавать расширения и инструменты для анализа, навигации и модификации кода.

  • Диаграммы, представленные выше, помогают визуализировать различные аспекты архитектуры PSI и ее взаимодействие с другими компонентами IDE.

Рекомендуемые шаги для дальнейшего изучения:

  • Изучение SDK JetBrains: Ознакомьтесь с официальной документацией по разработке плагинов и работе с PSI.

  • Практика разработки плагинов: Попробуйте создать простой плагин, использующий PSI для анализа или модификации кода.

  • Общение с сообществом: Присоединяйтесь к форумам и сообществам разработчиков плагинов для обмена опытом и знаниями.