Как стать автором
Обновить

Создаем свою инспекцию для IDEA

Время на прочтение5 мин
Количество просмотров4K

Disclaimer: я не являюсь сотрудником JetBrains (а жаль), поэтому код может не являться оптимальным и служит только для примера.

Введение

Кому не интересна вводная часть - можно сразу перейти к настройке проекта.

У каждой крупной организации со временем формируется набор правил по оформлению кода, который, в лучшем случае, фиксируется в Code Style (зачем он нужен можно прочитать здесь). Но самое сложное не написать свод правил, а заставить всех им следовать.

Поэтому в идеале все проверки должны быть автоматизированы. Для этого уже много что придумано, но самым удобным, как мне кажется, остаются средства проверки IDE. По результатам опроса наиболее популярной IDE для Java-разработчиков является IDEA от JetBrains.

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

Краткое ТЗ

Самая популярная коллекция (или точнее - почти коллекция) на собеседованиях - Map, самая популярная реализация - HashMap. И коронным вопросом является вопрос про equals и hashCode (почитать можно тут, тут, а лучше конечно в самой документации 1, 2).

Поэтому у нас в организации есть правило, что у класса, который используется как ключ для HashMap, эти методы должны быть переопределены (явно или неявно - например, с помощью Lombok). К сожалению, в IDEA это не добавили - вот issue. Но для простых случаев это не сложно сделать самим.

Будем поддерживать только следующие выражения:

  • new HashMap<>();

  • new HashSet<>();

  • .collect(Collectors.toSet());

  • .collect(Collectors.toMap());

С чего начать?

Главные отправные точки:

Так же можно посмотреть различные видео, например: habr, youtube канал JetBrains

На данный момент самым удобным способом создания каркаса плагина является - шаблон. После клонирования выполняется несколько предустановленных pipeline и на выходе мы получаем хорошую заготовку с шагами, которые необходимо выполнить.

## Template ToDo list
- [x] Create a new [IntelliJ Platform Plugin Template][template] project.
- [ ] Verify the [pluginGroup](/gradle.properties), [plugin ID](/src/main/resources/META-INF/plugin.xml) and [sources package](/src/main/kotlin).
- [ ] Review the [Legal Agreements](https://plugins.jetbrains.com/docs/marketplace/legal-agreements.html).
- [ ] [Publish a plugin manually](https://plugins.jetbrains.com/docs/intellij/publishing-plugin.html?from=IJPluginTemplate) for the first time.
- [ ] Set the Plugin ID in the above README badges.
- [ ] Set the [Deployment Token](https://plugins.jetbrains.com/docs/marketplace/plugin-upload.html).
- [ ] Click the <kbd>Watch</kbd> button on the top of the [IntelliJ Platform Plugin Template][template] to be notified about releases containing new features and fixes.

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

Писать код будем на Kotlin.

Реализация

Исходный код можно посмотреть здесь.

Плагин для IDEA должен содержать файл resources/META-INF/plugin.xml, в котором описывается какие действия и расширения он предоставляет. Допустимых точек расширения очень много - около 1000. Если посмотреть в примерах и в коде IDEA, то для реализации инспекции нам нужно реализовать localInspection (имплементацию класса LocalInspectionTool, для языка Java есть абстрактный класс AbstractBaseJavaLocalInspectionTool).

<extensions defaultExtensionNs="com.intellij">
  <localInspection language="JAVA"
                   displayName="Sniffer: Using HashMap with default hashcode"
                   groupPath="Java"
                   groupBundle="messages.SnifferInspectionsBundle"
                   groupKey="group.names.sniffer.probable.bugs"
                   enabledByDefault="true"
                   level="WEAK WARNING"
                   implementationClass="com.github.pyltsin.sniffer.EqualsHashCodeOverrideInspection"/>
</extensions>

Для реализации проверки я воспользуюсь методом

  public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder,
                                        final boolean isOnTheFly, 
                                        @NotNull LocalInspectionToolSession session)

который реализует паттер Visitor.

IDEA для кода строит дерево (PSI Tree, PSI - Program Structure Interface). Для того чтобы его увидеть можно воспользоваться PSI Viewer (Tools->View PSI Structure)

Наш Visitor будет посещать все узлы этого дерева и, если определит, что что-то не так, зарегистрирует проблему. В классе JavaElementVisitor уже есть весь список узлов, которые можно анализировать. Вот схема как парсится и строится это дерево из документации:

Мы работаем с PSI Tree, который является результатом этого процесса.

Сначала рассмотрим простой случай:

Узел дерева new HashMap<>() соответствует PsiNewExpression и поэтому в нашем Visitor нужно переопределить метод visitNewExpression:

    override fun buildVisitor(
        holder: ProblemsHolder,
        isOnTheFly: Boolean,
        session: LocalInspectionToolSession
    ): PsiElementVisitor {
        return object : JavaElementVisitor() {
            override fun visitNewExpression(expression: PsiNewExpression?) {
                super.visitNewExpression(expression)
            }
        }

А дальше нужно смотреть, какие методы есть у класса PsiNewExpression. Почти наверняка PSI-классы содержат все, что вам нужно. А если чего-то и нет, то стоит обратить внимание на утильные классы (например, PsiReferenceUtil, PsiTypesUtil). Сама IDEA содержит огромное число примеров кода.

Первое, что нужно определить, что этот узел ссылается на классы HashMap или HashSet:

expression.classOrAnonymousClassReference?.qualifiedName 
in (JAVA_UTIL_HASH_MAP, JAVA_UTIL_HASH_SET)

JAVA_UTIL_HASH_MAP, JAVA_UTIL_HASH_SET - String-константы из com.intellij.psi.CommonClassNames. Он содержит список наиболее используемых Java-классов.

Дальше нам нужно получить класс ключа. Спасибо IDEA, она для нас уже нашла это значение (даже для diamond-оператора - <>)

val keyType: PsiType = 
expression.classOrAnonymousClassReference?.parameterList?.typeArguments[0]

Осталось проверить, что класс, на который ссылается PsiType, содержит переопределенный hashCode метод. Я нашел следующий путь (возможно есть более оптимальный):

private fun hasOverrideHashCode(psiType: PsiType): Boolean {
  // получаем PsiClass (представление класса)
  val psiClass = PsiTypesUtil.getPsiClass(psiType) 
   // получаем список похожих методов
  val methods: Array<PsiMethod> =
  psiClass?.findMethodsByName(HardcodedMethodConstants.HASH_CODE, false) ?: arrayOf()
  // проверяем, если есть hashCode
  return methods.any { MethodUtils.isHashCode(it) }
}

C equals поступаем аналогично.

И если все плохо - осталось только зарегистрировать проблему:

holder.registerProblem(
		expression,
		"Все плохо, гипс сняли, hashCode не переопределили"
)

Теперь разберемся со Stream. Например, есть вот такой код:

Map<Clazz2, Clazz2> collect1 = Stream.of(new Clazz2(), new Clazz2())
.collect(Collectors.toMap(t -> t, t -> t));

В этом случае мы работаем с PsiMethodCallExpression.

override fun visitMethodCallExpression(expression: PsiMethodCallExpression?)

Для первичной проверки можно воспользоваться CallMatcher (удобное средство для проверки совпадения методов):

val matcher = CallMatcher.instanceCall(JAVA_UTIL_STREAM_STREAM, "collect")
val isCollect = matcher.matches(expression)

После этого можно проверить Collectors.toMap():

val collectorExpression = expression.argumentList.expressions[0]
val isToMap = CallMatcher.staticCall(JAVA_UTIL_STREAM_COLLECTORS, "toMap")
  .matches(collectorExpression)

Получаем первый generic для этого выражения:

val psiType = expression.methodExpression.type.parameters[0]

А дальше пользуемся уже реализованными функциями проверки переопределения equals и hashCode.

Для тестирования можно воспользоваться классом LightJavaInspectionTestCase. Примеры можно посмотреть в исходном коде данного плагина или в исходном коде IDEA. Стоит обратить внимание, что для функционирования тестов, нужен приложенный набор стандартных библиотек - папки mockJDK в IDEA.

И, наконец, сам результат

Этот пример покрывает не все места создания HashMap (HashSet) в стандартной библиотеке, но его можно быстро расширить на ваши случаи.

Краткий вывод

IDEA очень гибкий и удобный инструмент для разработки, который можно быстро расширить требуемыми функциями

Ссылки

Теги:
Хабы:
Всего голосов 14: ↑14 и ↓0+14
Комментарии0

Публикации

Истории

Работа

Java разработчик
356 вакансий

Ближайшие события

7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань