mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-04-20 13:31:28 +07:00
IDEA-380160 java: Add an inspection for Math.clamp()
For what it should have been, the final code feels a bit over-engineered. GitOrigin-RevId: 893e1fb82670568ef3fbf5106b132e4891b276cf
This commit is contained in:
committed by
intellij-monorepo-bot
parent
45b4219faf
commit
7dd10b82b1
@@ -654,3 +654,5 @@ inspection.unreachable.catch.name=Unreachable catch section
|
||||
inspection.unreachable.catch.message=Unreachable section: {1, choice, 0#exception|2#exceptions} ''{0}'' {1, choice, 0#has|2#have} already been caught
|
||||
module.service.unused=Service interface provided but not exported or used
|
||||
module.ambiguous=Ambiguous module reference: {0}
|
||||
math.clamp.migration.brave.mode=Warn even if the lower bound could be higher than the upper bound
|
||||
math.clamp.migration.do.not.report.ambiguous=Do not report ambiguous clamp conversions
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
<html>
|
||||
<body>
|
||||
Reports combinations of <code>Math.max()</code> and <code>Math.min()</code> that are equivalent to <code>Math.clamp()</code>.
|
||||
|
||||
<p>
|
||||
Example:
|
||||
</p>
|
||||
<pre><code>
|
||||
int min = 5, max = 10;
|
||||
/*before*/ input = Math.max(Math.min(max, input), min);
|
||||
/*after*/ input = Math.clamp(input, min, max);
|
||||
</code></pre>
|
||||
<!-- tooltip end -->
|
||||
|
||||
<p>
|
||||
By default, the inspection only shows when it can statically prove that <code>min</code> is lesser or equal than <code>max</code>.
|
||||
This is done to avoid any <code>IllegalArgumentException</code> due to semantics changes.
|
||||
If you want the inspection to show up nonetheless, you can enable the <b>Warn even if the lower bound could be higher than the upper
|
||||
bound</b> option.
|
||||
</p>
|
||||
|
||||
<p><small>New in 2026.1</small></p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -2390,6 +2390,8 @@ inspection.deconstruction.can.be.used.fix.family.name=Replace with record patter
|
||||
markdown.documentation.comments.migration.display.name=Javadoc comment can be Markdown documentation comment
|
||||
markdown.documentation.comments.migration.fix=Convert to Markdown documentation comment
|
||||
|
||||
inspection.clamp.migration.display.name=Clamp function can be used
|
||||
|
||||
array.hash.code.fix.family.name=Replace with 'Arrays.hashCode()' call
|
||||
objects.hash.fix.family.name=Wrap with 'Arrays.hashCode()'
|
||||
unqualified.static.access.fix.family.name=Qualify static access
|
||||
|
||||
@@ -222,6 +222,26 @@ public final class HardcodedContracts {
|
||||
staticCall(JAVA_LANG_INTEGER, "min").parameterTypes("int", "int"),
|
||||
staticCall(JAVA_LANG_LONG, "min").parameterTypes("long", "long")),
|
||||
(call, paramCount) -> mathMinMax(false))
|
||||
.register(staticCall(JAVA_LANG_MATH, "clamp").parameterTypes("long", "long", "long"),
|
||||
ContractProvider.of(
|
||||
singleConditionContract(ContractValue.argument(1), RelationType.GT, ContractValue.argument(2), fail()),
|
||||
singleConditionContract(ContractValue.argument(0), RelationType.LT, ContractValue.argument(1), returnParameter(1)),
|
||||
singleConditionContract(ContractValue.argument(0), RelationType.GT, ContractValue.argument(2), returnParameter(2)),
|
||||
trivialContract(returnParameter(0))
|
||||
))
|
||||
.register(anyOf(
|
||||
staticCall(JAVA_LANG_MATH, "clamp").parameterTypes("float", "float", "float"),
|
||||
staticCall(JAVA_LANG_MATH, "clamp").parameterTypes("double", "double", "double")
|
||||
),
|
||||
ContractProvider.of(
|
||||
failIfParameterIsNaN(1),
|
||||
failIfParameterIsNaN(2),
|
||||
/* Note that this does not cover the +0.0f -0.0f case */
|
||||
singleConditionContract(ContractValue.argument(1), RelationType.GT, ContractValue.argument(2), fail()),
|
||||
singleConditionContract(ContractValue.argument(0), RelationType.LT, ContractValue.argument(1), returnParameter(1)),
|
||||
singleConditionContract(ContractValue.argument(0), RelationType.GT, ContractValue.argument(2), returnParameter(2)),
|
||||
trivialContract(returnParameter(0))
|
||||
))
|
||||
.register(instanceCall(JAVA_LANG_STRING, "startsWith", "endsWith", "contains"),
|
||||
ContractProvider.of(
|
||||
singleConditionContract(
|
||||
@@ -617,6 +637,11 @@ public final class HardcodedContracts {
|
||||
return Collections.singletonList(failContract);
|
||||
}
|
||||
|
||||
/// @return A single contract that fails if the parameter is NaN. Only makes sense for floating point parameters
|
||||
private static @NotNull MethodContract failIfParameterIsNaN(int argIndex) {
|
||||
return singleConditionContract(ContractValue.argument(argIndex), RelationType.NE, ContractValue.argument(argIndex), fail());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the mutation signature for the methods that have hardcoded contracts
|
||||
*
|
||||
|
||||
@@ -0,0 +1,471 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.siyeh.ig.migration;
|
||||
|
||||
import com.intellij.codeInspection.CommonQuickFixBundle;
|
||||
import com.intellij.codeInspection.LocalQuickFix;
|
||||
import com.intellij.codeInspection.ProblemDescriptor;
|
||||
import com.intellij.codeInspection.UpdateInspectionOptionFix;
|
||||
import com.intellij.codeInspection.dataFlow.CommonDataflow;
|
||||
import com.intellij.codeInspection.dataFlow.types.DfConstantType;
|
||||
import com.intellij.codeInspection.dataFlow.types.DfType;
|
||||
import com.intellij.codeInspection.dataFlow.value.RelationType;
|
||||
import com.intellij.codeInspection.options.OptPane;
|
||||
import com.intellij.java.analysis.JavaAnalysisBundle;
|
||||
import com.intellij.java.refactoring.JavaRefactoringBundle;
|
||||
import com.intellij.modcommand.ActionContext;
|
||||
import com.intellij.modcommand.ModCommand;
|
||||
import com.intellij.modcommand.ModCommandAction;
|
||||
import com.intellij.modcommand.ModCommandQuickFix;
|
||||
import com.intellij.modcommand.ModPsiUpdater;
|
||||
import com.intellij.modcommand.Presentation;
|
||||
import com.intellij.modcommand.PsiUpdateModCommandAction;
|
||||
import com.intellij.openapi.project.Project;
|
||||
import com.intellij.openapi.util.Condition;
|
||||
import com.intellij.openapi.util.Couple;
|
||||
import com.intellij.pom.java.JavaFeature;
|
||||
import com.intellij.psi.CommonClassNames;
|
||||
import com.intellij.psi.PsiAssignmentExpression;
|
||||
import com.intellij.psi.PsiClass;
|
||||
import com.intellij.psi.PsiElement;
|
||||
import com.intellij.psi.PsiExpression;
|
||||
import com.intellij.psi.PsiExpressionList;
|
||||
import com.intellij.psi.PsiMethod;
|
||||
import com.intellij.psi.PsiMethodCallExpression;
|
||||
import com.intellij.psi.PsiModifier;
|
||||
import com.intellij.psi.PsiModifierList;
|
||||
import com.intellij.psi.PsiModifierListOwner;
|
||||
import com.intellij.psi.PsiVariable;
|
||||
import com.intellij.psi.util.PsiTreeUtil;
|
||||
import com.intellij.psi.util.PsiUtil;
|
||||
import com.intellij.util.ArrayUtil;
|
||||
import com.intellij.util.ObjectUtils;
|
||||
import com.intellij.util.containers.ContainerUtil;
|
||||
import com.siyeh.ig.BaseInspection;
|
||||
import com.siyeh.ig.BaseInspectionVisitor;
|
||||
import com.siyeh.ig.psiutils.CommentTracker;
|
||||
import com.siyeh.ig.psiutils.EquivalenceChecker;
|
||||
import org.jetbrains.annotations.NotNullByDefault;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/// Migration that takes a combination of `Math.min` and `Math.max` calls into a `Math.clamp` call
|
||||
@NotNullByDefault
|
||||
public final class MathClampMigrationInspection extends BaseInspection {
|
||||
private static final String MIN_FUNCTION = "min";
|
||||
private static final String MAX_FUNCTION = "max";
|
||||
|
||||
/// In brave mode, the inspection shows up more often
|
||||
/// May lead to code that doesn't work at runtime
|
||||
public boolean braveMode = true;
|
||||
|
||||
@Override
|
||||
public OptPane getOptionsPane() {
|
||||
return OptPane.pane(OptPane.checkbox("braveMode", JavaAnalysisBundle.message("math.clamp.migration.brave.mode")));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<JavaFeature> requiredFeatures() {
|
||||
return Set.of(JavaFeature.MATH_CLAMP_METHODS);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String buildErrorString(Object @Nullable ... infos) {
|
||||
return CommonQuickFixBundle.message("fix.can.replace.with.x", "Math.clamp()");
|
||||
}
|
||||
|
||||
@Override
|
||||
public BaseInspectionVisitor buildVisitor() {
|
||||
return new MathClampMigrationVisitor();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected LocalQuickFix[] buildFixes(Object... infos) {
|
||||
@SuppressWarnings("unchecked")
|
||||
List<String> targets = (List<String>)infos[0];
|
||||
if (braveMode) {
|
||||
return new LocalQuickFix[]{new MathClampMigrationFix(true, targets), LocalQuickFix.from(
|
||||
new UpdateInspectionOptionFix(this, "braveMode", JavaAnalysisBundle.message("math.clamp.migration.do.not.report.ambiguous"),
|
||||
false))};
|
||||
}
|
||||
return new LocalQuickFix[]{new MathClampMigrationFix(false, targets)};
|
||||
}
|
||||
|
||||
private class MathClampMigrationVisitor extends BaseInspectionVisitor {
|
||||
@Override
|
||||
public void visitMethodCallExpression(PsiMethodCallExpression expression) {
|
||||
InspectionResult inspectionResult = inspect(expression, braveMode);
|
||||
if (inspectionResult != null) {
|
||||
registerError(expression, ContainerUtil.map(inspectionResult.targets, expression1 -> expression1.getText()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@NotNullByDefault
|
||||
private static class MathClampMigrationFix extends ModCommandQuickFix {
|
||||
private final boolean braveMode;
|
||||
private final List<String> potentialTargets;
|
||||
|
||||
private MathClampMigrationFix(boolean braveMode, List<String> potentialTargets) {
|
||||
this.braveMode = braveMode;
|
||||
this.potentialTargets = potentialTargets;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFamilyName() {
|
||||
return CommonQuickFixBundle.message("fix.replace.with.x", "Math.clamp()");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ModCommand perform(Project project, ProblemDescriptor descriptor) {
|
||||
PsiMethodCallExpression expression = ObjectUtils.tryCast(descriptor.getStartElement(), PsiMethodCallExpression.class);
|
||||
if (expression == null) return ModCommand.nop();
|
||||
|
||||
if (braveMode) {
|
||||
List<ModCommandAction> actions = new ArrayList<>();
|
||||
for (int i = 0; i < potentialTargets.size(); i++) {
|
||||
actions.add(new MathClampMigrationAction(expression, potentialTargets.get(i), i, braveMode));
|
||||
}
|
||||
return ModCommand.chooseAction(getFamilyName(), actions);
|
||||
}
|
||||
|
||||
return ModCommand.chooseAction(getFamilyName(),
|
||||
new MathClampMigrationAction(expression, potentialTargets.getFirst(), 0, braveMode));
|
||||
}
|
||||
}
|
||||
|
||||
/// Fix that replace the entire call with a newly made one, taking into account the comments
|
||||
@NotNullByDefault
|
||||
private static class MathClampMigrationAction extends PsiUpdateModCommandAction<PsiMethodCallExpression> {
|
||||
private final String clampedTargetName;
|
||||
private final int targetIndex;
|
||||
private final boolean braveMode;
|
||||
|
||||
private MathClampMigrationAction(PsiMethodCallExpression callExpression, String clampedTargetName, int targetIndex, boolean braveMode) {
|
||||
super(callExpression);
|
||||
this.clampedTargetName = clampedTargetName;
|
||||
this.targetIndex = targetIndex;
|
||||
this.braveMode = braveMode;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected @Nullable Presentation getPresentation(ActionContext context, PsiMethodCallExpression element) {
|
||||
return Presentation.of(JavaRefactoringBundle.message("0.as.target", clampedTargetName));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void invoke(ActionContext context, PsiMethodCallExpression call, ModPsiUpdater updater) {
|
||||
InspectionResult inspectionResult = inspect(call, braveMode);
|
||||
if (inspectionResult == null) return;
|
||||
|
||||
CommentTracker tracker = new CommentTracker();
|
||||
Couple<ClampInfo> sortedInfos = inspectionResult.infos;
|
||||
if (braveMode) {
|
||||
sortedInfos = sortInfosBraveMode(inspectionResult.infos, inspectionResult.targets.get(targetIndex),
|
||||
inspectionResult.topLevelMethod.equals(MIN_FUNCTION));
|
||||
}
|
||||
|
||||
String newCall = buildClampCall(sortedInfos, inspectionResult.targets.get(targetIndex), tracker);
|
||||
tracker.replaceAndRestoreComments(call, newCall);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getFamilyName() {
|
||||
return CommonQuickFixBundle.message("fix.replace.with.x", "Math.clamp()");
|
||||
}
|
||||
}
|
||||
|
||||
/// Record registering the important information about arguments inside the expressionList
|
||||
///
|
||||
/// @param values The values of the expression, expected to be a constant value or a range.
|
||||
/// @param element a reference towards the element. Might be a constant, a variable or a method call
|
||||
/// @param modifiers If the element references a [PsiVariable], its modifier list.
|
||||
/// @param children If the argument is a method call we are interested in (min/max), the info about the method arguments. `null` otherwise
|
||||
@NotNullByDefault
|
||||
private record ClampInfo(DfType values, PsiExpression element, @Nullable PsiModifierList modifiers, Couple<ClampInfo> children) {
|
||||
public boolean hasChildren() {
|
||||
return !children.equals(Couple.getEmpty());
|
||||
}
|
||||
|
||||
public Stream<ClampInfo> flatten() {
|
||||
return hasChildren() ? childrenAsStream().flatMap(ClampInfo::flatten) : Stream.of(this);
|
||||
}
|
||||
|
||||
/// Utility function
|
||||
private Stream<ClampInfo> childrenAsStream() {
|
||||
return coupleAsStream(this.children);
|
||||
}
|
||||
|
||||
static <T> Stream<T> coupleAsStream(Couple<T> couple) {
|
||||
return Stream.of(couple.first, couple.second);
|
||||
}
|
||||
}
|
||||
|
||||
/// @return the string to be inserted by [MathClampMigrationFix]
|
||||
private static String buildClampCall(Couple<ClampInfo> infos, PsiElement target, CommentTracker tracker) {
|
||||
StringBuilder newCall =
|
||||
new StringBuilder().append("java.lang.Math.clamp(").append(textWithCommentsFromArgument(target, tracker)).append(',');
|
||||
|
||||
for (ClampInfo info : infos) {
|
||||
printInfo(info, target, newCall, tracker);
|
||||
}
|
||||
|
||||
return newCall.replace(newCall.length() - 1, newCall.length(), ")").toString();
|
||||
}
|
||||
|
||||
/// Prints the content of the `info` element and its children, skipping over the target and the function containing it.
|
||||
private static void printInfo(ClampInfo info, PsiElement target, StringBuilder builder, CommentTracker tracker) {
|
||||
if (info.hasChildren()) {
|
||||
// I need to know whether the target is a direct children
|
||||
// If so, we only want to print the other child(ren)
|
||||
List<ClampInfo> nonTargetChildren = info.childrenAsStream().filter(info1 -> !info1.element.equals(target)).toList();
|
||||
if (nonTargetChildren.size() == 1) {
|
||||
printInfo(nonTargetChildren.getFirst(), target, builder, tracker);
|
||||
}
|
||||
else {
|
||||
builder.append(((PsiMethodCallExpression)info.element).getMethodExpression().getText()).append('(');
|
||||
for (ClampInfo child : info.children) {
|
||||
printInfo(child, target, builder, tracker);
|
||||
}
|
||||
builder.replace(builder.length() - 1, builder.length(), "),");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
builder.append(textWithCommentsFromArgument(info.element, tracker));
|
||||
builder.append(',');
|
||||
}
|
||||
|
||||
/// @param isMin Whether the top level function was [Math#min] (or an equivalent)
|
||||
/// @return the `infos` in order of smaller to bigger ranges
|
||||
private static @Nullable Couple<ClampInfo> sortInfos(Couple<ClampInfo> infos, PsiElement target, boolean isMin) {
|
||||
DfType A = getRange(infos.getFirst(), target, !isMin);
|
||||
DfType B = getRange(infos.getSecond(), target, !isMin);
|
||||
|
||||
DfType andType = A.meet(B);
|
||||
if (andType == DfType.TOP) return null;
|
||||
boolean isA = andType.isSuperType(A);
|
||||
boolean isB = andType.isSuperType(B);
|
||||
boolean isBottom = andType == DfType.BOTTOM;
|
||||
if (!(isA || isB || isBottom || andType instanceof DfConstantType<?>)) return null;
|
||||
if (isA && isB && !(andType instanceof DfConstantType<?>)) return null;
|
||||
|
||||
ClampInfo maxInfo, minInfo;
|
||||
if (isA) {
|
||||
maxInfo = infos.getSecond();
|
||||
minInfo = infos.getFirst();
|
||||
}
|
||||
else if (isB) {
|
||||
maxInfo = infos.getFirst();
|
||||
minInfo = infos.getSecond();
|
||||
}
|
||||
else { // BOTTOM or const
|
||||
if (A.meetRelation(RelationType.GT, B) == DfType.BOTTOM) {
|
||||
// big one is the second one
|
||||
maxInfo = infos.getSecond();
|
||||
minInfo = infos.getFirst();
|
||||
}
|
||||
else {
|
||||
maxInfo = infos.getFirst();
|
||||
minInfo = infos.getSecond();
|
||||
}
|
||||
}
|
||||
|
||||
return Couple.of(minInfo, maxInfo);
|
||||
}
|
||||
|
||||
/// @param targetMin Whether we target the min or max range of DfType
|
||||
/// @return The DfType that corresponds to the element, if the call containing the target was not there
|
||||
private static DfType getRange(ClampInfo info, PsiElement target, boolean targetMin) {
|
||||
if (!info.hasChildren()) {
|
||||
return info.values;
|
||||
}
|
||||
|
||||
DfType borderType = DfType.TOP;
|
||||
RelationType rangeRelation = targetMin ? RelationType.LE : RelationType.GE;
|
||||
|
||||
for (ClampInfo childInfo : info.children) {
|
||||
assert childInfo != null;
|
||||
if (target.equals(childInfo.element)) continue; // We do not care about the range of the clamped value
|
||||
DfType currentType = getRange(childInfo, target, targetMin);
|
||||
|
||||
borderType = borderType.meetRelation(rangeRelation, currentType) == DfType.BOTTOM ? currentType : childInfo.values;
|
||||
}
|
||||
|
||||
return borderType;
|
||||
}
|
||||
|
||||
/// Variant of [#sortInfos(List, PsiElement, boolean)] for brave mode. **Not Dfa aware**
|
||||
private static Couple<ClampInfo> sortInfosBraveMode(Couple<ClampInfo> infos, PsiElement target, boolean isMin) {
|
||||
boolean isTargetFound = infos.getFirst().flatten().anyMatch(info -> info.element.equals(target));
|
||||
ClampInfo maxInfo, minInfo;
|
||||
|
||||
// The logic here is that in a max(min()) call, the bigger number should to be next to the target
|
||||
if (isTargetFound) {
|
||||
maxInfo = infos.getFirst();
|
||||
minInfo = infos.getSecond();
|
||||
}
|
||||
else {
|
||||
maxInfo = infos.getSecond();
|
||||
minInfo = infos.getFirst();
|
||||
}
|
||||
|
||||
if (isMin) {
|
||||
ClampInfo swapInfo = maxInfo;
|
||||
maxInfo = minInfo;
|
||||
minInfo = swapInfo;
|
||||
}
|
||||
|
||||
return Couple.of(minInfo, maxInfo);
|
||||
}
|
||||
|
||||
/// Returns all possible targets that represent the variable that should be clamped.
|
||||
///
|
||||
/// @param topLevelInfo The information about the top level math call
|
||||
/// @param assignmentTarget The variable that gets assigned the result of the "clamp" operation, if any.
|
||||
private static List<PsiExpression> getPossibleTargets(Couple<ClampInfo> topLevelInfo, @Nullable PsiExpression assignmentTarget) {
|
||||
List<Condition<ClampInfo>> filters = List.of(info -> {
|
||||
//Side effect, if the intended target "ends up" being a constant-like, the inspection will bail out.
|
||||
if (info.values instanceof DfConstantType<?> constant && constant.getValue() != null) return false;
|
||||
return true;
|
||||
}, info -> {
|
||||
if (info.modifiers != null) {
|
||||
return !info.modifiers.hasModifierProperty(PsiModifier.STATIC);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
List<ClampInfo> possibleTargets =
|
||||
ClampInfo.coupleAsStream(topLevelInfo)
|
||||
.filter(ClampInfo::hasChildren) // The target is forced to be a child and can never be at a top level
|
||||
.flatMap((info -> info.flatten())).distinct().toList();
|
||||
|
||||
EquivalenceChecker checker = EquivalenceChecker.getCanonicalPsiEquivalence();
|
||||
for (Condition<ClampInfo> filter : filters) {
|
||||
possibleTargets = ContainerUtil.filter(possibleTargets, filter);
|
||||
|
||||
// Happy path case, we reassign the value to itself, just clamped
|
||||
if (assignmentTarget != null && assignmentTarget.getText() != null) {
|
||||
for (ClampInfo info : possibleTargets) {
|
||||
if (checker.expressionsAreEquivalent(info.element, assignmentTarget)) {
|
||||
return List.of(info.element);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Less happy path, attempt to figure out the identifier
|
||||
if (possibleTargets.isEmpty()) return List.of();
|
||||
if (possibleTargets.size() == 1) return List.of(possibleTargets.getFirst().element);
|
||||
}
|
||||
|
||||
// More than one target with too much ambiguity
|
||||
return ContainerUtil.map(possibleTargets, info -> info.element);
|
||||
}
|
||||
|
||||
/// If the result of the `Math.max(Math.min())` operation is assigned to a variable, returns the corresponding [PsiVariable].
|
||||
/// `null` otherwise.
|
||||
private static @Nullable PsiExpression getAssignment(PsiMethodCallExpression expression) {
|
||||
if (!(PsiUtil.skipParenthesizedExprUp(expression.getParent()) instanceof PsiAssignmentExpression assignment)) return null;
|
||||
return assignment.getLExpression();
|
||||
}
|
||||
|
||||
/// @return the name of the found function that takes 2 arguments, or null if it isn't available.
|
||||
private static @Nullable String findMathFunction(PsiExpression expression, String... functionsNames) {
|
||||
if (!(expression instanceof PsiMethodCallExpression callExpression)) return null;
|
||||
|
||||
PsiElement nameElement = callExpression.getMethodExpression().getReferenceNameElement();
|
||||
if (nameElement == null) return null;
|
||||
PsiExpressionList argumentList = callExpression.getArgumentList();
|
||||
|
||||
if (argumentList.getExpressionCount() != 2) return null;
|
||||
int methodIndex = ArrayUtil.indexOf(functionsNames, nameElement.getText());
|
||||
if (methodIndex == -1) return null;
|
||||
|
||||
PsiMethod method = callExpression.resolveMethod();
|
||||
if (method == null) return null;
|
||||
PsiClass containingClass = method.getContainingClass();
|
||||
if (containingClass == null || !CommonClassNames.JAVA_LANG_MATH.equals(containingClass.getQualifiedName())) return null;
|
||||
|
||||
return functionsNames[methodIndex];
|
||||
}
|
||||
|
||||
/// Collect the necessary information about arguments and math functions sub-calls
|
||||
private static @Nullable Couple<ClampInfo> collectClampInfo(PsiExpressionList list, String... subMethods) {
|
||||
CommonDataflow.DataflowResult result = CommonDataflow.getDataflowResult(list);
|
||||
if (result == null) return null;
|
||||
PsiExpression[] arguments = list.getExpressions();
|
||||
return Couple.of(
|
||||
buildSingleClampInfo(result, arguments[0], subMethods),
|
||||
buildSingleClampInfo(result, arguments[1], subMethods)
|
||||
);
|
||||
}
|
||||
|
||||
private static ClampInfo buildSingleClampInfo(CommonDataflow.DataflowResult result, PsiExpression argument, String... subMethods) {
|
||||
argument = Objects.requireNonNull(PsiUtil.skipParenthesizedExprDown(argument));
|
||||
boolean isMathFunction = findMathFunction(argument, subMethods) != null;
|
||||
if (isMathFunction) {
|
||||
return new ClampInfo(result.getDfType(argument), argument, null,
|
||||
Objects.requireNonNull(collectClampInfo(((PsiMethodCallExpression)argument).getArgumentList(), subMethods)));
|
||||
}
|
||||
else {
|
||||
PsiModifierListOwner modifierListOwner =
|
||||
argument.getReference() != null ? ObjectUtils.tryCast(argument.getReference().resolve(), PsiModifierListOwner.class) : null;
|
||||
|
||||
return new ClampInfo(result.getDfType(argument), argument, modifierListOwner != null ? modifierListOwner.getModifierList() : null,
|
||||
Couple.getEmpty());
|
||||
}
|
||||
}
|
||||
|
||||
/// Record storing the detection phase results, useful to produce the fix from it
|
||||
private record InspectionResult(Couple<ClampInfo> infos, List<PsiExpression> targets, String topLevelMethod) {
|
||||
}
|
||||
|
||||
/// Perform the inspection detection from the `expression` call
|
||||
/// @return The inspection results. `null` if the inspection could not be completed
|
||||
private static @Nullable InspectionResult inspect(PsiMethodCallExpression expression, boolean braveMode) {
|
||||
String topLevelMethod = findMathFunction(expression, MIN_FUNCTION, MAX_FUNCTION);
|
||||
if (topLevelMethod == null) return null;
|
||||
|
||||
String subMethod = topLevelMethod.equals(MIN_FUNCTION) ? MAX_FUNCTION : MIN_FUNCTION;
|
||||
|
||||
// So we have either max or min call, check if we have another call within
|
||||
PsiExpressionList argumentExpressions = expression.getArgumentList();
|
||||
if (!ContainerUtil.exists(argumentExpressions.getExpressions(), expression1 -> PsiUtil.skipParenthesizedExprDown(
|
||||
expression1) instanceof PsiMethodCallExpression callExpression && (findMathFunction(callExpression, subMethod) != null))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Couple<ClampInfo> infos = collectClampInfo(expression.getArgumentList(), subMethod);
|
||||
if (infos == null) return null;
|
||||
|
||||
List<PsiExpression> targets = getPossibleTargets(infos, getAssignment(expression));
|
||||
if (targets.isEmpty()) return null;
|
||||
|
||||
if (braveMode) {
|
||||
return new InspectionResult(infos, targets, topLevelMethod);
|
||||
}
|
||||
|
||||
// full dfa verification
|
||||
if (targets.size() > 1) return null;
|
||||
|
||||
infos = sortInfos(infos, targets.getFirst(), topLevelMethod.equals(MIN_FUNCTION));
|
||||
if (infos == null) return null;
|
||||
|
||||
return new InspectionResult(infos, targets, topLevelMethod);
|
||||
}
|
||||
|
||||
/// @return The expression with comments around it. `expression` is expected to be an argument inside a function call
|
||||
private static String textWithCommentsFromArgument(PsiElement expression, CommentTracker tracker) {
|
||||
PsiElement prev = PsiTreeUtil.skipWhitespacesAndCommentsBackward(expression);
|
||||
PsiElement next = PsiTreeUtil.skipWhitespacesAndCommentsForward(expression);
|
||||
if (prev != null && next != null) {
|
||||
return tracker.rangeText(prev.getNextSibling(), next.getPrevSibling());
|
||||
}
|
||||
return tracker.text(expression);
|
||||
}
|
||||
}
|
||||
@@ -1342,6 +1342,11 @@
|
||||
key="markdown.documentation.comments.migration.display.name" groupBundle="messages.InspectionsBundle"
|
||||
groupKey="group.names.language.level.specific.issues.and.migration.aids23" enabledByDefault="true" level="INFORMATION"
|
||||
implementationClass="com.siyeh.ig.migration.MarkdownDocumentationCommentsMigrationInspection"/>
|
||||
<localInspection groupPathKey="group.path.names.java.language.level.specific.issues.and.migration.aids" language="JAVA"
|
||||
shortName="MathClampMigration" bundle="messages.InspectionGadgetsBundle"
|
||||
key="inspection.clamp.migration.display.name" groupBundle="messages.InspectionsBundle"
|
||||
groupKey="group.names.language.level.specific.issues.and.migration.aids21" enabledByDefault="true" level="WARNING"
|
||||
implementationClass="com.siyeh.ig.migration.MathClampMigrationInspection"/>
|
||||
|
||||
<!--group.names.java.language.level.issues-->
|
||||
<localInspection groupPath="Java" language="JAVA" shortName="AnnotationClass" bundle="messages.InspectionGadgetsBundle" key="annotation.class.display.name"
|
||||
|
||||
@@ -2934,6 +2934,10 @@
|
||||
"id": "MarkdownDocumentationCommentsMigration",
|
||||
"codeQualityCategory": "Code Style"
|
||||
},
|
||||
{
|
||||
"id": "MathClampMigration",
|
||||
"codeQualityCategory": "Sanity"
|
||||
},
|
||||
{
|
||||
"id": "SimpleDateFormatWithoutLocale",
|
||||
"codeQualityCategory": "Reliability"
|
||||
|
||||
@@ -87,6 +87,7 @@ internal object DefaultJavaSyntaxResources {
|
||||
"feature.local.interfaces" to "Local interfaces",
|
||||
"feature.lvti" to "Local variable type inference",
|
||||
"feature.markdown.comment" to "Markdown Documentation Comments",
|
||||
"feature.math.clamp.methods" to "Clamp methods in standard library",
|
||||
"feature.method.references" to "Method references",
|
||||
"feature.module.import.declarations" to "Module Import Declarations",
|
||||
"feature.modules" to "Modules",
|
||||
|
||||
@@ -60,6 +60,7 @@ feature.record.patterns.in.for.each=Record patterns in for-each loops
|
||||
feature.primitive.types.in.patterns=Primitive types in patterns, instanceof and switch
|
||||
feature.implicit.import.in.implicit.classes=Implicit import in a compact source file
|
||||
feature.enum.qualified.name.in.switch=Qualified enum as a constant in switch
|
||||
feature.math.clamp.methods=Clamp methods in standard library
|
||||
feature.string.templates=String templates
|
||||
feature.unnamed.vars=Unnamed patterns and variables
|
||||
feature.implicit.classes=Compact source files
|
||||
|
||||
@@ -84,6 +84,7 @@ enum class JavaFeature {
|
||||
LanguageLevel.JDK_19_PREVIEW, LanguageLevel.JDK_20_PREVIEW),
|
||||
ENUM_QUALIFIED_NAME_IN_SWITCH(LanguageLevel.JDK_21, "feature.enum.qualified.name.in.switch"),
|
||||
SEQUENCED_COLLECTIONS(LanguageLevel.JDK_21, "feature.sequenced.collections"),
|
||||
MATH_CLAMP_METHODS(LanguageLevel.JDK_21, "feature.math.clamp.methods"),
|
||||
STRING_TEMPLATES(LanguageLevel.JDK_21_PREVIEW, "feature.string.templates") {
|
||||
override fun isSufficient(useSiteLevel: LanguageLevel): Boolean {
|
||||
return super.isSufficient(useSiteLevel) && !useSiteLevel.isAtLeast(LanguageLevel.JDK_23)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
public final class FullyQualifiedName {
|
||||
void test(int val) {
|
||||
int res2 = java.lang.Math.clamp(val, 1, 100);
|
||||
}
|
||||
|
||||
static class Math {}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
public final class FullyQualifiedName {
|
||||
void test(int val) {
|
||||
int res2 = <warning descr="Can be replaced with 'Math.clamp()'">java.lang.<caret>Math.min(java.lang.Math.max(val, 1), 100)</warning>;
|
||||
}
|
||||
|
||||
static class Math {}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
class InvalidInputs {
|
||||
void ICannotCode(int input) {
|
||||
input = Math.min(<error descr="Expression expected">,</error>Math.max(5, input));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
class MathClampBraveMode {
|
||||
|
||||
void test(int[] val, int lo, int hi) {
|
||||
val[0] = ((Math.clamp(val[0], lo, hi)));
|
||||
System.out.println(val);
|
||||
}
|
||||
|
||||
int myClamp(int input, int min, int max) {
|
||||
input = Math.clamp(input, min, max);
|
||||
return input;
|
||||
}
|
||||
|
||||
float myClampButTheOrderOfArgsIsDifferent(float input, float min, float max) {
|
||||
input = Math.clamp(input, min, max);
|
||||
return input;
|
||||
}
|
||||
|
||||
|
||||
int fakeClamp(int input, int a, int b) {
|
||||
return Math.clamp(b, a, input);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
class MathClampBraveMode {
|
||||
|
||||
void test(int[] val, int lo, int hi) {
|
||||
val[0] = ((<warning descr="Can be replaced with 'Math.clamp()'">Math.min(hi, Math.max(lo, val[0]))</warning>));
|
||||
System.out.println(val);
|
||||
}
|
||||
|
||||
int myClamp(int input, int min, int max) {
|
||||
input = <warning descr="Can be replaced with 'Math.clamp()'">Math.<caret>min(max, Math.max(min, input))</warning>;
|
||||
return input;
|
||||
}
|
||||
|
||||
float myClampButTheOrderOfArgsIsDifferent(float input, float min, float max) {
|
||||
input = <warning descr="Can be replaced with 'Math.clamp()'">Math.max(min, Math.min(max, input))</warning>;
|
||||
return input;
|
||||
}
|
||||
|
||||
|
||||
int fakeClamp(int input, int a, int b) {
|
||||
return <warning descr="Can be replaced with 'Math.clamp()'">Math.max(a, Math.min(b, input))</warning>;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
package inspection;
|
||||
|
||||
public class MathClampMigration {
|
||||
|
||||
private static final int FIXED_INT_VALUE = 5;
|
||||
private static final long FIXED_LONG_VALUE = 5L;
|
||||
private static final float FIXED_FLOAT_VALUE = 5.0F;
|
||||
private static final double FIXED_DOUBLE_VALUE = 5.0D;
|
||||
|
||||
/* ints */
|
||||
|
||||
void intConstantTest(int input, int secondInput) {
|
||||
// Basic cases
|
||||
Math.clamp(input, 5, 10);
|
||||
Math.clamp(input, 5, 10);
|
||||
Math.clamp(input, 5, 10);
|
||||
|
||||
// Cases where the variable to clamp is (more or less) ambiguous
|
||||
int beep = 10;
|
||||
int boop = 5;
|
||||
// local variables (constant-like)
|
||||
Math.clamp(input, boop, beep);
|
||||
Math.clamp(input, boop, beep);
|
||||
Math.clamp(input, boop, beep);
|
||||
// static variables (constant-like ?)
|
||||
Math.clamp(input, FIXED_INT_VALUE, beep);
|
||||
Math.clamp(input, FIXED_INT_VALUE, beep);
|
||||
Math.clamp(input, FIXED_INT_VALUE, beep);
|
||||
|
||||
Math.clamp(/*3*/input/*4*/, /*5*/FIXED_INT_VALUE/*6*/,/*1*/beep/*2*/);
|
||||
|
||||
Math.clamp(input, Integer.MIN_VALUE,/* Hewwo */beep);
|
||||
|
||||
// Nested cases
|
||||
int blorp = 50;
|
||||
Math.clamp(input, boop, Math.min(beep, blorp));
|
||||
Math.clamp(input, boop, Math.min(blorp, beep));
|
||||
int blarp = 2;
|
||||
Math.clamp(input, Math.max(boop, blarp), boop);
|
||||
Math.clamp(input, boop, Math.max(blarp, boop));
|
||||
|
||||
// Case where the assignement make it less ambiguous
|
||||
if (secondInput > boop) {
|
||||
input = Math.clamp(input, boop, secondInput);
|
||||
}
|
||||
}
|
||||
|
||||
void intRelationTest(int input, int a, int b) {
|
||||
if (a >=10 && b == 10) {
|
||||
// Should work
|
||||
input = Math.clamp(input, b, a);
|
||||
input = Math.clamp(input, b, a);
|
||||
input = Math.clamp(input, b, a);
|
||||
input = Math.clamp(input, b, a);
|
||||
|
||||
// Should not work
|
||||
Math.max(b, Math.min(a, input));
|
||||
Math.min(Math.max(a, input), b);
|
||||
return;
|
||||
}
|
||||
|
||||
if (a <= 10 && b == 10) {
|
||||
input = Math.clamp(input, a, b);
|
||||
input = Math.clamp(input, a, b);
|
||||
}
|
||||
// Cannot know from pure variable relation
|
||||
if (a < b) {
|
||||
input = Math.max(a, Math.min(input, b));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
int deepInTheSauce(int a, int b, int c, int d, int e, int f, int g, int h) {
|
||||
// should not be seen outside of brave mode
|
||||
a = Math.max(Math.min(Math.min(a,Math.min(f,g)), b), Math.min(c, Math.min(d, e)));
|
||||
return a;
|
||||
}
|
||||
|
||||
|
||||
/* Floats */
|
||||
|
||||
void floatConstantTest(float input, float secondInput) {
|
||||
// Basic cases
|
||||
Math.clamp(input, 5, 10);
|
||||
Math.clamp(input, 5, 10);
|
||||
Math.clamp(input, 5, 10);
|
||||
|
||||
// Cases where the variable to clamp is (more or less) ambiguous
|
||||
float beep = 10f;
|
||||
float boop = 5f;
|
||||
// local variables (constant-like)
|
||||
Math.clamp(input, boop, beep);
|
||||
Math.clamp(input, boop, beep);
|
||||
Math.clamp(input, boop, beep);
|
||||
// static variables (constant-like ?)
|
||||
Math.clamp(input, FIXED_FLOAT_VALUE, beep);
|
||||
Math.clamp(input, FIXED_FLOAT_VALUE, beep);
|
||||
Math.clamp(input, FIXED_FLOAT_VALUE, beep);
|
||||
Math.clamp(input, FIXED_FLOAT_VALUE, beep);
|
||||
|
||||
// Nested cases
|
||||
float blorp = 50f;
|
||||
Math.clamp(input, boop, Math.min(beep, blorp));
|
||||
Math.clamp(input, boop, Math.min(blorp, beep));
|
||||
float blarp = 2f;
|
||||
Math.clamp(input, Math.max(boop, blarp), boop);
|
||||
Math.clamp(input, boop, Math.max(blarp, boop));
|
||||
|
||||
// Case where the assignement make it less ambiguous
|
||||
if (secondInput > boop) {
|
||||
input = Math.clamp(input, boop, secondInput);
|
||||
}
|
||||
}
|
||||
|
||||
void floatRelationTest(float input, float a, float b) {
|
||||
if (a >=10 && b == 10) {
|
||||
// Should work
|
||||
input = Math.clamp(input, b, a);
|
||||
input = Math.clamp(input, b, a);
|
||||
input = Math.clamp(input, b, a);
|
||||
input = Math.clamp(input, b, a);
|
||||
|
||||
// Should not work
|
||||
Math.max(b, Math.min(a, input));
|
||||
Math.min(Math.max(a, input), b);
|
||||
return;
|
||||
}
|
||||
|
||||
if (a <= 10 && b == 10) {
|
||||
input = Math.clamp(input, a, b);
|
||||
input = Math.clamp(input, a, b);
|
||||
}
|
||||
// Cannot know from pure variable relation
|
||||
if (a < b) {
|
||||
input = Math.max(a, Math.min(input, b));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/* longs */
|
||||
|
||||
void longConstantTest(long input, long secondInput) {
|
||||
// Basic cases
|
||||
Math.clamp(input, 5L, 10L);
|
||||
Math.clamp(input, 5L, 10L);
|
||||
Math.clamp(input, 5L, 10L);
|
||||
|
||||
// Cases where the variable to clamp is (more or less) ambiguous
|
||||
long beep = 10L;
|
||||
long boop = 5L;
|
||||
// local variables (constant-like)
|
||||
Math.clamp(input, boop, beep);
|
||||
Math.clamp(input, boop, beep);
|
||||
Math.clamp(input, boop, beep);
|
||||
// static variables (constant-like ?)
|
||||
Math.clamp(input, FIXED_LONG_VALUE, beep);
|
||||
Math.clamp(input, FIXED_LONG_VALUE, beep);
|
||||
Math.clamp(input, FIXED_LONG_VALUE, beep);
|
||||
Math.clamp(input, FIXED_LONG_VALUE, beep);
|
||||
|
||||
// Nested cases
|
||||
long blorp = 50L;
|
||||
Math.clamp(input, boop, Math.min(beep, blorp));
|
||||
Math.clamp(input, boop, Math.min(blorp, beep));
|
||||
long blarp = 2L;
|
||||
Math.clamp(input, Math.max(boop, blarp), boop);
|
||||
Math.clamp(input, boop, Math.max(blarp, boop));
|
||||
|
||||
// Case where the assignement make it less ambiguous
|
||||
if (secondInput > boop) {
|
||||
input = Math.clamp(input, boop, secondInput);
|
||||
}
|
||||
}
|
||||
|
||||
void longRelationTest(long input, long a, long b) {
|
||||
if (a >= 10L && b == 10L) {
|
||||
// Should work
|
||||
input = Math.clamp(input, b, a);
|
||||
input = Math.clamp(input, b, a);
|
||||
input = Math.clamp(input, b, a);
|
||||
input = Math.clamp(input, b, a);
|
||||
|
||||
// Should not work
|
||||
Math.max(b, Math.min(a, input));
|
||||
Math.min(Math.max(a, input), b);
|
||||
return;
|
||||
}
|
||||
|
||||
if (a <= 10L && b == 10L) {
|
||||
input = Math.clamp(input, a, b);
|
||||
input = Math.clamp(input, a, b);
|
||||
}
|
||||
// Cannot know from pure variable relation
|
||||
if (a < b) {
|
||||
input = Math.max(a, Math.min(input, b));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/* Doubles */
|
||||
|
||||
void doubleConstantTest(double input, double secondInput) {
|
||||
// Basic cases
|
||||
Math.clamp(input, 5.0, 10.0);
|
||||
Math.clamp(input, 5.0, 10.0);
|
||||
Math.clamp(input, 5.0, 10.0);
|
||||
|
||||
// Cases where the variable to clamp is (more or less) ambiguous
|
||||
double beep = 10.0;
|
||||
double boop = 5.0;
|
||||
// local variables (constant-like)
|
||||
Math.clamp(input, boop, beep);
|
||||
Math.clamp(input, boop, beep);
|
||||
Math.clamp(input, boop, beep);
|
||||
// static variables (constant-like ?)
|
||||
Math.clamp(input, FIXED_DOUBLE_VALUE, beep);
|
||||
Math.clamp(input, FIXED_DOUBLE_VALUE, beep);
|
||||
Math.clamp(input, FIXED_DOUBLE_VALUE, beep);
|
||||
Math.clamp(input, FIXED_DOUBLE_VALUE, beep);
|
||||
|
||||
// Nested cases
|
||||
double blorp = 50.0;
|
||||
Math.clamp(input, boop, Math.min(beep, blorp));
|
||||
Math.clamp(input, boop, Math.min(blorp, beep));
|
||||
double blarp = 2.0;
|
||||
Math.clamp(input, Math.max(boop, blarp), boop);
|
||||
Math.clamp(input, boop, Math.max(blarp, boop));
|
||||
|
||||
// Case where the assignement make it less ambiguous
|
||||
if (secondInput > boop) {
|
||||
input = Math.clamp(input, boop, secondInput);
|
||||
}
|
||||
}
|
||||
|
||||
void doubleRelationTest(double input, double a, double b) {
|
||||
if (a >= 10.0 && b == 10.0) {
|
||||
// Should work
|
||||
input = Math.clamp(input, b, a);
|
||||
input = Math.clamp(input, b, a);
|
||||
input = Math.clamp(input, b, a);
|
||||
input = Math.clamp(input, b, a);
|
||||
|
||||
// Should not work
|
||||
Math.max(b, Math.min(a, input));
|
||||
Math.min(Math.max(a, input), b);
|
||||
return;
|
||||
}
|
||||
|
||||
if (a <= 10.0 && b == 10.0) {
|
||||
input = Math.clamp(input, a, b);
|
||||
input = Math.clamp(input, a, b);
|
||||
}
|
||||
// Cannot know from pure variable relation
|
||||
if (a < b) {
|
||||
input = Math.max(a, Math.min(input, b));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,260 @@
|
||||
package inspection;
|
||||
|
||||
public class MathClampMigration {
|
||||
|
||||
private static final int FIXED_INT_VALUE = 5;
|
||||
private static final long FIXED_LONG_VALUE = 5L;
|
||||
private static final float FIXED_FLOAT_VALUE = 5.0F;
|
||||
private static final double FIXED_DOUBLE_VALUE = 5.0D;
|
||||
|
||||
/* ints */
|
||||
|
||||
void intConstantTest(int input, int secondInput) {
|
||||
// Basic cases
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math<caret>.max(5, Math.min(10, input))</warning>;
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.max(Math.min(input, 10), 5)</warning>;
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.min(Math.max(input, 5), 10)</warning>;
|
||||
|
||||
// Cases where the variable to clamp is (more or less) ambiguous
|
||||
int beep = 10;
|
||||
int boop = 5;
|
||||
// local variables (constant-like)
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.max(boop, Math.min(beep, input))</warning>;
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.max(Math.min(input, beep), boop)</warning>;
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.min(Math.max(input, boop), beep)</warning>;
|
||||
// static variables (constant-like ?)
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.max(FIXED_INT_VALUE, Math.min(beep, input))</warning>;
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.max(Math.min(input, beep), FIXED_INT_VALUE)</warning>;
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.min(Math.max(input, FIXED_INT_VALUE), beep)</warning>;
|
||||
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.min(/*1*/beep/*2*/, Math.max(/*3*/input/*4*/, /*5*/FIXED_INT_VALUE/*6*/))</warning>;
|
||||
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.min(((/* Hewwo */beep)), ((Math.max(input, Integer.MIN_VALUE))))</warning>;
|
||||
|
||||
// Nested cases
|
||||
int blorp = 50;
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.max(boop, Math.min(beep, Math.min(input, blorp)))</warning>;
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.max(Math.min(Math.min(input, blorp), beep), boop)</warning>;
|
||||
int blarp = 2;
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.min(Math.max(Math.max(input, boop), blarp), boop)</warning>;
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.min(boop, Math.max(blarp, Math.max(input, boop)))</warning>;
|
||||
|
||||
// Case where the assignement make it less ambiguous
|
||||
if (secondInput > boop) {
|
||||
input = <warning descr="Can be replaced with 'Math.clamp()'">Math.min(secondInput, Math.max(input, boop))</warning>;
|
||||
}
|
||||
}
|
||||
|
||||
void intRelationTest(int input, int a, int b) {
|
||||
if (a >=10 && b == 10) {
|
||||
// Should work
|
||||
input = <warning descr="Can be replaced with 'Math.clamp()'">Math.min(Math.max(b, input), a)</warning>;
|
||||
input = <warning descr="Can be replaced with 'Math.clamp()'">Math.min(a, Math.max(b, input))</warning>;
|
||||
input = <warning descr="Can be replaced with 'Math.clamp()'">Math.max(Math.min(input, b), a)</warning>;
|
||||
input = <warning descr="Can be replaced with 'Math.clamp()'">Math.max(a, Math.min(input, b))</warning>;
|
||||
|
||||
// Should not work
|
||||
Math.max(b, Math.min(a, input));
|
||||
Math.min(Math.max(a, input), b);
|
||||
return;
|
||||
}
|
||||
|
||||
if (a <= 10 && b == 10) {
|
||||
input = <warning descr="Can be replaced with 'Math.clamp()'">Math.max(a, Math.min(b, input))</warning>;
|
||||
input = <warning descr="Can be replaced with 'Math.clamp()'">Math.max(Math.min(b, input), a)</warning>;
|
||||
}
|
||||
// Cannot know from pure variable relation
|
||||
if (a < b) {
|
||||
input = Math.max(a, Math.min(input, b));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
int deepInTheSauce(int a, int b, int c, int d, int e, int f, int g, int h) {
|
||||
// should not be seen outside of brave mode
|
||||
a = Math.max(Math.min(Math.min(a,Math.min(f,g)), b), Math.min(c, Math.min(d, e)));
|
||||
return a;
|
||||
}
|
||||
|
||||
|
||||
/* Floats */
|
||||
|
||||
void floatConstantTest(float input, float secondInput) {
|
||||
// Basic cases
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.max(5, Math.min(10, input))</warning>;
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.max(Math.min(input, 10), 5)</warning>;
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.min(Math.max(input, 5), 10)</warning>;
|
||||
|
||||
// Cases where the variable to clamp is (more or less) ambiguous
|
||||
float beep = 10f;
|
||||
float boop = 5f;
|
||||
// local variables (constant-like)
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.max(boop, Math.min(beep, input))</warning>;
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.max(Math.min(input, beep), boop)</warning>;
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.min(Math.max(input, boop), beep)</warning>;
|
||||
// static variables (constant-like ?)
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.max(FIXED_FLOAT_VALUE, Math.min(beep, input))</warning>;
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.max(Math.min(input, beep), FIXED_FLOAT_VALUE)</warning>;
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.min(Math.max(input, FIXED_FLOAT_VALUE), beep)</warning>;
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.min(beep, Math.max(input, FIXED_FLOAT_VALUE))</warning>;
|
||||
|
||||
// Nested cases
|
||||
float blorp = 50f;
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.max(boop, Math.min(beep, Math.min(input, blorp)))</warning>;
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.max(Math.min(Math.min(input, blorp), beep), boop)</warning>;
|
||||
float blarp = 2f;
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.min(Math.max(Math.max(input, boop), blarp), boop)</warning>;
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.min(boop, Math.max(blarp, Math.max(input, boop)))</warning>;
|
||||
|
||||
// Case where the assignement make it less ambiguous
|
||||
if (secondInput > boop) {
|
||||
input = <warning descr="Can be replaced with 'Math.clamp()'">Math.min(secondInput, Math.max(input, boop))</warning>;
|
||||
}
|
||||
}
|
||||
|
||||
void floatRelationTest(float input, float a, float b) {
|
||||
if (a >=10 && b == 10) {
|
||||
// Should work
|
||||
input = <warning descr="Can be replaced with 'Math.clamp()'">Math.min(Math.max(b, input), a)</warning>;
|
||||
input = <warning descr="Can be replaced with 'Math.clamp()'">Math.min(a, Math.max(b, input))</warning>;
|
||||
input = <warning descr="Can be replaced with 'Math.clamp()'">Math.max(Math.min(input, b), a)</warning>;
|
||||
input = <warning descr="Can be replaced with 'Math.clamp()'">Math.max(a, Math.min(input, b))</warning>;
|
||||
|
||||
// Should not work
|
||||
Math.max(b, Math.min(a, input));
|
||||
Math.min(Math.max(a, input), b);
|
||||
return;
|
||||
}
|
||||
|
||||
if (a <= 10 && b == 10) {
|
||||
input = <warning descr="Can be replaced with 'Math.clamp()'">Math.max(a, Math.min(b, input))</warning>;
|
||||
input = <warning descr="Can be replaced with 'Math.clamp()'">Math.max(Math.min(b, input), a)</warning>;
|
||||
}
|
||||
// Cannot know from pure variable relation
|
||||
if (a < b) {
|
||||
input = Math.max(a, Math.min(input, b));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/* longs */
|
||||
|
||||
void longConstantTest(long input, long secondInput) {
|
||||
// Basic cases
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.max(5L, Math.min(10L, input))</warning>;
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.max(Math.min(input, 10L), 5L)</warning>;
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.min(Math.max(input, 5L), 10L)</warning>;
|
||||
|
||||
// Cases where the variable to clamp is (more or less) ambiguous
|
||||
long beep = 10L;
|
||||
long boop = 5L;
|
||||
// local variables (constant-like)
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.max(boop, Math.min(beep, input))</warning>;
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.max(Math.min(input, beep), boop)</warning>;
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.min(Math.max(input, boop), beep)</warning>;
|
||||
// static variables (constant-like ?)
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.max(FIXED_LONG_VALUE, Math.min(beep, input))</warning>;
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.max(Math.min(input, beep), FIXED_LONG_VALUE)</warning>;
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.min(Math.max(input, FIXED_LONG_VALUE), beep)</warning>;
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.min(beep, Math.max(input, FIXED_LONG_VALUE))</warning>;
|
||||
|
||||
// Nested cases
|
||||
long blorp = 50L;
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.max(boop, Math.min(beep, Math.min(input, blorp)))</warning>;
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.max(Math.min(Math.min(input, blorp), beep), boop)</warning>;
|
||||
long blarp = 2L;
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.min(Math.max(Math.max(input, boop), blarp), boop)</warning>;
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.min(boop, Math.max(blarp, Math.max(input, boop)))</warning>;
|
||||
|
||||
// Case where the assignement make it less ambiguous
|
||||
if (secondInput > boop) {
|
||||
input = <warning descr="Can be replaced with 'Math.clamp()'">Math.min(secondInput, Math.max(input, boop))</warning>;
|
||||
}
|
||||
}
|
||||
|
||||
void longRelationTest(long input, long a, long b) {
|
||||
if (a >= 10L && b == 10L) {
|
||||
// Should work
|
||||
input = <warning descr="Can be replaced with 'Math.clamp()'">Math.min(Math.max(b, input), a)</warning>;
|
||||
input = <warning descr="Can be replaced with 'Math.clamp()'">Math.min(a, Math.max(b, input))</warning>;
|
||||
input = <warning descr="Can be replaced with 'Math.clamp()'">Math.max(Math.min(input, b), a)</warning>;
|
||||
input = <warning descr="Can be replaced with 'Math.clamp()'">Math.max(a, Math.min(input, b))</warning>;
|
||||
|
||||
// Should not work
|
||||
Math.max(b, Math.min(a, input));
|
||||
Math.min(Math.max(a, input), b);
|
||||
return;
|
||||
}
|
||||
|
||||
if (a <= 10L && b == 10L) {
|
||||
input = <warning descr="Can be replaced with 'Math.clamp()'">Math.max(a, Math.min(b, input))</warning>;
|
||||
input = <warning descr="Can be replaced with 'Math.clamp()'">Math.max(Math.min(b, input), a)</warning>;
|
||||
}
|
||||
// Cannot know from pure variable relation
|
||||
if (a < b) {
|
||||
input = Math.max(a, Math.min(input, b));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/* Doubles */
|
||||
|
||||
void doubleConstantTest(double input, double secondInput) {
|
||||
// Basic cases
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.max(5.0, Math.min(10.0, input))</warning>;
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.max(Math.min(input, 10.0), 5.0)</warning>;
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.min(Math.max(input, 5.0), 10.0)</warning>;
|
||||
|
||||
// Cases where the variable to clamp is (more or less) ambiguous
|
||||
double beep = 10.0;
|
||||
double boop = 5.0;
|
||||
// local variables (constant-like)
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.max(boop, Math.min(beep, input))</warning>;
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.max(Math.min(input, beep), boop)</warning>;
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.min(Math.max(input, boop), beep)</warning>;
|
||||
// static variables (constant-like ?)
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.max(FIXED_DOUBLE_VALUE, Math.min(beep, input))</warning>;
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.max(Math.min(input, beep), FIXED_DOUBLE_VALUE)</warning>;
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.min(Math.max(input, FIXED_DOUBLE_VALUE), beep)</warning>;
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.min(beep, Math.max(input, FIXED_DOUBLE_VALUE))</warning>;
|
||||
|
||||
// Nested cases
|
||||
double blorp = 50.0;
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.max(boop, Math.min(beep, Math.min(input, blorp)))</warning>;
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.max(Math.min(Math.min(input, blorp), beep), boop)</warning>;
|
||||
double blarp = 2.0;
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.min(Math.max(Math.max(input, boop), blarp), boop)</warning>;
|
||||
<warning descr="Can be replaced with 'Math.clamp()'">Math.min(boop, Math.max(blarp, Math.max(input, boop)))</warning>;
|
||||
|
||||
// Case where the assignement make it less ambiguous
|
||||
if (secondInput > boop) {
|
||||
input = <warning descr="Can be replaced with 'Math.clamp()'">Math.min(secondInput, Math.max(input, boop))</warning>;
|
||||
}
|
||||
}
|
||||
|
||||
void doubleRelationTest(double input, double a, double b) {
|
||||
if (a >= 10.0 && b == 10.0) {
|
||||
// Should work
|
||||
input = <warning descr="Can be replaced with 'Math.clamp()'">Math.min(Math.max(b, input), a)</warning>;
|
||||
input = <warning descr="Can be replaced with 'Math.clamp()'">Math.min(a, Math.max(b, input))</warning>;
|
||||
input = <warning descr="Can be replaced with 'Math.clamp()'">Math.max(Math.min(input, b), a)</warning>;
|
||||
input = <warning descr="Can be replaced with 'Math.clamp()'">Math.max(a, Math.min(input, b))</warning>;
|
||||
|
||||
// Should not work
|
||||
Math.max(b, Math.min(a, input));
|
||||
Math.min(Math.max(a, input), b);
|
||||
return;
|
||||
}
|
||||
|
||||
if (a <= 10.0 && b == 10.0) {
|
||||
input = <warning descr="Can be replaced with 'Math.clamp()'">Math.max(a, Math.min(b, input))</warning>;
|
||||
input = <warning descr="Can be replaced with 'Math.clamp()'">Math.max(Math.min(b, input), a)</warning>;
|
||||
}
|
||||
// Cannot know from pure variable relation
|
||||
if (a < b) {
|
||||
input = Math.max(a, Math.min(input, b));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// Copyright 2000-2026 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.siyeh.ig.migration;
|
||||
|
||||
import com.intellij.codeInspection.InspectionProfileEntry;
|
||||
import com.siyeh.ig.LightJavaInspectionTestCase;
|
||||
|
||||
public final class MathClampMigrationInspectionTest extends LightJavaInspectionTestCase {
|
||||
|
||||
public void testMathClampMigration() {
|
||||
doTest();
|
||||
checkQuickFixAll();
|
||||
}
|
||||
|
||||
public void testInvalidInputs() {
|
||||
doTest();
|
||||
}
|
||||
|
||||
public void testFullyQualifiedName() {
|
||||
doTest();
|
||||
checkQuickFixAll();
|
||||
}
|
||||
|
||||
public void testMathClampBraveMode() {
|
||||
doTest();
|
||||
checkQuickFixAll();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected InspectionProfileEntry getInspection() {
|
||||
MathClampMigrationInspection inspection = new MathClampMigrationInspection();
|
||||
if (!this.getTestName(false).contains("BraveMode")) {
|
||||
inspection.braveMode = false;
|
||||
}
|
||||
return inspection;
|
||||
}
|
||||
}
|
||||
@@ -826,3 +826,5 @@ inline.popup.ignore.conflicts=Ignore writes and continue
|
||||
inline.warning.variables.used.in.initializer.are.updated=Unsafe Inline: Variables Used in Initializer Are Updated
|
||||
dialog.title.resolving.method.implementation=Resolving Method Implementation
|
||||
dialog.message.confirmation.to.process.only.implementation=An implementation of abstract method is found:<br><br><b>{0}</b><br><br>Do you want to inline this implementation?
|
||||
|
||||
0.as.target=''{0}'' as target
|
||||
Reference in New Issue
Block a user