MultiplePathConstructionsInspection: added inspection (IDEA-288529)

Reports multiple Path.of() or Paths.get() calls in a row with same expression

GitOrigin-RevId: 654825f7946243d0fd58ed2d2d4d577305ad0da5
This commit is contained in:
Artemiy Sartakov
2022-02-11 13:26:46 +07:00
committed by intellij-monorepo-bot
parent 11746b4456
commit 9dcbd71cc6
20 changed files with 430 additions and 1 deletions

View File

@@ -0,0 +1,194 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.codeInspection;
import com.intellij.codeInsight.daemon.impl.analysis.HighlightControlFlowUtil;
import com.intellij.codeInspection.duplicateExpressions.ExpressionHashingStrategy;
import com.intellij.java.JavaBundle;
import com.intellij.openapi.project.Project;
import com.intellij.psi.*;
import com.intellij.psi.codeStyle.JavaCodeStyleManager;
import com.intellij.psi.codeStyle.SuggestedNameInfo;
import com.intellij.psi.util.PsiTreeUtil;
import com.intellij.psi.util.PsiUtil;
import com.intellij.util.CommonJavaRefactoringUtil;
import com.intellij.util.ObjectUtils;
import com.intellij.util.SmartList;
import com.intellij.util.containers.CollectionFactory;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.HashingStrategy;
import com.siyeh.ig.callMatcher.CallMatcher;
import com.siyeh.ig.psiutils.CommentTracker;
import com.siyeh.ig.psiutils.ExpressionUtils;
import com.siyeh.ig.psiutils.HighlightUtils;
import com.siyeh.ig.psiutils.TypeUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.List;
import java.util.Map;
public class MultiplePathConstructionsInspection extends AbstractBaseJavaLocalInspectionTool {
private static final CallMatcher PATH_CONSTRUCTION_CALL = CallMatcher.anyOf(
CallMatcher.staticCall("java.nio.file.Path", "of"),
CallMatcher.staticCall("java.nio.file.Paths", "get")
);
@Override
public @NotNull PsiElementVisitor buildVisitor(@NotNull ProblemsHolder holder, boolean isOnTheFly) {
if (!PsiUtil.isLanguageLevel7OrHigher(holder.getFile())) return PsiElementVisitor.EMPTY_VISITOR;
return new JavaElementVisitor() {
@Override
public void visitMethod(PsiMethod method) {
super.visitMethod(method);
PsiCodeBlock methodBody = method.getBody();
if (methodBody == null) return;
PathCallsCollector pathCallsCollector = new PathCallsCollector();
methodBody.accept(pathCallsCollector);
pathCallsCollector.getCalls().forEach((args, calls) -> {
if (calls.size() < 2 || !isEffectivelyFinal(methodBody, args)) return;
calls.forEach(call -> holder.registerProblem(call,
JavaBundle.message("inspection.multiple.path.constructions.description"),
new ReplaceWithPathVariableFix(isOnTheFly)));
});
}
};
}
private static boolean isEffectivelyFinal(@NotNull PsiElement context, @NotNull PsiExpressionList expressionList) {
return ContainerUtil.and(expressionList.getExpressions(), e -> isEffectivelyFinal(context, e));
}
private static boolean isEffectivelyFinal(@NotNull PsiElement context, PsiExpression expression) {
return ExpressionUtils.nonStructuralChildren(expression).allMatch(c -> isEffectivelyFinal(context, expression, c));
}
private static boolean isEffectivelyFinal(PsiElement context, PsiExpression parent, PsiExpression child) {
if (child != parent) return isEffectivelyFinal(context, child);
if (child instanceof PsiLiteralExpression) return true;
if (child instanceof PsiPolyadicExpression) {
return ContainerUtil.and(((PsiPolyadicExpression)child).getOperands(), e -> isEffectivelyFinal(context, e));
}
if (!(child instanceof PsiReferenceExpression)) return false;
PsiVariable target = ObjectUtils.tryCast(((PsiReferenceExpression)child).resolve(), PsiVariable.class);
if (!PsiUtil.isJvmLocalVariable(target)) return false;
return HighlightControlFlowUtil.isEffectivelyFinal(target, context, null);
}
private static class PathCallsCollector extends JavaRecursiveElementWalkingVisitor {
private final Map<PsiExpressionList, List<PsiMethodCallExpression>> myCalls =
CollectionFactory.createCustomHashingStrategyMap(new ExpressionListHashingStrategy());
private static class ExpressionListHashingStrategy implements HashingStrategy<PsiExpressionList> {
private static final ExpressionHashingStrategy EXPRESSION_STRATEGY = new ExpressionHashingStrategy();
@Override
public int hashCode(PsiExpressionList list) {
if (list == null) return 0;
int hash = 0;
for (PsiExpression expression : list.getExpressions()) {
hash = hash * 31 + EXPRESSION_STRATEGY.hashCode(expression);
}
return hash;
}
@Override
public boolean equals(PsiExpressionList l1, PsiExpressionList l2) {
if (l1 == null || l2 == null) return l1 == l2;
PsiExpression[] e1 = l1.getExpressions();
PsiExpression[] e2 = l2.getExpressions();
if (e1.length != e2.length) return false;
for (int i = 0; i < e1.length; i++) {
if (!EXPRESSION_STRATEGY.equals(e1[i], e2[i])) return false;
}
return true;
}
}
@Override
public void visitMethodCallExpression(PsiMethodCallExpression expression) {
super.visitMethodCallExpression(expression);
if (!PATH_CONSTRUCTION_CALL.test(expression)) return;
PsiExpressionList args = expression.getArgumentList();
List<PsiMethodCallExpression> calls = myCalls.computeIfAbsent(args, k -> new SmartList<>());
calls.add(expression);
}
private Map<PsiExpressionList, List<PsiMethodCallExpression>> getCalls() {
return myCalls;
}
}
private static class ReplaceWithPathVariableFix implements LocalQuickFix {
private final boolean myIsOnTheFly;
private ReplaceWithPathVariableFix(boolean isOnTheFly) {
myIsOnTheFly = isOnTheFly;
}
@Override
public @NotNull String getFamilyName() {
return JavaBundle.message("inspection.extract.to.path.variable.fix.family.name");
}
@Override
public void applyFix(@NotNull Project project, @NotNull ProblemDescriptor descriptor) {
PsiMethodCallExpression call = ObjectUtils.tryCast(descriptor.getPsiElement(), PsiMethodCallExpression.class);
if (call == null) return;
PsiMethod containingMethod = PsiTreeUtil.getParentOfType(call, PsiMethod.class);
if (containingMethod == null) return;
PsiCodeBlock methodBody = containingMethod.getBody();
if (methodBody == null) return;
if (!PATH_CONSTRUCTION_CALL.test(call)) return;
PsiExpressionList args = call.getArgumentList();
PathCallsCollector callsCollector = new PathCallsCollector();
methodBody.accept(callsCollector);
List<PsiMethodCallExpression> argCalls = callsCollector.getCalls().get(args);
if (argCalls == null) return;
PsiExpression[] occurrences = argCalls.toArray(PsiExpression.EMPTY_ARRAY);
if (occurrences.length < 2) return;
PsiElement anchor = CommonJavaRefactoringUtil.getAnchorElementForMultipleExpressions(occurrences, containingMethod);
if (anchor == null) return;
PsiElement parent = anchor.getParent();
if (parent == null) return;
PsiDeclarationStatement declaration = createDeclaration(methodBody, call, anchor);
if (declaration == null) return;
declaration = (PsiDeclarationStatement)parent.addBefore(declaration, anchor);
JavaCodeStyleManager codeStyleManager = JavaCodeStyleManager.getInstance(parent.getProject());
declaration = (PsiDeclarationStatement)codeStyleManager.shortenClassReferences(declaration);
PsiVariable pathVar = ObjectUtils.tryCast(declaration.getDeclaredElements()[0], PsiVariable.class);
if (pathVar == null) return;
String pathVarName = pathVar.getName();
if (pathVarName == null) return;
PsiReference[] refs = new PsiReference[occurrences.length];
for (int i = 0; i < occurrences.length; i++) {
PsiExpression occurrence = occurrences[i];
refs[i] = (PsiReference)new CommentTracker().replaceAndRestoreComments(occurrence, pathVarName);
}
if (!myIsOnTheFly) return;
HighlightUtils.showRenameTemplate(containingMethod, pathVar, refs);
}
private static @Nullable PsiDeclarationStatement createDeclaration(@NotNull PsiCodeBlock block,
@NotNull PsiMethodCallExpression toPathCall,
@NotNull PsiElement anchor) {
PsiClassType type = TypeUtils.getType("java.nio.file.Path", block);
String varName = getSuggestedName(type, toPathCall, anchor);
if (varName == null) return null;
PsiElementFactory elementFactory = PsiElementFactory.getInstance(block.getProject());
return elementFactory.createVariableDeclarationStatement(varName, type, toPathCall);
}
private static @Nullable String getSuggestedName(@NotNull PsiType type,
@NotNull PsiExpression initializer,
@NotNull PsiElement anchor) {
SuggestedNameInfo nameInfo = CommonJavaRefactoringUtil.getSuggestedName(type, initializer, anchor);
String[] names = nameInfo.names;
return names.length == 0 ? null : names[0];
}
}
}

View File

@@ -1878,6 +1878,11 @@
groupKey="group.names.performance.issues" groupBundle="messages.InspectionsBundle"
enabledByDefault="true" level="WARNING"
implementationClass="com.intellij.codeInspection.BulkFileAttributesReadInspection"/>
<localInspection groupPath="Java" language="JAVA" shortName="MultiplePathConstructions"
key="inspection.multiple.path.constructions.description" bundle="messages.JavaBundle"
groupKey="group.names.performance.issues" groupBundle="messages.InspectionsBundle"
enabledByDefault="true" level="WEAK WARNING"
implementationClass="com.intellij.codeInspection.MultiplePathConstructionsInspection"/>
<globalInspection groupPath="Java" language="JAVA" shortName="EmptyMethod" groupKey="group.names.declaration.redundancy" enabledByDefault="true" groupBundle="messages.InspectionsBundle"
level="WARNING" implementationClass="com.intellij.codeInspection.emptyMethod.EmptyMethodInspection"
key="inspection.empty.method.display.name" bundle="messages.JavaBundle"/>

View File

@@ -12,7 +12,7 @@ import org.jetbrains.annotations.Nullable;
/**
* @author Pavel.Dolgov
*/
final class ExpressionHashingStrategy implements HashingStrategy<PsiExpression> {
public final class ExpressionHashingStrategy implements HashingStrategy<PsiExpression> {
private static final EquivalenceChecker EQUIVALENCE_CHECKER = new NoSideEffectExpressionEquivalenceChecker();
@Override

View File

@@ -0,0 +1,32 @@
<html>
<body>
Reports multiple <code>java.nio.file.Path</code> constructions <code>java.nio.file.Paths.get</code> or <code>java.nio.file.Path.of</code>
in a row when it is possible to replace them with a single <code>java.nio.file.Path</code> variable.
<p>Example:</p>
<pre><code>
if (Files.isRegularFile(Path.of(fileName))) {
try(InputStream is = Files.newInputStream(Path.of(fileName))) {
// some code
}
catch (IOException e) {
throw new UncheckedIOException(e);
}
}
</code></pre>
<p>After the quick-fix is applied:</p>
<pre><code>
Path path = Path.of(fileName);
if (Files.isRegularFile(path)) {
try(InputStream is = Files.newInputStream(path)) {
// some code
}
catch (IOException e) {
throw new UncheckedIOException(e);
}
}
</code></pre>
<!-- tooltip end -->
<p>This inspection only reports if the language level of the project or module is 7 or higher.</p>
<p><small>New in 2022.1</small></p>
</body>
</html>

View File

@@ -0,0 +1,13 @@
// "Extract 'java.nio.file.Path' constructions to variable" "true"
import java.io.*;
import java.nio.file.*;
class Foo {
public boolean isRelativeDirectory() {
String fileName = "foo";
Path of = Path.of(fileName);
if (of.isAbsolute()) return false;
System.out.println(fileName);
return Files.isDirectory(of);
}
}

View File

@@ -0,0 +1,13 @@
// "Extract 'java.nio.file.Path' constructions to variable" "true"
import java.io.*;
import java.nio.file.*;
class Foo {
public boolean isRelativeDirectory(String bar) {
String fileName = "foo";
Path of = Path.of(fileName, bar + "baz");
if (of.isAbsolute()) return false;
System.out.println(fileName);
return Files.isDirectory(of);
}
}

View File

@@ -0,0 +1,11 @@
// "Extract 'java.nio.file.Path' constructions to variable" "true"
import java.io.*;
import java.nio.file.*;
class Foo {
public boolean isRelativeDirectory(String fileName) {
Path of = Path.of(fileName);
if (of.isAbsolute()) return false;
return Files.isDirectory(of);
}
}

View File

@@ -0,0 +1,14 @@
// "Extract 'java.nio.file.Path' constructions to variable" "true"
import java.io.*;
import java.nio.file.*;
class Foo {
public boolean isRelativeDirectory(String start, String end) {
Path of = Path.of(start.length() > 2 ? start + end : "baz");
if (of.isAbsolute()) return false;
/*1*/
/*2*/
/*3*/
return Files.isDirectory(/*0*/of);
}
}

View File

@@ -0,0 +1,9 @@
// "Extract 'java.nio.file.Path' constructions to variable" "false"
import java.io.*;
import java.nio.file.*;
class Foo {
public boolean isDirectory(String fileName) {
return Files.isDirectory(Path.of(fileNa<caret>me));
}
}

View File

@@ -0,0 +1,14 @@
// "Extract 'java.nio.file.Path' constructions to variable" "false"
import java.io.*;
import java.nio.file.*;
class Foo {
public boolean isRelativeDirectory(boolean b) {
{
String fileName = "foo";
if (Path.of(fil<caret>eName).isAbsolute()) return false;
}
String fileName = "bar";
return Files.isDirectory(Path.of(fileName));
}
}

View File

@@ -0,0 +1,12 @@
// "Extract 'java.nio.file.Path' constructions to variable" "true"
import java.io.*;
import java.nio.file.*;
class Foo {
public boolean isRelativeDirectory() {
String fileName = "foo";
if (Path.of(fileName<caret>).isAbsolute()) return false;
System.out.println(fileName);
return Files.isDirectory(Path.of(fileName));
}
}

View File

@@ -0,0 +1,11 @@
// "Extract 'java.nio.file.Path' constructions to variable" "false"
import java.io.*;
import java.nio.file.*;
class Foo {
public boolean isRelativeDirectory(String start, String end) {
if (Path.of((start.<caret>length() > 2 ? (start + end) : ("baz"))).isAbsolute()) return false;
if (end.length() > 2) end = "fo";
return Files.isDirectory(Path.of((start.length() > 2 ? (start + end) : ("baz"))));
}
}

View File

@@ -0,0 +1,12 @@
// "Extract 'java.nio.file.Path' constructions to variable" "false"
import java.io.*;
import java.nio.file.*;
class Foo {
public boolean isRelativeDirectory(boolean b) {
String fileName = "foo";
if (Path.of(fileName<caret>).isAbsolute()) return false;
if (b) fileName = "baz";
return Files.isDirectory(Path.of(fileName));
}
}

View File

@@ -0,0 +1,13 @@
// "Extract 'java.nio.file.Path' constructions to variable" "false"
import java.io.*;
import java.nio.file.*;
class Foo {
public boolean isRelativeDirectory(boolean b, String bar) {
String fileName = "foo";
if (Path.of(fileName<caret>, b ? bar + "baz" : "baz").isAbsolute()) return false;
System.out.println(fileName);
bar = "bar";
return Files.isDirectory(Path.of(fileName, b ? bar + "baz" : "baz"));
}
}

View File

@@ -0,0 +1,12 @@
// "Extract 'java.nio.file.Path' constructions to variable" "true"
import java.io.*;
import java.nio.file.*;
class Foo {
public boolean isRelativeDirectory(String bar) {
String fileName = "foo";
if (Path.of(fileName<caret>, bar + "baz").isAbsolute()) return false;
System.out.println(fileName);
return Files.isDirectory(Path.of(fileName, bar + "baz"));
}
}

View File

@@ -0,0 +1,9 @@
// "Extract 'java.nio.file.Path' constructions to variable" "false"
import java.io.*;
import java.nio.file.*;
class Foo {
public boolean isDirectory(String fileName) {
return Files.isDirectory(Paths.get(fileNa<caret>me));
}
}

View File

@@ -0,0 +1,10 @@
// "Extract 'java.nio.file.Path' constructions to variable" "true"
import java.io.*;
import java.nio.file.*;
class Foo {
public boolean isRelativeDirectory(String fileName) {
if (Path.of(fileName<caret>).isAbsolute()) return false;
return Files.isDirectory(Paths.get(fileName));
}
}

View File

@@ -0,0 +1,10 @@
// "Extract 'java.nio.file.Path' constructions to variable" "true"
import java.io.*;
import java.nio.file.*;
class Foo {
public boolean isRelativeDirectory(String start, String end) {
if (Path.of(start.le<caret>ngth() > 2 ? start + end : "baz").isAbsolute()) return false;
return Files.isDirectory(/*0*/Paths.get(((((start/*1*/).length()) > 2) ? ((start)/*2*/ + end) : ("baz"))/*3*/));
}
}

View File

@@ -0,0 +1,33 @@
// Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.java.codeInspection;
import com.intellij.codeInsight.daemon.quickFix.LightQuickFixParameterizedTestCase;
import com.intellij.codeInspection.LocalInspectionTool;
import com.intellij.codeInspection.MultiplePathConstructionsInspection;
import com.intellij.openapi.projectRoots.Sdk;
import com.intellij.pom.java.LanguageLevel;
import com.intellij.testFramework.IdeaTestUtil;
import org.jetbrains.annotations.NotNull;
public class MultiplePathConstructionsInspectionTest extends LightQuickFixParameterizedTestCase {
@Override
protected LocalInspectionTool @NotNull [] configureLocalInspectionTools() {
return new LocalInspectionTool[]{new MultiplePathConstructionsInspection()};
}
@Override
protected Sdk getProjectJDK() {
return IdeaTestUtil.getMockJdk11();
}
@Override
protected LanguageLevel getLanguageLevel() {
return LanguageLevel.JDK_11;
}
@Override
protected String getBasePath() {
return "/codeInsight/daemonCodeAnalyzer/quickFix/multiplePathConstructions";
}
}

View File

@@ -1725,6 +1725,8 @@ inspection.output.stream.constructor.message='OutputStream' can be constructed u
inspection.bulk.file.attributes.read.description=Bulk 'Files.readAttributes' call can be used instead of multiple file attribute calls
inspection.replace.with.bulk.file.attributes.read.fix.family.name=Replace with bulk 'Files.readAttributes' call
inspection.bulk.file.attributes.read.message=Multiple file attribute calls can be replaced with single 'Files.readAttributes' call
inspection.multiple.path.constructions.description=Multiple 'java.nio.file.Path' constructions can be replaced with a single variable
inspection.extract.to.path.variable.fix.family.name=Extract 'java.nio.file.Path' constructions to variable
external.annotations.problem.title=Unable to read external annotations
external.annotations.problem.parse.error=File: {0}<br>Problem: {1}
external.annotations.open.file=Open annotations file