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:
Mathias
2026-02-09 15:45:18 +01:00
committed by intellij-monorepo-bot
parent 45b4219faf
commit 7dd10b82b1
19 changed files with 1159 additions and 0 deletions

View File

@@ -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

View File

@@ -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>

View File

@@ -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

View File

@@ -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
*

View File

@@ -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);
}
}

View File

@@ -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"

View File

@@ -2934,6 +2934,10 @@
"id": "MarkdownDocumentationCommentsMigration",
"codeQualityCategory": "Code Style"
},
{
"id": "MathClampMigration",
"codeQualityCategory": "Sanity"
},
{
"id": "SimpleDateFormatWithoutLocale",
"codeQualityCategory": "Reliability"

View File

@@ -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",

View File

@@ -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

View File

@@ -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)

View File

@@ -0,0 +1,7 @@
public final class FullyQualifiedName {
void test(int val) {
int res2 = java.lang.Math.clamp(val, 1, 100);
}
static class Math {}
}

View File

@@ -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 {}
}

View File

@@ -0,0 +1,5 @@
class InvalidInputs {
void ICannotCode(int input) {
input = Math.min(<error descr="Expression expected">,</error>Math.max(5, input));
}
}

View File

@@ -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);
}
}

View File

@@ -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>;
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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