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;
}
/**
* Helper method to check if a method body contains a super.dispose() call.
*
* <p>This is a simple implementation that only checks top-level statements.
* A production implementation would need to handle nested blocks, conditional
* statements, and other control flow structures.</p>
*
* @param body the method body to check
* @return true if super.dispose() is called, false otherwise
*/
private static boolean containsSuperDisposeCall(Block body) {
if (body == null) {
return false;
}
// Simple check - in production code, this would need to handle nested blocks
for (Object stmt : body.statements()) {
if (stmt instanceof ExpressionStatement) {
ExpressionStatement exprStmt = (ExpressionStatement) stmt;
if (exprStmt.getExpression() instanceof SuperMethodInvocation) {
SuperMethodInvocation superCall = (SuperMethodInvocation) exprStmt.getExpression();
if (LibStandardNames.METHOD_DISPOSE.equals(superCall.getName().getIdentifier())) {
return true;
}
}
}
}
return false;
}
}