SystemOutFixCore.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 org.eclipse.core.resources.IMarker;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.CompilationUnit;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.FieldAccess;
import org.eclipse.jdt.core.dom.IVariableBinding;
import org.eclipse.jdt.core.dom.MethodInvocation;
import org.eclipse.jdt.core.dom.Name;
import org.eclipse.jdt.core.dom.QualifiedName;
import org.sandbox.jdt.triggerpattern.eclipse.HintFinding;

/**
 * Fix core for detecting {@code System.out.println()} and {@code System.err.println()} calls.
 *
 * <p>This is a hint-only cleanup that flags direct usage of
 * {@code System.out}/{@code System.err} for printing.
 * Findings are reported as problem markers via {@link HintFinding}.</p>
 */
public class SystemOutFixCore {

	/**
	 * Finds System.out/System.err usage in the compilation unit.
	 *
	 * @param compilationUnit the compilation unit to search
	 * @param findings the list to collect hint-only findings into
	 */
	public static void findFindings(CompilationUnit compilationUnit,
			List<HintFinding> findings) {

		compilationUnit.accept(new ASTVisitor() {
			@Override
			public boolean visit(MethodInvocation node) {
				Expression expr = node.getExpression();
				if (expr == null) {
					return true;
				}
				if (isSystemOutOrErr(expr)) {
					findings.add(new HintFinding(
							"System.out/err usage \u2014 consider using a logger", //$NON-NLS-1$
							compilationUnit.getLineNumber(node.getStartPosition()),
							node.getStartPosition(),
							node.getStartPosition() + node.getLength(),
							IMarker.SEVERITY_WARNING));
				}
				return true;
			}
		});
	}

	private static boolean isSystemOutOrErr(Expression expr) {
		if (expr instanceof QualifiedName qn) {
			String fullName = qn.getFullyQualifiedName();
			if ("System.out".equals(fullName) || "System.err".equals(fullName) //$NON-NLS-1$ //$NON-NLS-2$
					|| "java.lang.System.out".equals(fullName) //$NON-NLS-1$
					|| "java.lang.System.err".equals(fullName)) { //$NON-NLS-1$
				return true;
			}
			// Fall through to binding-based check for aliases or static imports
			IVariableBinding binding = resolveFieldBinding(qn);
			if (binding != null) {
				return isSystemField(binding);
			}
		}
		if (expr instanceof FieldAccess fa) {
			IVariableBinding binding = fa.resolveFieldBinding();
			if (binding != null) {
				return isSystemField(binding);
			}
		}
		if (expr instanceof Name name) {
			IVariableBinding binding = resolveFieldBinding(name);
			if (binding != null) {
				return isSystemField(binding);
			}
		}
		return false;
	}

	private static boolean isSystemField(IVariableBinding binding) {
		String fieldName = binding.getName();
		if ("out".equals(fieldName) || "err".equals(fieldName)) { //$NON-NLS-1$ //$NON-NLS-2$
			return binding.getDeclaringClass() != null
					&& "java.lang.System".equals(binding.getDeclaringClass().getQualifiedName()); //$NON-NLS-1$
		}
		return false;
	}

	private static IVariableBinding resolveFieldBinding(Name name) {
		if (name.resolveBinding() instanceof IVariableBinding vb && vb.isField()) {
			return vb;
		}
		return null;
	}
}