VisibilityService.java

/*
 * SPDX-FileCopyrightText: 2025 kaumei.io
 * SPDX-License-Identifier: Apache-2.0
 */
package io.kaumei.jdbc.anno.model;

import javax.lang.model.element.*;
import javax.lang.model.type.*;
import javax.lang.model.util.Elements;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.Set;

public final class VisibilityService {

    enum VisibilityKind {
        PUBLIC,
        PACKAGE,
        NOT_VISIBLE;

        static VisibilityKind of(Element element) {
            var modifiers = element.getModifiers();
            if (modifiers.contains(Modifier.PRIVATE) || modifiers.contains(Modifier.PROTECTED)) {
                return VisibilityKind.NOT_VISIBLE;
            } else if (modifiers.contains(Modifier.PUBLIC)) {
                return VisibilityKind.PUBLIC;
            }
            return VisibilityKind.PACKAGE;
        }

        VisibilityKind and(Element elem) {
            var other = VisibilityKind.of(elem);
            if (this == NOT_VISIBLE || other == NOT_VISIBLE) {
                return NOT_VISIBLE;
            } else if (this == PUBLIC) {
                return other;
            } else if (other == PUBLIC) {
                return this;
            }
            return PACKAGE;
        }
    }

    // ------------------------------------------------------------------------

    private final Elements elements;

    public VisibilityService(Elements elements) {
        this.elements = elements;
    }

    public boolean isVisible(Element from, Visibility v) {
        return v.isPublic() || v.isVisible(this.elements.getPackageOf(from));
    }

    // ------------------------------------------------------------------------

    public Visibility ofElement(Element elem) {

        var visibility = VisibilityKind.of(elem);

        var current = elem.getEnclosingElement();
        while (current instanceof TypeElement type) {
            visibility = visibility.and(type);
            current = current.getEnclosingElement();
        }

        if (visibility != VisibilityKind.NOT_VISIBLE && current instanceof PackageElement pkg) {
            if (visibility == VisibilityKind.PUBLIC) {
                return Visibility.PUBLIC_VISIBILITY;
            }
            return Visibility.packageAccess(pkg);
        }
        return Visibility.NO_VISIBLE;
    }

    public Visibility ofMethod(ExecutableElement executable) {
        var result = ofElement(executable);
        result = result.and(ofTypeMirror(executable.getReturnType()));
        for (VariableElement parameter : executable.getParameters()) {
            result = result.and(ofTypeMirror(parameter.asType()));
        }
        for (TypeParameterElement parameter : executable.getTypeParameters()) {
            for (TypeMirror bound : parameter.getBounds()) {
                result = result.and(ofTypeMirror(bound));
            }
        }
        return result;
    }

    // ------------------------------------------------------------------------

    private Visibility ofTypeMirror(TypeMirror type) {
        Set<TypeMirror> visited = Collections.newSetFromMap(new IdentityHashMap<>());
        return of(type, visited);
    }

    private Visibility of(TypeMirror type, Set<TypeMirror> visited) {
        if (!visited.add(type)) {
            return Visibility.PUBLIC_VISIBILITY;
        }

        return switch (type.getKind()) {
            case ARRAY -> of(((ArrayType) type).getComponentType(), visited);
            case DECLARED, ERROR -> {
                DeclaredType declared = (DeclaredType) type;
                Visibility result = ofElement(declared.asElement());
                TypeMirror enclosingType = declared.getEnclosingType();
                if (enclosingType.getKind() != TypeKind.NONE) {
                    result = result.and(of(enclosingType, visited));
                }
                for (TypeMirror argument : declared.getTypeArguments()) {
                    result = result.and(of(argument, visited));
                }
                yield result;
            }
            case TYPEVAR -> {
                TypeVariable variable = (TypeVariable) type;
                yield of(variable.getUpperBound(), visited)
                        .and(of(variable.getLowerBound(), visited));
            }
            case WILDCARD -> {
                WildcardType wildcard = (WildcardType) type;
                Visibility result = Visibility.PUBLIC_VISIBILITY;
                if (wildcard.getExtendsBound() != null) {
                    result = result.and(of(wildcard.getExtendsBound(), visited));
                }
                if (wildcard.getSuperBound() != null) {
                    result = result.and(of(wildcard.getSuperBound(), visited));
                }
                yield result;
            }
            case INTERSECTION -> {
                Visibility result = Visibility.PUBLIC_VISIBILITY;
                for (TypeMirror bound : ((IntersectionType) type).getBounds()) {
                    result = result.and(of(bound, visited));
                }
                yield result;
            }
            default -> Visibility.PUBLIC_VISIBILITY;
        };
    }

}