DEV Community

pyltsin
pyltsin

Posted on • Originally published at habr.com

How to create a new inspecion for IDEA

Disclaimer: I don't work for JetBrains, so my code may have some errors and it is only an example.

Image description

Introduction

Every large company has its code style. And it is necessary to make people use it.

Image description

That's why all checks should be automated (ideally, with CI). For me, the most useful tool is your IDE. We are going to write a simple inspection for IDEA (might be the most popular IDE for Java)

Every IDEA is a base part and a set of plugins for it. Almost every feature can be considered as a plugin, so adding a new one is easy;

Technical specification

One of the most popular questions in a technical interview is about HashMap and equals and hashCode (links for hashCode, HashMap)

Image description

So in our company, we have the rule, that every class which we use as a key in HashMap (HashCode) must override equalsand hashCode. Unfortunately, IDEA doesn't contain an inspection for that (issue), but we can fix it!

We are going to support the next expressions:

  • new HashMap<>();
  • new HashSet<>();
  • .collect(Collectors.toSet());
  • .collect(Collectors.toMap());

Where do we begin?

The start points are:

Now we have a great template , which contains a useful todo list

Template ToDo list

Also, there are several simple examples, which help us to figure out how all is working.

Implementation

The source code of the plugin is here.

Every plugin for IDEA must contain a file resources/META-INF/plugin.xml, which describes used extension points. if we want to create a new inspection, we have to use localInspection and implement LocalInspectionTool or AbstractBaseJavaLocalInspectionTool for Java)

<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>
Enter fullscreen mode Exit fullscreen mode

The necessary method is buildVisitor

public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder,
                                        final boolean isOnTheFly, 
                                        @NotNull LocalInspectionToolSession session)
Enter fullscreen mode Exit fullscreen mode

It is an example of Visitor pattern

To work with your code, IDEA creates PSI Tree (PSI - Program Structure Interface). You can see this tree, using PSI Viewer (Tools->View PSI Structure)

Image description

If you want to read more about PSI you have to use the documentation

Our visitor will go through all leaves of the tree and decide if it contains something wrong or not.
Let's start with an easy case:

Image description

The leaf new HashMap<>() matches PsiNewExpression, so we have to override this method:

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

Enter fullscreen mode Exit fullscreen mode

IDEA contains a lot of useful utils classes, methods, constants, for example, PsiReferenceUtil, PsiTypesUtil.

Firstly, I need to check if this leaf implements HashMapor HashSet

expression.classOrAnonymousClassReference?.qualifiedName 
in (JAVA_UTIL_HASH_MAP, JAVA_UTIL_HASH_SET)
Enter fullscreen mode Exit fullscreen mode

JAVA_UTIL_HASH_MAP, JAVA_UTIL_HASH_SET- String constants from com.intellij.psi.CommonClassNames. This class contains almost all important names.

Secondly, I need to get a class of the key. Thank IDEA, it has already been done for us.

val keyType: PsiType = 
expression.classOrAnonymousClassReference?.parameterList?.typeArguments[0]
Enter fullscreen mode Exit fullscreen mode

Then I need to check this class if it overrides hashCode and equals method:

private fun hasOverrideHashCode(psiType: PsiType): Boolean {
  // get PsiClass
  val psiClass = PsiTypesUtil.getPsiClass(psiType) 
   // get similar methods by name
  val methods: Array<PsiMethod> =
  psiClass?.findMethodsByName(HardcodedMethodConstants.HASH_CODE, false) ?: arrayOf()
  // check, if it is hashCode
  return methods.any { MethodUtils.isHashCode(it) }
}
Enter fullscreen mode Exit fullscreen mode

Finally, I have to register the problem:

holder.registerProblem(
        expression,
        "hashCode is not overriden"
)
Enter fullscreen mode Exit fullscreen mode

Now let's consider an example with Stream:

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

Image description

In this case we work with PsiMethodCallExpression

override fun visitMethodCallExpression(expression: PsiMethodCallExpression?)
Enter fullscreen mode Exit fullscreen mode

For the first check CallMatcher is useful:

val matcher = CallMatcher.instanceCall(JAVA_UTIL_STREAM_STREAM, "collect")
val isCollect = matcher.matches(expression)
Enter fullscreen mode Exit fullscreen mode

After that, Collectors.toMap() must be checked:

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

To get class the next method can be applied:

val psiType = expression.methodExpression.type.parameters[0]
Enter fullscreen mode Exit fullscreen mode

After that, we can reuse our previous code to check overriden equals and hashCode.

For tests, Idea has LightJavaInspectionTestCaseclass. (examples)

Finally, our result:

Image description

Of course, you can simply enhance these ideas for other cases.

It is really important to get to know your tools!

Top comments (0)