JUnit3MigrationExclusions.java

/*******************************************************************************
 * Copyright (c) 2026 Carsten Hammer and others.
 *
 * 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.internal.corext.fix.helper.lib;

import java.util.Arrays;
import java.util.LinkedHashSet;
import java.util.Set;

import org.eclipse.jdt.core.dom.ITypeBinding;

/**
 * Base types whose JUnit 3 execution contract must not be migrated to Jupiter.
 *
 * <p>Some frameworks derive from {@code junit.framework.TestCase} only to reuse
 * the JUnit 3 runner while adding an execution contract that has no Jupiter
 * equivalent. {@code org.eclipse.test.performance.PerformanceTestCase} is the
 * dominant example in the Eclipse code base: it measures and asserts performance
 * through the JUnit 3 lifecycle and is executed by a dedicated harness.
 *
 * <p>The default list can be extended or replaced through the system property
 * {@value #EXCLUDED_SUPERTYPES_PROPERTY} using a comma-separated list of fully
 * qualified type names. A leading {@code +} keeps the built-in defaults and adds
 * the configured names; without it the configured names replace the defaults.
 */
public final class JUnit3MigrationExclusions {

	/** System property that configures additional or replacing excluded base types. */
	public static final String EXCLUDED_SUPERTYPES_PROPERTY= "sandbox.junit.migration.excludedSuperTypes"; //$NON-NLS-1$

	/** Stable reason code reported when an excluded base type blocks a migration. */
	public static final String EXCLUDED_BASE_TYPE_REASON= "EXCLUDED_JUNIT3_BASE_TYPE"; //$NON-NLS-1$

	private static final Set<String> DEFAULT_EXCLUDED_SUPERTYPES= Set.of(
			"org.eclipse.test.performance.PerformanceTestCase", //$NON-NLS-1$
			"junit.extensions.TestSetup", //$NON-NLS-1$
			"junit.extensions.TestDecorator", //$NON-NLS-1$
			"junit.framework.TestSuite"); //$NON-NLS-1$

	private JUnit3MigrationExclusions() {
	}

	/**
	 * Returns the configured excluded base types.
	 *
	 * @return fully qualified names of base types that block a JUnit 3 migration
	 */
	public static Set<String> excludedSuperTypes() {
		String configured= System.getProperty(EXCLUDED_SUPERTYPES_PROPERTY);
		if (configured == null || configured.isBlank()) {
			return DEFAULT_EXCLUDED_SUPERTYPES;
		}
		boolean additive= configured.startsWith("+"); //$NON-NLS-1$
		String names= additive ? configured.substring(1) : configured;
		Set<String> result= new LinkedHashSet<>();
		if (additive) {
			result.addAll(DEFAULT_EXCLUDED_SUPERTYPES);
		}
		Arrays.stream(names.split(",")) //$NON-NLS-1$
				.map(String::trim)
				.filter(name -> !name.isEmpty())
				.forEach(result::add);
		return Set.copyOf(result);
	}

	/**
	 * Returns whether the type itself or any of its superclasses is excluded.
	 *
	 * @param binding type binding to inspect, may be {@code null}
	 * @return {@code true} if the type must not be migrated
	 */
	public static boolean isExcluded(ITypeBinding binding) {
		return excludedSuperType(binding) != null;
	}

	/**
	 * Returns the first excluded type found in the superclass chain.
	 *
	 * @param binding type binding to inspect, may be {@code null}
	 * @return the excluded fully qualified type name, or {@code null} if none applies
	 */
	public static String excludedSuperType(ITypeBinding binding) {
		Set<String> excluded= excludedSuperTypes();
		ITypeBinding current= binding;
		while (current != null) {
			ITypeBinding erasure= current.getErasure();
			String name= erasure == null ? null : erasure.getQualifiedName();
			if (name != null && excluded.contains(name)) {
				return name;
			}
			current= current.getSuperclass();
		}
		return null;
	}
}