pyltsin
Posted on March 15, 2022
Disclaimer: I don't work for JetBrains, so my code may have some errors and it is only an example.
Introduction
Every large company has its code style. And it is necessary to make people use it.
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)
So in our company, we have the rule, that every class which we use as a key in HashMap (HashCode) must override equals
and 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
- [x] Create a new [IntelliJ Platform Plugin Template][template] project.
- [ ] Verify the pluginGroup, plugin ID and sources package.
- [ ] Review the Legal Agreements.
- [ ] Publish a plugin manually for the first time.
- [ ] Set the Plugin ID in the above README badges.
- [ ] Set the Deployment Token.
- [ ] Click the Watch button on the top of the [IntelliJ Platform Plugin Template][template] to be notified about releases containing new features and fixes.
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>
The necessary method is buildVisitor
public PsiElementVisitor buildVisitor(@NotNull final ProblemsHolder holder,
final boolean isOnTheFly,
@NotNull LocalInspectionToolSession session)
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)
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:
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)
}
}
IDEA contains a lot of useful utils classes, methods, constants, for example, PsiReferenceUtil
, PsiTypesUtil
.
Firstly, I need to check if this leaf implements HashMap
or HashSet
expression.classOrAnonymousClassReference?.qualifiedName
in (JAVA_UTIL_HASH_MAP, JAVA_UTIL_HASH_SET)
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]
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) }
}
Finally, I have to register the problem:
holder.registerProblem(
expression,
"hashCode is not overriden"
)
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));
In this case we work with PsiMethodCallExpression
override fun visitMethodCallExpression(expression: PsiMethodCallExpression?)
For the first check CallMatcher is useful:
val matcher = CallMatcher.instanceCall(JAVA_UTIL_STREAM_STREAM, "collect")
val isCollect = matcher.matches(expression)
After that, Collectors.toMap() must be checked:
val collectorExpression = expression.argumentList.expressions[0]
val isToMap = CallMatcher.staticCall(JAVA_UTIL_STREAM_COLLECTORS, "toMap")
.matches(collectorExpression)
To get class the next method can be applied:
val psiType = expression.methodExpression.type.parameters[0]
After that, we can reuse our previous code to check overriden equals and hashCode.
For tests, Idea has LightJavaInspectionTestCase
class. (examples)
Finally, our result:
Of course, you can simply enhance these ideas for other cases.
It is really important to get to know your tools!
Posted on March 15, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.