TokenOwnerDetector.java

/*
 * Copyright (C) 2007-2010 J��lio Vilmar Gesser.
 * Copyright (C) 2011, 2013-2026 The JavaParser Team.
 *
 * This file is part of JavaParser.
 *
 * JavaParser can be used either under the terms of
 * a) the GNU Lesser General Public License as published by
 *     the Free Software Foundation, either version 3 of the License, or
 *     (at your option) any later version.
 * b) the terms of the Apache License
 *
 * You should have received a copy of both licenses in LICENCE.LGPL and
 * LICENCE.APACHE. Please refer to those files for details.
 *
 * JavaParser is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 */
package com.github.javaparser.printer.lexicalpreservation;

import com.github.javaparser.ast.Node;
import com.github.javaparser.ast.body.FieldDeclaration;
import com.github.javaparser.ast.type.Type;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;

/**
 * Detects which node actually owns the tokens for a given node in the AST.
 *
 * <p>This utility is essential for the LexicalPreservingPrinter because the token
 * assignment algorithm assigns tokens based on position in the source code, not
 * necessarily to the logical AST node.
 *
 * <p><b>Core Problem:</b> In JavaParser's AST, child nodes may appear in the source
 * code <i>before</i> their parent node's position. The LPP assigns tokens to the
 * <i>nearest enclosing node</i> whose range includes the token's position.
 *
 * <p><b>Example:</b> In {@code Set<Pair<String, String>> x;}, the tokens for
 * {@code Pair<String, String>} are assigned to {@code VariableDeclarationExpr},
 * not to the {@code Pair} type node. When replacing {@code Pair}, the LPP needs
 * to know to regenerate {@code VariableDeclarationExpr}.
 *
 * <p><b>Implementation:</b> Uses the Strategy pattern with multiple detection
 * strategies, each handling a specific category of nodes (Types, Annotations,
 * Modifiers, etc.). Strategies are tried in priority order.
 *
 * @since 3.28.0
 */
class TokenOwnerDetector {

    /**
     * Strategy interface for detecting token owners.
     *
     * <p>Each strategy implementation checks if it applies to the given node,
     * then searches for the token owner by walking up the AST.
     */
    @FunctionalInterface
    interface DetectionStrategy {

        /**
         * Attempts to find the token owner for the given node.
         *
         * @param node the node to analyze
         * @return the token owner if this strategy applies, empty otherwise
         */
        Optional<Node> detect(Node node);
    }

    /**
     * Detection strategies in priority order.
     *
     * <p>Order matters: strategies are tried sequentially, first match wins.
     * Most frequent cases are placed first for performance (early exit).
     *
     * <p>Priority rationale:
     * <ol>
     *   <li>TypeOwnerStrategy - Most common, critical for Issue #3365</li>
     *   <li>AnnotationOwnerStrategy - Common in modern Java code</li>
     *   <li>ModifierOwnerStrategy - Moderately common</li>
     *   <li>TypeParameterOwnerStrategy - Less common (generics only)</li>
     *   <li>NameInExpressionStrategy - Rare edge cases</li>
     * </ol>
     */
    private static final List<DetectionStrategy> STRATEGIES = Arrays.asList(new TypeOwnerStrategy());

    /**
     * Finds the node that owns the tokens for the given node.
     *
     * <p>Algorithm:
     * <ol>
     *   <li>Try each detection strategy in priority order</li>
     *   <li>Return the first non-null owner found</li>
     *   <li>If no strategy applies, the node owns its own tokens</li>
     * </ol>
     *
     * @param node the node to find the token owner for
     * @return the node that owns the tokens, never null
     * @throws IllegalArgumentException if node is null
     */
    static Node findTokenOwner(Node node) {
        if (node == null) {
            throw new IllegalArgumentException("node cannot be null");
        }
        // Try each strategy in order
        for (DetectionStrategy strategy : STRATEGIES) {
            Optional<Node> owner = strategy.detect(node);
            if (owner.isPresent() && owner.get() != node) {
                return owner.get();
            }
        }
        // Default: node owns its own tokens
        return node;
    }

    /**
     * Determines if token owner regeneration is needed after a node replacement.
     *
     * <p><b>Context:</b> When a node is replaced in the AST (e.g., replacing {@code Pair}
     * with {@code SimpleImmutableEntry} in Issue #3365), the LexicalPreservingPrinter's
     * Observer notifies the change. However, the LPP only regenerates the NodeText for
     * the immediate parent of the replaced node by default.
     *
     * <p><b>Problem:</b> If the tokens for the replaced node are actually owned by an
     * ancestor further up the tree (as detected by {@link #findTokenOwner(Node)}), the
     * LPP won't regenerate the correct NodeText, resulting in the change not appearing
     * in the output.
     *
     * <p><b>Example where regeneration is needed:</b>
     * <pre>{@code
     * Set<Pair<String, String>> x;
     *
     * // When replacing Pair type:
     * // - parent: TypeArguments (immediate parent of Pair)
     * // - tokenOwner: VariableDeclarationExpr (owns the tokens)
     * // - replacedNode: ClassOrInterfaceType (Pair)
     *
     * // Result: needsRegeneration = true (tokenOwner != parent)
     * }</pre>
     *
     * <p><b>Example where regeneration is NOT needed:</b>
     * <pre>{@code
     * x = 5;
     *
     * // When replacing the literal 5:
     * // - parent: AssignExpr (immediate parent of literal)
     * // - tokenOwner: AssignExpr (same as parent)
     * // - replacedNode: IntegerLiteralExpr (5)
     *
     * // Result: needsRegeneration = false (normal LPP handling works)
     * }</pre>
     *
     * <p><b>Decision criteria:</b>
     * <ol>
     *   <li>If tokenOwner == parent: No regeneration needed (normal path)</li>
     *   <li>If replacedNode is a Type: Regeneration needed (Issue #3365 case)</li>
     *   <li>If replacedNode is inside a Type: Regeneration needed (nested case)</li>
     *   <li>Otherwise: No regeneration needed</li>
     * </ol>
     *
     * @param parent the immediate parent of the replaced node (where LPP would normally regenerate)
     * @param tokenOwner the actual owner of the tokens (as detected by findTokenOwner)
     * @param replacedNode the node being replaced in the AST
     * @return true if the tokenOwner's NodeText should be regenerated, false if normal LPP handling is sufficient
     */
    static boolean needsRegeneration(Node parent, Node tokenOwner, Node replacedNode) {
        // Case 1: Token owner is the same as the immediate parent
        // This is the normal case where LPP's default behavior (regenerating the parent) works correctly.
        // Example: x = 5; ��� replacing 5 in AssignExpr
        if (tokenOwner.equals(parent)) {
            return false;
        }
        // WORKAROUND: Multiple variable declarations share same type
        if (tokenOwner instanceof FieldDeclaration) {
            FieldDeclaration field = (FieldDeclaration) tokenOwner;
            if (field.getVariables().size() > 1) {
                // Let LPP handle it normally
                return false;
            }
        }
        // Case 2: Replaced node is directly a Type
        // This is the most common case requiring special handling (Issue #3365).
        // Types in declarations have their tokens owned by the declaration, not by the Type node itself.
        // Example: Set<Pair<...>> x; ��� replacing Pair type
        if (replacedNode instanceof Type) {
            return true;
        }
        // Case 3: Replaced node is contained within a Type
        // This handles nested cases where a node inside a type (e.g., type arguments) is replaced.
        // We walk up from the replaced node to the parent, checking if we pass through a Type node.
        // Example: Set<Pair<String, String>> ��� replacing "String" inside Pair's type arguments
        Node current = replacedNode.getParentNode().orElse(null);
        while (current != null && current != parent) {
            if (current instanceof Type) {
                // Found a Type node in the ancestry chain ��� regeneration needed
                return true;
            }
            current = current.getParentNode().orElse(null);
        }
        // Case 4: None of the above
        // The replaced node is not type-related and tokenOwner != parent.
        // This is rare but possible (e.g., annotations, modifiers in some cases).
        // Conservative approach: don't regenerate unless we're sure we need to.
        // If this causes issues, we can add more cases (annotations, modifiers, etc.)
        return false;
    }
}