MissingSuperDisposePlugin.java

/*******************************************************************************
 * Copyright (c) 2025 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 - initial API and implementation
 *******************************************************************************/
package org.sandbox.jdt.internal.corext.fix.helper;

import org.eclipse.jdt.core.dom.AST;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.Block;
import org.eclipse.jdt.core.dom.ExpressionStatement;
import org.eclipse.jdt.core.dom.MethodDeclaration;
import org.eclipse.jdt.core.dom.SuperMethodInvocation;
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.internal.common.LibStandardNames;
import org.sandbox.jdt.triggerpattern.api.BodyConstraint;
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;

/**
 * TriggerPattern-based helper for detecting missing super.dispose() calls in SWT Widget subclasses.
 *
 * <p>This plugin demonstrates the TriggerPattern framework for detecting missing super calls
 * in overridden methods. It specifically targets the common pattern in SWT/JFace where
 * Widget.dispose() must call super.dispose().</p>
 *
 * <p><b>Pattern Detected:</b> Methods that override {@code org.eclipse.swt.widgets.Widget.dispose()}
 * but don't call {@code super.dispose()}.</p>
 *
 * <p><b>Example:</b></p>
 * <pre>
 * class MyWidget extends Widget {
 *     {@literal @}Override
 *     public void dispose() {
 *         // Missing super.dispose() call
 *         cleanup();
 *     }
 * }
 * </pre>
 *
 * <p><b>Note:</b> This is a demonstration of the TriggerPattern API. The actual implementation
 * of override detection and body constraint checking is not yet fully implemented
 * in the TriggerPattern engine and will be completed in future updates.</p>
 *
 * @see org.eclipse.swt.widgets.Widget#dispose()
 * @since 1.2.6
 */
public class MissingSuperDisposePlugin {

	/**
	 * Detects missing super.dispose() call in dispose() method overrides.
	 *
	 * <p>This method demonstrates the proposed API for detecting missing super calls.
	 * When the TriggerPattern engine is fully implemented, this will automatically match
	 * any dispose() method that overrides Widget.dispose() but doesn't call super.dispose().</p>
	 *
	 * <p>The {@code @TriggerPattern} annotation specifies:</p>
	 * <ul>
	 * <li>Pattern: {@code void dispose()} - matches methods with this signature</li>
	 * <li>Kind: METHOD_DECLARATION - matches method declarations (not invocations)</li>
	 * <li>Overrides: org.eclipse.swt.widgets.Widget - only matches if overriding Widget.dispose()</li>
	 * </ul>
	 *
	 * <p>The {@code @BodyConstraint} annotation specifies:</p>
	 * <ul>
	 * <li>mustContain: {@code super.dispose()} - the pattern to look for in method body</li>
	 * <li>negate: true - triggers when the pattern is NOT found (i.e., missing super call)</li>
	 * </ul>
	 *
	 * @param ctx the hint context containing the matched node and rewrite utilities
	 * @return a completion proposal to add the missing super.dispose() call, or null if not applicable
	 */
	@TriggerPattern(
		value = "void dispose()",
		kind = PatternKind.METHOD_DECLARATION,
		overrides = "org.eclipse.swt.widgets.Widget"
	)
	@BodyConstraint(mustContain = "super.dispose()", negate = true)
	@Hint(
		displayName = "Missing super.dispose() call",
		description = "Methods overriding Widget.dispose() should call super.dispose()"
	)
	public static IJavaCompletionProposal detectMissingDisposeCall(HintContext ctx) {
		ASTNode matchedNode = ctx.getMatch().getMatchedNode();

		if (!(matchedNode instanceof MethodDeclaration)) {
			return null;
		}

		MethodDeclaration method = (MethodDeclaration) matchedNode;
		Block body = method.getBody();

		// Cannot add super call if there's no body (e.g., abstract/interface method)
		if (body == null) {
			return null;
		}

		// For now, we manually check if super.dispose() is called
		// In the future, this would be handled by the BodyConstraint annotation
		if (containsSuperDisposeCall(body)) {
			return null; // Super call already present
		}

		// Create a fix that adds super.dispose() at the end of the method
		AST ast = ctx.getASTRewrite().getAST();

		SuperMethodInvocation superCall = ast.newSuperMethodInvocation();
		superCall.setName(ast.newSimpleName(LibStandardNames.METHOD_DISPOSE));

		ExpressionStatement superCallStmt = ast.newExpressionStatement(superCall);

		// Add the super call as the last statement in the method
		ctx.getASTRewrite().getListRewrite(body, Block.STATEMENTS_PROPERTY)
			.insertLast(superCallStmt, null);

		String label = "Add missing super.dispose() call"; //$NON-NLS-1$
		ASTRewriteCorrectionProposal proposal = new ASTRewriteCorrectionProposal(
			label,
			ctx.getICompilationUnit(),
			ctx.getASTRewrite(),
			10, // relevance
			(Image) null
		);

		return proposal;
	}

	/**
	 * Checks if a method body contains a top-level {@code super.dispose()} call.
	 *
	 * <p>This intentionally implements the current conservative production contract:
	 * nested control-flow paths are not treated as an unconditional disposal call.</p>
	 *
	 * @param body the method body to check, or {@code null}
	 * @return {@code true} if a top-level {@code super.dispose()} call exists
	 */
	public static boolean containsSuperDisposeCall(Block body) {
		if (body == null) {
			return false;
		}

		for (Object stmt : body.statements()) {
			if (stmt instanceof ExpressionStatement exprStmt
					&& exprStmt.getExpression() instanceof SuperMethodInvocation superCall
					&& LibStandardNames.METHOD_DISPOSE.equals(superCall.getName().getIdentifier())) {
				return true;
			}
		}

		return false;
	}
}