MultiFilePlanningBudget.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
*******************************************************************************/
package org.sandbox.jdt.cleanup.multifile;
import java.util.HashSet;
import java.util.Set;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.OperationCanceledException;
import org.eclipse.jdt.core.ICompilationUnit;
import org.eclipse.jdt.core.JavaModelException;
import org.eclipse.ltk.core.refactoring.RefactoringStatus;
/** Measures source scope and applies warning/hard limits before AST creation. */
public final class MultiFilePlanningBudget {
/** Result of measuring one source scope. */
public record Assessment(MultiFilePlanningMetrics metrics, RefactoringStatus status) {
public Assessment {
if (metrics == null || status == null) {
throw new NullPointerException();
}
}
/** @return whether planning may safely continue */
public boolean mayProceed() {
return !status.hasFatalError();
}
}
private MultiFilePlanningBudget() {
}
/**
* Measures distinct primary compilation units using their current working-copy
* source, including unsaved edits. UTF-8 byte length is counted without
* allocating a second byte array. Hard thresholds abort measurement as soon as
* they are exceeded.
*
* @param units planned source units
* @param limits effective limits
* @param monitor progress monitor, may be {@code null}
* @return metrics and warning/fatal diagnostics
* @throws JavaModelException if current source cannot be read
*/
public static Assessment assess(ICompilationUnit[] units, MultiFilePlanningLimits limits,
IProgressMonitor monitor) throws JavaModelException {
if (units == null || limits == null) {
throw new NullPointerException();
}
RefactoringStatus status= new RefactoringStatus();
Set<String> measuredHandles= new HashSet<>();
int unitCount= 0;
long sourceBytes= 0;
for (ICompilationUnit unit : units) {
checkCanceled(monitor);
if (unit == null || !unit.exists()) {
status.addFatalError("The coordinated cleanup scope contains a missing compilation unit."); //$NON-NLS-1$
break;
}
ICompilationUnit primary= unit.getPrimary();
if (!measuredHandles.add(primary.getHandleIdentifier())) {
continue;
}
unitCount++;
if (unitCount > limits.hardCompilationUnits()) {
status.addFatalError("Coordinated cleanup planning aborted: " + unitCount //$NON-NLS-1$
+ " source units exceed the hard limit of " + limits.hardCompilationUnits() + '.'); //$NON-NLS-1$
break;
}
String source= primary.getSource();
if (source == null) {
status.addFatalError("The current source of " + primary.getElementName() //$NON-NLS-1$
+ " is unavailable for coordinated cleanup planning."); //$NON-NLS-1$
break;
}
sourceBytes= saturatedAdd(sourceBytes, utf8Length(source));
if (sourceBytes > limits.hardSourceBytes()) {
status.addFatalError("Coordinated cleanup planning aborted: " + sourceBytes //$NON-NLS-1$
+ " source bytes exceed the hard limit of " + limits.hardSourceBytes() + '.'); //$NON-NLS-1$
break;
}
}
MultiFilePlanningMetrics metrics= MultiFilePlanningMetrics.scope(unitCount, sourceBytes);
if (status.hasFatalError()) {
return new Assessment(metrics, status);
}
if (unitCount > limits.warningCompilationUnits() || sourceBytes > limits.warningSourceBytes()) {
status.addWarning("Coordinated cleanup planning will analyse " + unitCount + " source units (" //$NON-NLS-1$ //$NON-NLS-2$
+ sourceBytes + " UTF-8 bytes). Warning limits are " //$NON-NLS-1$
+ limits.warningCompilationUnits() + " units and " + limits.warningSourceBytes() + " bytes."); //$NON-NLS-1$ //$NON-NLS-2$
}
return new Assessment(metrics, status);
}
static long utf8Length(CharSequence source) {
long length= 0;
for (int index= 0; index < source.length(); index++) {
char character= source.charAt(index);
if (character <= 0x7f) {
length= saturatedAdd(length, 1);
} else if (character <= 0x7ff) {
length= saturatedAdd(length, 2);
} else if (Character.isHighSurrogate(character) && index + 1 < source.length()
&& Character.isLowSurrogate(source.charAt(index + 1))) {
length= saturatedAdd(length, 4);
index++;
} else if (Character.isSurrogate(character)) {
length= saturatedAdd(length, 1);
} else {
length= saturatedAdd(length, 3);
}
}
return length;
}
private static long saturatedAdd(long left, long right) {
return Long.MAX_VALUE - left < right ? Long.MAX_VALUE : left + right;
}
/** Throws immediately when the user or caller cancels planning. */
public static void checkCanceled(IProgressMonitor monitor) {
if (monitor != null && monitor.isCanceled()) {
throw new OperationCanceledException();
}
}
}