ShiftOutOfRangeFixCore.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.internal.corext.fix;

import java.util.List;
import java.util.Set;

import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.CompilationUnit;
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.core.dom.rewrite.ASTRewrite;
import org.eclipse.jdt.internal.corext.fix.CompilationUnitRewriteOperationsFixCore.CompilationUnitRewriteOperation;
import org.eclipse.jdt.internal.corext.fix.LinkedProposalModelCore;
import org.eclipse.jdt.internal.corext.refactoring.structure.CompilationUnitRewrite;
import org.eclipse.text.edits.TextEditGroup;
import org.sandbox.jdt.triggerpattern.api.Match;
import org.sandbox.jdt.triggerpattern.api.Pattern;
import org.sandbox.jdt.triggerpattern.api.PatternKind;
import org.sandbox.jdt.triggerpattern.api.TriggerPatternEngine;

/**
 * Fix core for shift out of range detection using TriggerPattern hints.
 *
 * <p>This class applies shift out of range patterns as cleanup operations,
 * detecting shift operations where the shift amount is out of range and
 * replacing them with the effective masked value.</p>
 *
 * @since 1.2.5
 */
public class ShiftOutOfRangeFixCore {

	private static final TriggerPatternEngine ENGINE = new TriggerPatternEngine();

	private static final int INT_SHIFT_MASK = 31;
	private static final int LONG_SHIFT_MASK = 63;

	/**
	 * Finds shift out of range operations in the compilation unit.
	 *
	 * @param compilationUnit the compilation unit to search
	 * @param operations the set to add found operations to
	 */
	public static void findOperations(CompilationUnit compilationUnit,
			Set<CompilationUnitRewriteOperation> operations) {

		// Pattern 1: $v << $c
		Pattern leftShiftPattern = Pattern.of("$v << $c", PatternKind.EXPRESSION); //$NON-NLS-1$
		List<Match> leftShiftMatches = ENGINE.findMatches(compilationUnit, leftShiftPattern);
		for (Match match : leftShiftMatches) {
			addOperationIfOutOfRange(match, operations);
		}

		// Pattern 2: $v >> $c
		Pattern rightShiftPattern = Pattern.of("$v >> $c", PatternKind.EXPRESSION); //$NON-NLS-1$
		List<Match> rightShiftMatches = ENGINE.findMatches(compilationUnit, rightShiftPattern);
		for (Match match : rightShiftMatches) {
			addOperationIfOutOfRange(match, operations);
		}

		// Pattern 3: $v >>> $c
		Pattern unsignedRightShiftPattern = Pattern.of("$v >>> $c", PatternKind.EXPRESSION); //$NON-NLS-1$
		List<Match> unsignedRightShiftMatches = ENGINE.findMatches(compilationUnit, unsignedRightShiftPattern);
		for (Match match : unsignedRightShiftMatches) {
			addOperationIfOutOfRange(match, operations);
		}
	}

	private static void addOperationIfOutOfRange(Match match, Set<CompilationUnitRewriteOperation> operations) {
		ASTNode matchedNode = match.getMatchedNode();
		if (!(matchedNode instanceof InfixExpression)) {
			return;
		}

		// Get the shift amount
		ASTNode cNode = match.getBinding("$c"); //$NON-NLS-1$
		if (cNode == null || !(cNode instanceof Expression)) {
			return;
		}

		Expression shiftAmountExpr = (Expression) cNode;
		Object constantValue = shiftAmountExpr.resolveConstantExpressionValue();
		if (constantValue == null || !(constantValue instanceof Number)) {
			return;
		}
		long shiftAmount = ((Number) constantValue).longValue();

		// Get the left operand type
		ASTNode vNode = match.getBinding("$v"); //$NON-NLS-1$
		if (vNode == null || !(vNode instanceof Expression)) {
			return;
		}

		Expression leftOperand = (Expression) vNode;
		ITypeBinding typeBinding = leftOperand.resolveTypeBinding();
		if (typeBinding == null) {
			return;
		}

		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; // in range
			}
			maskedValue = shiftAmount & INT_SHIFT_MASK;
		} else if (isLongLike) {
			if (shiftAmount >= 0 && shiftAmount <= LONG_SHIFT_MASK) {
				return; // in range
			}
			maskedValue = shiftAmount & LONG_SHIFT_MASK;
		} else {
			return;
		}

		operations.add(new ShiftOutOfRangeOperation(match, maskedValue));
	}

	/**
	 * Rewrite operation for shift out of range replacement.
	 */
	private static class ShiftOutOfRangeOperation extends CompilationUnitRewriteOperation {

		private final Match match;
		private final long maskedValue;

		public ShiftOutOfRangeOperation(Match match, long maskedValue) {
			this.match = match;
			this.maskedValue = maskedValue;
		}

		@Override
		public void rewriteAST(CompilationUnitRewrite cuRewrite, LinkedProposalModelCore linkedModel) {
			ASTRewrite rewrite = cuRewrite.getASTRewrite();
			AST ast = cuRewrite.getRoot().getAST();
			TextEditGroup group = createTextEditGroup("Replace out-of-range shift amount", cuRewrite); //$NON-NLS-1$

			ASTNode matchedNode = match.getMatchedNode();
			if (!(matchedNode instanceof InfixExpression)) {
				return;
			}

			InfixExpression infixExpr = (InfixExpression) matchedNode;
			NumberLiteral newLiteral = ast.newNumberLiteral(String.valueOf(maskedValue));
			rewrite.replace(infixExpr.getRightOperand(), newLiteral, group);
		}
	}
}