ShiftOutOfRangeHintProvider.java
/*******************************************************************************
* Copyright (c) 2026 Carsten Hammer.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Carsten Hammer
*******************************************************************************/
package org.sandbox.jdt.triggerpattern.shift;
import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.ITypeBinding;
import org.eclipse.jdt.core.dom.InfixExpression;
import org.eclipse.jdt.core.dom.NumberLiteral;
import org.eclipse.jdt.ui.text.java.IJavaCompletionProposal;
import org.eclipse.jdt.ui.text.java.correction.ASTRewriteCorrectionProposal;
import org.eclipse.swt.graphics.Image;
import org.sandbox.jdt.triggerpattern.api.Hint;
import org.sandbox.jdt.triggerpattern.eclipse.HintContext;
import org.sandbox.jdt.triggerpattern.api.PatternKind;
import org.sandbox.jdt.triggerpattern.api.TriggerPattern;
/**
* Hint provider for shift out of range detection using TriggerPattern.
*
* <p>In Java, shift amounts are automatically masked:</p>
* <ul>
* <li>For {@code int} (and {@code byte}, {@code short}, {@code char}): {@code amount & 0x1f} (range 0-31)</li>
* <li>For {@code long}: {@code amount & 0x3f} (range 0-63)</li>
* </ul>
*
* <p>This hint detects out-of-range shift amounts and suggests replacing them
* with the effective masked value.</p>
*
* <p>Inspired by
* <a href="https://github.com/apache/netbeans/blob/master/java/java.hints/src/org/netbeans/modules/java/hints/ShiftOutOfRange.java">
* NetBeans ShiftOutOfRange</a>.</p>
*
* @since 1.2.5
*/
public class ShiftOutOfRangeHintProvider {
private static final int INT_SHIFT_MASK = 31;
private static final int LONG_SHIFT_MASK = 63;
/**
* Detects {@code $v << $c} where $c is out of range.
*
* @param ctx the hint context
* @return a completion proposal, or null if not applicable
*/
@TriggerPattern(value = "$v << $c", kind = PatternKind.EXPRESSION)
@Hint(displayName = "Shift left amount out of range",
description = "The shift amount is out of the valid range and will be masked by Java")
public static IJavaCompletionProposal checkLeftShift(HintContext ctx) {
return checkShiftOutOfRange(ctx);
}
/**
* Detects {@code $v >> $c} where $c is out of range.
*
* @param ctx the hint context
* @return a completion proposal, or null if not applicable
*/
@TriggerPattern(value = "$v >> $c", kind = PatternKind.EXPRESSION)
@Hint(displayName = "Signed right shift amount out of range",
description = "The shift amount is out of the valid range and will be masked by Java")
public static IJavaCompletionProposal checkRightShiftSigned(HintContext ctx) {
return checkShiftOutOfRange(ctx);
}
/**
* Detects {@code $v >>> $c} where $c is out of range.
*
* @param ctx the hint context
* @return a completion proposal, or null if not applicable
*/
@TriggerPattern(value = "$v >>> $c", kind = PatternKind.EXPRESSION)
@Hint(displayName = "Unsigned right shift amount out of range",
description = "The shift amount is out of the valid range and will be masked by Java")
public static IJavaCompletionProposal checkRightShiftUnsigned(HintContext ctx) {
return checkShiftOutOfRange(ctx);
}
private static IJavaCompletionProposal checkShiftOutOfRange(HintContext ctx) {
ASTNode matchedNode = ctx.getMatch().getMatchedNode();
if (!(matchedNode instanceof InfixExpression)) {
return null;
}
InfixExpression infixExpr = (InfixExpression) matchedNode;
// Get the shift amount
ASTNode cNode = ctx.getMatch().getBinding("$c"); //$NON-NLS-1$
if (cNode == null || !(cNode instanceof Expression)) {
return null;
}
Expression shiftAmountExpr = (Expression) cNode;
Object constantValue = shiftAmountExpr.resolveConstantExpressionValue();
if (constantValue == null || !(constantValue instanceof Number)) {
return null;
}
long shiftAmount = ((Number) constantValue).longValue();
// Get the left operand type
ASTNode vNode = ctx.getMatch().getBinding("$v"); //$NON-NLS-1$
if (vNode == null || !(vNode instanceof Expression)) {
return null;
}
Expression leftOperand = (Expression) vNode;
ITypeBinding typeBinding = leftOperand.resolveTypeBinding();
if (typeBinding == null) {
return null;
}
String qualifiedName = typeBinding.getQualifiedName();
long maskedValue;
boolean isIntLike = "int".equals(qualifiedName) //$NON-NLS-1$
|| "byte".equals(qualifiedName) //$NON-NLS-1$
|| "short".equals(qualifiedName) //$NON-NLS-1$
|| "char".equals(qualifiedName) //$NON-NLS-1$
|| "java.lang.Integer".equals(qualifiedName) //$NON-NLS-1$
|| "java.lang.Byte".equals(qualifiedName) //$NON-NLS-1$
|| "java.lang.Short".equals(qualifiedName) //$NON-NLS-1$
|| "java.lang.Character".equals(qualifiedName); //$NON-NLS-1$
boolean isLongLike = "long".equals(qualifiedName) //$NON-NLS-1$
|| "java.lang.Long".equals(qualifiedName); //$NON-NLS-1$
if (isIntLike) {
if (shiftAmount >= 0 && shiftAmount <= INT_SHIFT_MASK) {
return null; // in range
}
maskedValue = shiftAmount & INT_SHIFT_MASK;
} else if (isLongLike) {
if (shiftAmount >= 0 && shiftAmount <= LONG_SHIFT_MASK) {
return null; // in range
}
maskedValue = shiftAmount & LONG_SHIFT_MASK;
} else {
return null;
}
// Create the replacement
AST ast = ctx.getASTRewrite().getAST();
NumberLiteral newLiteral = ast.newNumberLiteral(String.valueOf(maskedValue));
ctx.getASTRewrite().replace(infixExpr.getRightOperand(), newLiteral, null);
String label = "Replace out-of-range shift amount " + shiftAmount + " with " + maskedValue; //$NON-NLS-1$ //$NON-NLS-2$
ASTRewriteCorrectionProposal proposal = new ASTRewriteCorrectionProposal(
label,
ctx.getICompilationUnit(),
ctx.getASTRewrite(),
10,
(Image) null
);
return proposal;
}
}