TypeOwnerStrategy.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.*;
import com.github.javaparser.ast.expr.*;
import com.github.javaparser.ast.stmt.ExplicitConstructorInvocationStmt;
import com.github.javaparser.ast.stmt.ExpressionStmt;
import com.github.javaparser.ast.type.Type;
import com.github.javaparser.utils.Optionals;
import java.util.List;
import java.util.Optional;
/**
* Strategy for detecting token owners of Type nodes and their children.
*
* <p>This is the most complex strategy because types appear in many contexts:
* <ul>
* <li>Variable declarations (local, field)</li>
* <li>Method return types</li>
* <li>Parameters</li>
* <li>Extends/implements clauses</li>
* <li>Cast expressions</li>
* <li>Instanceof expressions</li>
* <li>Type parameters and bounds</li>
* </ul>
*
* <p>This strategy is critical for fixing Issue #3365 where nested generic types
* (e.g., {@code Set<Pair<String, String>>}) were not properly updated by the LPP.
*/
class TypeOwnerStrategy implements TokenOwnerDetector.DetectionStrategy {
@Override
public Optional<Node> detect(Node node) {
// Check if this strategy applies
Type type = findTypeNode(node);
if (type == null) {
return Optional.empty();
}
// Find the owner
Node owner = findTypeOwner(type);
return Optional.ofNullable(owner);
}
/**
* Finds the Type node for the given node.
* Returns the node itself if it's a Type, or searches ancestors.
*
* @param node the node to analyze
* @return the Type node, or null if not found or not in a type context
*/
private Type findTypeNode(Node node) {
if (node instanceof Type) {
return (Type) node;
}
// Search ancestors, but stop at boundaries
Node current = node;
while (current != null) {
if (current instanceof Type) {
return (Type) current;
}
// Stop at boundaries that indicate we're not in a type context
if (current instanceof BodyDeclaration || current instanceof ExpressionStmt) {
return null;
}
current = current.getParentNode().orElse(null);
}
return null;
}
/**
* Finds the declaration that owns the tokens for the given type.
*
* <p>Algorithm: Walk up the AST from the type, checking each parent
* against known contexts where types appear. Return the first matching
* declaration.
*
* @param type the type node (never null)
* @return the token owner, or the type itself if no owner found
*/
private Node findTypeOwner(Type type) {
Node current = type;
while (current != null) {
Node parent = current.getParentNode().orElse(null);
if (parent == null) {
break;
}
final Node currentNode = current;
// Check each context using Optional chaining for clean code
Optional<Node> owner = Optionals.or(
() -> checkVariableContext(parent, currentNode),
() -> checkParameterContext(parent, currentNode),
() -> checkMethodContext(parent, currentNode),
() -> checkClassContext(parent, currentNode),
() -> checkExpressionContext(parent, currentNode),
() -> checkStatementContext(parent, currentNode));
if (owner.isPresent()) {
return owner.get();
}
current = parent;
}
// Fallback: type owns its own tokens
return type;
}
// ========================================================================
// CONTEXT CHECKERS
// Each method checks if the parent is a specific context that owns tokens
// ========================================================================
/**
* Checks variable declaration contexts: local variables, fields, enum constants.
*
* @param parent the potential owner
* @param current the current node in the walk
* @return the owner if this context applies
*/
private Optional<Node> checkVariableContext(Node parent, Node current) {
// Local variable declaration
if (parent instanceof VariableDeclarationExpr) {
return Optional.of(parent);
}
// Field declaration
if (parent instanceof FieldDeclaration) {
return Optional.of(parent);
}
// Enum constant declaration
if (parent instanceof EnumConstantDeclaration) {
return Optional.of(parent);
}
return Optional.empty();
}
/**
* Checks parameter contexts: method parameters, receiver parameters.
*
* @param parent the potential owner
* @param current the current node in the walk
* @return the owner if this context applies
*/
private Optional<Node> checkParameterContext(Node parent, Node current) {
// Method/constructor parameter
if (parent instanceof Parameter) {
return Optional.of(parent);
}
// Receiver parameter (e.g., void method(MyClass MyClass.this))
if (parent instanceof ReceiverParameter) {
return Optional.of(parent);
}
return Optional.empty();
}
/**
* Checks method/constructor contexts: return types, constructor declarations.
*
* @param parent the potential owner
* @param current the current node in the walk
* @return the owner if this context applies
*/
private Optional<Node> checkMethodContext(Node parent, Node current) {
// Method return type
if (parent instanceof MethodDeclaration) {
MethodDeclaration method = (MethodDeclaration) parent;
// Verify that current is actually the return type (not a parameter type)
if (current.equals(method.getType()) || isAncestorOf(method.getType(), current)) {
return Optional.of(parent);
}
}
// Annotation member declaration
if (parent instanceof AnnotationMemberDeclaration) {
AnnotationMemberDeclaration member = (AnnotationMemberDeclaration) parent;
if (current.equals(member.getType()) || isAncestorOf(member.getType(), current)) {
return Optional.of(parent);
}
}
// Constructor declaration
if (parent instanceof ConstructorDeclaration) {
return Optional.of(parent);
}
return Optional.empty();
}
/**
* Checks class/interface contexts: extends, implements, permits clauses.
*
* @param parent the potential owner
* @param current the current node in the walk
* @return the owner if this context applies
*/
private Optional<Node> checkClassContext(Node parent, Node current) {
// Class or interface declaration
if (parent instanceof ClassOrInterfaceDeclaration) {
ClassOrInterfaceDeclaration classDecl = (ClassOrInterfaceDeclaration) parent;
// Check extends clause
if (isInTypeList(classDecl.getExtendedTypes(), current)) {
return Optional.of(parent);
}
// Check implements clause
if (isInTypeList(classDecl.getImplementedTypes(), current)) {
return Optional.of(parent);
}
// Check permits clause (sealed classes - Java 17+)
if (isInTypeList(classDecl.getPermittedTypes(), current)) {
return Optional.of(parent);
}
}
// Record declaration (Java 14+)
if (parent instanceof RecordDeclaration) {
RecordDeclaration recordDecl = (RecordDeclaration) parent;
// Check implements clause
if (isInTypeList(recordDecl.getImplementedTypes(), current)) {
return Optional.of(parent);
}
}
// Enum declaration
if (parent instanceof EnumDeclaration) {
EnumDeclaration enumDecl = (EnumDeclaration) parent;
// Check implements clause
if (isInTypeList(enumDecl.getImplementedTypes(), current)) {
return Optional.of(parent);
}
}
return Optional.empty();
}
/**
* Checks expression contexts: cast, instanceof, record pattern.
*
* @param parent the potential owner
* @param current the current node in the walk
* @return the owner if this context applies
*/
private Optional<Node> checkExpressionContext(Node parent, Node current) {
// Cast expression: (String) obj
if (parent instanceof CastExpr) {
CastExpr cast = (CastExpr) parent;
// Verify current is the cast type
if (current.equals(cast.getType()) || isAncestorOf(cast.getType(), current)) {
// The cast expression itself owns the type tokens
return Optional.of(parent);
}
}
// Instanceof expression: obj instanceof String
if (parent instanceof InstanceOfExpr) {
InstanceOfExpr instanceOf = (InstanceOfExpr) parent;
// Verify current is the type being checked
if (current.equals(instanceOf.getType()) || isAncestorOf(instanceOf.getType(), current)) {
// The instanceof expression owns the type tokens
return Optional.of(parent);
}
}
// Record pattern expression
if (parent instanceof RecordPatternExpr) {
RecordPatternExpr pattern = (RecordPatternExpr) parent;
if (current.equals(pattern.getType()) || isAncestorOf(pattern.getType(), current)) {
return Optional.of(parent);
}
}
return Optional.empty();
}
/**
* Checks statement contexts: explicit constructor invocations.
*
* @param parent the potential owner
* @param current the current node in the walk
* @return the owner if this context applies
*/
private Optional<Node> checkStatementContext(Node parent, Node current) {
// Explicit constructor invocation: this(...), super(...)
if (parent instanceof ExplicitConstructorInvocationStmt) {
return Optional.of(parent);
}
return Optional.empty();
}
// ========================================================================
// UTILITY METHODS
// ========================================================================
/**
* Checks if a node is contained in a list of types (extends, implements, permits).
*
* @param types the list of types to check
* @param node the node to search for
* @return true if node is in the list or is a descendant of a type in the list
*/
private boolean isInTypeList(List<? extends Type> types, Node node) {
for (Type type : types) {
if (type.equals(node) || isAncestorOf(type, node)) {
return true;
}
}
return false;
}
/**
* Checks if potentialAncestor is actually an ancestor of node.
*
* @param potentialAncestor the potential ancestor
* @param node the node to check
* @return true if potentialAncestor is an ancestor of node
*/
private boolean isAncestorOf(Node potentialAncestor, Node node) {
Node current = node.getParentNode().orElse(null);
while (current != null) {
if (current.equals(potentialAncestor)) {
return true;
}
current = current.getParentNode().orElse(null);
}
return false;
}
}