diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 00000000..75e7d785 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @ljnelson diff --git a/.github/workflows/mvn-release-prepare-perform.yaml b/.github/workflows/mvn-release-prepare-perform.yaml new file mode 100644 index 00000000..1097ac98 --- /dev/null +++ b/.github/workflows/mvn-release-prepare-perform.yaml @@ -0,0 +1,126 @@ +name: 'Workflow: Maven Release: Prepare and Perform' +run-name: 'Workflow Run: Maven Release: Prepare and Perform' +on: + workflow_dispatch: + inputs: + dryRun: + default: true + description: 'Dry run?' + type: 'boolean' + mvnDebug: + default: false + description: 'Debug?' + type: 'boolean' + mvnTransferLogging: + default: false + description: 'Log Maven artifact transfers?' + type: 'boolean' +jobs: + job-mvn-release-prepare-perform: + name: 'Job: Maven Release: Prepare and Perform' + permissions: + contents: 'read' + runs-on: 'ubuntu-latest' + steps: + - id: 'checkout' + name: 'Step: Check Out Project' + uses: 'actions/checkout@v4' + with: + fetch-depth: 1 + persist-credentials: false + - id: 'setup-java' + name: 'Step: Set Up Java and Maven' + uses: 'actions/setup-java@v3' + with: + cache: 'maven' + distribution: 'temurin' + gpg-passphrase: 'GPG_PASSPHRASE' + gpg-private-key: '${{ secrets.GPG_PRIVATE_KEY }}' + java-version: '21' + mvn-toolchain-id: 'Temurin 21' + mvn-toolchain-vendor: 'openjdk' # see ../../pom.xml + server-id: 'sonatype-oss-repository-hosting' # see https://github.com/microbean/microbean-parent/blob/master/pom.xml#L38 + server-password: 'SONATYPE_OSSRH_PASSWORD' + server-username: 'SONATYPE_OSSRH_USERNAME' + - id: 'setup-askpass' + name: 'Step: Set Up GIT_ASKPASS' + run: | + install -m 700 /dev/null "${RUNNER_TEMP}/.askpass" # atomically create empty file with appropriate permissions + cat >> "${RUNNER_TEMP}/.askpass" <<<'#!/bin/bash + case "${1}" in + Username*) exec echo x-access-token ;; + Password*) exec echo "${PUSH_TOKEN}" ;; + esac' + - id: 'setup-gpg' + name: 'Step: Set Up GPG' + run: | + echo 'pinentry-mode loopback' >> ~/.gnupg/gpg.conf + - id: 'mvn-release-prepare' + name: 'Step: Maven Release: Prepare, Perform and Publish Site' + env: + DRY_RUN: '${{ inputs.dryRun }}' + GIT_ASKPASS: '${{ runner.temp }}/.askpass' + GPG_PASSPHRASE: '${{ secrets.GPG_PASSPHRASE }}' + MVN_DEBUG: ${{ inputs.mvnDebug && '--debug' || '' }} + MVN_TRANSFER_LOGGING: ${{ inputs.mvnTransferLogging && '' || '--no-transfer-progress' }} + PUSH_TOKEN : '${{ secrets.PUSH_TOKEN }}' # critical; see ${GIT_ASKPASS} file + SCM_GIT_HTTPS_URL: 'scm:git:${{ github.server_url }}/${{ github.repository }}.git' + SONATYPE_OSSRH_PASSWORD: '${{ secrets.SONATYPE_OSSRH_PASSWORD }}' + SONATYPE_OSSRH_STAGING_PROFILE_ID: '${{ vars.SONATYPE_OSSRH_STAGING_PROFILE_ID }}' + SONATYPE_OSSRH_USERNAME: '${{ secrets.SONATYPE_OSSRH_USERNAME }}' + shell: 'bash -e {0}' + run: > + git config --global user.email 'ci@microbean.org' + + git config --global user.name 'microbean' + + echo "::group::Running mvn prepare" + + mvn --batch-mode ${MVN_DEBUG} --errors ${MVN_TRANSFER_LOGGING} release:prepare + -DdryRun="${DRY_RUN}" + -Darguments="${MVN_TRANSFER_LOGGING}" + -Dscm.url="${SCM_GIT_HTTPS_URL}" + + scm_tag="$(grep '^scm.tag=' release.properties | cut -f 2 -d =)" + + echo "Prepared ${scm_tag}" >> "${GITHUB_STEP_SUMMARY}" + + echo "scm_tag=${scm_tag}" >> "${GITHUB_OUTPUT}" + + echo "::endgroup::" + + echo "::group::Running mvn perform" + + set +e + + { + mvn --batch-mode ${MVN_DEBUG} --errors ${MVN_TRANSFER_LOGGING} release:perform + -Darguments="${MVN_TRANSFER_LOGGING} -Dscmpublish.dryRun=${DRY_RUN} -Dscmpublish.pubScmUrl=${SCM_GIT_HTTPS_URL} -DskipTests -DstagingProfileId=${SONATYPE_OSSRH_STAGING_PROFILE_ID}" + -DdryRun="${DRY_RUN}" + -Dgoals="verify,post-site,scm-publish:publish-scm,deploy" + -Dscm.url="${SCM_GIT_HTTPS_URL}" + | + tee /dev/fd/3 + | + grep --invert-match --silent 'Java class com.sonatype.nexus.staging.api.dto.StagingProfileRepositoryDTO' || cat > /dev/null + ; + } + 3>&1 + + exit_codes=(${PIPESTATUS[@]}) + + echo "::endgroup::" + + set -e + + if [ "${exit_codes[2]}" -ne 0 ] ; then + # grep "failed" (found com.sonatype.nexus.staging.api.dto.StagingProfileRepositoryDTO) and mvn failed + echo "Released ${scm_tag} successfully, but verify that the staging repository was successfully released" >> "${GITHUB_STEP_SUMMARY}"; + # Treat this as a successful run + exit 0; + elif [ "${exit_codes[0]}" -eq 0 ] ; then + # mvn succeeded and grep "succeeded" (did not find com.sonatype.nexus.staging.api.dto.StagingProfileRepositoryDTO) + echo "Released ${scm_tag} successfully" >> "${GITHUB_STEP_SUMMARY}"; + fi + + exit "${exit_codes[0]}" diff --git a/.github/workflows/mvn-verify.yaml b/.github/workflows/mvn-verify.yaml new file mode 100644 index 00000000..1c5a9f19 --- /dev/null +++ b/.github/workflows/mvn-verify.yaml @@ -0,0 +1,30 @@ +name: 'Workflow: Maven Verify' +run-name: 'Workflow Run: Maven Verify' +on: + - 'pull_request' + - 'push' +jobs: + job-mvn-verify: + name: 'Job: Maven Verify' + permissions: + contents: 'read' + runs-on: 'ubuntu-latest' + steps: + - id: 'checkout' + name: 'Step: Checkout' + uses: 'actions/checkout@v4' + with: + fetch-depth: 1 + persist-credentials: false + - id: 'setup-java' + name: 'Step: Set Up Java and Maven' + uses: 'actions/setup-java@v3' + with: + cache: 'maven' + distribution: 'temurin' + java-version: '21' + mvn-toolchain-id: 'Temurin 21' + mvn-toolchain-vendor: 'openjdk' # see ../../pom.xml + - id: 'mvn-verify' + name: 'Step: Maven Verify' + run: 'mvn --batch-mode --color never --errors --no-transfer-progress -Dorg.slf4j.simpleLogger.defaultLogLevel=info --threads 2C verify' diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..261eeb9e --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 00000000..8216234a --- /dev/null +++ b/README.md @@ -0,0 +1,37 @@ +# microBean™ Lang + +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.microbean/microbean-lang/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.microbean/microbean-lang) + +The microBean™ Lang project provides classes and interfaces that implement the Java language model. + +# Status + +This project is currently experimental, in a pre-alpha state, and unsuitable for production use. + +# Compatibility + +**Until further notice, this project's APIs are subject to frequent backwards-incompatible signature and behavior +changes, regardless of project version and without notice.** + +# Requirements + +microBean™ Lang requires a Java runtime of version 19 or higher. + +# Installation + +microBean™ Lang is or will be available on [Maven Central](https://search.maven.org/). Include microBean™ Lang as a +Maven dependency: + +```xml + + org.microbean + microbean-lang + + 0.0.1 + +``` + +# Documentation + +Full documentation is or will be available at +[microbean.github.io/microbean-lang](https://microbean.github.io/microbean-lang/). diff --git a/bytebuddy/NOTES.md b/bytebuddy/NOTES.md new file mode 100644 index 00000000..0b1d0d50 --- /dev/null +++ b/bytebuddy/NOTES.md @@ -0,0 +1,24 @@ +# Notes + +These notes are subject to change at any point for any reason without prior notice. + +## Elements + +ByteBuddy represents `javax.lang.model.element.TypeElement` objects as `TypeDescription`s. It represents their +underlying types as `TypeDescription.Generic`s. + +Some commonality between "elements" and "types" is stored at the `TypeDefinition` level. `TypeDefinition` appears (to +me) to be an abstraction not really suited to adapting to the `javax.lang.model.*` classes. Note, for example, that +`TypeDescription.Generic` (loosely equivalent to certain `TypeMirror` subclasses) overrides `getDeclaredFields` to +feature a more specific return type. The drawback to this abstraction is that if you view the world through this lens +it is unclear what is a type _usage_ (`TypeMirror`) and what is a type _declaration_ (`Element`). + +``` +(TODO: convert to true tables) + +javax.lang.model.* construct | Byte Buddy analog +------------------------------------------------ +TypeElement | TypeDescription +ExecutableElement | MethodDescription.InDefinedShape (I think?) +DeclaredType | TypeDescription.Generic of a partiuclar Sort +``` diff --git a/bytebuddy/README.md b/bytebuddy/README.md new file mode 100644 index 00000000..26e59ca8 --- /dev/null +++ b/bytebuddy/README.md @@ -0,0 +1,38 @@ +# microBean™ Lang: Byte Buddy + +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.microbean/microbean-lang-byte-buddy/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.microbean/microbean-lang-byte-buddy) + +The microBean™ Lang: Byte Buddy project provides classes and interfaces that implement the Java language model in terms +of Byte Buddy constructs. + +# Status + +This project is currently experimental, in a pre-alpha state, and unsuitable for production use. + +# Compatibility + +**Until further notice, this project's APIs are subject to frequent backwards-incompatible signature and behavior +changes, regardless of project version and without notice.** + +# Requirements + +microBean™ Lang: Byte Buddy requires a Java runtime of version 19 or higher. + +# Installation + +microBean™ Lang: Byte Buddy is or will be available on [Maven Central](https://search.maven.org/). Include microBean™ +Lang: Byte Buddy as a Maven dependency: + +```xml + + org.microbean + microbean-lang-byte-buddy + + 0.0.1 + +``` + +# Documentation + +Full documentation is or will be available at +[microbean.github.io/microbean-lang](https://microbean.github.io/microbean-lang/). diff --git a/bytebuddy/pom.xml b/bytebuddy/pom.xml new file mode 100644 index 00000000..dc37a805 --- /dev/null +++ b/bytebuddy/pom.xml @@ -0,0 +1,75 @@ + + + 4.0.0 + + microbean-lang-bytebuddy + + + org.microbean + microbean-lang-parent + 0.0.1-SNAPSHOT + + + microBean™ Lang: Byte Buddy + microBean™ Lang: Byte Buddy: An implementation of the Java language model backed by Byte Buddy. + + + + + + + net.bytebuddy + byte-buddy + jar + compile + + + + ${project.groupId} + microbean-lang + jar + compile + + + + + + org.hamcrest + hamcrest + jar + test + + + + org.junit.jupiter + junit-jupiter-api + jar + test + + + + org.junit.jupiter + junit-jupiter-engine + jar + test + + + + + + + + + maven-surefire-plugin + + + --add-opens jdk.compiler/com.sun.tools.javac.comp=org.microbean.lang + --add-opens jdk.compiler/com.sun.tools.javac.model=org.microbean.lang + + + + + + + + diff --git a/bytebuddy/src/main/java/module-info.java b/bytebuddy/src/main/java/module-info.java new file mode 100644 index 00000000..a98cd2e1 --- /dev/null +++ b/bytebuddy/src/main/java/module-info.java @@ -0,0 +1,28 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +/** + * Provides packages related to the Java language model and Byte Buddy. + * + * @author Laird Nelson + */ +module org.microbean.lang.bytebuddy { + + requires java.compiler; + requires transitive net.bytebuddy; + requires transitive org.microbean.lang; + + exports org.microbean.lang.bytebuddy; + +} diff --git a/bytebuddy/src/main/java/org/microbean/lang/bytebuddy/ByteBuddy2.java b/bytebuddy/src/main/java/org/microbean/lang/bytebuddy/ByteBuddy2.java new file mode 100644 index 00000000..a735b144 --- /dev/null +++ b/bytebuddy/src/main/java/org/microbean/lang/bytebuddy/ByteBuddy2.java @@ -0,0 +1,193 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.microbean.lang.bytebuddy; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import javax.lang.model.element.QualifiedNameable; +import javax.lang.model.element.TypeElement; + +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; +import javax.lang.model.type.WildcardType; + +import net.bytebuddy.description.type.TypeDefinition; +import net.bytebuddy.description.type.TypeDescription; + +import net.bytebuddy.pool.TypePool; + +import org.microbean.lang.CompletionLock; + +import static net.bytebuddy.description.type.TypeDescription.Generic.Builder; + +import static org.microbean.lang.Lang.generic; + +public final class ByteBuddy2 { + + private final TypePool typePool; + + public ByteBuddy2(final TypePool typePool) { + super(); + this.typePool = Objects.requireNonNull(typePool, "typePool"); + } + + public final TypePool typePool() { + return this.typePool; + } + + public final TypeDescription typeDescription(final TypeMirror t) { + CompletionLock.acquire(); + try { + return switch (t.getKind()) { + case NONE -> null; + + case BOOLEAN -> TypeDefinition.Sort.describe(boolean.class).asErasure(); + case BYTE -> TypeDefinition.Sort.describe(byte.class).asErasure(); + case CHAR -> TypeDefinition.Sort.describe(char.class).asErasure(); + case DOUBLE -> TypeDefinition.Sort.describe(double.class).asErasure(); + case FLOAT -> TypeDefinition.Sort.describe(float.class).asErasure(); + case INT -> TypeDefinition.Sort.describe(int.class).asErasure(); + case LONG -> TypeDefinition.Sort.describe(long.class).asErasure(); + case SHORT -> TypeDefinition.Sort.describe(short.class).asErasure(); + + case VOID -> TypeDefinition.Sort.describe(void.class).asErasure(); + + case ARRAY -> TypeDescription.ArrayProjection.of(typeDescription(((ArrayType)t).getComponentType())); + + case DECLARED -> typeDescription((QualifiedNameable)((DeclaredType)t).asElement()); + + default -> throw new IllegalArgumentException("t: " + t + "; kind: " + t.getKind()); + }; + } finally { + CompletionLock.release(); + } + } + + public final TypeDescription typeDescription(final QualifiedNameable qn) { + CompletionLock.acquire(); + try { + return this.typeDescription(qn.getQualifiedName().toString()); + } finally { + CompletionLock.release(); + } + } + + public final TypeDescription typeDescription(final String name) { // TODO: better named name; maybe canonical name following JLS? + return this.typePool.describe(name).resolve(); + } + + public final TypeDescription.Generic typeDescriptionGeneric(final TypeMirror t) { + CompletionLock.acquire(); + try { + return switch (t.getKind()) { + + case NONE -> null; + + // Primitives are easy. ByteBuddy caches them. + case BOOLEAN -> TypeDefinition.Sort.describe(boolean.class); + case BYTE -> TypeDefinition.Sort.describe(byte.class); + case CHAR -> TypeDefinition.Sort.describe(char.class); + case DOUBLE -> TypeDefinition.Sort.describe(double.class); + case FLOAT -> TypeDefinition.Sort.describe(float.class); + case INT -> TypeDefinition.Sort.describe(int.class); + case LONG -> TypeDefinition.Sort.describe(long.class); + case SHORT -> TypeDefinition.Sort.describe(short.class); + + // void is easy. ByteBuddy caches it. + case VOID -> TypeDefinition.Sort.describe(void.class); + + // Arrays are easy. + case ARRAY -> Builder.of(typeDescriptionGeneric(((ArrayType)t).getComponentType())).asArray().build(); + + // Declared types: + case DECLARED -> { + final DeclaredType dt = (DeclaredType)t; + final TypeElement te = (TypeElement)dt.asElement(); + final String n = te.getQualifiedName().toString(); + yield switch (n) { + // Certain declared types are easy because ByteBuddy also caches them, so the classes are loaded already. We add + // a few simple ones to the list. + case "java.lang.Boolean" -> TypeDefinition.Sort.describe(Boolean.class); + case "java.lang.Byte" -> TypeDefinition.Sort.describe(Byte.class); + case "java.lang.Character" -> TypeDefinition.Sort.describe(Character.class); + case "java.lang.Class" -> TypeDefinition.Sort.describe(Class.class); + case "java.lang.Double" -> TypeDefinition.Sort.describe(Double.class); + case "java.lang.Float" -> TypeDefinition.Sort.describe(Float.class); + case "java.lang.Integer" -> TypeDefinition.Sort.describe(Integer.class); + case "java.lang.Long" -> TypeDefinition.Sort.describe(Long.class); + case "java.lang.Object" -> TypeDefinition.Sort.describe(Object.class); + case "java.lang.Short" -> TypeDefinition.Sort.describe(Short.class); + case "java.lang.String" -> TypeDefinition.Sort.describe(String.class); + case "java.lang.Throwable" -> TypeDefinition.Sort.describe(Throwable.class); + // case "java.lang.Void" -> TypeDefinition.Sort.describe(Void.class); // ByteBuddy doesn't cache this for some reason + case "java.lang.annotation.Annotation" -> TypeDefinition.Sort.describe(java.lang.annotation.Annotation.class); + case "net.bytebuddy.dynamic.TargetType" -> TypeDefinition.Sort.describe(net.bytebuddy.dynamic.TargetType.class); + default -> { + // Some other declared type + final TypeDescription rawType = typeDescription(n); + if (!generic(te)) { + yield rawType.asGenericType(); + } + final TypeMirror dtEnclosingType = dt.getEnclosingType(); + final TypeDescription.Generic enclosingType = + dtEnclosingType == null ? TypeDescription.Generic.UNDEFINED : typeDescriptionGeneric(dtEnclosingType); + final List typeArgumentMirrors = dt.getTypeArguments(); + if (typeArgumentMirrors.isEmpty()) { + yield Builder.parameterizedType(rawType, enclosingType).build(); + } + final List typeArguments = new ArrayList<>(typeArgumentMirrors.size()); + for (final TypeMirror typeArgumentMirror : typeArgumentMirrors) { + typeArguments.add(typeDescriptionGeneric(typeArgumentMirror)); + } + yield Builder.parameterizedType(rawType, + enclosingType == null ? TypeDescription.Generic.UNDEFINED : enclosingType, + typeArguments) + .build(); + } + }; + } + + case TYPEVAR -> Builder.typeVariable(((TypeVariable)t).asElement().getSimpleName().toString()).build(); + + case WILDCARD -> { + final WildcardType w = (WildcardType)t; + final TypeMirror extendsBound = w.getExtendsBound(); + final TypeMirror superBound = w.getSuperBound(); + if (superBound == null) { + if (extendsBound == null) { + yield Builder.unboundWildcard(); + } + yield Builder.of(typeDescriptionGeneric(extendsBound)).asWildcardUpperBound(); + } else if (extendsBound == null) { + yield Builder.of(typeDescriptionGeneric(superBound)).asWildcardLowerBound(); + } else { + throw new AssertionError(); + } + } + + case ERROR, EXECUTABLE, INTERSECTION, MODULE, NULL, OTHER, PACKAGE, UNION -> throw new IllegalArgumentException("t: " + + t + + "; kind: " + + t.getKind()); + }; + } finally { + CompletionLock.release(); + } + } + +} diff --git a/bytebuddy/src/main/java/org/microbean/lang/bytebuddy/TypeElementTypePool.java b/bytebuddy/src/main/java/org/microbean/lang/bytebuddy/TypeElementTypePool.java new file mode 100644 index 00000000..9d55f65e --- /dev/null +++ b/bytebuddy/src/main/java/org/microbean/lang/bytebuddy/TypeElementTypePool.java @@ -0,0 +1,388 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.microbean.lang.bytebuddy; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.NestingKind; +import javax.lang.model.element.RecordComponentElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; + +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; + +import net.bytebuddy.ClassFileVersion; + +import net.bytebuddy.description.annotation.AnnotationValue; + +import net.bytebuddy.dynamic.ClassFileLocator; + +import net.bytebuddy.pool.TypePool; + +import org.microbean.lang.Lang; +import org.microbean.lang.TypeAndElementSource; + +import org.microbean.lang.element.DelegatingElement; + +import org.microbean.lang.type.DelegatingTypeMirror; + +public final class TypeElementTypePool extends TypePool.Default { + + private final ClassFileVersion classFileVersion; + + private final TypeAndElementSource tes; + + public TypeElementTypePool() { + this(ClassFileVersion.ofThisVm(), new TypePool.CacheProvider.Simple(), Lang.typeAndElementSource()); + } + + public TypeElementTypePool(final TypePool.CacheProvider cacheProvider) { + this(ClassFileVersion.ofThisVm(), cacheProvider, Lang.typeAndElementSource()); + } + + public TypeElementTypePool(final TypeAndElementSource tes) { + this(ClassFileVersion.ofThisVm(), new TypePool.CacheProvider.Simple(), tes); + } + + public TypeElementTypePool(final TypePool.CacheProvider cacheProvider, + final TypeAndElementSource tes) { + this(ClassFileVersion.ofThisVm(), cacheProvider, tes); + } + + public TypeElementTypePool(final ClassFileVersion classFileVersion, + final TypePool.CacheProvider cacheProvider, + final TypeAndElementSource tes) { + super(cacheProvider == null ? new TypePool.CacheProvider.Simple() : cacheProvider, + ClassFileLocator.NoOp.INSTANCE, + TypePool.Default.ReaderMode.FAST /* actually irrelevant */); + this.classFileVersion = classFileVersion == null ? ClassFileVersion.ofThisVm() : classFileVersion; + this.tes = Objects.requireNonNull(tes, "tes"); + } + + @Override + protected final Resolution doDescribe(final String name) { + final TypeElement e = this.tes.typeElement(name); + return e == null ? new Resolution.Illegal(name) : new Resolution.Simple(new TypeDescription(e)); + } + + private final class TypeDescription extends LazyTypeDescription { + + private static final String[] EMPTY_STRING_ARRAY = new String[0]; + + // Binary names in this mess are JVM binary names, not JLS binary names. Raph calls them "internal names" which + // isn't a thing. + private TypeDescription(final TypeElement e) { + super(TypeElementTypePool.this, + actualModifiers(e), + modifiers(e), + Lang.binaryName(e).toString(), // "internalName" + binaryName(e.getSuperclass()), // "superClassName" + interfaceBinaryNames(e), // "interfaceName" (yes, singular for some reason) + genericSignature(e), // "genericSignature"; ASM just calls it a "signature" and seems to be expecting a *type* signature in the JVM parlance + typeContainment(e), + declaringTypeBinaryName(e), + declaredTypeDescriptors(e), + e.getNestingKind() == NestingKind.ANONYMOUS, + nestHostBinaryName(e), + nestMemberBinaryNames(e), + superclassAnnotationTokens(e), + interfaceAnnotationTokens(e), + typeVariableAnnotationTokens(e), + typeVariableBoundsAnnotationTokens(e), + annotationTokens(e), + fieldTokens(e), + methodTokens(e), + recordComponentTokens(e), + permittedSubclassBinaryNames(e), + classFileVersion); + } + + private static final String binaryName(final TypeMirror t) { + assert t instanceof DelegatingTypeMirror; + return switch (t.getKind()) { + case DECLARED -> Lang.binaryName((TypeElement)((DeclaredType)t).asElement()).toString(); + case NONE -> null; // or empty string? + case TYPEVAR -> Lang.binaryName((TypeElement)((TypeVariable)t).asElement()).toString(); + default -> throw new IllegalArgumentException("t: " + t); + }; + } + + private static final int actualModifiers(final Element e) { + assert e instanceof DelegatingElement; + return (int)Lang.modifiers(e); + } + + private static final int modifiers(final Element e) { + assert e instanceof DelegatingElement; + return (int)Lang.modifiers(e); + } + + private static final String[] interfaceBinaryNames(final TypeElement e) { + assert e instanceof DelegatingElement; + final List ifaces = e.getInterfaces(); + if (ifaces.isEmpty()) { + return EMPTY_STRING_ARRAY; + } + final String[] rv = new String[ifaces.size()]; + for (int i = 0; i < rv.length; i++) { + rv[i] = binaryName(ifaces.get(i)); + } + return rv; + } + + private static final String genericSignature(final Element e) { + return Lang.elementSignature(e); + } + + private static final TypeContainment typeContainment(final Element e) { + assert e instanceof DelegatingElement; + final TypeElement ee = (TypeElement)e.getEnclosingElement(); + if (ee == null) { + return TypeContainment.SelfContained.INSTANCE; + } + return switch (ee.getKind()) { + case METHOD -> + new TypeContainment.WithinMethod(Lang.binaryName((TypeElement)ee.getEnclosingElement()).toString(), + ee.getSimpleName().toString(), // TODO: maybe? needs to be method's "internal name" which is just its "unqualified name" (4.2.2 JVM) + Lang.descriptor(ee.asType())) {}; + case ANNOTATION_TYPE, CLASS, ENUM, INTERFACE, RECORD -> + new TypeContainment.WithinType(Lang.binaryName(ee).toString(), + ee.getNestingKind() == NestingKind.LOCAL) {}; // TODO: this is for the enclosing element, yes? + case PACKAGE -> TypeContainment.SelfContained.INSTANCE; + default -> throw new IllegalStateException(); // I guess? + }; + } + + private static final String declaringTypeBinaryName(final TypeElement e) { + assert e instanceof DelegatingElement; + // TODO: triple check: getEnclosingType()? or getEnclosingElement.asType()? + final TypeMirror t = ((DeclaredType)e.asType()).getEnclosingType(); + if (t == null || t.getKind() == TypeKind.NONE) { + return null; + } + return Lang.binaryName((TypeElement)((DeclaredType)t).asElement()).toString(); + } + + private static final List declaredTypeDescriptors(final Element e) { + assert e instanceof DelegatingElement; + final ArrayList l = new ArrayList<>(); + for (final Element ee : e.getEnclosedElements()) { + if (ee.getKind().isDeclaredType()) { + l.add(Lang.descriptor(ee.asType())); + } + } + l.trimToSize(); + return Collections.unmodifiableList(l); + } + + private static final String nestHostBinaryName(final Element e) { + return null; + } + + private static final List nestMemberBinaryNames(final Element e) { + return List.of(); + } + + private static final Map> superclassAnnotationTokens(final Element e) { + return Map.of(); + } + + private static final Map>> interfaceAnnotationTokens(final Element e) { + return Map.of(); + } + + private static final Map>> typeVariableAnnotationTokens(final Element e) { + return Map.of(); + } + + private static final Map>>> typeVariableBoundsAnnotationTokens(final Element e) { + return Map.of(); + } + + private static final List annotationTokens(final Element e) { + return List.of(); + } + + private static final List fieldTokens(final Element e) { + assert e instanceof DelegatingElement; + final ArrayList l = new ArrayList<>(); + for (final Element ee : e.getEnclosedElements()) { + if (ee.getKind().isField()) { + l.add(fieldToken((VariableElement)ee)); + } + } + l.trimToSize(); + return Collections.unmodifiableList(l); + } + + private static final List methodTokens(final Element e) { + assert e instanceof DelegatingElement; + final ArrayList l = new ArrayList<>(); + for (final Element ee : e.getEnclosedElements()) { + if (ee.getKind().isExecutable()) { + l.add(methodToken((ExecutableElement)ee)); + } + } + l.trimToSize(); + return Collections.unmodifiableList(l); + } + + private static final List recordComponentTokens(final Element e) { + assert e instanceof DelegatingElement; + final ArrayList l = new ArrayList<>(); + for (final Element ee : e.getEnclosedElements()) { + if (ee.getKind() == ElementKind.RECORD_COMPONENT) { + l.add(recordComponentToken((RecordComponentElement)ee)); + } + } + l.trimToSize(); + return Collections.unmodifiableList(l); + } + + private static final List permittedSubclassBinaryNames(final TypeElement e) { + assert e instanceof DelegatingElement; + final List ts = e.getPermittedSubclasses(); + if (ts.isEmpty()) { + return List.of(); + } + final List l = new ArrayList<>(ts.size()); + for (final TypeMirror t : ts) { + l.add(Lang.binaryName((TypeElement)((DeclaredType)t).asElement()).toString()); + } + return Collections.unmodifiableList(l); + } + + private static final FieldToken fieldToken(final VariableElement e) { + assert e instanceof DelegatingElement; + if (!e.getKind().isField()) { + throw new IllegalArgumentException("e: " + e); + } + return + new FieldToken(e.getSimpleName().toString(), + (int)Lang.modifiers(e), + Lang.descriptor(e.asType()), + genericSignature(e), + Map.of(), // TODO: typeAnnotationTokens + List.of()) {}; // TODO: annotationTokens + } + + private static final MethodToken methodToken(final ExecutableElement e) { + assert e instanceof DelegatingElement; + final List thrownTypes = e.getThrownTypes(); + final String[] exceptionBinaryNames; + if (thrownTypes.isEmpty()) { + exceptionBinaryNames = EMPTY_STRING_ARRAY; + } else { + exceptionBinaryNames = new String[thrownTypes.size()]; + for (int i = 0; i < exceptionBinaryNames.length; i++) { + exceptionBinaryNames[i] = Lang.binaryName((TypeElement)((DeclaredType)thrownTypes.get(i)).asElement()).toString(); + } + } + final ArrayList parameterTokens = new ArrayList<>(); + for (final VariableElement p : e.getParameters()) { + parameterTokens.add(parameterToken(p)); + } + parameterTokens.trimToSize(); + return + new MethodTokenSubclass(e.getSimpleName().toString(), + (int)Lang.modifiers(e), + Lang.descriptor(e.asType()), + genericSignature(e), + exceptionBinaryNames, + Map.of(), // typeVariableAnnotationTokens (Map>>) + Map.of(), // typeVariableBoundAnnotationTokens + Map.of(), // returnTypeAnnotationTokens + Map.of(), // parameterTypeAnnotationTokens + Map.of(), // exceptionTypeAnnotationTokens + Map.of(), // receiverTypeAnnotationTokens + List.of(), // annotationTokens + Map.of(), // parameterAnnotationTokens + Collections.unmodifiableList(parameterTokens), + null); // defaultValue + } + + private static final MethodTokenSubclass.ParameterTokenSubclass parameterToken(final VariableElement e) { + assert e instanceof DelegatingElement; + final int modifiers = (int)Lang.modifiers(e); + return new MethodTokenSubclass.ParameterTokenSubclass(e.getSimpleName().toString(), modifiers == 0 ? null : Integer.valueOf(modifiers)); + } + + private static final RecordComponentToken recordComponentToken(final RecordComponentElement e) { + assert e instanceof DelegatingElement; + return + new RecordComponentToken(e.getSimpleName().toString(), + Lang.descriptor(e.asType()), + genericSignature(e), + Map.of(), + List.of()) {}; // annotationTokens + } + + private static final class MethodTokenSubclass extends MethodToken { + + @SuppressWarnings("unchecked") + private MethodTokenSubclass(final String name, + final int modifiers, + final String descriptor, + final String genericSignature, + final String[] exceptionName, + final Map>> typeVariableAnnotationTokens, + final Map>>> typeVariableBoundAnnotationTokens, + final Map> returnTypeAnnotationTokens, + final Map>> parameterTypeAnnotationTokens, + final Map>> exceptionTypeAnnotationTokens, + final Map> receiverTypeAnnotationTokens, + final List annotationTokens, + final Map> parameterAnnotationTokens, + final List parameterTokens, + final AnnotationValue defaultValue) { + super(name, + modifiers, + descriptor, + genericSignature, + exceptionName, + typeVariableAnnotationTokens, + typeVariableBoundAnnotationTokens, + returnTypeAnnotationTokens, + parameterTypeAnnotationTokens, + exceptionTypeAnnotationTokens, + receiverTypeAnnotationTokens, + annotationTokens, + parameterAnnotationTokens, + (List)parameterTokens, + defaultValue); + } + + private static final class ParameterTokenSubclass extends ParameterToken { + + private ParameterTokenSubclass(final String name, final Integer modifiers) { + super(name, modifiers); + } + + } + + } + + } + +} diff --git a/bytebuddy/src/main/java/org/microbean/lang/bytebuddy/package-info.java b/bytebuddy/src/main/java/org/microbean/lang/bytebuddy/package-info.java new file mode 100644 index 00000000..7c335847 --- /dev/null +++ b/bytebuddy/src/main/java/org/microbean/lang/bytebuddy/package-info.java @@ -0,0 +1,20 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +/** + * Provides classes and interfaces related to the Java language model and Byte Buddy. + * + * @author Laird Nelson + */ +package org.microbean.lang.bytebuddy; diff --git a/bytebuddy/src/main/javadoc/css/fonts.css b/bytebuddy/src/main/javadoc/css/fonts.css new file mode 100644 index 00000000..bd00dc52 --- /dev/null +++ b/bytebuddy/src/main/javadoc/css/fonts.css @@ -0,0 +1,62 @@ +@import url('https://fonts.googleapis.com/css2?2?family=Roboto+Mono:ital,wght@0,400;1,400&family=Roboto:ital,wght@0,400;0,700;1,400;1,700&family=Lobster&display=swap'); +body { + font-family: 'Roboto', 'DejaVu Sans', Arial, Helvetica, sans-serif; +} +button { + font-family: 'Roboto', 'DejaVu Sans', Arial, Helvetica, sans-serif; +} +h1, h2, h3 { + font-weight: 700 +} +input { + font-family: 'Roboto', 'DejaVu Sans', Arial, Helvetica, sans-serif; +} +code, tt, pre { + font-family: 'Roboto Mono', 'DejaVu Sans Mono', monospace; +} +.contentContainer .description dl dd, .contentContainer .details dl dd, .serializedFormContainer dl dd { + font-family: 'Roboto', 'DejaVu Sans', Arial, Helvetica, sans-serif; +} +div.block { + font-family: 'Roboto', 'DejaVu Sans', Arial, Helvetica, sans-serif; +} +div.table-tabs > button { + font-weight: 700 +} +dl.notes > dt { + font-family: 'Roboto', 'DejaVu Sans', Arial, Helvetica, sans-serif; + font-weight: 700 +} +dl.notes > dd { + font-family: 'Roboto', 'DejaVu Sans', Arial, Helvetica, sans-serif; +} +strong { + font-weight: 700 +} +.caption { + font-weight: 700 +} +.table-header { + font-weight: 700 +} +.col-first a:link, .col-first a:visited, .col-second a:link, .col-second a:visited, .col-first a:link, .col-first a:visited, .col-second a:link, .col-second a:visited, .col-constructor-name a:link, .col-constructor-name a:visited, .col-summary-item-name a:link, .col-summary-item-name a:visited, .constant-values-container a:link, .constant-values-container a:visited, .all-classes-container a:link, .all-classes-container a:visited, .all-packages-container a:link, .all-packages-container a:visited { + font-weight: 700 +} +.deprecated-label, .descfrm-type-label, .implementation-label, .member-name-label, .member-name-link, .module-label-in-package, .module-label-in-type, .override-specify-label, .package-label-in-type, .package-hierarchy-label, .type-name-label, .type-name-link, .search-tag-link, .preview-label { + font-weight: 700 +} +.module-signature, .package-signature, .type-signature, .member-signature { + font-family: 'Roboto Mono', 'DejaVu Sans Mono', monospace; +} +main a[href*="://"]::after { + all: unset; +} +.result-highlight { + font-weight: 700; +} +.ui-widget { + font-family: 'Roboto', 'DejaVu Sans', Arial, Helvetica, sans-serif !important; +} +.ui-autocomplete-category { + font-weight: 700; +} diff --git a/bytebuddy/src/site/markdown/index.md.vm b/bytebuddy/src/site/markdown/index.md.vm new file mode 100644 index 00000000..c79baf67 --- /dev/null +++ b/bytebuddy/src/site/markdown/index.md.vm @@ -0,0 +1 @@ +#include("../../../README.md") diff --git a/bytebuddy/src/test/java/org/microbean/lang/bytebuddy/TestByteBuddy.java b/bytebuddy/src/test/java/org/microbean/lang/bytebuddy/TestByteBuddy.java new file mode 100644 index 00000000..d6108908 --- /dev/null +++ b/bytebuddy/src/test/java/org/microbean/lang/bytebuddy/TestByteBuddy.java @@ -0,0 +1,119 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.bytebuddy; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.annotation.ElementType; + +import java.util.List; + +import net.bytebuddy.description.annotation.AnnotationDescription; + +import net.bytebuddy.description.type.TypeDefinition; +import net.bytebuddy.description.type.TypeDescription; + +import net.bytebuddy.pool.TypePool; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; + +final class TestByteBuddy { + + private TestByteBuddy() { + super(); + } + + @Test + final void testModeling() { + // This gives you what amounts to a parameterized type with no type arguments. Byte Buddy calls it a + // TypeDescription.Generic of Sort NON_GENERIC (which is nonsensical since List is definitely a generic class). You + // can't get its type arguments because the Sort is NON_GENERIC. This is like a crippled DeclaredType resulting + // from elements.getTypeElement("java.util.List").asType() (note that you can call DeclaredType#getTypeArguments() + // and you will get back an array of TypeVariable types, not an IllegalStateException!). + final TypeDescription.Generic listType = TypeDefinition.Sort.describe(List.class); + assertSame(TypeDefinition.Sort.NON_GENERIC, listType.getSort()); + assertThrows(IllegalStateException.class, listType::getTypeArguments); // this is just plain weird + + // To "see" its element's type parameters you "erase" the type (which didn't have any arguments anyway so it's not + // really erasing). This is really kind of like DeclaredType#asElement(), which is not really an erasing operation. + // This whole thing is exceptionally strange. + final TypeDescription listElement = listType.asErasure(); // this is also weird + final List typeParameters = listElement.getTypeVariables(); // these are type PARAMETERS, dang it + assertEquals(1, typeParameters.size()); + final TypeDescription.Generic e = typeParameters.get(0); // this is basically a TypeParameterElement + assertSame(TypeDefinition.Sort.VARIABLE, e.getSort()); // this is kind of like TypeParameterElement#asType() + assertEquals("E", e.getSymbol()); // this is also weird; types don't have names + + } + + @Test + final void testModeling2() { + final TypeDescription.Generic listTypeViaSortDescribe = TypeDefinition.Sort.describe(List.class); + final TypeDescription listTypeViaTypePoolDescribe = TypePool.Default.ofSystemLoader().describe(List.class.getName()).resolve(); + assertEquals(listTypeViaSortDescribe, listTypeViaTypePoolDescribe); + assertEquals(listTypeViaTypePoolDescribe, listTypeViaSortDescribe); + + final TypeDescription.Generic x = listTypeViaTypePoolDescribe.asGenericType(); + assertEquals(listTypeViaTypePoolDescribe, x); + assertEquals(x, listTypeViaTypePoolDescribe); + assertEquals(listTypeViaSortDescribe, x); + assertEquals(x, listTypeViaSortDescribe); + + assertEquals(listTypeViaSortDescribe.asErasure(), listTypeViaTypePoolDescribe); + assertSame(listTypeViaTypePoolDescribe, listTypeViaTypePoolDescribe.asErasure()); + } + + @Test + final void testWeirdTypeUseAnnotations() { + // The upshot of all of this is that if you have a TypeDescription.Generic in your hand, you don't know whether it + // represents a type *declaration* or a type *use* (an element or a type). + + // Here is an example of a TypeDescription.Generic describing a type *use* (the type underlying "@Glop Goop" in + // "implements @Glop Goop" in the Gorp class below). Note that it has annotations, namely type use annotations: + final TypeDescription.Generic goopType = TypeDefinition.Sort.describe(Gorp.class).getInterfaces().get(0); + final AnnotationDescription glopTypeUse = goopType.getDeclaredAnnotations().get(0); + assertEquals("Glop", glopTypeUse.getAnnotationType().getSimpleName()); + + // Here is an example of a TypeDescription.Generic describing a type *declaration* (the actual Goop interface + // below). Note that it has no annotations. + final TypeDescription.Generic goop = TypeDefinition.Sort.describe(Goop.class); + assertEquals(0, goop.getDeclaredAnnotations().size()); + } + + private static final class Gorp implements @Glop Goop { + + } + + private static interface Goop { + + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.TYPE, ElementType.TYPE_USE }) + private @interface Glop { + + } + +} diff --git a/bytebuddy/src/test/java/org/microbean/lang/bytebuddy/TestByteBuddy2.java b/bytebuddy/src/test/java/org/microbean/lang/bytebuddy/TestByteBuddy2.java new file mode 100644 index 00000000..099699ea --- /dev/null +++ b/bytebuddy/src/test/java/org/microbean/lang/bytebuddy/TestByteBuddy2.java @@ -0,0 +1,111 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.microbean.lang.bytebuddy; + +import java.util.List; + +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; + +import net.bytebuddy.description.type.TypeDefinition; +import net.bytebuddy.description.type.TypeDescription; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import static org.microbean.lang.Lang.arrayType; +import static org.microbean.lang.Lang.arrayTypeOf; +import static org.microbean.lang.Lang.declaredType; +import static org.microbean.lang.Lang.primitiveType; +import static org.microbean.lang.Lang.typeElement; +import static org.microbean.lang.Lang.wildcardType; + +final class TestByteBuddy2 { + + private ByteBuddy2 bb; + + private TestByteBuddy2() { + super(); + } + + @BeforeEach + final void setup() { + this.bb = new ByteBuddy2(new TypeElementTypePool()); + } + + @Test + final void testBoolean() { + final TypeDescription.Generic tdg = this.bb.typeDescriptionGeneric(primitiveType(TypeKind.BOOLEAN)); + assertSame(TypeDefinition.Sort.NON_GENERIC, tdg.getSort()); + assertTrue(tdg.represents(boolean.class)); + } + + @Test + final void testThisClass() { // significant for not being cached anywhere + final TypeMirror t = declaredType(this.getClass()); + final TypeDescription.Generic tdg = this.bb.typeDescriptionGeneric(t); + assertSame(TypeDefinition.Sort.NON_GENERIC, tdg.getSort()); + assertTrue(tdg.represents(this.getClass())); + final TypeDescription td = this.bb.typeDescription(t); + assertSame(td, tdg.asErasure()); + } + + @Test + final void testListThisClass() { // significant for not being cached and for being generic + final TypeMirror t = declaredType(null, typeElement(List.class), declaredType(this.getClass())); + final TypeDescription.Generic tdg = this.bb.typeDescriptionGeneric(t); + assertSame(TypeDefinition.Sort.PARAMETERIZED, tdg.getSort()); + } + + @Test + final void testListThisClassArray() { + final TypeMirror t = arrayTypeOf(declaredType(null, typeElement(List.class), declaredType(this.getClass()))); + assertNotNull(t); + final TypeDescription.Generic tdg = this.bb.typeDescriptionGeneric(t); + assertSame(TypeDefinition.Sort.GENERIC_ARRAY, tdg.getSort()); + } + + @Test + final void testListUnboundedWildcard() { + final TypeMirror t = declaredType(null, typeElement(List.class), wildcardType()); + final TypeDescription.Generic tdg = this.bb.typeDescriptionGeneric(t); + assertSame(TypeDefinition.Sort.PARAMETERIZED, tdg.getSort()); + } + + @Test + final void testRawList() { + final TypeMirror t = declaredType(null, typeElement(List.class)); + final TypeDescription.Generic tdg = this.bb.typeDescriptionGeneric(t); + assertSame(TypeDefinition.Sort.PARAMETERIZED, tdg.getSort()); + } + + @Test + final void testObjectArray() { + final TypeMirror t = arrayType(Object[].class); + final TypeDescription.Generic tdg = this.bb.typeDescriptionGeneric(t); + assertSame(TypeDefinition.Sort.NON_GENERIC, tdg.getSort()); + assertTrue(tdg.isArray()); + final TypeDescription td = this.bb.typeDescription(t); + assertTrue(td.isArray()); + assertEquals(td, tdg.asErasure()); // interesting; not the same + } + +} diff --git a/bytebuddy/src/test/java/org/microbean/lang/bytebuddy/TestTypeElementTypePool.java b/bytebuddy/src/test/java/org/microbean/lang/bytebuddy/TestTypeElementTypePool.java new file mode 100644 index 00000000..210967a4 --- /dev/null +++ b/bytebuddy/src/test/java/org/microbean/lang/bytebuddy/TestTypeElementTypePool.java @@ -0,0 +1,459 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.microbean.lang.bytebuddy; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.NestingKind; +import javax.lang.model.element.RecordComponentElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; + +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; + +import net.bytebuddy.ClassFileVersion; + +import net.bytebuddy.description.annotation.AnnotationValue; + +import net.bytebuddy.pool.TypePool; +// import net.bytebuddy.pool.TypePool.Default.LazyTypeDescription; +// import net.bytebuddy.pool.TypePool.Default.LazyTypeDescription.TypeContainment; + +import net.bytebuddy.description.type.TypeDescription; + +import net.bytebuddy.dynamic.ClassFileLocator; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import org.microbean.lang.Lang; + +import org.microbean.lang.element.DelegatingElement; + +import org.microbean.lang.type.DelegatingTypeMirror; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import static org.microbean.lang.Lang.modifiers; +import static org.microbean.lang.Lang.wrap; + +final class TestTypeElementTypePool { + + private TypePool tp; + + private TestTypeElementTypePool() { + super(); + } + + @BeforeEach + final void setup() { + this.tp = new TypeElementTypePool(new TypePool.CacheProvider.Simple()); + } + + @Test + final void testFirstSpike() { + final TypeDescription td = tp.describe("java.lang.Integer").resolve(); + System.out.println("*** td: " + td); + } + + @Test + final void testModifiers() { + final long modifiers = modifiers(EnumSet.of(Modifier.PUBLIC, Modifier.PRIVATE)); + assertEquals(java.lang.reflect.Modifier.PUBLIC | java.lang.reflect.Modifier.PRIVATE, modifiers); + } + + @Test + final void testFrob() { + assertEquals(1<<12, 0x1000); + } + + @Test + final void testJavaLangReflectModifierAndJavaxLangModelElementModifierEquality() { + assertTrue(java.lang.reflect.Modifier.isPublic(1)); + assertTrue(java.lang.reflect.Modifier.isPrivate(2)); + assertTrue(java.lang.reflect.Modifier.isPrivate(3)); + + // How about AccessFlag? + assertEquals(java.lang.reflect.Modifier.PUBLIC, java.lang.reflect.AccessFlag.PUBLIC.mask()); + assertEquals(java.lang.reflect.Modifier.SYNCHRONIZED, java.lang.reflect.AccessFlag.SYNCHRONIZED.mask()); + } + + @Test + final void testMyTypePool() { + final MyTypePool tp = new MyTypePool(ClassFileVersion.JAVA_V20); + final TypeDescription javaLangInteger = new MyTypePool.MyTypeDescription(ClassFileVersion.JAVA_V20, tp, Lang.typeElement("java.lang.Integer")); + System.out.println(javaLangInteger); + for (final TypeDescription.Generic i : javaLangInteger.getInterfaces()) { + System.out.println(i); + } + } + + @Test + final void testSignatureStuff() { + final TypeElement e = Lang.typeElement("java.util.List"); + System.out.println(Lang.elementSignature(e)); + } + + private static final class MyTypePool extends TypePool.Default { + + private static final long RECORD = 1L << 61; + + private final ClassFileVersion classFileVersion; + + private MyTypePool(final ClassFileVersion classFileVersion) { + super(new TypePool.CacheProvider.Simple(), ClassFileLocator.NoOp.INSTANCE, TypePool.Default.ReaderMode.FAST); + this.classFileVersion = classFileVersion; + } + + @Override + protected final Resolution doDescribe(final String name) { + final TypeElement e = Lang.typeElement(name); + if (e == null) { + return new Resolution.Illegal(name); + } + return new Resolution.Simple(new MyTypeDescription(this.classFileVersion, this, Lang.typeElement(name))); + } + + static final class MyTypeDescription extends LazyTypeDescription { + + private static final String[] EMPTY_STRING_ARRAY = new String[0]; + + // Binary names in this mess are JVM binary names, not JLS binary names. Raph calls them "internal names" which isn't a thing. + MyTypeDescription(final ClassFileVersion classFileVersion, final TypePool typePool, TypeElement e) { + super(typePool, + actualModifiers(e), + modifiers(e), + Lang.binaryName(e).toString(), + binaryName(e.getSuperclass()), + interfaceBinaryNames(e), + genericSignature(e), + typeContainment(e), + declaringTypeBinaryName(e), + declaredTypeDescriptors(e), + e.getNestingKind() == NestingKind.ANONYMOUS, + nestHostBinaryName(e), + nestMemberBinaryNames(e), + superclassAnnotationTokens(e), + interfaceAnnotationTokens(e), + typeVariableAnnotationTokens(e), + typeVariableBoundsAnnotationTokens(e), + annotationTokens(e), + fieldTokens(e), + methodTokens(e), + recordComponentTokens(e), + permittedSubclassBinaryNames(e), + classFileVersion); + } + + private static final String binaryName(TypeMirror t) { + t = wrap(t); + return switch (t.getKind()) { + case DECLARED -> Lang.binaryName((TypeElement)((DeclaredType)t).asElement()).toString(); + case NONE -> null; // or empty string? + case TYPEVAR -> Lang.binaryName((TypeElement)((TypeVariable)t).asElement()).toString(); + default -> throw new IllegalArgumentException("t: " + t); + }; + } + + private static final int actualModifiers(Element e) { + e = wrap(e); + return (int)Lang.modifiers(e); + } + + private static final int modifiers(Element e) { + e = wrap(e); + return (int)Lang.modifiers(e); + } + + private static final String[] interfaceBinaryNames(TypeElement e) { + e = wrap(e); + final List ifaces = e.getInterfaces(); + if (ifaces.isEmpty()) { + return EMPTY_STRING_ARRAY; + } + final String[] rv = new String[ifaces.size()]; + for (int i = 0; i < rv.length; i++) { + rv[i] = binaryName(ifaces.get(i)); + } + return rv; + } + + private static final String genericSignature(Element e) { + return Lang.elementSignature(e); + } + + private static final TypeContainment typeContainment(Element e) { + e = wrap(e); + final Element ee = e.getEnclosingElement(); + if (ee == null) { + return TypeContainment.SelfContained.INSTANCE; + } + // ee should be wrapped already + return switch (ee.getKind()) { + case METHOD -> + new TypeContainment.WithinMethod(Lang.binaryName(wrap((TypeElement)ee.getEnclosingElement())).toString(), + ee.getSimpleName().toString(), // TODO: maybe? needs to be method's "internal name" which I think is just its "unqualified name" (4.2.2 JVM) + Lang.descriptor(ee.asType())) {}; + case ANNOTATION_TYPE, CLASS, ENUM, INTERFACE, RECORD -> + new TypeContainment.WithinType(Lang.binaryName((TypeElement)ee).toString(), + ((TypeElement)ee).getNestingKind() == NestingKind.LOCAL) {}; // TODO: this is for the enclosing element, yes? + case PACKAGE -> TypeContainment.SelfContained.INSTANCE; + default -> throw new IllegalStateException(); // I guess? + }; + } + + private static final String declaringTypeBinaryName(TypeElement e) { + e = wrap(e); + // TODO: triple check + final TypeMirror t = ((DeclaredType)e.asType()).getEnclosingType(); + // t should already be wrapped + if (t == null || t.getKind() == TypeKind.NONE) { + return null; + } + return Lang.binaryName((TypeElement)((DeclaredType)t).asElement()).toString(); + } + + private static final List declaredTypeDescriptors(Element e) { + e = wrap(e); + final ArrayList l = new ArrayList<>(); + for (final Element ee : e.getEnclosedElements()) { + // ee should already be wrapped + assert ee instanceof DelegatingElement; + if (ee.getKind().isDeclaredType()) { + l.add(Lang.descriptor(ee.asType())); + } + } + l.trimToSize(); + return Collections.unmodifiableList(l); + } + + private static final String nestHostBinaryName(Element e) { + e = wrap(e); + return null; + } + + private static final List nestMemberBinaryNames(Element e) { + e = wrap(e); + return List.of(); + } + + private static final Map> superclassAnnotationTokens(Element e) { + e = wrap(e); + return Map.of(); + } + + private static final Map>> interfaceAnnotationTokens(Element e) { + e = wrap(e); + return Map.of(); + } + + private static final Map>> typeVariableAnnotationTokens(Element e) { + e = wrap(e); + return Map.of(); + } + + private static final Map>>> typeVariableBoundsAnnotationTokens(Element e) { + e = wrap(e); + return Map.of(); + } + + private static final List annotationTokens(Element e) { + e = wrap(e); + return List.of(); + } + + private static final List fieldTokens(Element e) { + e = wrap(e); + final ArrayList l = new ArrayList<>(); + for (final Element ee : e.getEnclosedElements()) { + assert ee instanceof DelegatingElement; + if (ee.getKind() == ElementKind.FIELD) { // TODO: and enum constants? + l.add(fieldToken((VariableElement)ee)); + } + } + l.trimToSize(); + return Collections.unmodifiableList(l); + } + + private static final List methodTokens(Element e) { + e = wrap(e); + final ArrayList l = new ArrayList<>(); + for (final Element ee : e.getEnclosedElements()) { + assert ee instanceof DelegatingElement; + if (ee.getKind().isExecutable()) { + l.add(methodToken((ExecutableElement)ee)); + } + } + l.trimToSize(); + return Collections.unmodifiableList(l); + } + + private static final List recordComponentTokens(Element e) { + e = wrap(e); + final ArrayList l = new ArrayList<>(); + for (final Element ee : e.getEnclosedElements()) { + assert ee instanceof DelegatingElement; + if (ee.getKind() == ElementKind.RECORD_COMPONENT) { + l.add(recordComponentToken((RecordComponentElement)ee)); + } + } + l.trimToSize(); + return Collections.unmodifiableList(l); + } + + private static final List permittedSubclassBinaryNames(TypeElement e) { + e = wrap(e); + final List ts = e.getPermittedSubclasses(); + if (ts.isEmpty()) { + return List.of(); + } + final List l = new ArrayList<>(ts.size()); + // every t should already be wrapped + for (final TypeMirror t : ts) { + assert t instanceof DelegatingTypeMirror; + l.add(Lang.binaryName((TypeElement)((DeclaredType)t).asElement()).toString()); + } + return Collections.unmodifiableList(l); + } + + private static final FieldToken fieldToken(Element e) { + e = wrap(e); + if (e.getKind() != ElementKind.FIELD) { + throw new IllegalArgumentException("e: " + e); + } + return + new FieldToken(e.getSimpleName().toString(), + (int)Lang.modifiers(e), + Lang.descriptor(e.asType()), + genericSignature(e), + Map.of(), // TODO: typeAnnotationTokens + List.of()) {}; // TODO: annotationTokens + } + + private static final MethodToken methodToken(ExecutableElement e) { + e = wrap(e); + final List thrownTypes = e.getThrownTypes(); + final String[] exceptionBinaryNames; + if (thrownTypes.isEmpty()) { + exceptionBinaryNames = EMPTY_STRING_ARRAY; + } else { + exceptionBinaryNames = new String[thrownTypes.size()]; + for (int i = 0; i < exceptionBinaryNames.length; i++) { + exceptionBinaryNames[i] = Lang.binaryName((TypeElement)((DeclaredType)thrownTypes.get(i)).asElement()).toString(); + } + } + final ArrayList parameterTokens = new ArrayList<>(); + for (final VariableElement p : e.getParameters()) { + assert p instanceof DelegatingElement; + parameterTokens.add(parameterToken(p)); + } + parameterTokens.trimToSize(); + return + new MyMethodToken(e.getSimpleName().toString(), + (int)Lang.modifiers(e), + Lang.descriptor(e.asType()), + genericSignature(e), + exceptionBinaryNames, + Map.of(), // typeVariableAnnotationTokens (Map>>) + Map.of(), // typeVariableBoundAnnotationTokens + Map.of(), // returnTypeAnnotationTokens + Map.of(), // parameterTypeAnnotationTokens + Map.of(), // exceptionTypeAnnotationTokens + Map.of(), // receiverTypeAnnotationTokens + List.of(), // annotationTokens + Map.of(), // parameterAnnotationTokens + Collections.unmodifiableList(parameterTokens), + null); // defaultValue + } + + private static final MyMethodToken.MyParameterToken parameterToken(VariableElement e) { + e = wrap(e); + final int modifiers = (int)Lang.modifiers(e); + return new MyMethodToken.MyParameterToken(e.getSimpleName().toString(), modifiers == 0 ? null : Integer.valueOf(modifiers)); + } + + private static final RecordComponentToken recordComponentToken(Element e) { + e = wrap(e); + return + new RecordComponentToken(e.getSimpleName().toString(), + Lang.descriptor(e.asType()), + genericSignature(e), + Map.of(), + List.of()) {}; // annotationTokens + } + + private static final class MyMethodToken extends MethodToken { + + @SuppressWarnings("unchecked") + private MyMethodToken(final String name, + final int modifiers, + final String descriptor, + final String genericSignature, + final String[] exceptionName, + final Map>> typeVariableAnnotationTokens, + final Map>>> typeVariableBoundAnnotationTokens, + final Map> returnTypeAnnotationTokens, + final Map>> parameterTypeAnnotationTokens, + final Map>> exceptionTypeAnnotationTokens, + final Map> receiverTypeAnnotationTokens, + final List annotationTokens, + final Map> parameterAnnotationTokens, + final List parameterTokens, + final AnnotationValue defaultValue) { + super(name, + modifiers, + descriptor, + genericSignature, + exceptionName, + typeVariableAnnotationTokens, + typeVariableBoundAnnotationTokens, + returnTypeAnnotationTokens, + parameterTypeAnnotationTokens, + exceptionTypeAnnotationTokens, + receiverTypeAnnotationTokens, + annotationTokens, + parameterAnnotationTokens, + (List)parameterTokens, + defaultValue); + } + + static final class MyParameterToken extends ParameterToken { + + MyParameterToken(final String name, final Integer modifiers) { + super(name, modifiers); + } + + } + + } + + } + + } + +} diff --git a/jandex/README.md b/jandex/README.md new file mode 100644 index 00000000..aba30e6f --- /dev/null +++ b/jandex/README.md @@ -0,0 +1,43 @@ +# microBean™ Lang: Jandex + +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.microbean/microbean-lang-jandex/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.microbean/microbean-lang-jandex) + +The microBean™ Lang: Jandex project provides classes and interfaces that implement the Java language model from Jandex +constructs. + +# Status + +This project is currently experimental, in a pre-alpha state, and unsuitable for production use. + +# Compatibility + +**Until further notice, this project's APIs are subject to frequent backwards-incompatible signature and behavior +changes, regardless of project version and without notice.** + +# Requirements + +microBean™ Lang: Jandex requires a Java runtime of version 19 or higher. + +# Installation + +microBean™ Lang: Jandex is or will be available on [Maven Central](https://search.maven.org/). Include microBean™ Lang: +Jandex as a Maven dependency: + +```xml + + org.microbean + microbean-lang-jandex + + 0.0.1 + +``` + +# Documentation + +Full documentation is or will be available at +[microbean.github.io/microbean-lang-jandex](https://microbean.github.io/microbean-lang-jandex/). + +# Related Issues + +* https://github.com/smallrye/jandex/issues/167 + diff --git a/jandex/pom.xml b/jandex/pom.xml new file mode 100644 index 00000000..4349d28c --- /dev/null +++ b/jandex/pom.xml @@ -0,0 +1,59 @@ + + + 4.0.0 + + microbean-lang-jandex + + + org.microbean + microbean-lang-parent + 0.0.1-SNAPSHOT + + + microBean™ Lang: Jandex + microBean™ Lang: Jandex: An implementation of the Java language model using Jandex constructs. + + + + + + + io.smallrye + jandex + jar + compile + + + + ${project.groupId} + microbean-lang + jar + compile + + + + + + org.hamcrest + hamcrest + jar + test + + + + org.junit.jupiter + junit-jupiter-api + jar + test + + + + org.junit.jupiter + junit-jupiter-engine + jar + test + + + + + diff --git a/jandex/src/main/java/module-info.java b/jandex/src/main/java/module-info.java new file mode 100644 index 00000000..59736fc9 --- /dev/null +++ b/jandex/src/main/java/module-info.java @@ -0,0 +1,33 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +/** + * Provides packages related to the Java language model and Jandex. + * + * @author Laird Nelson + */ +@SuppressWarnings({ "requires-automatic", "requires-transitive-automatic" }) +module org.microbean.lang.jandex { + + requires transitive java.compiler; + requires transitive org.jboss.jandex; // yes, even though it's at SmallRye + requires transitive org.microbean.lang; + + exports org.microbean.lang.jandex; + +} diff --git a/jandex/src/main/java/org/microbean/lang/jandex/Jandex.java b/jandex/src/main/java/org/microbean/lang/jandex/Jandex.java new file mode 100644 index 00000000..0ff1d14d --- /dev/null +++ b/jandex/src/main/java/org/microbean/lang/jandex/Jandex.java @@ -0,0 +1,1492 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.jandex; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import java.util.function.BiFunction; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.ModuleElement; +import javax.lang.model.element.NestingKind; +import javax.lang.model.element.PackageElement; +import javax.lang.model.element.RecordComponentElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.TypeParameterElement; +import javax.lang.model.element.VariableElement; + +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.ExecutableType; +import javax.lang.model.type.NoType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; +import javax.lang.model.type.WildcardType; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.ClassInfo.EnclosingMethodInfo; +import org.jboss.jandex.ClassInfo.NestingType; +import org.jboss.jandex.ClassType; +import org.jboss.jandex.DotName; +import org.jboss.jandex.FieldInfo; +import org.jboss.jandex.IndexView; +import org.jboss.jandex.MethodInfo; +import org.jboss.jandex.MethodParameterInfo; +import org.jboss.jandex.ModuleInfo; +import org.jboss.jandex.ModuleInfo.ExportedPackageInfo; +import org.jboss.jandex.ModuleInfo.OpenedPackageInfo; +import org.jboss.jandex.ModuleInfo.ProvidedServiceInfo; +import org.jboss.jandex.ModuleInfo.RequiredModuleInfo; +import org.jboss.jandex.ParameterizedType; +import org.jboss.jandex.PrimitiveType; +import org.jboss.jandex.RecordComponentInfo; +import org.jboss.jandex.Type; +import org.jboss.jandex.TypeParameterTypeTarget; + +import org.microbean.lang.Modeler; + +public final class Jandex extends Modeler { + + private static final Type[] EMPTY_TYPE_ARRAY = new Type[0]; + + private final IndexView i; + + private final BiFunction unindexedClassnameFunction; + + public Jandex(final IndexView i) { + this(i, (n, j) -> { + throw new IllegalArgumentException("class " + n + " not found in IndexView " + j); + }); + } + + public Jandex(final IndexView i, final BiFunction unindexedClassnameFunction) { + super(); + this.i = Objects.requireNonNull(i, "i"); + this.unindexedClassnameFunction = Objects.requireNonNull(unindexedClassnameFunction, "unindexedClassnameFunction"); + } + + public final AnnotationMirror annotation(final AnnotationInstance k) { + return this.annotation(k, org.microbean.lang.element.AnnotationMirror::new, this::build); + } + + public final javax.lang.model.element.AnnotationValue annotationValue(final Object v) { + return switch (v) { + case null -> null; + case AnnotationInstance a -> this.annotationValue(a); + case AnnotationMirror a -> this.annotationValue(a); + case javax.lang.model.element.AnnotationValue a -> this.annotationValue(a); + case org.jboss.jandex.AnnotationValue a -> this.annotationValue(a); + case Boolean b -> this.annotationValue(b); + case Byte b -> this.annotationValue(b); + case CharSequence s -> this.annotationValue(s); + case Character c -> this.annotationValue(c); + case Collection c -> this.annotationValue(c); + case Double d -> this.annotationValue(d); + case FieldInfo f -> this.annotationValue(f); + case Float f -> this.annotationValue(f); + case Integer i -> this.annotationValue(i); + case Long l -> this.annotationValue(l); + case Object[] o -> this.annotationValue(o); + case Short s -> this.annotationValue(s); + case TypeMirror t -> this.annotationValue(t); + case VariableElement ve -> this.annotationValue(ve); + case boolean[] o -> this.annotationValue(o); + case byte[] o -> this.annotationValue(o); + case char[] o -> this.annotationValue(o); + case double[] o -> this.annotationValue(o); + case float[] o -> this.annotationValue(o); + case int[] o -> this.annotationValue(o); + case long[] o -> this.annotationValue(o); + case short[] o -> this.annotationValue(o); + default -> throw new IllegalArgumentException("v: " + v); + }; + } + + public final javax.lang.model.element.AnnotationValue annotationValue(final AnnotationMirror a) { + return a == null ? null : new org.microbean.lang.element.AnnotationValue(a); + } + + public final javax.lang.model.element.AnnotationValue annotationValue(final AnnotationInstance a) { + return a == null ? null : this.annotationValue(this.annotation(a)); + } + + public final V annotationValue(final V v) { + return v; + } + + public final javax.lang.model.element.AnnotationValue annotationValue(final org.jboss.jandex.AnnotationValue v) { + return v == null ? null : switch (v.kind()) { + case ARRAY, BOOLEAN, BYTE, CHARACTER, CLASS, DOUBLE, FLOAT, INTEGER, LONG, NESTED, SHORT, STRING -> this.annotationValue(v.value()); + case ENUM -> this.annotationValue(this.classInfoFor(v.asEnumType()).field(v.asEnum())); + case UNKNOWN -> new org.microbean.lang.element.AnnotationValue(List.of()); + }; + } + + public final javax.lang.model.element.AnnotationValue annotationValue(final Boolean b) { + return b == null ? null : new org.microbean.lang.element.AnnotationValue(b); + } + + public final javax.lang.model.element.AnnotationValue annotationValue(final Byte b) { + return b == null ? null : new org.microbean.lang.element.AnnotationValue(b); + } + + public final javax.lang.model.element.AnnotationValue annotationValue(final Character c) { + return c == null ? null : new org.microbean.lang.element.AnnotationValue(c); + } + + public final javax.lang.model.element.AnnotationValue annotationValue(final CharSequence s) { + return new org.microbean.lang.element.AnnotationValue(s == null ? "" : s.toString()); + } + + public final javax.lang.model.element.AnnotationValue annotationValue(final Collection c) { + return new org.microbean.lang.element.AnnotationValue(this.annotationValues(c == null ? List.of() : c)); + } + + public final javax.lang.model.element.AnnotationValue annotationValue(final Double d) { + return d == null ? null : new org.microbean.lang.element.AnnotationValue(d); + } + + public final javax.lang.model.element.AnnotationValue annotationValue(final FieldInfo enumConstant) { + if (enumConstant == null) { + return null; + } else if (enumConstant.isEnumConstant() && enumConstant.declaringClass().isEnum()) { + return new org.microbean.lang.element.AnnotationValue(this.element(enumConstant)); + } + throw new IllegalArgumentException("enumConstant: " + enumConstant); + } + + public final javax.lang.model.element.AnnotationValue annotationValue(final Float f) { + return f == null ? null : new org.microbean.lang.element.AnnotationValue(f); + } + + public final javax.lang.model.element.AnnotationValue annotationValue(final Integer i) { + return i == null ? null : new org.microbean.lang.element.AnnotationValue(i); + } + + public final javax.lang.model.element.AnnotationValue annotationValue(final Long l) { + return l == null ? null : new org.microbean.lang.element.AnnotationValue(l); + } + + public final javax.lang.model.element.AnnotationValue annotationValue(final Object[] o) { + return new org.microbean.lang.element.AnnotationValue(this.annotationValues(o)); + } + + public final javax.lang.model.element.AnnotationValue annotationValue(final Short s) { + return s == null ? null : new org.microbean.lang.element.AnnotationValue(s); + } + + public final javax.lang.model.element.AnnotationValue annotationValue(final TypeMirror t) { + return t == null ? null : new org.microbean.lang.element.AnnotationValue(t); + } + + public final javax.lang.model.element.AnnotationValue annotationValue(final VariableElement enumConstant) { + if (enumConstant == null) { + return null; + } else if (enumConstant.getKind() == ElementKind.ENUM_CONSTANT) { + return new org.microbean.lang.element.AnnotationValue(enumConstant); + } + throw new IllegalArgumentException("enumConstant: " + enumConstant); + } + + public final List annotationValues(final Collection c) { + if (c == null || c.isEmpty()) { + return List.of(); + } + final List list = new ArrayList<>(c.size()); + for (final Object value : c) { + list.add(this.annotationValue(value)); + } + return Collections.unmodifiableList(list); + } + + public final javax.lang.model.element.AnnotationValue annotationValue(final boolean[] o) { + return o == null ? null : this.annotationValue(this.annotationValues(o)); + } + + public final List annotationValues(final boolean[] o) { + if (o == null || o.length <= 0) { + return List.of(); + } + final List list = new ArrayList<>(o.length); + for (final boolean value : o) { + list.add(this.annotationValue(value)); + } + return Collections.unmodifiableList(list); + } + + public final javax.lang.model.element.AnnotationValue annotationValue(final byte[] o) { + return o == null ? null : this.annotationValue(this.annotationValues(o)); + } + + public final List annotationValues(final byte[] o) { + if (o == null || o.length <= 0) { + return List.of(); + } + final List list = new ArrayList<>(o.length); + for (final byte value : o) { + list.add(this.annotationValue(value)); + } + return Collections.unmodifiableList(list); + } + + public final javax.lang.model.element.AnnotationValue annotationValue(final char[] o) { + return o == null ? null : this.annotationValue(this.annotationValues(o)); + } + + public final List annotationValues(final char[] o) { + if (o == null || o.length <= 0) { + return List.of(); + } + final List list = new ArrayList<>(o.length); + for (final char value : o) { + list.add(this.annotationValue(value)); + } + return Collections.unmodifiableList(list); + } + + public final javax.lang.model.element.AnnotationValue annotationValue(final double[] o) { + return o == null ? null : this.annotationValue(this.annotationValues(o)); + } + + public final List annotationValues(final double[] o) { + if (o == null || o.length <= 0) { + return List.of(); + } + final List list = new ArrayList<>(o.length); + for (final double value : o) { + list.add(this.annotationValue(value)); + } + return Collections.unmodifiableList(list); + } + + public final javax.lang.model.element.AnnotationValue annotationValue(final float[] o) { + return o == null ? null : this.annotationValue(this.annotationValues(o)); + } + + public final List annotationValues(final float[] o) { + if (o == null || o.length <= 0) { + return List.of(); + } + final List list = new ArrayList<>(o.length); + for (final float value : o) { + list.add(this.annotationValue(value)); + } + return Collections.unmodifiableList(list); + } + + public final javax.lang.model.element.AnnotationValue annotationValue(final int[] o) { + return o == null ? null : this.annotationValue(this.annotationValues(o)); + } + + public final List annotationValues(final int[] o) { + if (o == null || o.length <= 0) { + return List.of(); + } + final List list = new ArrayList<>(o.length); + for (final int value : o) { + list.add(this.annotationValue(value)); + } + return Collections.unmodifiableList(list); + } + + public final javax.lang.model.element.AnnotationValue annotationValue(final long[] o) { + return o == null ? null : this.annotationValue(this.annotationValues(o)); + } + + public final List annotationValues(final long[] o) { + if (o == null || o.length <= 0) { + return List.of(); + } + final List list = new ArrayList<>(o.length); + for (final long value : o) { + list.add(this.annotationValue(value)); + } + return Collections.unmodifiableList(list); + } + + public final javax.lang.model.element.AnnotationValue annotationValue(final short[] o) { + return o == null ? null : this.annotationValue(this.annotationValues(o)); + } + + public final List annotationValues(final short[] o) { + if (o == null || o.length <= 0) { + return List.of(); + } + final List list = new ArrayList<>(o.length); + for (final short value : o) { + list.add(this.annotationValue(value)); + } + return Collections.unmodifiableList(list); + } + + public final List annotationValues(final Object[] o) { + if (o == null || o.length <= 0) { + return List.of(); + } + final List list = new ArrayList<>(o.length); + for (final Object value : o) { + list.add(this.annotationValue(value)); + } + return Collections.unmodifiableList(list); + } + + + /* + * Element methods. + */ + + + public final Element element(final DotName n) { + final ClassInfo ci = this.classInfoFor(n); + return ci == null ? null : this.element(ci); + } + + @Override // ElementSource + public final TypeElement typeElement(final CharSequence m, final CharSequence n) { + return this.typeElement(n); + } + + @Override // ElementSource + public final TypeElement typeElement(final CharSequence n) { + final ClassInfo ci = this.classInfoFor(n.toString()); + return + ci == null || ci.isModule() ? null : + (TypeElement)this.element(ci); + } + + public final PackageElement packageElement(final DotName n) { + return n == null ? null : this.element(new PackageInfo(n)); + } + + public final PackageElement packageElement(final String n) { + return n == null ? null : this.packageElement(DotName.createSimple(n)); + } + + public final ModuleElement element(final ModuleInfo mi) { + return mi == null ? null : this.element(mi, () -> new org.microbean.lang.element.ModuleElement(mi.isOpen()), this::build); + } + + public final PackageElement element(final PackageInfo pi) { + return pi == null ? null : this.element(pi, org.microbean.lang.element.PackageElement::new, this::build); + } + + public final TypeElement element(final AnnotationInstance ai) { + return ai == null ? null : (TypeElement)this.element(this.classInfoFor(ai.name())); + } + + public final Element element(final ClassInfo ci) { + if (ci == null) { + return null; + } else if (ci.isModule()) { + return this.element(ci.module()); + } else { + return this.element(ci, () -> new org.microbean.lang.element.TypeElement(kind(ci), nestingKind(ci)), this::build); + } + } + + public final Element element(final ClassType ct) { + return ct == null ? null : this.element(this.classInfoFor(ct)); + } + + public final E element(final E e) { + return e; + } + + public final ExecutableElement element(final EnclosingMethodInfo emi) { + return emi == null ? null : this.element(this.classInfoFor(emi.enclosingClass()).method(emi.name(), emi.parameters().toArray(EMPTY_TYPE_ARRAY))); + } + + public final VariableElement element(final FieldInfo fi) { + return fi == null ? null : this.element(fi, () -> new org.microbean.lang.element.VariableElement(kind(fi)), this::build); + } + + public final ExecutableElement element(final MethodInfo mi) { + return mi == null ? null : this.element(mi, () -> new org.microbean.lang.element.ExecutableElement(kind(mi)), this::build); + } + + public final VariableElement element(final MethodParameterInfo mpi) { + return mpi == null ? null : this.element(mpi, () -> new org.microbean.lang.element.VariableElement(ElementKind.PARAMETER), this::build); + } + + public final RecordComponentElement element(final RecordComponentInfo rci) { + return rci == null ? null : this.element(rci, org.microbean.lang.element.RecordComponentElement::new, this::build); + } + + public final TypeParameterElement element(final TypeParameterInfo tpi) { + return tpi == null ? null : this.element(tpi, org.microbean.lang.element.TypeParameterElement::new, this::build); + } + + public final TypeParameterElement element(final TypeParameterTypeTarget tt) { + return tt == null ? null : this.element(new TypeParameterInfo(tt.enclosingTarget(), tt.target().asTypeVariable())); + } + + + /* + * Type methods. + */ + + + public final DeclaredType type(final AnnotationInstance ai) { + return ai == null ? null : this.type(this.classInfoFor(ai)); + } + + public final DeclaredType type(final ClassInfo ci) { + if (ci == null) { + return null; + } else if (ci.isModule()) { + throw new UnsupportedOperationException("Not yet handled"); + } else { + return this.type(ci, org.microbean.lang.type.DeclaredType::new, this::build); + } + } + + public final TypeMirror type(final FieldInfo fi) { + if (fi == null) { + return null; + } + final Type t = fi.type(); + return switch (t.kind()) { + case ARRAY -> this.type(fi, org.microbean.lang.type.ArrayType::new, this::build); + case CLASS, PARAMETERIZED_TYPE -> this.type(fi, org.microbean.lang.type.DeclaredType::new, this::build); + case PRIMITIVE -> this.type(fi, () -> new org.microbean.lang.type.PrimitiveType(kind(t.asPrimitiveType())), this::build); + case TYPE_VARIABLE -> this.type(fi, () -> new org.microbean.lang.type.TypeVariable(this), this::build); + default -> throw new IllegalStateException("t: " + t); + }; + } + + public final ExecutableType type(final MethodInfo mi) { + return mi == null ? null : this.type(mi, org.microbean.lang.type.ExecutableType::new, this::build); + } + + public final TypeMirror type(final MethodParameterInfo mpi) { + if (mpi == null) { + return null; + } + final Type t = mpi.type(); + return switch (t.kind()) { + case ARRAY -> this.type(mpi, org.microbean.lang.type.ArrayType::new, this::build); + case CLASS, PARAMETERIZED_TYPE -> this.type(mpi, org.microbean.lang.type.DeclaredType::new, this::build); + case PRIMITIVE -> this.type(mpi, () -> new org.microbean.lang.type.PrimitiveType(kind(t.asPrimitiveType())), this::build); + case TYPE_VARIABLE -> this.type(mpi, () -> new org.microbean.lang.type.TypeVariable(this), this::build); + default -> throw new IllegalStateException("t: " + t); + }; + } + + public final TypeMirror type(final RecordComponentInfo rci) { + if (rci == null) { + return null; + } + final Type t = rci.type(); + return switch (t.kind()) { + case ARRAY -> this.type(rci, org.microbean.lang.type.ArrayType::new, this::build); + case CLASS, PARAMETERIZED_TYPE -> this.type(rci, org.microbean.lang.type.DeclaredType::new, this::build); + case PRIMITIVE -> this.type(rci, () -> new org.microbean.lang.type.PrimitiveType(kind(t.asPrimitiveType())), this::build); + case TYPE_VARIABLE -> this.type(rci, () -> new org.microbean.lang.type.TypeVariable(this), this::build); + default -> throw new IllegalStateException("rci.type(): " + t); + }; + } + + public final TypeMirror type(final TypeContext k) { + final Type type = k == null ? null : k.type(); + return type == null ? null : switch (type.kind()) { + case ARRAY -> this.type(k, org.microbean.lang.type.ArrayType::new, this::build); + case CLASS, PARAMETERIZED_TYPE -> this.type(k, org.microbean.lang.type.DeclaredType::new, this::build); + case PRIMITIVE -> this.type(type.asPrimitiveType(), () -> new org.microbean.lang.type.PrimitiveType(kind(type.asPrimitiveType())), this::build); + case TYPE_VARIABLE -> this.type(this.typeParameterInfoFor(k)); + // k.kind() had better be TYPE_ARGUMENT below? + case TYPE_VARIABLE_REFERENCE -> this.type(new TypeContext(k.context(), type.asTypeVariableReference().follow(), k.position(), k.kind())); + case UNRESOLVED_TYPE_VARIABLE -> throw new AssertionError(); + case VOID -> org.microbean.lang.type.NoType.VOID; + case WILDCARD_TYPE -> this.type(k, org.microbean.lang.type.WildcardType::new, this::build); + }; + } + + public final T type(final T t) { + return t; + } + + public final javax.lang.model.type.TypeVariable type(final TypeParameterInfo tpi) { + return tpi == null ? null : this.type(tpi, () -> new org.microbean.lang.type.TypeVariable(this), this::build); + } + + @Override // TypeAndElementSource + public final javax.lang.model.type.ArrayType arrayTypeOf(final TypeMirror componentType) { + throw new UnsupportedOperationException("TODO"); + } + + @Override // TypeAndElementSource + public final boolean assignable(final TypeMirror t, final TypeMirror s) { + throw new UnsupportedOperationException("TODO"); + } + + @Override // TypeAndElementSource + public final TypeElement boxedClass(final javax.lang.model.type.PrimitiveType t) { + throw new UnsupportedOperationException("TODO"); + } + + @Override // TypeAndElementSource + public final T erasure(final T t) { + throw new UnsupportedOperationException("TODO"); + } + + @Override // TypeAndElementSource + public final NoType noType(final TypeKind k) { + throw new UnsupportedOperationException("TODO"); + } + + @Override // TypeAndElementSource + public final javax.lang.model.type.PrimitiveType primitiveType(final TypeKind k) { + throw new UnsupportedOperationException("TODO"); + } + + @Override // TypeAndElementSource + public final DeclaredType declaredType(final DeclaredType containingType, final TypeElement typeElement, final TypeMirror... arguments) { + throw new UnsupportedOperationException("TODO"); + } + + @Override // TypeAndElementSource + public final DeclaredType declaredType(final TypeElement typeElement, final TypeMirror... arguments) { + throw new UnsupportedOperationException("TODO"); + } + + @Override // TypeAndElementSource + public final boolean sameType(final TypeMirror t, final TypeMirror s) { + throw new UnsupportedOperationException("TODO"); + } + + @Override // TypeAndElementSource + public final TypeVariable typeVariable(final java.lang.reflect.TypeVariable t) { + throw new UnsupportedOperationException("TODO"); + } + + public final WildcardType wildcardType(final TypeMirror extendsBound, final TypeMirror superBound) { + throw new UnsupportedOperationException("TODO"); + } + + + + + /* + * Annotation builders. + */ + + + private final void build(final AnnotationInstance ai, final org.microbean.lang.element.AnnotationMirror am) { + final ClassInfo annotationClass = this.classInfoFor(ai.name()); + + final org.microbean.lang.type.DeclaredType t = (org.microbean.lang.type.DeclaredType)this.type(annotationClass); + assert t.asElement() != null; + am.setAnnotationType(t); + + for (final org.jboss.jandex.AnnotationValue v : ai.values()) { + final javax.lang.model.element.ExecutableElement ee = this.element(this.annotationElementFor(annotationClass, v.name())); + assert ee.getEnclosingElement() != null : "ee: " + ee + "; ai: " + ai + "; annotationClass: " + annotationClass + "; v: " + v; + am.putElementValue(ee, this.annotationValue(v)); + } + } + + + /* + * Element builders. + */ + + + private final void build(final ModuleInfo mi, final org.microbean.lang.element.ModuleElement e) { + // Simple name. + e.setSimpleName(mi.name().toString()); + + // Type. + e.setType(org.microbean.lang.type.NoType.MODULE); + + for (final RequiredModuleInfo rmi : mi.requires()) { + e.addDirective(new org.microbean.lang.element.ModuleElement.RequiresDirective(this.element(this.i.getModuleByName(rmi.name())), + rmi.isStatic(), + rmi.isTransitive())); + } + + for (final ExportedPackageInfo epi : mi.exports()) { + final List epiTargets = epi.targets(); + final List targets = new ArrayList<>(epiTargets.size()); + for (final DotName epiTarget : epiTargets) { + targets.add(this.element(this.i.getModuleByName(epiTarget))); + } + e.addDirective(new org.microbean.lang.element.ModuleElement.ExportsDirective(this.packageElement(epi.source()), targets)); + } + + for (final OpenedPackageInfo opi : mi.opens()) { + final List opiTargets = opi.targets(); + final List targets = new ArrayList<>(opiTargets.size()); + for (final DotName opiTarget : opiTargets) { + targets.add(this.element(this.i.getModuleByName(opiTarget))); + } + e.addDirective(new org.microbean.lang.element.ModuleElement.OpensDirective(this.packageElement(opi.source()), targets)); + } + + for (final ProvidedServiceInfo psi : mi.provides()) { + final List psiProviders = psi.providers(); + final List providers = new ArrayList<>(psiProviders.size()); + for (final DotName psiTarget : psiProviders) { + providers.add((TypeElement)this.element(this.classInfoFor(psiTarget))); + } + e.addDirective(new org.microbean.lang.element.ModuleElement.ProvidesDirective((TypeElement)this.element(psi.service()), providers)); + } + + e.setEnclosedElementsGenerator(() -> { + for (final DotName pn : mi.packages()) { + this.packageElement(pn); + } + }); + + for (final AnnotationInstance ai : mi.annotations()) { + e.addAnnotationMirror(this.annotation(ai)); + } + + } + + private final void build(final PackageInfo pi, final org.microbean.lang.element.PackageElement e) { + final DotName pn = pi.name(); + + // Simple name. + e.setSimpleName(pn.toString()); + + // Type. + e.setType(org.microbean.lang.type.NoType.PACKAGE); + + // Enclosing element. + e.setEnclosingElement(this.element(this.i.getKnownModules().stream() + .filter(m -> m.packages().contains(pn)) + .findFirst() + .orElse(null))); + + e.setEnclosedElementsGenerator(() -> this.i.getClassesInPackage(pn).forEach(this::element)); + } + + private final void build(final ClassInfo ci, final org.microbean.lang.element.TypeElement e) { + // Simple name. + e.setSimpleName(ci.name().local()); + + // Type. Note that we haven't done type parameters yet, so the type arguments belonging to the type won't have + // corresponding type parameters yet. + final org.microbean.lang.type.DeclaredType t = (org.microbean.lang.type.DeclaredType)this.type(ci); + e.setType(t); + + // Defining element. + t.setDefiningElement(e); + + e.setEnclosingElement(switch (e.getNestingKind()) { + // Anonymous and local classes are effectively ignored in the javax.lang.model.* hierarchy. The documentation for + // javax.lang.model.element.Element#getEnclosedElements() says, in part: "A class or interface is considered to + // enclose the fields, methods, constructors, record components, and member classes and interfaces that it directly + // declares. A package encloses the top-level classes and interfaces within it, but is not considered to enclose + // subpackages. A module encloses packages within it. Enclosed elements may include implicitly declared mandated + // elements. Other kinds of elements are not currently considered to enclose any elements; however, that may change + // as this API or the programming language evolves." + // + // Additionally, Jandex provides no access to local or anonymous classes at all. + case ANONYMOUS, LOCAL -> null; + case MEMBER -> this.element(this.classInfoFor(ci.enclosingClass())); + case TOP_LEVEL -> this.packageElement(ci.name().prefix()); + }); + + // Modifiers. + final short modifiers = ci.flags(); + if (java.lang.reflect.Modifier.isAbstract(modifiers)) { + e.addModifier(javax.lang.model.element.Modifier.ABSTRACT); + } else if (java.lang.reflect.Modifier.isFinal(modifiers)) { + e.addModifier(javax.lang.model.element.Modifier.FINAL); + } + if (java.lang.reflect.Modifier.isPrivate(modifiers)) { + e.addModifier(javax.lang.model.element.Modifier.PRIVATE); + } else if (java.lang.reflect.Modifier.isProtected(modifiers)) { + e.addModifier(javax.lang.model.element.Modifier.PROTECTED); + } else if (java.lang.reflect.Modifier.isPublic(modifiers)) { + e.addModifier(javax.lang.model.element.Modifier.PUBLIC); + } + // TODO: no way to tell if a ClassInfo is sealed. See https://github.com/smallrye/jandex/issues/167. + if (java.lang.reflect.Modifier.isStatic(modifiers)) { + e.addModifier(javax.lang.model.element.Modifier.STATIC); + } + + // Supertypes. + int position = 0; + final Type superclassType = ci.superClassType(); + if (superclassType != null) { + assert superclassType.kind() == Type.Kind.CLASS || superclassType.kind() == Type.Kind.PARAMETERIZED_TYPE; + e.setSuperclass(this.type(new TypeContext(ci, superclassType, position++, TypeContext.Kind.EXTENDS))); + } + for (final Type iface : ci.interfaceTypes()) { + e.addInterface(this.type(new TypeContext(ci, iface, position++, TypeContext.Kind.EXTENDS))); + } + position = 0; + + // Type parameters. + for (final org.jboss.jandex.TypeVariable tp : ci.typeParameters()) { + final org.microbean.lang.element.TypeParameterElement tpe = + (org.microbean.lang.element.TypeParameterElement)this.element(new TypeParameterInfo(ci, tp)); + assert ((javax.lang.model.type.TypeVariable)tpe.asType()).asElement() == tpe : + "tpe.asType(): " + tpe.asType() + + "; tpe.asType().asElement(): " + ((javax.lang.model.type.TypeVariable)tpe.asType()).asElement(); + e.addTypeParameter(tpe); + } + assert e.getTypeParameters().size() == ci.typeParameters().size(); + + e.setEnclosedElementsGenerator(() -> { + ci.constructors().forEach(this::element); + ci.unsortedRecordComponents().forEach(this::element); + ci.unsortedFields().forEach(this::element); + ci.unsortedMethods().forEach(this::element); + ci.memberClasses().forEach(this::element); + }); + + for (final AnnotationInstance ai : ci.declaredAnnotations()) { + e.addAnnotationMirror(this.annotation(ai)); + } + } + + private final void build(final FieldInfo fi, final org.microbean.lang.element.VariableElement e) { + // Simple name. + e.setSimpleName(fi.name()); + + // Type. + e.setType(this.type(fi)); + + // Enclosing element. + e.setEnclosingElement(this.element(fi.declaringClass())); + + // Annotations. + for (final AnnotationInstance ai : fi.declaredAnnotations()) { + e.addAnnotationMirror(this.annotation(ai)); + } + } + + private final void build(final MethodInfo mi, final org.microbean.lang.element.ExecutableElement e) { + // Simple name. + if (!mi.isConstructor()) { + e.setSimpleName(mi.name()); + } + + // Type. + e.setType(this.type(mi)); + + // Enclosing element. + final TypeElement ee = (TypeElement)this.element(mi.declaringClass()); + assert ee != null; + e.setEnclosingElement(ee); + assert e.getEnclosingElement() == ee : "e: " + e + "; ee: " + ee; + + // Modifiers. + final short modifiers = mi.flags(); + if (isDefault(mi)) { + e.setDefault(true); + e.addModifier(javax.lang.model.element.Modifier.DEFAULT); + } else { + e.setDefault(false); + if (java.lang.reflect.Modifier.isAbstract(modifiers)) { + e.addModifier(javax.lang.model.element.Modifier.ABSTRACT); + } else if (java.lang.reflect.Modifier.isFinal(modifiers)) { + e.addModifier(javax.lang.model.element.Modifier.FINAL); + } else if (java.lang.reflect.Modifier.isNative(modifiers)) { + e.addModifier(javax.lang.model.element.Modifier.NATIVE); + } + if (java.lang.reflect.Modifier.isStatic(modifiers)) { + e.addModifier(javax.lang.model.element.Modifier.STATIC); + } + if (java.lang.reflect.Modifier.isSynchronized(modifiers)) { + e.addModifier(javax.lang.model.element.Modifier.SYNCHRONIZED); + } + } + if (java.lang.reflect.Modifier.isPrivate(modifiers)) { + e.addModifier(javax.lang.model.element.Modifier.PRIVATE); + } else if (java.lang.reflect.Modifier.isProtected(modifiers)) { + e.addModifier(javax.lang.model.element.Modifier.PROTECTED); + } else if (java.lang.reflect.Modifier.isPublic(modifiers)) { + e.addModifier(javax.lang.model.element.Modifier.PUBLIC); + } + + // Type parameters. + for (final org.jboss.jandex.TypeVariable tp : mi.typeParameters()) { + e.addTypeParameter((org.microbean.lang.element.TypeParameterElement)this.element(new TypeParameterInfo(mi, tp))); + } + + // Parameters. + for (final MethodParameterInfo p : mi.parameters()) { + e.addParameter((org.microbean.lang.element.VariableElement)this.element(p)); + } + + for (final AnnotationInstance ai : mi.declaredAnnotations()) { + e.addAnnotationMirror(this.annotation(ai)); + } + } + + private final void build(final MethodParameterInfo mpi, final org.microbean.lang.element.VariableElement e) { + // Simple name. + String n = mpi.name(); + if (n == null) { + n = "arg" + mpi.position(); + } + e.setSimpleName(n); + + // Type. + e.setType(this.type(mpi)); + + // (No enclosing element.) + // e.setEnclosingElement(this.element(mpi.method())); // interestingly not supported by the javax.lang.model.* api + + for (final AnnotationInstance ai : mpi.declaredAnnotations()) { + e.addAnnotationMirror(this.annotation(ai)); + } + } + + private final void build(final RecordComponentInfo r, final org.microbean.lang.element.RecordComponentElement e) { + // Simple name. + e.setSimpleName(r.name()); + + // Type. + e.setType(this.type(r)); + + // Enclosing element. + e.setEnclosingElement(this.element(r)); + + e.setAccessor((org.microbean.lang.element.ExecutableElement)this.element(r.declaringClass().method(r.name()))); + + for (final AnnotationInstance ai : r.declaredAnnotations()) { + e.addAnnotationMirror(this.annotation(ai)); + } + } + + private final void build(final TypeParameterInfo tpi, final org.microbean.lang.element.TypeParameterElement e) { + // Simple name. + e.setSimpleName(tpi.identifier()); + + // Type. + final org.microbean.lang.type.TypeVariable t = (org.microbean.lang.type.TypeVariable)this.type(tpi); + e.setType(t); + + // Defining element. + t.setDefiningElement(e); + + // Enclosing element. + e.setEnclosingElement(switch (tpi.kind()) { + case CLASS -> this.element(tpi.annotationTarget().asClass()); + case METHOD -> this.element(tpi.annotationTarget().asMethod()); + default -> throw new AssertionError(); + }); + + for (final AnnotationInstance ai : tpi.typeVariable().annotations()) { + // This is nice, in a way. We know all of these annotations will be type use annotations, because Jandex doesn't + // really reify type parameters. + // + // Then we know that they can't be CLASS_EXTENDS, EMPTY, METHOD_PARAMETER, THROWS or TYPE_PARAMETER_BOUND, so they + // must be TYPE_PARAMETER. We can't check, because ai.target() is guaranteed by javadoc to be null. But as you can + // see we don't need to check. + e.addAnnotationMirror(this.annotation(ai)); + } + } + + + /* + * Type builders. + */ + + + private final void build(final TypeContext tc, final org.microbean.lang.type.ArrayType t) { + final org.jboss.jandex.ArrayType a = tc.type().asArrayType(); + final Type componentType = a.constituent(); + t.setComponentType(this.type(new TypeContext(tc.context(), componentType, 0, TypeContext.Kind.COMPONENT_TYPE))); + for (final AnnotationInstance ai : a.annotations()) { + t.addAnnotationMirror(this.annotation(ai)); + } + } + + private final void build(final TypeContext tc, final org.microbean.lang.type.DeclaredType t) { + final Type tct = tc.type(); + final ClassInfo ci = this.classInfoFor(tct); + t.setDefiningElement((TypeElement)this.element(ci)); + switch (tct.kind()) { + case CLASS: + t.setEnclosingType(this.type(this.classInfoFor(ci.enclosingClass()))); + break; + case PARAMETERIZED_TYPE: + final ParameterizedType pt = tct.asParameterizedType(); + final Type ownerType = pt.owner(); + t.setEnclosingType(this.type(ownerType == null ? this.classInfoFor(ci.enclosingClass()) : this.classInfoFor(ownerType))); + int position = 0; + for (final Type arg : pt.arguments()) { + t.addTypeArgument(this.type(new TypeContext(tc.context(), arg, position++, TypeContext.Kind.TYPE_ARGUMENT))); + } + break; + default: + throw new AssertionError(); + } + for (final AnnotationInstance ai : tct.annotations()) { + t.addAnnotationMirror(this.annotation(ai)); + } + } + + private final void build(final TypeContext tc, final org.microbean.lang.type.WildcardType w) { + final org.jboss.jandex.WildcardType tcType = tc.type().asWildcardType(); + Type bound = tcType.extendsBound(); + if (bound != null) { + w.setExtendsBound(this.type(new TypeContext(tc.context(), bound, 0, TypeContext.Kind.EXTENDS))); + } + bound = tcType.superBound(); + if (bound != null) { + w.setSuperBound(this.type(new TypeContext(tc.context(), bound, 0, TypeContext.Kind.EXTENDS))); + } + for (final AnnotationInstance ai : tcType.annotations()) { + w.addAnnotationMirror(this.annotation(ai)); + } + } + + private final void build(final ClassInfo ci, final org.microbean.lang.type.DeclaredType t) { + final org.microbean.lang.element.TypeElement e = (org.microbean.lang.element.TypeElement)this.element(ci); + + // Type. + e.setType(t); + + // Defining element. + t.setDefiningElement(e); + + // Need to do enclosing type, if there is one + final ClassInfo enclosingClass = this.classInfoEnclosing(ci); + if (enclosingClass != null) { + t.setEnclosingType(this.type(enclosingClass)); + } + + // Now type arguments (which will be type variables), if there are any. + final List tps = ci.typeParameters(); + for (int i = 0; i < tps.size(); i++) { + t.addTypeArgument(this.type(new TypeContext(ci, tps.get(i), i, TypeContext.Kind.TYPE_ARGUMENT))); + } + + // There isn't a way to get type use annotations on ci. + + } + + private final void build(final FieldInfo fi, final org.microbean.lang.type.ArrayType t) { + final org.jboss.jandex.ArrayType ft = fi.type().asArrayType(); + final org.microbean.lang.element.VariableElement e = (org.microbean.lang.element.VariableElement)this.element(fi); + e.setType(t); + t.setComponentType(this.type(new TypeContext(fi, ft.constituent(), 0, TypeContext.Kind.COMPONENT_TYPE))); + for (final AnnotationInstance ai : ft.annotations()) { + t.addAnnotationMirror(this.annotation(ai)); + } + } + + private final void build(final FieldInfo fi, final org.microbean.lang.type.DeclaredType t) { + final org.jboss.jandex.Type ft = fi.type(); + final org.microbean.lang.element.VariableElement e = (org.microbean.lang.element.VariableElement)this.element(fi); + e.setType(t); + t.setDefiningElement((org.microbean.lang.element.TypeElement)this.element(ft.name())); + for (final AnnotationInstance ai : ft.annotations()) { + t.addAnnotationMirror(this.annotation(ai)); + } + } + + private final void build(final FieldInfo fi, final org.microbean.lang.type.PrimitiveType t) { + final org.microbean.lang.element.VariableElement e = (org.microbean.lang.element.VariableElement)this.element(fi); + e.setType(t); + // Primitive types cannot bear annotations. + } + + private final void build(final FieldInfo fi, final org.microbean.lang.type.TypeVariable t) { + final org.jboss.jandex.TypeVariable ft = fi.type().asTypeVariable(); + final org.microbean.lang.element.VariableElement e = (org.microbean.lang.element.VariableElement)this.element(fi); + e.setType(t); + t.setDefiningElement((org.microbean.lang.element.TypeParameterElement)this.element(typeParameterInfoFor(fi, ft))); + for (final AnnotationInstance ai : ft.annotations()) { + t.addAnnotationMirror(this.annotation(ai)); + } + } + + private final void build(final MethodInfo mi, final org.microbean.lang.type.ExecutableType t) { + for (final MethodParameterInfo mpi : mi.parameters()) { + t.addParameterType(this.type(mpi)); + } + + final Type receiverType; + if (mi.isConstructor()) { + receiverType = mi.declaringClass().enclosingClass() == null ? null : mi.receiverType(); + } else { + receiverType = java.lang.reflect.Modifier.isStatic(mi.flags()) ? null : mi.receiverType(); + } + t.setReceiverType(receiverType == null ? + org.microbean.lang.type.NoType.NONE : + this.type(new TypeContext(mi, receiverType, 0, TypeContext.Kind.RECEIVER))); + t.setReturnType(this.type(new TypeContext(mi, mi.returnType(), 0, TypeContext.Kind.RETURN))); + + int position = 0; + for (final Type et : mi.exceptions()) { + t.addThrownType(this.type(new TypeContext(mi, et, position++, TypeContext.Kind.THROWS))); + } + + position = 0; + for (final org.jboss.jandex.TypeVariable tv : mi.typeParameters()) { + t.addTypeVariable((org.microbean.lang.type.TypeVariable)this.type(new TypeContext(mi, tv, position++, TypeContext.Kind.TYPE_ARGUMENT))); + } + } + + private final void build(final MethodParameterInfo mpi, final org.microbean.lang.type.ArrayType t) { + final org.jboss.jandex.ArrayType mpit = mpi.type().asArrayType(); + final org.microbean.lang.element.VariableElement e = (org.microbean.lang.element.VariableElement)this.element(mpi); + e.setType(t); + t.setComponentType(this.type(new TypeContext(mpi, mpit.constituent(), 0, TypeContext.Kind.COMPONENT_TYPE))); + for (final AnnotationInstance ai : mpit.annotations()) { + t.addAnnotationMirror(this.annotation(ai)); + } + } + + private final void build(final MethodParameterInfo mpi, final org.microbean.lang.type.DeclaredType t) { + final org.jboss.jandex.Type mpit = mpi.type(); + final org.microbean.lang.element.VariableElement e = (org.microbean.lang.element.VariableElement)this.element(mpi); + e.setType(t); + t.setDefiningElement((org.microbean.lang.element.TypeElement)this.element(mpit.name())); + for (final AnnotationInstance ai : mpit.annotations()) { + t.addAnnotationMirror(this.annotation(ai)); + } + } + + private final void build(final MethodParameterInfo mpi, final org.microbean.lang.type.PrimitiveType t) { + final org.microbean.lang.element.VariableElement e = (org.microbean.lang.element.VariableElement)this.element(mpi); + e.setType(t); + // Primitive types cannot bear annotations. + } + + private final void build(final MethodParameterInfo mpi, final org.microbean.lang.type.TypeVariable t) { + final org.jboss.jandex.TypeVariable mpit = mpi.type().asTypeVariable(); + final org.microbean.lang.element.VariableElement e = (org.microbean.lang.element.VariableElement)this.element(mpi); + e.setType(t); + t.setDefiningElement((org.microbean.lang.element.TypeParameterElement)this.element(typeParameterInfoFor(mpi, mpit))); + for (final AnnotationInstance ai : mpit.annotations()) { + t.addAnnotationMirror(this.annotation(ai)); + } + } + + private final void build(final RecordComponentInfo rci, final org.microbean.lang.type.ArrayType t) { + final org.jboss.jandex.ArrayType rcit = rci.type().asArrayType(); + final org.microbean.lang.element.RecordComponentElement e = (org.microbean.lang.element.RecordComponentElement)this.element(rci); + e.setType(t); + t.setComponentType(this.type(new TypeContext(rci, rcit.constituent(), 0, TypeContext.Kind.COMPONENT_TYPE))); + for (final AnnotationInstance ai : rcit.annotations()) { + t.addAnnotationMirror(this.annotation(ai)); + } + } + + private final void build(final RecordComponentInfo rci, final org.microbean.lang.type.DeclaredType t) { + final org.jboss.jandex.Type rcit = rci.type(); + final org.microbean.lang.element.RecordComponentElement e = (org.microbean.lang.element.RecordComponentElement)this.element(rci); + e.setType(t); + t.setDefiningElement((org.microbean.lang.element.TypeElement)this.element(rcit.name())); + for (final AnnotationInstance ai : rcit.annotations()) { + t.addAnnotationMirror(this.annotation(ai)); + } + } + + private final void build(final RecordComponentInfo rci, final org.microbean.lang.type.PrimitiveType t) { + final org.microbean.lang.element.RecordComponentElement e = (org.microbean.lang.element.RecordComponentElement)this.element(rci); + e.setType(t); + // Primitive types cannot bear annotations. + } + + private final void build(final RecordComponentInfo rci, final org.microbean.lang.type.TypeVariable t) { + final org.jboss.jandex.TypeVariable rcit = rci.type().asTypeVariable(); + final org.microbean.lang.element.RecordComponentElement e = (org.microbean.lang.element.RecordComponentElement)this.element(rci); + e.setType(t); + t.setDefiningElement((org.microbean.lang.element.TypeParameterElement)this.element(typeParameterInfoFor(rci, rcit))); + for (final AnnotationInstance ai : rcit.annotations()) { + t.addAnnotationMirror(this.annotation(ai)); + } + } + + private final void build(final TypeParameterInfo tpi, final org.microbean.lang.type.TypeVariable t) { + final org.microbean.lang.element.TypeParameterElement e = (org.microbean.lang.element.TypeParameterElement)this.element(tpi); + + // Type. + e.setType(t); + + // Defining element. + t.setDefiningElement(e); + + final AnnotationTarget context = tpi.annotationTarget(); + final List bounds = tpi.typeVariable().bounds(); + switch (bounds.size()) { + case 0: + break; + case 1: + t.setUpperBound(this.type(new TypeContext(context, bounds.get(0), 0, TypeContext.Kind.BOUND))); + break; + default: + final org.microbean.lang.type.IntersectionType upperBound = new org.microbean.lang.type.IntersectionType(); + int position = 0; + for (final Type bound : bounds) { + upperBound.addBound(this.type(new TypeContext(context, bound, position++, TypeContext.Kind.BOUND))); + } + t.setUpperBound(upperBound); + break; + } + + // I *believe* that when all is said and done, tpi.type().annotations() will reflect annotations on the type + // parameter *element*, not the type use, and so there's no way to get type use annotations here in Jandex. + + } + + // Having PrimitiveType as a parameter here is OK because primitive types cannot bear annotations. + private final void build(final PrimitiveType p, final org.microbean.lang.type.PrimitiveType t) { + + } + + + /* + * Housekeeping. + */ + + + private final MethodInfo annotationElementFor(final ClassInfo ci, final String name) { + if (ci.isAnnotation()) { + return ci.method(name); + } + throw new IllegalArgumentException("ci: " + ci); + } + + private final ClassInfo classInfoFor(final AnnotationInstance ai) { + return ai == null ? null : this.classInfoFor(ai.name()); + } + + private final ClassInfo classInfoFor(final Type t) { + return t == null ? null : switch(t.kind()) { + case CLASS -> this.classInfoFor(t.asClassType()); + case PARAMETERIZED_TYPE -> this.classInfoFor(t.asParameterizedType()); + default -> null; + }; + } + + private final ClassInfo classInfoFor(final ClassType t) { + return t == null ? null : this.classInfoFor(t.name()); + } + + private final ClassInfo classInfoFor(final ParameterizedType t) { + return t == null ? null : this.classInfoFor(t.name()); + } + + private final ClassInfo classInfoFor(final DotName n) { + if (n == null) { + return null; + } + final ClassInfo ci = this.i.getClassByName(n); + return ci == null ? this.unindexedClassnameFunction.apply(n.toString(), this.i) : ci; + } + + private final ClassInfo classInfoFor(final String n) { + if (n == null) { + return null; + } + final ClassInfo ci = this.i.getClassByName(n); + return ci == null ? this.unindexedClassnameFunction.apply(n, this.i) : ci; + } + + private final ClassInfo classInfoEnclosing(final ClassInfo ci) { + return ci == null ? null : this.classInfoFor(ci.enclosingClass()); + } + + final TypeParameterInfo typeParameterInfoFor(final AnnotationTarget context, final org.jboss.jandex.TypeVariable tv) { + return switch (context.kind()) { + case CLASS -> this.typeParameterInfoFor(context.asClass(), tv); + case FIELD -> this.typeParameterInfoFor(context.asField(), tv); + case METHOD -> this.typeParameterInfoFor(context.asMethod(), tv); + case METHOD_PARAMETER -> this.typeParameterInfoFor(context.asMethodParameter(), tv); + case RECORD_COMPONENT -> this.typeParameterInfoFor(context.asRecordComponent(), tv); + case TYPE -> throw new UnsupportedOperationException(); + }; + } + + final TypeParameterInfo typeParameterInfoFor(final ClassInfo context, final org.jboss.jandex.TypeVariable tv) { + final String id = tv.identifier(); + for (final org.jboss.jandex.TypeVariable tp : context.typeParameters()) { + if (tp.identifier().equals(id)) { + return new TypeParameterInfo(context, tp); + } + } + final EnclosingMethodInfo enclosingMethod = context.enclosingMethod(); + return enclosingMethod == null ? this.typeParameterInfoFor(context.enclosingClass(), tv) : this.typeParameterInfoFor(enclosingMethod, tv); + } + + final TypeParameterInfo typeParameterInfoFor(final DotName context, final org.jboss.jandex.TypeVariable tv) { + return this.typeParameterInfoFor(this.classInfoFor(context), tv); + } + + final TypeParameterInfo typeParameterInfoFor(final EnclosingMethodInfo context, final org.jboss.jandex.TypeVariable tv) { + return this.typeParameterInfoFor(this.classInfoFor(context.enclosingClass()).method(context.name(), context.parameters().toArray(new Type[0])), tv); + } + + final TypeParameterInfo typeParameterInfoFor(final FieldInfo context, final org.jboss.jandex.TypeVariable tv) { + return this.typeParameterInfoFor(context.declaringClass(), tv); + } + + final TypeParameterInfo typeParameterInfoFor(final MethodInfo context, final org.jboss.jandex.TypeVariable tv) { + final String id = tv.identifier(); + for (final org.jboss.jandex.TypeVariable tp : context.typeParameters()) { + if (tp.identifier().equals(id)) { + return new TypeParameterInfo(context, tp); + } + } + return this.typeParameterInfoFor(context.declaringClass(), tv); + } + + final TypeParameterInfo typeParameterInfoFor(final MethodParameterInfo context, final org.jboss.jandex.TypeVariable tv) { + return this.typeParameterInfoFor(context.method(), tv); + } + + final TypeParameterInfo typeParameterInfoFor(final RecordComponentInfo context, final org.jboss.jandex.TypeVariable tv) { + return this.typeParameterInfoFor(context.declaringClass(), tv); + } + + final TypeParameterInfo typeParameterInfoFor(final TypeContext tc) { + return this.typeParameterInfoFor(tc.context(), tc.type().asTypeVariable()); + } + + + /* + * Static methods. + */ + + + private static final boolean isDefault(final MethodInfo mi) { + if (mi.declaringClass().isInterface()) { + final short flags = mi.flags(); + return !java.lang.reflect.Modifier.isStatic(flags) && !java.lang.reflect.Modifier.isAbstract(flags); + } + return false; + } + + private static final ElementKind kind(final ClassInfo ci) { + return + ci.isAnnotation() ? ElementKind.ANNOTATION_TYPE : + ci.isEnum() ? ElementKind.ENUM : + ci.isInterface() ? ElementKind.INTERFACE : + ci.isModule() ? ElementKind.MODULE : + ci.isRecord() ? ElementKind.RECORD : + ElementKind.CLASS; + } + + private static final ElementKind kind(final FieldInfo f) { + return f.isEnumConstant() ? ElementKind.ENUM_CONSTANT : ElementKind.FIELD; + } + + private static final ElementKind kind(final MethodInfo m) { + return + m.isConstructor() ? ElementKind.CONSTRUCTOR : + m.name().equals("") ? ElementKind.STATIC_INIT : + m.name().equals("") ? ElementKind.INSTANCE_INIT : + ElementKind.METHOD; + } + + private static final TypeKind kind(final PrimitiveType p) { + return kind(p.primitive()); + } + + private static final TypeKind kind(final PrimitiveType.Primitive p) { + return switch (p) { + case BOOLEAN -> TypeKind.BOOLEAN; + case BYTE -> TypeKind.BYTE; + case CHAR -> TypeKind.CHAR; + case DOUBLE -> TypeKind.DOUBLE; + case FLOAT -> TypeKind.FLOAT; + case INT -> TypeKind.INT; + case LONG -> TypeKind.LONG; + case SHORT -> TypeKind.SHORT; + }; + } + + private static final NestingKind nestingKind(final ClassInfo ci) { + return nestingKind(ci.nestingType()); + } + + private static final NestingKind nestingKind(final NestingType n) { + return switch (n) { + case ANONYMOUS -> NestingKind.ANONYMOUS; // In fact, Jandex will never supply this. + case INNER -> NestingKind.MEMBER; + case LOCAL -> NestingKind.LOCAL; // In fact, Jandex will never supply this. + case TOP_LEVEL -> NestingKind.TOP_LEVEL; + }; + } + + + /* + * Inner and nested classes. + */ + + + public static final record PackageInfo(DotName name) { + + } + + public static final record TypeParameterInfo(AnnotationTarget annotationTarget, org.jboss.jandex.TypeVariable typeVariable) { + + public TypeParameterInfo { + switch (annotationTarget.kind()) { + case CLASS: + case METHOD: + break; + default: + throw new IllegalArgumentException("annotationTarget: " + annotationTarget); + } + Objects.requireNonNull(typeVariable, "typeVariable"); + } + + /** + * Returns the value of the {@code annotationTarget} record component, which will either be a {@link ClassInfo} or a + * {@link MethodInfo}. + * + * @return the value of the {@code annotationTarget} record component, which will either be a {@link ClassInfo} or a + * {@link MethodInfo} + */ + @Override + public final AnnotationTarget annotationTarget() { + return this.annotationTarget; + } + + public final AnnotationTarget.Kind kind() { + return this.annotationTarget().kind(); + } + + public final String identifier() { + return this.typeVariable().identifier(); + } + + } + + // Represents a "type context" in the parlance of + // https://docs.oracle.com/javase/specs/jls/se19/html/jls-4.html#jls-4.11. Not all such type contexts are represented + // here. + public static final class TypeContext { + + private final TypeContext parent; + + private final AnnotationTarget context; + + private final Type type; + + private final int position; + + private final Kind kind; + + private TypeContext(final AnnotationTarget context, final Type type, final int position, final Kind kind) { + this(null, context, type, position, kind); + } + + private TypeContext(final TypeContext parent, final AnnotationTarget context, final Type type, final int position, final Kind kind) { + super(); + validate(parent, context, type, position, kind); + this.parent = parent; + this.context = context; + this.type = type; + this.position = position; + this.kind = kind; + } + + private final TypeContext parent() { + return this.parent; + } + + private final AnnotationTarget context() { + return this.context; + } + + private final Type type() { + return this.type; + } + + private final int position() { + return this.position; + } + + private final Kind kind() { + return this.kind; + } + + @Override + public final int hashCode() { + int hashCode = 17; + Object v = this.parent(); + int c = v == null ? 0 : v.hashCode(); + hashCode = 37 * hashCode + c; + + v = this.context(); + c = v == null ? 0 : v.hashCode(); + hashCode = 37 * hashCode + c; + + v = this.type(); + c = v == null ? 0 : v.hashCode(); + hashCode = 37 * hashCode + c; + + hashCode = 37 * hashCode + this.position(); + + hashCode = 37 * hashCode + this.kind().hashCode(); + + return hashCode; + } + + @Override + public final boolean equals(final Object other) { + if (other == this) { + return true; + } else if (other != null && other.getClass() == this.getClass()) { + final TypeContext her = (TypeContext)other; + return + Objects.equals(this.parent(), her.parent()) && + Objects.equals(this.context(), her.context()) && + Objects.equals(this.type(), her.type()) && + Objects.equals(this.position(), her.position()) && + Objects.equals(this.kind(), her.kind()); + } else { + return false; + } + } + + private static final void validate(final TypeContext parent, final AnnotationTarget context, final Type type, final int position, final Kind kind) { + if (position < 0) { + throw new IndexOutOfBoundsException("position: " + position); + } + } + + // See relevant type contexts from https://docs.oracle.com/javase/specs/jls/se19/html/jls-4.html#jls-4.11 + private static enum Kind { + + // See also https://github.com/openjdk/jdk/blob/jdk-21%2B13/src/jdk.compiler/share/classes/com/sun/tools/javac/code/TargetType.java + + BOUND, // e.g. the "@Bar Glug" in "public class Foo {}" + EXTENDS, // e.g. the "Glug", "Bar" or "Qux" in "public class Foo extends @Baz Bar implements Qux {}" + RETURN, // method return type + THROWS, // (method) throws type + RECEIVER, // method receiver type + TYPE_ARGUMENT, // e.g. the "String" in "public class Foo extends Bar<@Baz String> {}" + COMPONENT_TYPE // e.g. "@Baz"-annotated "[]" in "@Qux String @Bar [] @Baz []" + + } + + } + +} diff --git a/jandex/src/main/java/org/microbean/lang/jandex/package-info.java b/jandex/src/main/java/org/microbean/lang/jandex/package-info.java new file mode 100644 index 00000000..bb9fb7b4 --- /dev/null +++ b/jandex/src/main/java/org/microbean/lang/jandex/package-info.java @@ -0,0 +1,24 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +/** + * Provides classes and interfaces related to the Java language model and Jandex. + * + * @author Laird Nelson + */ +package org.microbean.lang.jandex; diff --git a/jandex/src/main/javadoc/css/fonts.css b/jandex/src/main/javadoc/css/fonts.css new file mode 100644 index 00000000..bd00dc52 --- /dev/null +++ b/jandex/src/main/javadoc/css/fonts.css @@ -0,0 +1,62 @@ +@import url('https://fonts.googleapis.com/css2?2?family=Roboto+Mono:ital,wght@0,400;1,400&family=Roboto:ital,wght@0,400;0,700;1,400;1,700&family=Lobster&display=swap'); +body { + font-family: 'Roboto', 'DejaVu Sans', Arial, Helvetica, sans-serif; +} +button { + font-family: 'Roboto', 'DejaVu Sans', Arial, Helvetica, sans-serif; +} +h1, h2, h3 { + font-weight: 700 +} +input { + font-family: 'Roboto', 'DejaVu Sans', Arial, Helvetica, sans-serif; +} +code, tt, pre { + font-family: 'Roboto Mono', 'DejaVu Sans Mono', monospace; +} +.contentContainer .description dl dd, .contentContainer .details dl dd, .serializedFormContainer dl dd { + font-family: 'Roboto', 'DejaVu Sans', Arial, Helvetica, sans-serif; +} +div.block { + font-family: 'Roboto', 'DejaVu Sans', Arial, Helvetica, sans-serif; +} +div.table-tabs > button { + font-weight: 700 +} +dl.notes > dt { + font-family: 'Roboto', 'DejaVu Sans', Arial, Helvetica, sans-serif; + font-weight: 700 +} +dl.notes > dd { + font-family: 'Roboto', 'DejaVu Sans', Arial, Helvetica, sans-serif; +} +strong { + font-weight: 700 +} +.caption { + font-weight: 700 +} +.table-header { + font-weight: 700 +} +.col-first a:link, .col-first a:visited, .col-second a:link, .col-second a:visited, .col-first a:link, .col-first a:visited, .col-second a:link, .col-second a:visited, .col-constructor-name a:link, .col-constructor-name a:visited, .col-summary-item-name a:link, .col-summary-item-name a:visited, .constant-values-container a:link, .constant-values-container a:visited, .all-classes-container a:link, .all-classes-container a:visited, .all-packages-container a:link, .all-packages-container a:visited { + font-weight: 700 +} +.deprecated-label, .descfrm-type-label, .implementation-label, .member-name-label, .member-name-link, .module-label-in-package, .module-label-in-type, .override-specify-label, .package-label-in-type, .package-hierarchy-label, .type-name-label, .type-name-link, .search-tag-link, .preview-label { + font-weight: 700 +} +.module-signature, .package-signature, .type-signature, .member-signature { + font-family: 'Roboto Mono', 'DejaVu Sans Mono', monospace; +} +main a[href*="://"]::after { + all: unset; +} +.result-highlight { + font-weight: 700; +} +.ui-widget { + font-family: 'Roboto', 'DejaVu Sans', Arial, Helvetica, sans-serif !important; +} +.ui-autocomplete-category { + font-weight: 700; +} diff --git a/jandex/src/site/markdown/index.md.vm b/jandex/src/site/markdown/index.md.vm new file mode 100644 index 00000000..c79baf67 --- /dev/null +++ b/jandex/src/site/markdown/index.md.vm @@ -0,0 +1 @@ +#include("../../../README.md") diff --git a/jandex/src/site/site.xml b/jandex/src/site/site.xml new file mode 100644 index 00000000..3249f3d5 --- /dev/null +++ b/jandex/src/site/site.xml @@ -0,0 +1,37 @@ + + + + + + + + + org.apache.maven.skins + maven-fluido-skin + 2.0.0-M8 + + + + + + + + + + + + + + + true + false + + + + diff --git a/jandex/src/test/java/org/microbean/lang/jandex/TestIsAssignable.java b/jandex/src/test/java/org/microbean/lang/jandex/TestIsAssignable.java new file mode 100644 index 00000000..8b9296f2 --- /dev/null +++ b/jandex/src/test/java/org/microbean/lang/jandex/TestIsAssignable.java @@ -0,0 +1,17 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.jandex; diff --git a/jandex/src/test/java/org/microbean/lang/jandex/TestJandex.java b/jandex/src/test/java/org/microbean/lang/jandex/TestJandex.java new file mode 100644 index 00000000..896d78a2 --- /dev/null +++ b/jandex/src/test/java/org/microbean/lang/jandex/TestJandex.java @@ -0,0 +1,299 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.jandex; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.UncheckedIOException; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import java.net.URI; +import java.net.URISyntaxException; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +import java.util.Collection; + +import java.util.function.Predicate; + +import java.util.stream.Stream; + +import javax.lang.model.element.Element; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.DotName; +import org.jboss.jandex.EmptyTypeTarget; +import org.jboss.jandex.Index; +import org.jboss.jandex.IndexReader; +import org.jboss.jandex.IndexView; +import org.jboss.jandex.IndexWriter; +import org.jboss.jandex.Indexer; +import org.jboss.jandex.MethodInfo; +import org.jboss.jandex.PrimitiveType; +import org.jboss.jandex.TypeParameterTypeTarget; +import org.jboss.jandex.TypeTarget; +import org.jboss.jandex.TypeVariable; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.fail; + +final class TestJandex { + + private static IndexView jdk; + + private Jandex jandex; + + private TestJandex() { + super(); + } + + @BeforeAll + static final void indexJdk() { + // See https://stackoverflow.com/a/46451977/208288 + jdk = + index(Path.of(URI.create("jrt:/")), + p -> { + // System.out.println("*** p: " + p); + return p.getFileName().toString().endsWith(".class"); + }); + // p -> (p.getNameCount() < 3 || p.getName(2).toString().startsWith("java")) && p.getFileName().toString().endsWith(".class")); + } + + @BeforeEach + final void setup() { + this.jandex = new Jandex(jdk); + } + + @Test + final void testClassInfoIdentity() { + final ClassInfo ci = jdk.getClassByName("java.lang.String"); + assertNotNull(ci); + assertSame(ci, jdk.getClassByName("java.lang.String")); + DotName javaLangString = DotName.createComponentized(null, "java"); + javaLangString = DotName.createComponentized(javaLangString, "lang"); + javaLangString = DotName.createComponentized(javaLangString, "String"); + assertEquals("java.lang.String", javaLangString.toString()); + assertSame(ci, jdk.getClassByName(javaLangString)); + } + + @Test + final void testMethodInfoNonIdentity() { + final MethodInfo mi = jdk.getClassByName("java.lang.String").method("charAt", PrimitiveType.INT); + assertNotNull(mi); + assertEquals(mi, jdk.getClassByName("java.lang.String").method("charAt", PrimitiveType.INT)); + assertNotSame(mi, jdk.getClassByName("java.lang.String").method("charAt", PrimitiveType.INT)); + } + + @Test + final void testPackageStuff() throws IOException, URISyntaxException { + final IndexView i = index(Path.of(org.microbean.lang.jandex.testjandex.Frobnicator.class.getProtectionDomain().getCodeSource().getLocation().toURI()), + p -> p.endsWith(".class")); + final Collection as = i.getAnnotations(org.microbean.lang.jandex.testjandex.Flabrous.class); + // Note that Jandex does not allow you to get package annotations. There is no such thing as PackageInfo. This is + // true even for inherited annotations. + assertEquals(0, as.size()); + } + + @Test + final void testEnum() { + // Nice class to test "circular" type variables. + final Element e = jandex.element(jdk.getClassByName("java.lang.Enum")); + } + + @Test + final void testObject() { + jandex.element(jdk.getClassByName("java.lang.Object")); + } + + @Test + final void testClass() { + jandex.element(jdk.getClassByName("java.lang.Class")); + } + + @Test + final void testDocumented() { + final Element e = jandex.element(jdk.getClassByName("java.lang.annotation.Documented")); + } + + @Test + final void testTypeParameterAnnotations() throws IOException { + final Indexer indexer = new Indexer(); + indexer.indexClass(Flob.class); + final IndexView i = indexer.complete(); + final ClassInfo ci = i.getClassByName(Flob.class.getName()); + + // The class element itself has no declared annotations. + assertEquals(0, ci.declaredAnnotations().size()); + + // The only way to get to the annotations declared on its sole type parameter element is using this horrible + // mechanism. annotations() is documented to return "the annotation instances declared on this annotation target + // and nested annotation targets". This doesn't include nested classes, just fields, constructors and methods, I + // guess. + + // 3 is: + // * 1 @Borf annotation on T, the type parameter declared by Flob + // * 1 @Borf annotation on yeet() + // * 1 @Borf type-use annotation on yeet()'s return value, a String + // * 0 @Borf annotations on Bozo because it isn't seen + assertEquals(3, ci.annotations().size(), "ci.annotations(): " + ci.annotations()); + + // As you can see, Jandex gets confused about what a type parameter is versus what a type variable is. + for (final AnnotationInstance ai : ci.annotations()) { + final AnnotationTarget target = ai.target(); + switch (target.kind()) { + case TYPE: + final TypeTarget tt = target.asType(); + switch (tt.usage()) { + case EMPTY: + final EmptyTypeTarget ett = tt.asEmpty(); + assertFalse(ett.isReceiver()); + assertEquals("java.lang.String", ett.target().name().toString()); + assertEquals("yeet", ett.enclosingTarget().asMethod().name()); + break; + case TYPE_PARAMETER: + // expected + break; + default: + fail(); + } + break; + case METHOD: + break; + default: + fail(); + } + } + + } + + @Test + final void testEnclosingElementOfLocalClassInsideMethod() throws ClassNotFoundException, IOException { + final Indexer indexer = new Indexer(); + indexer.indexClass(Flob.class); + final IndexView i = indexer.complete(); + final ClassInfo ci = i.getClassByName(Flob.class.getName()); + + // Flob declares a method, yeet(), that declares a local class. You can't "get to" the local class at all from + // Jandex. See https://github.com/smallrye/jandex/issues/180#issue-1179577350. + // + // This turns out to be OK, because the javax.lang.model.* hierarchy also doesn't let you "get to" local or + // anonymous classes in any way. + + assertEquals(2, ci.memberClasses().size()); + + // You can't get it from the index… + assertNull(i.getClassByName(Flob.class.getName() + "$1Bozo")); + + // …but it does exist under that name: + assertNotNull(Class.forName(Flob.class.getName() + "$1Bozo")); + + for (final AnnotationInstance a : ci.annotations()) { + final AnnotationTarget target = a.target(); + switch (target.kind()) { + case METHOD: + assertEquals("yeet", target.asMethod().name()); + break; + case TYPE: + // This is where things get stupid. + final TypeTarget tt = target.asType(); + switch (tt.usage()) { + case EMPTY: + final EmptyTypeTarget ett = tt.asEmpty(); + assertFalse(ett.isReceiver()); + assertEquals("java.lang.String", ett.target().name().toString()); + assertEquals("yeet", ett.enclosingTarget().asMethod().name()); + break; + case TYPE_PARAMETER: + // Stupid. Actually an *element* annotation. + assertEquals(0, tt.asTypeParameter().position()); // T in Flob<@Borf T> + break; + default: + fail(); + } + break; + default: + fail(); + } + } + } + + private static final IndexView index(final Path indexRoot, + final Predicate filterPredicate) { + final Indexer indexer = new Indexer(); + try (final Stream pathStream = Files.walk(indexRoot, Integer.MAX_VALUE)) { + pathStream + .filter(filterPredicate) + .forEach(p -> { + try (final InputStream inputStream = Files.newInputStream(p)) { + indexer.index(inputStream); + } catch (final IOException ioException) { + throw new UncheckedIOException(ioException.getMessage(), ioException); + } + }); + } catch (final IOException e) { + throw new UncheckedIOException(e.getMessage(), e); + } + return indexer.complete(); + } + + private static interface Grop { + + void flop(Grop this); + + } + + private static final class Flob<@Borf T> { + + @Borf + private static final String yeet() { + // (Jandex will see nothing in here.) + @Borf + class Bozo {}; + return "Yeet"; + } + + @Borf + private final class Blotz {} + + @Borf + private static final class Greep {} + + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.TYPE, ElementType.TYPE_USE, ElementType.TYPE_PARAMETER, ElementType.METHOD }) + private @interface Borf {} + +} diff --git a/jandex/src/test/java/org/microbean/lang/jandex/TestJandexAndTypeParameters.java b/jandex/src/test/java/org/microbean/lang/jandex/TestJandexAndTypeParameters.java new file mode 100644 index 00000000..e53ce0ec --- /dev/null +++ b/jandex/src/test/java/org/microbean/lang/jandex/TestJandexAndTypeParameters.java @@ -0,0 +1,82 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.jandex; + +import java.io.IOException; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import java.util.List; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.IndexView; +import org.jboss.jandex.Indexer; +import org.jboss.jandex.Type; +import org.jboss.jandex.TypeTarget; +import org.jboss.jandex.TypeVariable; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +final class TestJandexAndTypeParameters { + + private TestJandexAndTypeParameters() { + super(); + } + + @Test + final void testTypeParameters() throws IOException { + final Indexer indexer = new Indexer(); + indexer.indexClass(A.class); + indexer.indexClass(C.class); + final IndexView i = indexer.complete(); + final ClassInfo a = i.getClassByName(A.class.getName()); + assertEquals("B", a.typeParameters().get(0).identifier()); + assertEquals(1, a.annotations().size()); // remember that annotations() "descends into" the class, so indexes all of its "contained" annotations + + // Jandex doesn't have the notion of a type parameter + // element/declaration. javax.lang.model.element.TypeParameterElement does. + + // This sucks. The annotation is clearly labeled as TYPE_PARAMETER and NOT TYPE_USE, but Jandex sees it as TYPE_USE + // anyway (!). However, annotations intended for type-variable-types-underlying-type-parameters are not a thing in the + // Java language, apparently, though they show up in the javax.lang.model.* hierarchy. The proper thing to do seems + // to be to treat all cases that fall under this case as element/declaration annotations on the type parameter + // elements, so this is "expected" according to Jandex's odd interpretation. + // + // Probably there will be a Jandex bug and/or refactoring in this area later. + assertSame(TypeTarget.Usage.TYPE_PARAMETER, a.annotations().get(0).target().asType().usage()); + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.TYPE_PARAMETER }) + private @interface C{} + + private static final class A<@C B> { + + } + +} diff --git a/jandex/src/test/java/org/microbean/lang/jandex/TestJandexTypeUseIssue.java b/jandex/src/test/java/org/microbean/lang/jandex/TestJandexTypeUseIssue.java new file mode 100644 index 00000000..56f185ef --- /dev/null +++ b/jandex/src/test/java/org/microbean/lang/jandex/TestJandexTypeUseIssue.java @@ -0,0 +1,124 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.jandex; + +import java.io.IOException; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.EmptyTypeTarget; +import org.jboss.jandex.Indexer; +import org.jboss.jandex.IndexView; +import org.jboss.jandex.TypeTarget; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.fail; + +final class TestJandexTypeUseIssue { + + private TestJandexTypeUseIssue() { + super(); + } + + @D + private static final class A { + + // This @D is both: + // * an annotation on the b() element + // * a type-use annotation on the return type of b() (String) + @D + private static final String b() { + + // This @D is never seen + @D + class C {}; + + return "Hello"; + + } + + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.METHOD, ElementType.TYPE, ElementType.TYPE_USE }) + private @interface D {} + + @Test + final void testTypeUse() throws ClassNotFoundException, IOException { + final Indexer indexer = new Indexer(); + indexer.indexClass(A.class); + indexer.indexClass(Class.forName(A.class.getName() + "$1C")); + final IndexView i = indexer.complete(); + final ClassInfo ci = i.getClassByName(A.class.getName()); + + // C does not show up as a member class, as it should not. + assertEquals(0, ci.memberClasses().size()); + + // 3 is: + // * 1 occurrence of @D on A + // * 2 occurrences of @D: + // * 1 occurrence of @D on the usage of the return type of the b() method + // * 1 occurrence of @D on the b() method element + // * 0 occurrences of @D on C (C is never seen by Jandex) + assertEquals(3, ci.annotations().size()); + + int count = 0; + for (final AnnotationInstance a : ci.annotations()) { + final AnnotationTarget target = a.target(); + switch (target.kind()) { + case CLASS: + if (target.asClass() != ci) { + fail(); + } + count++; + break; + case METHOD: + assertEquals("b", target.asMethod().name()); + count++; + break; + case TYPE: + final TypeTarget tt = target.asType(); + switch (tt.usage()) { + case EMPTY: + final EmptyTypeTarget ett = tt.asEmpty(); + assertFalse(ett.isReceiver()); + assertEquals("java.lang.String", ett.target().name().toString()); + assertEquals("b", ett.enclosingTarget().asMethod().name()); + count++; + break; + default: + fail(); + } + break; + default: + fail(); + } + } + assertEquals(3, count); + + } +} diff --git a/jandex/src/test/java/org/microbean/lang/jandex/TestObject.java b/jandex/src/test/java/org/microbean/lang/jandex/TestObject.java new file mode 100644 index 00000000..50b74fa4 --- /dev/null +++ b/jandex/src/test/java/org/microbean/lang/jandex/TestObject.java @@ -0,0 +1,97 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.jandex; + +import java.io.IOException; +import java.io.InputStream; +import java.io.UncheckedIOException; + +import java.net.URI; + +import java.nio.file.Files; +import java.nio.file.Path; + +import java.util.List; + +import java.util.function.Predicate; + +import java.util.stream.Stream; + +import javax.lang.model.element.Element; + +import org.jboss.jandex.Indexer; +import org.jboss.jandex.IndexView; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +final class TestObject { + + private static IndexView jdk; + + private TestObject() { + super(); + } + + @BeforeAll + static final void indexJdk() { + // See https://stackoverflow.com/a/46451977/208288 + jdk = index(Path.of(URI.create("jrt:/")), p -> p.getFileName().toString().endsWith(".class")); + } + + @Test + final void testObject() { + final Element e = new Jandex(jdk).typeElement("java.lang.Object"); + final List es = e.getEnclosedElements(); + // es.forEach(System.out::println); + } + + @Test + final void testString() { + final Element e = new Jandex(jdk).typeElement("java.lang.String"); + final List es = e.getEnclosedElements(); + es.forEach(ee -> { System.out.println(ee + "; class: " + ee.getClass()); }); + } + + + /* + * Static methods. + */ + + + private static final IndexView index(final Path indexRoot, final Predicate filterPredicate) { + final Indexer indexer = new Indexer(); + try (final Stream pathStream = Files.walk(indexRoot, Integer.MAX_VALUE)) { + pathStream + .filter(filterPredicate) + .forEach(p -> { + try (final InputStream inputStream = Files.newInputStream(p)) { + indexer.index(inputStream); + } catch (final IOException ioException) { + throw new UncheckedIOException(ioException.getMessage(), ioException); + } + }); + } catch (final IOException e) { + throw new UncheckedIOException(e.getMessage(), e); + } + return indexer.complete(); + } + + +} diff --git a/jandex/src/test/java/org/microbean/lang/jandex/TestTypeUsages.java b/jandex/src/test/java/org/microbean/lang/jandex/TestTypeUsages.java new file mode 100644 index 00000000..04cf908a --- /dev/null +++ b/jandex/src/test/java/org/microbean/lang/jandex/TestTypeUsages.java @@ -0,0 +1,250 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.jandex; + +import java.io.IOException; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import java.util.List; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.ArrayType; +import org.jboss.jandex.ClassExtendsTypeTarget; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.EmptyTypeTarget; +import org.jboss.jandex.Indexer; +import org.jboss.jandex.IndexView; +import org.jboss.jandex.MethodParameterTypeTarget; +import org.jboss.jandex.ParameterizedType; +import org.jboss.jandex.Type; +import org.jboss.jandex.TypeTarget; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +final class TestTypeUsages { + + private IndexView i; + + private TestTypeUsages() { + super(); + } + + @BeforeEach + final void setup() throws IOException { + final Indexer indexer = new Indexer(); + indexer.indexClass(A.class); + indexer.indexClass(B.class); + indexer.indexClass(E.class); + indexer.indexClass(F.class); + indexer.indexClass(G.class); + indexer.indexClass(H.class); + indexer.indexClass(String.class); + this.i = indexer.complete(); + } + + @AfterEach + final void tearDown() { + + } + + @Test + final void testB() { + final ClassInfo ci = this.i.getClassByName(B.class.getName()); + + for (final AnnotationInstance ai : ci.annotations()) { + final AnnotationTarget target = ai.target(); + switch (target.kind()) { + case CLASS: + // @A on the class itself + assertEquals("A", ai.name().local()); + assertSame(ci, target); + break; + case TYPE: + final TypeTarget tt = target.asType(); + switch (tt.usage()) { + case METHOD_PARAMETER: + final MethodParameterTypeTarget mptt = tt.asMethodParameterType(); + assertEquals(0, mptt.position()); + assertEquals("c", mptt.enclosingTarget().name()); + assertEquals("A", ai.name().local()); + break; + case EMPTY: + final EmptyTypeTarget ett = tt.asEmpty(); + assertFalse(ett.isReceiver()); + final ArrayType at = ett.target().asArrayType(); + switch (ai.name().local()) { + case "A": + assertEquals(1, at.dimensions()); // surprising + assertEquals(1, at.constituent().asArrayType().dimensions()); + break; + case "E": + assertEquals(1, at.dimensions()); + assertEquals("String", at.constituent().name().local()); + break; + default: + fail(); + } + break; + default: + fail(tt.usage().toString()); + } + break; + default: + fail(); + } + } + + } + + @Test + final void testG() { + final ClassInfo ci = this.i.getClassByName(G.class.getName()); + + final List ais = ci.annotations(); + assertEquals(3, ais.size()); + for (final AnnotationInstance ai : ais) { + final AnnotationTarget annotationTarget = ai.target(); + switch (annotationTarget.kind()) { + case TYPE: + final TypeTarget tt = annotationTarget.asType(); + switch (tt.usage()) { + case CLASS_EXTENDS: // really?! + final ClassExtendsTypeTarget cett = tt.asClassExtends(); + + final ClassInfo enclosingTarget = cett.enclosingTarget().asClass(); + assertSame(ci, enclosingTarget); + + assertEquals(65535, cett.position()); // 65535 means superclass; see javadoc + // How do we know this? How do we know that it isn't, say, F? + // + // Obviously for this test we can just assert that the target is the type named by "java.lang.String". But in a + // generic library we'll need a recipe, so we'll do the recipe here. + final Type relevantSupertype = cett.position() == 65535 ? enclosingTarget.superClassType() : enclosingTarget.interfaceTypes().get(cett.position()); + + final Type type = cett.target(); + if (type == relevantSupertype) { + // The annotation in question is a type use annotation on the supertype itself (in this test, it's @H-on-F<@E String, @E String>). + assertEquals("H", ai.name().local()); + } else { + // The annotation in question is a type use annotation on the supertype's type arguments, which implies that + // the supertype is a parameterized type. + assertSame(Type.Kind.PARAMETERIZED_TYPE, relevantSupertype.kind()); + + for (final Type typeArgument : relevantSupertype.asParameterizedType().arguments()) { + // In this test, there are two type arguments, both of type String, both annotated with @E. What is + // particularly interesting is that Jandex stores them with one object. + assertSame(type, typeArgument); + + // Anyway, the point is there is no way to distinguish them at all. + } + } + + switch (relevantSupertype.kind()) { + case CLASS: + // (In this test we know this to be false. In the recipe, this would tell us unambiguously the type usage + // annotation is on the actual thing being extended/implemented.) + fail(); + break; + case PARAMETERIZED_TYPE: + // (In this test we know this to be true.) + + break; + default: + fail(); + } + + // This is really stupid. I guess once the position is 65535, you get the supertype directly? + final Type supertype = ci.superClassType(); + // assertEquals("String", cett.target().name().local()); + break; + default: + fail(); + } + break; + default: + fail(); + } + } + } + + + /* + * Inner and nested classes. + */ + + + @Retention(RUNTIME) + // Note as an interesting curiosity that TYPE_USE implies ANNOTATION_TYPE, TYPE and TYPE_PARAMETER as well. See + // https://mail.openjdk.org/pipermail/compiler-dev/2023-February/022200.html. + @Target({ TYPE, TYPE_USE }) + public @interface A {} + + @Retention(RUNTIME) + // Note as an interesting curiosity that TYPE_USE implies ANNOTATION_TYPE, TYPE and TYPE_PARAMETER as well. See + // https://mail.openjdk.org/pipermail/compiler-dev/2023-February/022200.html. + @Target({ TYPE_USE }) + public @interface E {} + + @Retention(RUNTIME) + // Note as an interesting curiosity that TYPE_USE implies ANNOTATION_TYPE, TYPE and TYPE_PARAMETER as well. See + // https://mail.openjdk.org/pipermail/compiler-dev/2023-February/022200.html. + @Target({ TYPE_USE }) + public @interface H {} + + @A + private static final class B { + + private B() { + super(); + } + + public static final void c(@A String s) {} + + // Yow. @A annotates the array type (of the type denoted by String[][]). @E annotaes the component type (denoted by + // String[]). + public static final String @A [] @E [] d() { + return null; + } + + } + + private static class F {} + + private static class G extends @H F<@E String, @E String> {} + + + +} diff --git a/jandex/src/test/java/org/microbean/lang/jandex/TestTypeUseAnnotationOnMethodParameterType.java b/jandex/src/test/java/org/microbean/lang/jandex/TestTypeUseAnnotationOnMethodParameterType.java new file mode 100644 index 00000000..4f355097 --- /dev/null +++ b/jandex/src/test/java/org/microbean/lang/jandex/TestTypeUseAnnotationOnMethodParameterType.java @@ -0,0 +1,101 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.jandex; + +import java.io.IOException; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.AnnotationTarget; +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.EmptyTypeTarget; +import org.jboss.jandex.Indexer; +import org.jboss.jandex.IndexView; +import org.jboss.jandex.MethodParameterTypeTarget; +import org.jboss.jandex.TypeTarget; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; + +final class TestTypeUseAnnotationOnMethodParameterType { + + private TestTypeUseAnnotationOnMethodParameterType() { + super(); + } + + /** + * This test exists to show that Jandex does something properly that javac does not. + * + *

See the {@code org.microbean.lang.TestTypeUsages} test class for the {@code javac}-based analog, which does not + * handle a type use annotation on a method parameter. I am still not sure if Jandex is wrong, or {@code javac} is + * wrong.

+ * + * @exception IOException if there was a problem with the {@link Indexer} + */ + @Test + final void testTypeUseAnnotationOnMethodParameterType() throws IOException { + final Indexer indexer = new Indexer(); + indexer.indexClass(B.class); + final IndexView i = indexer.complete(); + final ClassInfo ci = i.getClassByName(B.class.getName()); + for (final AnnotationInstance a : ci.annotations()) { + final AnnotationTarget target = a.target(); + switch (target.kind()) { + case TYPE: + final TypeTarget tt = target.asType(); + switch (tt.usage()) { + case METHOD_PARAMETER: + final MethodParameterTypeTarget mptt = tt.asMethodParameterType(); + assertEquals(0, mptt.position()); + assertEquals("c", mptt.enclosingTarget().name()); + assertEquals("A", a.name().local()); + break; + default: + fail(); + } + break; + default: + fail(); + } + } + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.TYPE_USE }) + public @interface A {} + + public static final class B { + + public B() { + super(); + } + + public static final void c(@A String s) {} + + } + +} diff --git a/jandex/src/test/java/org/microbean/lang/jandex/testjandex/Flabrous.java b/jandex/src/test/java/org/microbean/lang/jandex/testjandex/Flabrous.java new file mode 100644 index 00000000..1f85ad5a --- /dev/null +++ b/jandex/src/test/java/org/microbean/lang/jandex/testjandex/Flabrous.java @@ -0,0 +1,30 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.jandex.testjandex; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Inherited +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PACKAGE) +public @interface Flabrous { + +} diff --git a/jandex/src/test/java/org/microbean/lang/jandex/testjandex/Frobnicator.java b/jandex/src/test/java/org/microbean/lang/jandex/testjandex/Frobnicator.java new file mode 100644 index 00000000..7782cefb --- /dev/null +++ b/jandex/src/test/java/org/microbean/lang/jandex/testjandex/Frobnicator.java @@ -0,0 +1,25 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.jandex.testjandex; + +public class Frobnicator { + + public Frobnicator() { + super(); + } + +} diff --git a/jandex/src/test/java/org/microbean/lang/jandex/testjandex/package-info.java b/jandex/src/test/java/org/microbean/lang/jandex/testjandex/package-info.java new file mode 100644 index 00000000..1de3b281 --- /dev/null +++ b/jandex/src/test/java/org/microbean/lang/jandex/testjandex/package-info.java @@ -0,0 +1,18 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +@Flabrous +package org.microbean.lang.jandex.testjandex; diff --git a/lang/README.md b/lang/README.md new file mode 100644 index 00000000..c9dbba37 --- /dev/null +++ b/lang/README.md @@ -0,0 +1,47 @@ +# microBean™ Lang + +[![Maven Central](https://maven-badges.herokuapp.com/maven-central/org.microbean/microbean-lang/badge.svg)](https://maven-badges.herokuapp.com/maven-central/org.microbean/microbean-lang) + +The microBean™ Lang project provides classes and interfaces that implement the Java language model. + +# Status + +This project is currently experimental, in a pre-alpha state, and therefore entirely unsuitable for production use. + +# Compatibility + +**Until further notice, this project's APIs are subject to frequent backwards-incompatible signature and behavior +changes, regardless of project version and without notice.** + +# Requirements + +microBean™ Lang requires a Java runtime of version 20 or higher. + +# Installation + +microBean™ Lang is or will be available on [Maven Central](https://search.maven.org/). Include microBean™ Lang as a +Maven dependency: + +```xml + + org.microbean + microbean-lang + + 0.0.1 + +``` + +# Documentation + +Full documentation is or will be available at +[microbean.github.io/microbean-lang](https://microbean.github.io/microbean-lang/). + +# Overview + +The microBean™ Lang project arranges for some of the `javax.lang.model.*` classes to be available at runtime. + +# Related Issues + +* https://bugs.openjdk.org/browse/JDK-8225377 +* https://bugs.openjdk.org/browse/JDK-8303784 + diff --git a/lang/TRACES.txt b/lang/TRACES.txt new file mode 100644 index 00000000..a8af619f --- /dev/null +++ b/lang/TRACES.txt @@ -0,0 +1,8 @@ +[ERROR] org.microbean.lang.TestModuleRelatedIssues.testModuleRelatedIssues Time elapsed: 0.003 s <<< FAILURE! +org.opentest4j.AssertionFailedError: Unexpected exception type thrown, expected: but was: + at org.junit.jupiter.api.AssertionFailureBuilder.build(AssertionFailureBuilder.java:151) + at org.junit.jupiter.api.AssertThrows.assertThrows(AssertThrows.java:67) + at org.junit.jupiter.api.AssertThrows.assertThrows(AssertThrows.java:35) + at org.junit.jupiter.api.Assertions.assertThrows(Assertions.java:3083) + at org.microbean.lang@0.0.1-SNAPSHOT/org.microbean.lang.TestModuleRelatedIssues.testModuleRelatedIssues(TestModuleRelatedIssues.java:77) + diff --git a/lang/pom.xml b/lang/pom.xml new file mode 100644 index 00000000..91ba90ac --- /dev/null +++ b/lang/pom.xml @@ -0,0 +1,101 @@ + + + 4.0.0 + + microbean-lang + + + org.microbean + microbean-lang-parent + 0.0.1-SNAPSHOT + + + microBean™ Lang: Core + microBean™ Lang: Core: An implementation of the Java language model. + + + + + + + org.hamcrest + hamcrest + jar + test + + + + org.junit.jupiter + junit-jupiter-api + jar + test + + + + org.junit.jupiter + junit-jupiter-engine + jar + test + + + + + + + + + maven-compiler-plugin + + + default-compile + + + --add-exports + jdk.compiler/com.sun.tools.javac.comp=org.microbean.lang + --add-exports + jdk.compiler/com.sun.tools.javac.model=org.microbean.lang + + + + + default-testCompile + + + --add-exports + jdk.compiler/com.sun.tools.javac.code=org.microbean.lang + --add-exports + jdk.compiler/com.sun.tools.javac.model=org.microbean.lang + --add-exports + jdk.compiler/com.sun.tools.javac.util=org.microbean.lang + -Xlint:-exports + + + + + + + maven-surefire-plugin + + + --add-exports jdk.compiler/com.sun.tools.javac.code=org.microbean.lang + --add-exports jdk.compiler/com.sun.tools.javac.comp=org.microbean.lang + --add-exports jdk.compiler/com.sun.tools.javac.util=org.microbean.lang + --add-opens jdk.compiler/com.sun.tools.javac.model=org.microbean.lang + + + src/test/java/logging.properties + ${org.microbean.lang.Lang.verbose} + + + + + + + + + + + false + + + diff --git a/lang/src/main/java/module-info.java b/lang/src/main/java/module-info.java new file mode 100644 index 00000000..6f2cf37c --- /dev/null +++ b/lang/src/main/java/module-info.java @@ -0,0 +1,30 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2022–2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +/** + * Provides packages related to the Java language model. + * + * @author Laird Nelson + */ +module org.microbean.lang { + + requires static jdk.compiler; + requires transitive java.compiler; + + exports org.microbean.lang.element; + exports org.microbean.lang.type; + exports org.microbean.lang.visitor; + exports org.microbean.lang; + +} diff --git a/lang/src/main/java/org/microbean/lang/AnnotatedConstruct.java b/lang/src/main/java/org/microbean/lang/AnnotatedConstruct.java new file mode 100644 index 00000000..ba281a7a --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/AnnotatedConstruct.java @@ -0,0 +1,93 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2022–2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang; + +import java.lang.annotation.Annotation; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import javax.lang.model.element.AnnotationMirror; + +import org.microbean.lang.element.Element; + +import org.microbean.lang.type.TypeMirror; + +public abstract sealed class AnnotatedConstruct implements javax.lang.model.AnnotatedConstruct permits Element, TypeMirror { + + private static final Annotation[] EMPTY_ANNOTATION_ARRAY = new Annotation[0]; + + private final List annotationMirrors; + + private final List unmodifiableAnnotationMirrors; + + protected AnnotatedConstruct() { + this(List.of()); + } + + protected AnnotatedConstruct(final AnnotationMirror annotationMirror) { + this(List.of(annotationMirror)); + } + + protected AnnotatedConstruct(final Collection annotationMirrors) { + super(); + if (annotationMirrors == null) { + this.annotationMirrors = new ArrayList<>(5); + } else { + this.annotationMirrors = new ArrayList<>(Math.max(5, annotationMirrors.size())); + this.addAnnotationMirrors(annotationMirrors); + } + this.unmodifiableAnnotationMirrors = Collections.unmodifiableList(this.annotationMirrors); + } + + @Override // AnnotatedConstruct + public A getAnnotation(final Class annotationType) { + throw new UnsupportedOperationException(); + } + + @Override // AnnotatedConstruct + @SuppressWarnings("unchecked") + public A[] getAnnotationsByType(final Class annotationType) { + throw new UnsupportedOperationException(); + } + + @Override // AnnotatedConstruct + public List getAnnotationMirrors() { + return this.unmodifiableAnnotationMirrors; + } + + public void addAnnotationMirror(final AnnotationMirror a) { + if (this.annotationMirrors.contains(a)) { + throw new IllegalArgumentException("a: " + a); + } + this.annotationMirrors.add(this.validateAnnotationMirror(a)); + } + + public final void addAnnotationMirrors(final Iterable as) { + for (final AnnotationMirror a : as) { + this.addAnnotationMirror(a); + } + } + + protected AnnotationMirror validateAnnotationMirror(final AnnotationMirror a) { + return Objects.requireNonNull(a, "a"); + } + +} diff --git a/lang/src/main/java/org/microbean/lang/ClassesThenInterfacesTypeMirrorComparator.java b/lang/src/main/java/org/microbean/lang/ClassesThenInterfacesTypeMirrorComparator.java new file mode 100644 index 00000000..e8d231fc --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/ClassesThenInterfacesTypeMirrorComparator.java @@ -0,0 +1,55 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2022–2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.microbean.lang; + +import java.util.Comparator; + +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.IntersectionType; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; + +@Deprecated(forRemoval = true) +// Only works on reference types (and IntersectionTypes). +public final class ClassesThenInterfacesTypeMirrorComparator implements Comparator { + + public static final ClassesThenInterfacesTypeMirrorComparator INSTANCE = new ClassesThenInterfacesTypeMirrorComparator(); + + private ClassesThenInterfacesTypeMirrorComparator() { + super(); + } + + @Override // Comparator + public final int compare(final TypeMirror t, final TypeMirror s) { + // See ../../../../../site/markdown/type-variables-and-intersection-types.md. + return + t == s ? 0 : + t == null ? 1 : + s == null ? -1 : + iface(t) ? iface(s) ? 0 : 1 : + iface(s) ? -1 : 0; + } + + private static final boolean iface(final TypeMirror t) { + return switch (t.getKind()) { + case ARRAY -> false; + case DECLARED -> ((DeclaredType)t).asElement().getKind().isInterface(); + case INTERSECTION -> iface(((IntersectionType)t).getBounds().get(0)); // will be DeclaredType (nothing else) + case TYPEVAR -> iface(((TypeVariable)t).getUpperBound()); // will be DeclaredType, IntersectionType or TypeVariable (notably never ArrayType) + case ERROR -> throw new AssertionError("t.getKind() == TypeKind.ERROR; t: " + t); + default -> throw new IllegalArgumentException("t: " + t); + }; + } + +} diff --git a/lang/src/main/java/org/microbean/lang/CompletionLock.java b/lang/src/main/java/org/microbean/lang/CompletionLock.java new file mode 100644 index 00000000..0e1bc16e --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/CompletionLock.java @@ -0,0 +1,39 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2022–2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.microbean.lang; + +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; + +public final class CompletionLock { + + private static final long serialVersionUID = 1L; + + private static final Lock LOCK = new ReentrantLock(); + + private CompletionLock() { + super(); + } + + public static final Lock acquire() { + LOCK.lock(); + return LOCK; + } + + public static final Lock release() { + LOCK.unlock(); + return LOCK; + } + +} diff --git a/lang/src/main/java/org/microbean/lang/ConstantDescs.java b/lang/src/main/java/org/microbean/lang/ConstantDescs.java new file mode 100644 index 00000000..65242306 --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/ConstantDescs.java @@ -0,0 +1,28 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.microbean.lang; + +import java.lang.constant.ClassDesc; + +public final class ConstantDescs { + + public static final ClassDesc CD_TypeAndElementSource = ClassDesc.of("org.microbean.lang.TypeAndElementSource"); + + public static final ClassDesc CD_TypeMirror = ClassDesc.of("javax.lang.model.type.TypeMirror"); + + private ConstantDescs() { + super(); + } + +} diff --git a/lang/src/main/java/org/microbean/lang/Equality.java b/lang/src/main/java/org/microbean/lang/Equality.java new file mode 100644 index 00000000..cc15333f --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/Equality.java @@ -0,0 +1,1032 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2022–2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang; + +import java.lang.constant.ClassDesc; +import java.lang.constant.Constable; +import java.lang.constant.DynamicConstantDesc; +import java.lang.constant.MethodHandleDesc; + +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map.Entry; +import java.util.Map; +import java.util.Optional; +import java.util.TreeMap; + +import javax.lang.model.AnnotatedConstruct; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.ModuleElement.Directive; +import javax.lang.model.element.ModuleElement.DirectiveKind; +import javax.lang.model.element.ModuleElement.ExportsDirective; +import javax.lang.model.element.ModuleElement.OpensDirective; +import javax.lang.model.element.ModuleElement.ProvidesDirective; +import javax.lang.model.element.ModuleElement.RequiresDirective; +import javax.lang.model.element.ModuleElement.UsesDirective; +import javax.lang.model.element.ModuleElement; +import javax.lang.model.element.Name; +import javax.lang.model.element.PackageElement; +import javax.lang.model.element.RecordComponentElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.TypeParameterElement; +import javax.lang.model.element.VariableElement; + +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.ExecutableType; +import javax.lang.model.type.IntersectionType; +import javax.lang.model.type.NoType; +import javax.lang.model.type.NullType; +import javax.lang.model.type.PrimitiveType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; +import javax.lang.model.type.WildcardType; + +import static java.lang.constant.ConstantDescs.BSM_INVOKE; +import static java.lang.constant.ConstantDescs.CD_boolean; +import static java.lang.constant.ConstantDescs.FALSE; +import static java.lang.constant.ConstantDescs.TRUE; + +/** + * Provides determinate hashcode and equality calculations chiefly for {@code javax.lang.model} implementations. + * + * @author Laird Nelson + * + * @see #hashCode(Object) + * + * @see #equals(Object, Object) + */ +public class Equality implements Constable { + + private static final ClassDesc CD_Equality = ClassDesc.of("org.microbean.lang.Equality"); + + private final boolean ia; + + /** + * Creates a new {@link Equality}. + * + * @param includeAnnotations whether annotations on objects should be taken into consideration while performing + * hashcode and equality operations + * + * @see #includeAnnotations() + */ + public Equality(final boolean includeAnnotations) { + super(); + this.ia = includeAnnotations; + } + + public final boolean includeAnnotations() { + return this.ia; + } + + @Override // Constable + public Optional> describeConstable() { + return Optional.of(DynamicConstantDesc.of(BSM_INVOKE, + MethodHandleDesc.ofConstructor(CD_Equality, CD_boolean), + this.ia ? TRUE : FALSE)); + } + + public int hashCode(final Object o1) { + return hashCode(o1, this.ia); + } + + public boolean equals(final Object o1, final Object o2) { + return equals(o1, o2, this.ia); + } + + + /* + * Static methods. + */ + + + public static final int hashCodeIncludingAnnotations(final Object o) { + return hashCode(o, true); + } + + public static final int hashCodeNotIncludingAnnotations(final Object o) { + return hashCode(o, false); + } + + public static final int hashCode(final Object o, final boolean ia) { + return switch (o) { + case null -> 0; + case AnnotationMirror am -> hashCode(am, ia); + case AnnotationValue av -> hashCode(av, ia); + case AnnotatedConstruct ac -> hashCode(ac, ia); + case CharSequence c -> hashCode(c); + case List list -> hashCode(list, ia); + case int[] hashCodes -> hashCode(hashCodes); + case Object[] array -> hashCode(array, ia); + case Directive d -> hashCode(d, ia); + default -> System.identityHashCode(o); + }; + } + + private static final int hashCode(final int... hashCodes) { + if (hashCodes == null) { + return 0; + } else if (hashCodes.length <= 0) { + return 1; + } + int result = 1; + for (final int hashCode : hashCodes) { + result = 31 * result + hashCode; + } + return result; + } + + private static final int hashCode(final Object[] os, final boolean ia) { + if (os == null) { + return 0; + } else if (os.length <= 0) { + return 1; + } + int result = 1; + for (final Object o : os) { + result = 31 * result + (o == null ? 0 : hashCode(o, ia)); + } + return result; + } + + private static final int hashCode(final List list, final boolean ia) { + if (list == null) { + return 0; + } else if (list.isEmpty()) { + return 1; + } + // This calculation is mandated by java.util.List#hashCode(). + int hashCode = 1; + for (final Object o : list) { + hashCode = 31 * hashCode + (o == null ? 0 : hashCode(o, ia)); + } + return hashCode; + } + + private static final int hashCode(final CharSequence c) { + return switch (c) { + case null -> 0; + case Name n -> n.toString().hashCode(); + default -> c.hashCode(); + }; + } + + private static final int hashCode(final AnnotationMirror am, final boolean ia) { + return am == null ? 0 : hashCode(values(am), ia); + } + + private static final int hashCode(final AnnotationValue av, final boolean ia) { + return switch (av) { + case null -> 0; + case AnnotationMirror am -> hashCode(am, ia); + case List list -> hashCode(list, ia); + case TypeMirror t -> hashCode(t, ia); + case VariableElement e -> hashCode(e, ia); + default -> System.identityHashCode(av); // illegal argument + }; + } + + private static final int hashCode(final AnnotatedConstruct ac, final boolean ia) { + return switch (ac) { + case null -> 0; + case Element e -> hashCode(e, ia); + case TypeMirror t -> hashCode(t, ia); + default -> System.identityHashCode(ac); // basically illegal argument + }; + } + + private static final int hashCode(final Element e, final boolean ia) { + // This is the entry point for all Element hashCode calculations. We have to synchronize on the completion lock + // because various elements' getKind() methods may trigger symbol completion. + CompletionLock.acquire(); + try { + return e == null ? 0 : switch (e.getKind()) { + case ANNOTATION_TYPE, CLASS, ENUM, INTERFACE, RECORD -> hashCode((TypeElement)e, ia); + case TYPE_PARAMETER -> hashCode((TypeParameterElement)e, ia); + case BINDING_VARIABLE, ENUM_CONSTANT, EXCEPTION_PARAMETER, FIELD, LOCAL_VARIABLE, PARAMETER, RESOURCE_VARIABLE -> hashCode((VariableElement)e, ia); + case RECORD_COMPONENT -> hashCode((RecordComponentElement)e, ia); + case CONSTRUCTOR, INSTANCE_INIT, METHOD, STATIC_INIT -> hashCode((ExecutableElement)e, ia); + case PACKAGE -> hashCode((PackageElement)e, ia); + case MODULE -> hashCode((ModuleElement)e, ia); + default -> System.identityHashCode(e); // basically illegal argument + }; + } finally { + CompletionLock.release(); + } + } + + private static final int hashCode(final ExecutableElement e, final boolean ia) { + if (e == null) { + return 0; + } else if (!e.getKind().isExecutable()) { + return System.identityHashCode(e); // illegal argument + } + // This gets tricky. If we're truly trying to do value-based hashcodes, we have a catch-22: a method's hashcode must + // include the hashcodes of its parameters, and parameters, taken in isolation, must include the hashcodes of their + // enclosing element (method). That's an infinite loop. + // + // Next, VariableElement, which is the class assigned to executable/method/constructor parameters, is also used for + // things like fields. So we can't make assumptions about its hashcode calculations, save for one: + // + // Given that VariableElements are always enclosed + // (https://docs.oracle.com/en/java/javase/19/docs/api/java.compiler/javax/lang/model/element/VariableElement.html#getEnclosingElement()), + // it is reasonable to assume that any given VariableElement implementation will likely include the return value of + // getEnclosingElement() in its hashcode calculations. But the only enclosing "thing" that would "normally" include + // the hashcode calculations of its *enclosed* elements is an ExecutableElement (because its parameters make up its + // identity). + // + // So probably the best place to break that infinite loop is here, not there. + // + // Now, these are hashcodes, not equality comparisons, so the worst that happens if we eliminate parameters from the + // mix is a collision for overridden methods. That's what the java.lang.reflect.* Executable/Parameter split does. + return + hashCode(e.getKind().hashCode(), + hashCode(e.getEnclosingElement(), ia), // this is OK + hashCode(e.getSimpleName()), + hashCode(e.getParameters().size(), ia), + // hashCode(e.getParameters(), ia), // this is problematic because each parameter includes this in its hashcode calculations + hashCode(e.getReturnType(), ia), + ia ? hashCode(e.getAnnotationMirrors(), ia) : 0); + } + + private static final int hashCode(final ModuleElement e, final boolean ia) { + if (e == null) { + return 0; + } else if (e.getKind() != ElementKind.MODULE) { + return System.identityHashCode(e); // illegal argument + } + return + hashCode(e.getKind().hashCode(), + hashCode(e.getQualifiedName()), + ia ? hashCode(e.getAnnotationMirrors(), ia) : 0); + } + + private static final int hashCode(final PackageElement e, final boolean ia) { + if (e == null) { + return 0; + } else if (e.getKind() != ElementKind.PACKAGE) { + return System.identityHashCode(e); // illegal argument + } + return + hashCode(e.getKind().hashCode(), + hashCode(e.getQualifiedName()), + ia ? hashCode(e.getAnnotationMirrors(), ia) : 0); + } + + private static final int hashCode(final RecordComponentElement e, final boolean ia) { + if (e == null) { + return 0; + } else if (e.getKind() != ElementKind.RECORD_COMPONENT) { + return System.identityHashCode(e); // illegal argument + } + return + hashCode(e.getKind().hashCode(), + hashCode(e.getSimpleName(), ia), + hashCode(e.getEnclosingElement(), ia), + ia ? hashCode(e.getAnnotationMirrors(), ia) : 0); + } + + private static final int hashCode(final TypeElement e, final boolean ia) { + if (e == null) { + return 0; + } + final ElementKind k = e.getKind(); + if (!k.isClass() && !k.isInterface()) { + return System.identityHashCode(e); // illegal argument + } + return + hashCode(k.hashCode(), + hashCode(e.getQualifiedName(), ia), + ia ? hashCode(e.getAnnotationMirrors(), ia) : 0); + } + + private static final int hashCode(final TypeParameterElement e, final boolean ia) { + if (e == null) { + return 0; + } else if (e.getKind() != ElementKind.TYPE_PARAMETER) { + return System.identityHashCode(e); // illegal argument + } + return + hashCode(e.getKind().hashCode(), + hashCode(e.getGenericElement(), ia), + hashCode(e.getSimpleName(), ia), + ia ? hashCode(e.getAnnotationMirrors(), ia) : 0); + } + + private static final int hashCode(final VariableElement e, final boolean ia) { + if (e == null) { + return 0; + } + final ElementKind k = e.getKind(); + if (!k.isVariable()) { + return System.identityHashCode(e); // illegal argument + } + return + hashCode(k.hashCode(), + hashCode(e.getSimpleName(), ia), + hashCode(e.getEnclosingElement(), ia), + ia ? hashCode(e.getAnnotationMirrors(), ia) : 0); + } + + private static final int hashCode(final TypeMirror t, final boolean ia) { + // This is the entry point for all TypeMirror hashCode calculations. We have to synchronize on the completion lock + // because a declared type's getKind() method may trigger symbol completion. + CompletionLock.acquire(); + try { + return t == null ? 0 : switch (t.getKind()) { + case ARRAY -> hashCode((ArrayType)t, ia); + case DECLARED -> hashCode((DeclaredType)t, ia); + case EXECUTABLE -> hashCode((ExecutableType)t, ia); + case INTERSECTION -> hashCode((IntersectionType)t, ia); + case MODULE, NONE, PACKAGE, VOID -> hashCode((NoType)t, ia); + case NULL -> hashCode((NullType)t, ia); + case BOOLEAN, BYTE, CHAR, DOUBLE, FLOAT, INT, LONG, SHORT -> hashCode((PrimitiveType)t, ia); + case TYPEVAR -> hashCode((TypeVariable)t, ia); + case WILDCARD -> hashCode((WildcardType)t, ia); + case ERROR, OTHER, UNION -> System.identityHashCode(t); // basically illegal argument + }; + } finally { + CompletionLock.release(); + } + } + + private static final int hashCode(final ArrayType t, final boolean ia) { + if (t == null) { + return 0; + } else if (t.getKind() != TypeKind.ARRAY) { + return System.identityHashCode(t); // illegal argument + } + return + hashCode(t.getKind().hashCode(), + hashCode(t.getComponentType(), ia), + ia ? hashCode(t.getAnnotationMirrors(), ia) : 0); + } + + private static final int hashCode(final DeclaredType t, final boolean ia) { + if (t == null) { + return 0; + } else if (t.getKind() != TypeKind.DECLARED) { + return System.identityHashCode(t); // illegal argument + } + return + hashCode(t.getKind().hashCode(), + hashCode(t.asElement(), ia), + hashCode(t.getTypeArguments(), ia), + ia ? hashCode(t.getAnnotationMirrors(), ia) : 0); + } + + private static final int hashCode(final ExecutableType t, final boolean ia) { + if (t == null) { + return 0; + } else if (t.getKind() != TypeKind.EXECUTABLE) { + return System.identityHashCode(t); // illegal argument + } + // See https://github.com/openjdk/jdk/blob/jdk-20+16/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L4194-L4202 + return + hashCode(t.getKind().hashCode(), + hashCode(t.getParameterTypes(), ia), + // hashCode(t.getReceiverType(), ia), // not sure this is necessary + hashCode(t.getReturnType(), ia), + // hashCode(t.getTypeVariables(), ia), // not sure this is necessary + ia ? hashCode(t.getAnnotationMirrors(), ia) : 0); + } + + private static final int hashCode(final IntersectionType t, final boolean ia) { + if (t == null) { + return 0; + } else if (t.getKind() != TypeKind.INTERSECTION) { + return System.identityHashCode(t); // illegal argument + } + return + hashCode(t.getKind().hashCode(), + hashCode(t.getBounds(), ia), + ia ? hashCode(t.getAnnotationMirrors(), ia) : 0); + } + + private static final int hashCode(final NoType t, final boolean ia) { + if (t == null) { + return 0; + } + final TypeKind k = t.getKind(); + return switch (k) { + case MODULE, PACKAGE, NONE, VOID -> k.hashCode(); // No need to check ia; a NoType cannot have annotations + default -> System.identityHashCode(t); // illegal argument + }; + } + + private static final int hashCode(final NullType t, final boolean ia) { + if (t == null) { + return 0; + } + final TypeKind k = t.getKind(); + // No need to check ia because a NullType cannot have annotations. + return k == TypeKind.NULL ? k.hashCode() : System.identityHashCode(t); + } + + private static final int hashCode(final PrimitiveType t, final boolean ia) { + if (t == null) { + return 0; + } + final TypeKind k = t.getKind(); + if (!k.isPrimitive()) { + return System.identityHashCode(t); // illegal argument, actually + } + return ia ? hashCode(k.hashCode(), hashCode(t.getAnnotationMirrors(), ia)) : k.hashCode(); + } + + private static final int hashCode(final TypeVariable t, final boolean ia) { + if (t == null) { + return 0; + } + final TypeKind k = t.getKind(); + if (k != TypeKind.TYPEVAR) { + return System.identityHashCode(t); // illegal argument + } + return + hashCode(k.hashCode(), + hashCode(t.asElement(), ia), + hashCode(t.getUpperBound(), ia), + hashCode(t.getLowerBound(), ia), + ia ? hashCode(t.getAnnotationMirrors(), ia) : 0); + } + + private static final int hashCode(final WildcardType t, final boolean ia) { + if (t == null) { + return 0; + } + final TypeKind k = t.getKind(); + if (k != TypeKind.WILDCARD) { + return System.identityHashCode(t); // illegal argument + } + return + hashCode(k.hashCode(), + hashCode(t.getExtendsBound(), ia), + hashCode(t.getSuperBound(), ia), + ia ? hashCode(t.getAnnotationMirrors(), ia) : 0); + } + + private static final int hashCode(final Directive d, final boolean ia) { + // TODO: do we need to synchronize on the completion lock here? + return d == null ? 0 : switch (d.getKind()) { + case EXPORTS -> hashCode((ExportsDirective)d, ia); + case OPENS -> hashCode((OpensDirective)d, ia); + case PROVIDES -> hashCode((ProvidesDirective)d, ia); + case REQUIRES -> hashCode((RequiresDirective)d, ia); + case USES -> hashCode((UsesDirective)d, ia); + }; + } + + private static final int hashCode(final ExportsDirective d, final boolean ia) { + if (d == null) { + return 0; + } + final DirectiveKind k = d.getKind(); + if (k != DirectiveKind.EXPORTS) { + return System.identityHashCode(d); + } + return + hashCode(k.hashCode(), + hashCode(d.getPackage(), ia)); + } + + private static final int hashCode(final OpensDirective d, final boolean ia) { + if (d == null) { + return 0; + } + final DirectiveKind k = d.getKind(); + if (k != DirectiveKind.OPENS) { + return System.identityHashCode(d); + } + return + hashCode(k.hashCode(), + hashCode(d.getPackage(), ia), + hashCode(d.getTargetModules(), ia)); + } + + private static final int hashCode(final ProvidesDirective d, final boolean ia) { + if (d == null) { + return 0; + } + final DirectiveKind k = d.getKind(); + if (k != DirectiveKind.PROVIDES) { + return System.identityHashCode(d); + } + return + hashCode(k.hashCode(), + hashCode(d.getImplementations(), ia), + hashCode(d.getService(), ia)); + } + + private static final int hashCode(final RequiresDirective d, final boolean ia) { + if (d == null) { + return 0; + } + final DirectiveKind k = d.getKind(); + if (k != DirectiveKind.REQUIRES) { + return System.identityHashCode(d); + } + return + hashCode(k.hashCode(), + hashCode(d.getDependency(), ia), + d.isStatic() ? 1 : 0, + d.isTransitive() ? 1 : 0); + } + + private static final int hashCode(final UsesDirective d, final boolean ia) { + if (d == null) { + return 0; + } + final DirectiveKind k = d.getKind(); + if (k != DirectiveKind.USES) { + return System.identityHashCode(d); + } + return + hashCode(k.hashCode(), + hashCode(d.getService(), ia)); + } + + + /* + * equals() + */ + + + public static final boolean equalsIncludingAnnotations(final Object o1, final Object o2) { + return equals(o1, o2, true); + } + + public static final boolean equalsNotIncludingAnnotations(final Object o1, final Object o2) { + return equals(o1, o2, false); + } + + public static final boolean equals(final Object o1, final Object o2, final boolean ia) { + return o1 == o2 || o2 != null && switch (o1) { + case null -> false; + case AnnotationMirror am1 -> o2 instanceof AnnotationMirror am2 && equals(am1, am2, ia); + case AnnotationValue av1 -> o2 instanceof AnnotationValue av2 && equals(av1, av2, ia); + case AnnotatedConstruct ac1 -> o2 instanceof AnnotatedConstruct ac2 && equals(ac1, ac2, ia); + case CharSequence c1 -> o2 instanceof CharSequence c2 && equals(c1, c2, ia); + case List list1 -> o2 instanceof List list2 && equals(list1, list2, ia); + case Directive d1 -> o2 instanceof Directive d2 && equals(d1, d2, ia); + default -> o1.equals(o2); + }; + } + + private static final boolean equals(final List list1, final List list2, final boolean ia) { + if (list1 == list2) { + return true; + } else if (list1 == null || list2 == null) { + return false; + } + final int size = list1.size(); + if (size != list2.size()) { + return false; + } + for (int i = 0; i < size; i++) { + if (!equals(list1.get(i), list2.get(i), ia)) { + return false; + } + } + return true; + } + + private static final boolean equals(final CharSequence c1, final CharSequence c2) { + if (c1 == c2) { + return true; + } else if (c1 == null || c2 == null) { + return false; + } else if (c1 instanceof Name n1) { + return n1.contentEquals(c2); + } else if (c2 instanceof Name n2) { + return n2.contentEquals(c1); + } else { + return c1.equals(c2); + } + } + + private static final boolean equals(final Name n1, final Name n2) { + if (n1 == n2) { + return true; + } else if (n1 == null || n2 == null) { + return false; + } + return n1.contentEquals(n2); + } + + private static final boolean equals(final AnnotationMirror am1, final AnnotationMirror am2, final boolean ia) { + if (am1 == am2) { + return true; + } else if (am1 == null || am2 == null || !equals(am1.getAnnotationType(), am2.getAnnotationType(), ia)) { + return false; + } + return equals(values(am1), values(am2), ia); + } + + private static final List values(final AnnotationMirror am) { + final Collection v = toMap(am).values(); + return v instanceof List ? Collections.unmodifiableList((List)v) : List.copyOf(v); + } + + private static final Map toMap(final AnnotationMirror am) { + if (am == null) { + return Map.of(); + } + final DeclaredType at = am.getAnnotationType(); + // See https://mail.openjdk.org/pipermail/compiler-dev/2023-July/023750.html + // assert at.getKind() == TypeKind.DECLARED : "Unexpected kind for am (" + am + "): " + at.getKind(); + // assert at.asElement().getKind() == ElementKind.ANNOTATION_TYPE : "Unexpected kind for at.asElement() (" + at.asElement() + "): " + at.asElement().getKind(); + final Map map = new TreeMap<>(); + for (final Object e : at.asElement().getEnclosedElements()) { + if (e instanceof ExecutableElement ee && ee.getKind() == ElementKind.METHOD) { + final AnnotationValue dv = ee.getDefaultValue(); + if (dv != null) { + map.put(((TypeElement)ee.getEnclosingElement()).getQualifiedName().toString() + '.' + ee.getSimpleName().toString(), + dv instanceof org.microbean.lang.element.AnnotationValue av ? av : org.microbean.lang.element.AnnotationValue.of(dv.getValue())); + } + } + } + for (final Entry entry : am.getElementValues().entrySet()) { + final ExecutableElement ee = entry.getKey(); + final AnnotationValue av = entry.getValue(); + map.put(((TypeElement)ee.getEnclosingElement()).getQualifiedName().toString() + '.' + ee.getSimpleName().toString(), + av instanceof org.microbean.lang.element.AnnotationValue mav ? mav : org.microbean.lang.element.AnnotationValue.of(av.getValue())); + } + return Collections.unmodifiableMap(map); + } + + private static final boolean equals(final AnnotationValue av1, final AnnotationValue av2, final boolean ia) { + if (av1 == av2) { + return true; + } else if (av1 == null || av2 == null) { + return false; + } + final Object v1 = av1.getValue(); // annotation elements cannot return null + return switch (v1) { + case AnnotationMirror am1 -> av2.getValue() instanceof AnnotationMirror am2 && equals(am1, am2, ia); + case List list1 -> av2.getValue() instanceof List list2 && equals(list1, list2, ia); + case TypeMirror t1 -> av2.getValue() instanceof TypeMirror t2 && equals(t1, t2, ia); + case VariableElement ve1 -> av2.getValue() instanceof VariableElement ve2 && equals(ve1, ve2, ia); + default -> v1.equals(av2.getValue()); // illegal argument + }; + } + + @SuppressWarnings("deprecation") + private static final boolean equals(final AnnotatedConstruct ac1, final AnnotatedConstruct ac2, final boolean ia) { + return ac1 == ac2 || ac2 != null && switch (ac1) { + case null -> false; + case Element e1 -> ac2 instanceof Element e2 && equals(e1, e2, ia); + case TypeMirror t1 -> ac2 instanceof TypeMirror t2 && equals(t1, t2, ia); + default -> ac1.equals(ac2); // illegal argument + }; + } + + private static final boolean equals(final Element e1, final Element e2, final boolean ia) { + if (e1 == e2) { + return true; + } else if (e1 == null || e2 == null) { + return false; + } + // This is the entry point for all Element equality calculations. We have to synchronize on the completion lock + // because various elements' getKind() methods may trigger symbol completion. + CompletionLock.acquire(); + try { + final ElementKind k = e1.getKind(); + return k == e2.getKind() && switch (k) { + case ANNOTATION_TYPE, CLASS, ENUM, INTERFACE, RECORD -> equals((TypeElement)e1, (TypeElement)e2, ia); + case TYPE_PARAMETER -> equals((TypeParameterElement)e1, (TypeParameterElement)e2, ia); + case BINDING_VARIABLE, ENUM_CONSTANT, EXCEPTION_PARAMETER, FIELD, LOCAL_VARIABLE, PARAMETER, RESOURCE_VARIABLE -> equals((VariableElement)e1, (VariableElement)e2, ia); + case RECORD_COMPONENT -> equals((RecordComponentElement)e1, (RecordComponentElement)e2, ia); + case CONSTRUCTOR, INSTANCE_INIT, METHOD, STATIC_INIT -> equals((ExecutableElement)e1, (ExecutableElement)e2, ia); + case PACKAGE -> equals((PackageElement)e1, (PackageElement)e2, ia); + case MODULE -> equals((ModuleElement)e1, (ModuleElement)e2, ia); + case OTHER -> false; + }; + } finally { + CompletionLock.release(); + } + } + + private static final boolean equals(final ExecutableElement e1, final ExecutableElement e2, final boolean ia) { + if (e1 == e2) { + return true; + } else if (e1 == null || e2 == null || ia && !equals(e1.getAnnotationMirrors(), e2.getAnnotationMirrors(), ia)) { + return false; + } + final ElementKind k1 = e1.getKind(); + // This is kind of the runtime equality contract of, say, java.lang.reflect.Method. Note in particular that + // TypeParameterElements are not evaluated. + return + k1 == e2.getKind() && + k1.isExecutable() && + equals(e1.getSimpleName(), e2.getSimpleName()) && + equals(e1.getParameters(), e2.getParameters(), ia) && + equals(e1.getReturnType(), e2.getReturnType(), ia) && + equals(e1.getEnclosingElement(), e2.getEnclosingElement(), ia); + } + + private static final boolean equals(final ModuleElement e1, final ModuleElement e2, final boolean ia) { + if (e1 == e2) { + return true; + } else if (e1 == null || e2 == null || ia && !equals(e1.getAnnotationMirrors(), e2.getAnnotationMirrors(), ia)) { + return false; + } + return + e1.getKind() == ElementKind.MODULE && e2.getKind() == ElementKind.MODULE && + equals(e1.getQualifiedName(), e2.getQualifiedName()); + } + + private static final boolean equals(final PackageElement e1, final PackageElement e2, final boolean ia) { + if (e1 == e2) { + return true; + } else if (e1 == null || e2 == null || ia && !equals(e1.getAnnotationMirrors(), e2.getAnnotationMirrors(), ia)) { + return false; + } + return + e1.getKind() == ElementKind.PACKAGE && e2.getKind() == ElementKind.PACKAGE && + equals(e1.getQualifiedName(), e2.getQualifiedName()); + } + + private static final boolean equals(final RecordComponentElement e1, final RecordComponentElement e2, final boolean ia) { + if (e1 == e2) { + return true; + } else if (e1 == null || e2 == null || ia && !equals(e1.getAnnotationMirrors(), e2.getAnnotationMirrors(), ia)) { + return false; + } + return + e1.getKind() == ElementKind.RECORD_COMPONENT && e2.getKind() == ElementKind.RECORD_COMPONENT && + equals(e1.getSimpleName(), e2.getSimpleName()) && + equals(e1.getEnclosingElement(), e2.getEnclosingElement(), ia); + } + + private static final boolean equals(final TypeElement e1, final TypeElement e2, final boolean ia) { + if (e1 == e2) { + return true; + } else if (e1 == null || e2 == null || ia && !equals(e1.getAnnotationMirrors(), e2.getAnnotationMirrors(), ia)) { + return false; + } + final ElementKind k1 = e1.getKind(); + return + k1 == e2.getKind() && + (k1.isClass() || k1.isInterface()) && + equals(e1.getQualifiedName(), e2.getQualifiedName()); + } + + private static final boolean equals(final TypeParameterElement e1, final TypeParameterElement e2, final boolean ia) { + if (e1 == e2) { + return true; + } else if (e1 == null || e2 == null || ia && !equals(e1.getAnnotationMirrors(), e2.getAnnotationMirrors(), ia)) { + return false; + } + // This is also the equality contract of sun.reflect.generics.reflectiveObjects.TypeVariableImpl, interestingly. + return + e1.getKind() == ElementKind.TYPE_PARAMETER && e2.getKind() == ElementKind.TYPE_PARAMETER && + equals(e1.getSimpleName(), e2.getSimpleName()) && + equals(e1.getGenericElement(), e2.getGenericElement(), ia); + } + + private static final boolean equals(final VariableElement e1, final VariableElement e2, final boolean ia) { + if (e1 == e2) { + return true; + } else if (e1 == null || e2 == null || ia && !equals(e1.getAnnotationMirrors(), e2.getAnnotationMirrors(), ia)) { + return false; + } + final ElementKind k1 = e1.getKind(); + return + k1 == e2.getKind() && + k1.isVariable() && + equals(e1.getSimpleName(), e2.getSimpleName()) && + equals(e1.getEnclosingElement(), e2.getEnclosingElement(), ia); + } + + private static final boolean equals(final TypeMirror t1, final TypeMirror t2, final boolean ia) { + if (t1 == t2) { + return true; + } else if (t1 == null || t2 == null || ia && !equals(t1.getAnnotationMirrors(), t2.getAnnotationMirrors(), ia)) { + return false; + } + // This is the entry point for all TypeMirror equality calculations. We have to synchronize on the completion lock + // because a declared type's getKind() method may trigger symbol completion. + CompletionLock.acquire(); + try { + final TypeKind k = t1.getKind(); + return k == t2.getKind() && switch (k) { + case ARRAY -> equals((ArrayType)t1, (ArrayType)t2, ia); + case DECLARED -> equals((DeclaredType)t1, (DeclaredType)t2, ia); + case EXECUTABLE -> equals((ExecutableType)t1, (ExecutableType)t2, ia); + case INTERSECTION -> equals((IntersectionType)t1, (IntersectionType)t2, ia); + case MODULE, NONE, PACKAGE, VOID -> equals((NoType)t1, (NoType)t2, ia); + case NULL -> equals((NullType)t1, (NullType)t2, ia); + case BOOLEAN, BYTE, CHAR, DOUBLE, FLOAT, INT, LONG, SHORT -> equals((PrimitiveType)t1, (PrimitiveType)t2, ia); + case TYPEVAR -> equals((TypeVariable)t1, (TypeVariable)t2, ia); + case WILDCARD -> equals((WildcardType)t1, (WildcardType)t2, ia); + case ERROR, OTHER, UNION -> t1.equals(t2); // unhandled argument + }; + } finally { + CompletionLock.release(); + } + } + + private static final boolean equals(final ArrayType t1, final ArrayType t2, final boolean ia) { + if (t1 == t2) { + return true; + } else if (t1 == null || t2 == null || ia && !equals(t1.getAnnotationMirrors(), t2.getAnnotationMirrors(), ia)) { + return false; + } + return + t1.getKind() == TypeKind.ARRAY && t2.getKind() == TypeKind.ARRAY && + equals(t1.getComponentType(), t2.getComponentType(), ia); + } + + private static final boolean equals(final DeclaredType t1, final DeclaredType t2, final boolean ia) { + if (t1 == t2) { + return true; + } else if (t1 == null || t2 == null || ia && !equals(t1.getAnnotationMirrors(), t2.getAnnotationMirrors(), ia)) { + return false; + } + return + t1.getKind() == TypeKind.DECLARED && t2.getKind() == TypeKind.DECLARED && + equals(t1.asElement(), t2.asElement(), ia) && + equals(t1.getTypeArguments(), t2.getTypeArguments(), ia); + } + + private static final boolean equals(final ExecutableType t1, final ExecutableType t2, final boolean ia) { + if (t1 == t2) { + return true; + } else if (t1 == null || t2 == null || ia && !equals(t1.getAnnotationMirrors(), t2.getAnnotationMirrors(), ia)) { + return false; + } + return + t1.getKind() == TypeKind.EXECUTABLE && t2.getKind() == TypeKind.EXECUTABLE && + equals(t1.getParameterTypes(), t2.getParameterTypes(), ia) && + equals(t1.getReceiverType(), t2.getReceiverType(), ia) && + equals(t1.getReturnType(), t2.getReturnType(), ia) && + // no thrown types + equals(t1.getTypeVariables(), t2.getTypeVariables(), ia); // not super sure this is necessary + } + + private static final boolean equals(final IntersectionType t1, final IntersectionType t2, final boolean ia) { + if (t1 == t2) { + return true; + } else if (t1 == null || t2 == null || ia && !equals(t1.getAnnotationMirrors(), t2.getAnnotationMirrors(), ia)) { + return false; + } + return + t1.getKind() == TypeKind.INTERSECTION && t2.getKind() == TypeKind.INTERSECTION && + equals(t1.getBounds(), t2.getBounds(), ia); + } + + private static final boolean equals(final NoType t1, final NoType t2, final boolean ia) { + if (t1 == t2) { + return true; + } else if (t1 == null || t2 == null || ia && !equals(t1.getAnnotationMirrors(), t2.getAnnotationMirrors(), ia)) { + return false; + } + final TypeKind k = t1.getKind(); + return switch (k) { + case MODULE, PACKAGE, NONE, VOID -> k == t2.getKind(); + default -> false; + }; + } + + private static final boolean equals(final NullType t1, final NullType t2, final boolean ia) { + if (t1 == t2) { + return true; + } else if (t1 == null || t2 == null) { + return false; + } + return t1.getKind() == TypeKind.NULL && t2.getKind() == TypeKind.NULL; + } + + private static final boolean equals(final PrimitiveType t1, final PrimitiveType t2, final boolean ia) { + if (t1 == t2) { + return true; + } else if (t1 == null || t2 == null || ia && !equals(t1.getAnnotationMirrors(), t2.getAnnotationMirrors(), ia)) { + return false; + } + final TypeKind k = t1.getKind(); + return k.isPrimitive() && k == t2.getKind(); + } + + private static final boolean equals(final TypeVariable t1, final TypeVariable t2, final boolean ia) { + if (t1 == t2) { + return true; + } else if (t1 == null || t2 == null || ia && !equals(t1.getAnnotationMirrors(), t2.getAnnotationMirrors(), ia)) { + return false; + } + return + t1.getKind() == TypeKind.TYPEVAR && t2.getKind() == TypeKind.TYPEVAR && + equals(t1.asElement(), t2.asElement(), ia) && + equals(t1.getUpperBound(), t2.getUpperBound(), ia) && + equals(t2.getLowerBound(), t2.getLowerBound(), ia); + } + + private static final boolean equals(final WildcardType t1, final WildcardType t2, final boolean ia) { + if (t1 == t2) { + return true; + } else if (t1 == null || t2 == null) { + return false; + } + // The Java type system doesn't actually say that a wildcard type is a type. It also says that "?" is "equivalent + // to" "? extends Object". Let's start by simply comparing bounds exactly. + return + t1.getKind() == TypeKind.WILDCARD && t2.getKind() == TypeKind.WILDCARD && + equals(t1.getExtendsBound(), t2.getExtendsBound(), ia) && + equals(t1.getSuperBound(), t2.getSuperBound(), ia); + } + + private static final boolean equals(final Directive d1, final Directive d2, final boolean ia) { + final DirectiveKind k = d1.getKind(); + if (d2.getKind() != k) { + return false; + } + return k == d2.getKind() && switch (k) { + case EXPORTS -> equals((ExportsDirective)d1, (ExportsDirective)d2, ia); + case OPENS -> equals((OpensDirective)d1, (OpensDirective)d2, ia); + case PROVIDES -> equals((ProvidesDirective)d1, (ProvidesDirective)d2, ia); + case REQUIRES -> equals((RequiresDirective)d1, (RequiresDirective)d2, ia); + case USES -> equals((UsesDirective)d1, (UsesDirective)d2, ia); + }; + } + + private static final boolean equals(final ExportsDirective d1, final ExportsDirective d2, final boolean ia) { + if (d1 == d2) { + return true; + } else if (d1 == null || d2 == null) { + return false; + } + return + d1.getKind() == DirectiveKind.EXPORTS && d2.getKind() == DirectiveKind.EXPORTS && + equals(d1.getPackage(), d2.getPackage(), ia) && + equals(d1.getTargetModules(), d2.getTargetModules(), ia); + } + + private static final boolean equals(final OpensDirective d1, final OpensDirective d2, final boolean ia) { + if (d1 == d2) { + return true; + } else if (d1 == null || d2 == null) { + return false; + } + return + d1.getKind() == DirectiveKind.OPENS && d2.getKind() == DirectiveKind.OPENS && + equals(d1.getPackage(), d2.getPackage(), ia) && + equals(d1.getTargetModules(), d2.getTargetModules(), ia); + } + + private static final boolean equals(final ProvidesDirective d1, final ProvidesDirective d2, final boolean ia) { + if (d1 == d2) { + return true; + } else if (d1 == null || d2 == null) { + return false; + } + return + d1.getKind() == DirectiveKind.PROVIDES && d2.getKind() == DirectiveKind.PROVIDES && + equals(d1.getImplementations(), d2.getImplementations(), ia) && + equals(d1.getService(), d2.getService(), ia); + } + + private static final boolean equals(final RequiresDirective d1, final RequiresDirective d2, final boolean ia) { + if (d1 == d2) { + return true; + } else if (d1 == null || d2 == null) { + return false; + } + return + d1.getKind() == DirectiveKind.REQUIRES && d2.getKind() == DirectiveKind.REQUIRES && + equals(d1.getDependency(), d2.getDependency(), ia) && + d1.isStatic() && d2.isStatic() && + d1.isTransitive() && d2.isTransitive(); + } + + private static final boolean equals(final UsesDirective d1, final UsesDirective d2, final boolean ia) { + if (d1 == d2) { + return true; + } else if (d1 == null || d2 == null) { + return false; + } + return + d1.getKind() == DirectiveKind.USES && d2.getKind() == DirectiveKind.USES && + equals(d1.getService(), d2.getService(), ia); + } + +} diff --git a/lang/src/main/java/org/microbean/lang/Key.java b/lang/src/main/java/org/microbean/lang/Key.java new file mode 100644 index 00000000..4266142c --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/Key.java @@ -0,0 +1,80 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang; + +/** + * An object that uses an {@link Equality} implementation for its {@link #hashCode()} and {@link #equals(Object)} + * implementations, chiefly for use as a key in a {@link java.util.Map} (or a value in a {@link java.util.Set}). + * + * {@snippet lang="java" : + * Map, Object> map = new HashMap<>(); // @link substring="Map" target="java.util.Map" @replace substring="new HashMap<>()" replacement="..." + * map.put(new Key<>(myTypeMirror, new Equality(false)), someValue); // @link substring="Equality" target="Equality" + * assert map.containsKey(new Key<>(myTypeMirror, new Equality(false))); + * } + * + * @param the type of object a {@link Key} uses; most often an {@link javax.lang.model.AnnotatedConstruct} or subinterface + * + * @author Laird Nelson + * + * @see Equality + */ +public final class Key { + + private final Equality eq; + + private final T k; + + public Key() { + this(null, new Equality(true)); + } + + public Key(final T key) { + this(key, new Equality(true)); + } + + public Key(final T key, final boolean includeAnnotations) { + this(key, new Equality(includeAnnotations)); + } + + public Key(final T key, final Equality eq) { + super(); + this.k = key; + this.eq = eq == null ? new Equality(true) : eq; + } + + @Override + public final int hashCode() { + return this.eq.hashCode(this.k); + } + + @Override + public final boolean equals(final Object other) { + if (this == other) { + return true; + } else if (other != null && other.getClass() == Key.class) { + return this.eq.equals(this.k, ((Key)other).k); + } else { + return false; + } + } + + @Override + public final String toString() { + return String.valueOf(this.k); + } + +} diff --git a/lang/src/main/java/org/microbean/lang/Lang.java b/lang/src/main/java/org/microbean/lang/Lang.java new file mode 100644 index 00000000..faf8b1d1 --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/Lang.java @@ -0,0 +1,2848 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.microbean.lang; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.Reader; +import java.io.Writer; +import java.io.UncheckedIOException; + +import java.lang.constant.ClassDesc; +import java.lang.constant.Constable; +import java.lang.constant.ConstantDesc; +import java.lang.constant.DynamicConstantDesc; +import java.lang.constant.MethodHandleDesc; +import java.lang.constant.MethodTypeDesc; + +import java.lang.module.ModuleFinder; +import java.lang.module.ResolvedModule; +import java.lang.module.ModuleReader; +import java.lang.module.ModuleReference; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.Field; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.GenericDeclaration; +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +import java.lang.System.Logger; + +import java.net.URI; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Deque; +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; + +import java.util.function.Predicate; + +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.RoundEnvironment; + +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.ModuleElement; +import javax.lang.model.element.Name; +import javax.lang.model.element.NestingKind; +import javax.lang.model.element.PackageElement; +import javax.lang.model.element.Parameterizable; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.TypeParameterElement; +import javax.lang.model.element.VariableElement; + +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.NoType; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.ExecutableType; +import javax.lang.model.type.NullType; +import javax.lang.model.type.PrimitiveType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; +import javax.lang.model.type.WildcardType; + +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; + +import javax.tools.FileObject; +import javax.tools.ForwardingJavaFileManager; +import javax.tools.JavaCompiler.CompilationTask; +import javax.tools.JavaCompiler; +import javax.tools.JavaFileManager.Location; +import javax.tools.JavaFileObject; +import javax.tools.StandardJavaFileManager; +import javax.tools.StandardLocation; + +import javax.tools.ToolProvider; + +import javax.lang.model.SourceVersion; + +import org.microbean.lang.element.DelegatingElement; + +import org.microbean.lang.type.DelegatingTypeMirror; + +import static java.lang.System.Logger.Level.DEBUG; +import static java.lang.System.Logger.Level.WARNING; +import static java.lang.constant.ConstantDescs.BSM_INVOKE; +import static java.lang.constant.ConstantDescs.CD_String; +import static java.lang.constant.ConstantDescs.NULL; +import static java.lang.constant.DirectMethodHandleDesc.Kind.STATIC; +import static java.lang.constant.DirectMethodHandleDesc.Kind.STATIC_GETTER; + +import static javax.lang.model.util.ElementFilter.constructorsIn; +import static javax.lang.model.util.ElementFilter.fieldsIn; +import static javax.lang.model.util.ElementFilter.methodsIn; + +public final class Lang { + + + /* + * Static fields. + */ + + + private static final ClassDesc CD_ArrayType = ClassDesc.of("javax.lang.model.type.ArrayType"); + + private static final ClassDesc CD_CharSequence = ClassDesc.of("java.lang.CharSequence"); + + private static final ClassDesc CD_DeclaredType = ClassDesc.of("javax.lang.model.type.DeclaredType"); + + private static final ClassDesc CD_Element = ClassDesc.of("javax.lang.model.element.Element"); + + private static final ClassDesc CD_Lang = ClassDesc.of("org.microbean.lang.Lang"); + + private static final ClassDesc CD_ModuleElement = ClassDesc.of("javax.lang.model.element.ModuleElement"); + + private static final ClassDesc CD_Name = ClassDesc.of("javax.lang.model.element.Name"); + + private static final ClassDesc CD_NoType = ClassDesc.of("javax.lang.model.type.NoType"); + + private static final ClassDesc CD_NullType = ClassDesc.of("javax.lang.model.type.NullType"); + + private static final ClassDesc CD_PackageElement = ClassDesc.of("javax.lang.model.element.PackageElement"); + + private static final ClassDesc CD_PrimitiveType = ClassDesc.of("javax.lang.model.type.PrimitiveType"); + + private static final ClassDesc CD_TypeElement = ClassDesc.of("javax.lang.model.element.TypeElement"); + + private static final ClassDesc CD_TypeKind = ClassDesc.of("javax.lang.model.type.TypeKind"); + + private static final ClassDesc CD_TypeMirror = ClassDesc.of("javax.lang.model.type.TypeMirror"); + + private static final ClassDesc CD_WildcardType = ClassDesc.of("javax.lang.model.type.WildcardType"); + + private static final TypeMirror[] EMPTY_TYPEMIRROR_ARRAY = new TypeMirror[0]; + + private static final Logger LOGGER = System.getLogger(Lang.class.getName()); + + // Flags pulled from the Java Virtual Machine Specification, version 20, stored in 16 bits: + + private static final int ACC_PUBLIC = 1 << 0; // 0x0001 // class, field, method + private static final int ACC_PRIVATE = 1 << 1; // 0x0002 // field, method + private static final int ACC_PROTECTED = 1 << 2; // 0x0004 // field, method + private static final int ACC_STATIC = 1 << 3; // 0x0008 // field, method + private static final int ACC_FINAL = 1 << 4; // 0x0010 // class, field, method + + private static final int ACC_OPEN = 1 << 5; // 0x0020 // module // same as ACC_SYNCHRONIZED, ACC_SUPER, ACC_TRANSITIVE + private static final int ACC_SUPER = 1 << 5; // 0x0020 // class // same as ACC_SYNCHRONIZED, ACC_OPEN, ACC_TRANSITIVE + private static final int ACC_SYNCHRONIZED = 1 << 5; // 0x0020 // method // same as ACC_SUPER, ACC_OPEN + private static final int ACC_TRANSITIVE = 1 << 5; // 0x0020 // module // same as ACC_SYNCHRONIZED, ACC_SUPER, ACC_OPEN + + private static final int ACC_BRIDGE = 1 << 6; // 0x0040 // method // same as ACC_VOLATILE, ACC_STATIC_PHASE + private static final int ACC_STATIC_PHASE = 1 << 6; // 0x0040 // module // same as ACC_BRIDGE, ACC_VOLATILE + private static final int ACC_VOLATILE = 1 << 6; // 0x0040 // field // same as ACC_BRIDGE, ACC_STATIC_PHASE + + private static final int ACC_TRANSIENT = 1 << 7; // 0x0080 // field // same as ACC_VARARGS + private static final int ACC_VARARGS = 1 << 7; // 0x0080 // method // same as ACC_TRANSIENT + + private static final int ACC_NATIVE = 1 << 8; // 0x0100 // method + private static final int ACC_INTERFACE = 1 << 9; // 0x0200 // class + private static final int ACC_ABSTRACT = 1 << 10; // 0x0400 // class, method + private static final int ACC_STRICTFP = 1 << 11; // 0x0800 // method + private static final int ACC_SYNTHETIC = 1 << 12; // 0x1000 // class, field, method + private static final int ACC_ANNOTATION = 1 << 13; // 0x2000 // class + private static final int ACC_ENUM = 1 << 14; // 0x4000 // class, field + + private static final int ACC_MANDATED = 1 << 15; // 0x8000 // module-info.class // same as ACC_MODULE + private static final int ACC_MODULE = 1 << 15; // 0x8000 // module-info.class // same as ACC_MANDATED + + // Flags pulled from the ASM library (https://gitlab.ow2.org/asm/asm/-/blob/master/asm/src/main/java/org/objectweb/asm/Opcodes.java): + + private static final int ASM_RECORD = 1 << 16; // 0x10000 // class // (see for example https://github.com/raphw/byte-buddy/blob/byte-buddy-1.14.5/byte-buddy-dep/src/main/java/net/bytebuddy/pool/TypePool.java#L2949) + + // Not needed: + // private static final int ASM_DEPRECATED = 1 << 17; // 0x20000 // class + + // Flags pulled from the javac compiler and the innards of java.lang.reflect.Modifier. Not sure we need these. + /* + private static final long JAVAC_PARAMETER = 1L << 33; // 0x0000000200000000 + private static final long JAVAC_DEFAULT = 1L << 43; // 0x0000080000000000 + private static final long JAVAC_COMPACT_RECORD_CONSTRUCTOR = 1L << 51; // 0x0008000000000000 // same as JAVAC_MODULE + private static final long JAVAC_RECORD = 1L << 61; // 0x2000000000000000 + private static final long JAVAC_SEALED = 1L << 62; // 0x4000000000000000 + private static final long JAVAC_NON_SEALED = 1L << 63; // 0x8000000000000000 + */ + + private static final Map modifierMasks; + + private static final CountDownLatch initLatch = new CountDownLatch(1); + + // For debugging only + private static final Field modulesField; + + // For debugging only + private static final Method getDefaultModuleMethod; + + static { + try { + // For debugging only. + getDefaultModuleMethod = com.sun.tools.javac.comp.Modules.class.getDeclaredMethod("getDefaultModule"); + getDefaultModuleMethod.trySetAccessible(); + + // For debugging only. + modulesField = com.sun.tools.javac.model.JavacElements.class.getDeclaredField("modules"); + modulesField.trySetAccessible(); + } catch (final ReflectiveOperationException x) { + throw (Error)new ExceptionInInitializerError(x.getMessage()).initCause(x); + } + } + + private static volatile ProcessingEnvironment pe; + + + /* + * Static initializer. + */ + + + static { + final EnumMap m = new EnumMap<>(Modifier.class); + m.put(Modifier.ABSTRACT, Long.valueOf(ACC_ABSTRACT)); + // m.put(Modifier.DEFAULT, Long.valueOf(JAVAC_DEFAULT)); // not Java Virtual Machine Specification-defined + m.put(Modifier.DEFAULT, 0L); + m.put(Modifier.FINAL, Long.valueOf(ACC_FINAL)); + m.put(Modifier.NATIVE, Long.valueOf(ACC_NATIVE)); + // m.put(Modifier.NON_SEALED, Long.valueOf(JAVAC_NON_SEALED)); // not Java Virtual Machine Specification-defined + m.put(Modifier.NON_SEALED, 0L); + m.put(Modifier.PRIVATE, Long.valueOf(ACC_PRIVATE)); + m.put(Modifier.PROTECTED, Long.valueOf(ACC_PROTECTED)); + m.put(Modifier.PUBLIC, Long.valueOf(ACC_PUBLIC)); + // m.put(Modifier.SEALED, Long.valueOf(JAVAC_SEALED)); // not Java Virtual Machine Specification-defined + m.put(Modifier.SEALED, 0L); + m.put(Modifier.STATIC, Long.valueOf(ACC_STATIC)); + m.put(Modifier.STRICTFP, Long.valueOf(ACC_STRICTFP)); + m.put(Modifier.SYNCHRONIZED, Long.valueOf(ACC_SYNCHRONIZED)); + m.put(Modifier.TRANSIENT, Long.valueOf(ACC_TRANSIENT)); + m.put(Modifier.VOLATILE, Long.valueOf(ACC_VOLATILE)); + assert m.size() == Modifier.values().length; + modifierMasks = Collections.unmodifiableMap(m); + initialize(); + } + + + /* + * Constructors. + */ + + + private Lang() { + super(); + } + + + /* + * Constable support methods. + */ + + + public static final Optional describeConstable(final Name n) { + if (n == null) { + return Optional.of(NULL); + } else if (n instanceof Constable c) { + return c.describeConstable(); + } else if (n instanceof ConstantDesc cd) { + // Future proofing? + return Optional.of(cd); + } + final String s; + CompletionLock.acquire(); + try { + s = n.toString(); + } finally { + CompletionLock.release(); + } + return Optional.of(DynamicConstantDesc.of(BSM_INVOKE, + MethodHandleDesc.ofMethod(STATIC, + CD_Lang, + "name", + MethodTypeDesc.of(CD_Name, + CD_String)), + s)); + } + + public static final Optional describeConstable(final Element e) { + if (e == null) { + return Optional.of(NULL); + } + final ElementKind k = e.getKind(); + return switch (k) { + case MODULE -> describeConstable((ModuleElement)e); + case PACKAGE -> describeConstable((PackageElement)e); + // TODO: others probably need to be handled but not as urgently + case ElementKind ek when ek.isDeclaredType() -> describeConstable((TypeElement)e); + default -> Optional.empty(); + }; + } + + public static final Optional describeConstable(final ModuleElement e) { + if (e == null) { + return Optional.of(NULL); + } else if (e instanceof Constable c) { + return c.describeConstable(); + } else if (e instanceof ConstantDesc cd) { + // Future proofing? + return Optional.of(cd); + } + return describeConstable(e.getQualifiedName()) + .map(nameDesc -> DynamicConstantDesc.of(BSM_INVOKE, + MethodHandleDesc.ofMethod(STATIC, + CD_Lang, + "moduleElement", + MethodTypeDesc.of(CD_ModuleElement, + CD_CharSequence)), + nameDesc)); + } + + public static final Optional describeConstable(final PackageElement e) { + if (e == null) { + return Optional.of(NULL); + } else if (e instanceof Constable c) { + return c.describeConstable(); + } else if (e instanceof ConstantDesc cd) { + // Future proofing? + return Optional.of(cd); + } + return describeConstable(moduleOf(e)) + .flatMap(moduleDesc -> describeConstable(e.getQualifiedName()) + .map(nameDesc -> DynamicConstantDesc.of(BSM_INVOKE, + MethodHandleDesc.ofMethod(STATIC, + CD_Lang, + "packageElement", + MethodTypeDesc.of(CD_PackageElement, + CD_ModuleElement, + CD_CharSequence)), + moduleDesc, + nameDesc))); + } + + public static final Optional describeConstable(final TypeElement e) { + if (e == null) { + return Optional.of(NULL); + } else if (e instanceof Constable c) { + return c.describeConstable(); + } else if (e instanceof ConstantDesc cd) { + // Future proofing? + return Optional.of(cd); + } + return describeConstable(moduleOf(e)) + .flatMap(moduleDesc -> describeConstable(e.getQualifiedName()) // moduleDesc is sometimes NULL for java.base classes for some reason + .map(nameDesc -> DynamicConstantDesc.of(BSM_INVOKE, + MethodHandleDesc.ofMethod(STATIC, + CD_Lang, + "typeElement", + MethodTypeDesc.of(CD_TypeElement, + CD_ModuleElement, + CD_CharSequence)), + moduleDesc, + nameDesc))); + } + + public static final Optional describeConstable(final TypeMirror t) { + if (t == null) { + return Optional.of(NULL); + } + return switch (t.getKind()) { + case ARRAY -> describeConstable((ArrayType)t); + case BOOLEAN, BYTE, CHAR, DOUBLE, FLOAT, INT, LONG, SHORT -> describeConstable((PrimitiveType)t); + case DECLARED, ERROR -> describeConstable((DeclaredType)t); + case EXECUTABLE, INTERSECTION, OTHER, TYPEVAR, UNION -> Optional.empty(); + case MODULE, NONE, PACKAGE, VOID -> describeConstable((NoType)t); + case NULL -> describeConstable((NullType)t); + case WILDCARD -> describeConstable((WildcardType)t); + }; + } + + public static final Optional describeConstable(final ArrayType t) { + if (t == null) { + return Optional.of(NULL); + } else if (t instanceof Constable c) { + return c.describeConstable(); + } else if (t instanceof ConstantDesc cd) { + // Future proofing? + return Optional.of(cd); + } + return describeConstable(t.getComponentType()) + .map(componentTypeDesc -> DynamicConstantDesc.of(BSM_INVOKE, + MethodHandleDesc.ofMethod(STATIC, + CD_Lang, + "arrayTypeOf", + MethodTypeDesc.of(CD_ArrayType, + CD_TypeMirror)), + componentTypeDesc)); + } + + public static final Optional describeConstable(final DeclaredType t) { + if (t == null) { + return Optional.of(NULL); + } else if (t instanceof Constable c) { + return c.describeConstable(); + } else if (t instanceof ConstantDesc cd) { + // Future proofing? + return Optional.of(cd); + } else if (t.getKind() == TypeKind.ERROR) { + return Optional.empty(); + } + // Ugh; this is tricky thanks to varargs. We'll do it imperatively for clarity. + final TypeMirror enclosingType = t.getEnclosingType(); + final ConstantDesc enclosingTypeDesc = + enclosingType.getKind() == TypeKind.NONE ? NULL : describeConstable(enclosingType).orElse(null); + if (enclosingTypeDesc == null) { + return Optional.empty(); + } + final ConstantDesc typeElementDesc = describeConstable((TypeElement)t.asElement()).orElse(null); + if (typeElementDesc == null) { + return Optional.empty(); + } + final List typeArguments = t.getTypeArguments(); + final ConstantDesc[] cds = new ConstantDesc[typeArguments.size() + 3]; + cds[0] = MethodHandleDesc.ofMethod(STATIC, + CD_Lang, + "declaredType", + MethodTypeDesc.of(CD_DeclaredType, + CD_DeclaredType, + CD_TypeElement, + CD_TypeMirror.arrayType())); + cds[1] = enclosingTypeDesc; + cds[2] = typeElementDesc; + for (int i = 3; i < cds.length; i++) { + final ConstantDesc cd = describeConstable(typeArguments.get(i)).orElse(null); + if (cd == null) { + return Optional.empty(); + } + cds[i] = cd; + } + return Optional.of(DynamicConstantDesc.of(BSM_INVOKE, cds)); + } + + public static final Optional describeConstable(final NoType t) { + if (t == null) { + return Optional.of(NULL); + } else if (t instanceof Constable c) { + return c.describeConstable(); + } else if (t instanceof ConstantDesc cd) { + // Future proofing? + return Optional.of(cd); + } + return Optional.of(DynamicConstantDesc.of(BSM_INVOKE, + MethodHandleDesc.ofMethod(STATIC, + CD_Lang, + "noType", + MethodTypeDesc.of(CD_NoType, + CD_TypeKind)), + t.getKind().describeConstable().orElseThrow())); + } + + public static final Optional describeConstable(final NullType t) { + if (t == null) { + return Optional.of(NULL); + } else if (t instanceof Constable c) { + return c.describeConstable(); + } else if (t instanceof ConstantDesc cd) { + // Future proofing? + return Optional.of(cd); + } + return Optional.of(DynamicConstantDesc.of(BSM_INVOKE, + MethodHandleDesc.ofMethod(STATIC, + CD_Lang, + "nullType", + MethodTypeDesc.of(CD_NullType)))); + } + + public static final Optional describeConstable(final PrimitiveType t) { + if (t == null) { + return Optional.of(NULL); + } else if (t instanceof Constable c) { + return c.describeConstable(); + } else if (t instanceof ConstantDesc cd) { + // Future proofing? + return Optional.of(cd); + } + return Optional.of(DynamicConstantDesc.of(BSM_INVOKE, + MethodHandleDesc.ofMethod(STATIC, + CD_Lang, + "primitiveType", + MethodTypeDesc.of(CD_PrimitiveType, + CD_TypeKind)), + t.getKind().describeConstable().orElseThrow())); + + } + + public static final Optional describeConstable(final WildcardType t) { + if (t == null) { + return Optional.of(NULL); + } else if (t instanceof Constable c) { + return c.describeConstable(); + } else if (t instanceof ConstantDesc cd) { + // Future proofing? + return Optional.of(cd); + } + final TypeMirror extendsBound = t.getExtendsBound(); + final ConstantDesc extendsBoundDesc = extendsBound == null ? NULL : describeConstable(extendsBound).orElse(null); + if (extendsBoundDesc == null) { + return Optional.empty(); + } + final TypeMirror superBound = t.getSuperBound(); + final ConstantDesc superBoundDesc = superBound == null ? NULL : describeConstable(superBound).orElse(null); + if (superBoundDesc == null) { + return Optional.empty(); + } + return Optional.of(DynamicConstantDesc.of(BSM_INVOKE, + MethodHandleDesc.ofMethod(STATIC, + CD_Lang, + "wildcardType", + MethodTypeDesc.of(CD_WildcardType, + CD_TypeMirror, + CD_TypeMirror)), + extendsBoundDesc, + superBoundDesc)); + } + + + /* + * Type and element support methods. + */ + + + // Apparently only one Symbol completion can occur at any time. Good grief. + // + // Sample stack trace: + /* + java.lang.AssertionError: Filling jrt:/java.base/java/io/Serializable.class during DirectoryFileObject[/modules/java.base:java/lang/CharSequence.class] + at jdk.compiler/com.sun.tools.javac.util.Assert.error(Assert.java:162) + at jdk.compiler/com.sun.tools.javac.code.ClassFinder.fillIn(ClassFinder.java:365) + at jdk.compiler/com.sun.tools.javac.code.ClassFinder.complete(ClassFinder.java:301) + at jdk.compiler/com.sun.tools.javac.code.Symtab$1.complete(Symtab.java:326) + at jdk.compiler/com.sun.tools.javac.code.Symbol.complete(Symbol.java:682) + at jdk.compiler/com.sun.tools.javac.code.Symbol$ClassSymbol.complete(Symbol.java:1410) + at jdk.compiler/com.sun.tools.javac.code.Symbol.apiComplete(Symbol.java:688) + at jdk.compiler/com.sun.tools.javac.code.Type$ClassType.getKind(Type.java:1181) + */ + + public static final Set allModuleElements() { + final Elements elements = pe().getElementUtils(); + final Set mes; + CompletionLock.acquire(); + try { + mes = elements.getAllModuleElements(); + } finally { + CompletionLock.release(); + } + final Set rv = new HashSet<>(); + for (final ModuleElement me : mes) { + rv.add(wrap(me)); + } + return Collections.unmodifiableSet(rv); + } + + public static final Name binaryName(final TypeElement e) { + // This does not cause completion. + return pe().getElementUtils().getBinaryName(unwrap(e)); + } + + public static final boolean functionalInterface(TypeElement e) { + e = unwrap(e); + final Elements elements = pe().getElementUtils(); + // JavacElements#isFunctionalInterface(Element) calls Element#getKind(). + CompletionLock.acquire(); + try { + return elements.isFunctionalInterface(e); + } finally { + CompletionLock.release(); + } + } + + public static final boolean generic(final Element e) { + CompletionLock.acquire(); + try { + return switch (e.getKind()) { + case CLASS, CONSTRUCTOR, ENUM, INTERFACE, METHOD, RECORD -> !((Parameterizable)e).getTypeParameters().isEmpty(); + default -> false; + }; + } finally { + CompletionLock.release(); + } + } + + public static final TypeMirror capture(TypeMirror t) { + t = unwrap(t); + // JavacTypes#capture(TypeMirror) calls TypeMirror#getKind(). + final Types types = pe().getTypeUtils(); + final TypeMirror rv; + CompletionLock.acquire(); + try { + rv = types.capture(t); + } finally { + CompletionLock.release(); + } + return wrap(rv); + } + + public static final boolean contains(TypeMirror t, TypeMirror s) { + t = unwrap(t); + s = unwrap(s); + // JavacTypes#contains(TypeMirror, TypeMirror) calls TypeMirror#getKind(). + final Types types = pe().getTypeUtils(); + CompletionLock.acquire(); + try { + return types.contains(t, s); + } finally { + CompletionLock.release(); + } + } + + public static final Element element(TypeMirror t) { + t = unwrap(t); + final Types types = pe().getTypeUtils(); + final Element rv; + // JavacTypes#asElement(TypeMirror) calls TypeMirror#getKind(). + CompletionLock.acquire(); + try { + rv = types.asElement(t); + } finally { + CompletionLock.release(); + } + return wrap(rv); + } + + public static final TypeMirror memberOf(DeclaredType t, Element e) { + t = unwrap(t); + e = unwrap(e); + final Types types = pe().getTypeUtils(); + final TypeMirror rv; + CompletionLock.acquire(); + try { + rv = types.asMemberOf(t, e); + } finally { + CompletionLock.release(); + } + return wrap(rv); + } + + public static final TypeMirror box(final TypeMirror t) { + final TypeKind k; + CompletionLock.acquire(); + try { + k = t.getKind(); + } finally { + CompletionLock.release(); + } + return k.isPrimitive() ? boxedClass((PrimitiveType)t).asType() : t; + } + + public static final TypeMirror unbox(final TypeMirror t) { + return unboxedType(t); + } + + public static final TypeElement boxedClass(final PrimitiveType t) { + // JavacTypes#boxedClass(TypeMirror) eventually calls com.sun.tools.javac.code.Symtab#defineClass(Name, Symbol) but + // that doesn't seem to actually do completion. + return wrap(pe().getTypeUtils().boxedClass(unwrap(t))); + } + + public static final boolean bridge(Element e) { + e = unwrap(e); + final Elements elements = pe().getElementUtils(); + CompletionLock.acquire(); + try { + return e.getKind() == ElementKind.METHOD && elements.isBridge((ExecutableElement)e); + } finally { + CompletionLock.release(); + } + } + + public static final boolean compactConstructor(Element e) { + e = unwrap(e); + final Elements elements = pe().getElementUtils(); + CompletionLock.acquire(); + try { + return e.getKind() == ElementKind.CONSTRUCTOR && elements.isCompactConstructor((ExecutableElement)e); + } finally { + CompletionLock.release(); + } + } + + public static final boolean canonicalConstructor(Element e) { + e = unwrap(e); + final Elements elements = pe().getElementUtils(); + CompletionLock.acquire(); + try { + return e.getKind() == ElementKind.CONSTRUCTOR && elements.isCanonicalConstructor((ExecutableElement)e); + } finally { + CompletionLock.release(); + } + } + + public static final PrimitiveType unboxedType(TypeMirror t) { + t = unwrap(t); + final Types types = pe().getTypeUtils(); + // JavacTypes#unboxedType(TypeMirror) calls TypeMirror#getKind(). + CompletionLock.acquire(); + try { + return types.unboxedType(t); + } finally { + CompletionLock.release(); + } + } + + + /* + * JVMS productions related to signatures and descriptors. + */ + + + public static final String elementSignature(final Element e) { + CompletionLock.acquire(); + try { + return switch (e.getKind()) { + case CLASS, ENUM, INTERFACE, RECORD -> classSignature((TypeElement)e); + case CONSTRUCTOR, METHOD, INSTANCE_INIT, STATIC_INIT -> methodSignature((ExecutableElement)e); + case ENUM_CONSTANT, FIELD, LOCAL_VARIABLE, PARAMETER, RECORD_COMPONENT -> fieldSignature(e); + default -> throw new IllegalArgumentException("e: " + e); + }; + } finally { + CompletionLock.release(); + } + } + + private static final String classSignature(final TypeElement e) { + CompletionLock.acquire(); + try { + return switch (e.getKind()) { + case CLASS, ENUM, INTERFACE, RECORD -> { + if (!generic(e) && ((DeclaredType)e.getSuperclass()).getTypeArguments().isEmpty()) { + boolean signatureRequired = false; + for (final TypeMirror iface : e.getInterfaces()) { + if (!((DeclaredType)iface).getTypeArguments().isEmpty()) { + signatureRequired = true; + break; + } + } + if (!signatureRequired) { + yield null; + } + } + final StringBuilder sb = new StringBuilder(); + classSignature(e, sb); + yield sb.toString(); + } + default -> throw new IllegalArgumentException("e: " + e + "; kind: " + e.getKind()); + }; + } finally { + CompletionLock.release(); + } + } + + private static final void classSignature(final TypeElement e, final StringBuilder sb) { + CompletionLock.acquire(); + try { + switch (e.getKind()) { + case CLASS, ENUM, INTERFACE, RECORD -> { // note: no ANNOTATION_TYPE on purpose + typeParameters(e.getTypeParameters(), sb); + final List directSupertypes = directSupertypes(e.asType()); + if (directSupertypes.isEmpty()) { + assert e.getQualifiedName().contentEquals("java.lang.Object") : "DeclaredType with no supertypes: " + e.asType(); + // See + // https://stackoverflow.com/questions/76453947/in-the-jvms-what-is-the-classsignature-for-java-lang-object-given-that-supercl + // + // Do nothing (and thereby violate the grammar? Derp?). + } else { + final DeclaredType firstSupertype = (DeclaredType)directSupertypes.get(0); + assert firstSupertype.getKind() == TypeKind.DECLARED; + // "For an interface type with no direct super-interfaces, a type mirror representing java.lang.Object is + // returned." Therefore in all situations, given a non-empty list of direct supertypes, the first element will + // always be a non-interface class. + assert !((TypeElement)firstSupertype.asElement()).getKind().isInterface() : "Contract violation"; + superclassSignature(firstSupertype, sb); + superinterfaceSignatures(directSupertypes.subList(1, directSupertypes.size()), sb); + } + } + default -> throw new IllegalArgumentException("e: " + e + "; kind: " + e.getKind()); + } + } finally { + CompletionLock.release(); + } + } + + private static final String methodSignature(final ExecutableElement e) { + CompletionLock.acquire(); + try { + if (e.getKind().isExecutable()) { + boolean throwsClauseRequired = false; + for (final TypeMirror exceptionType : e.getThrownTypes()) { + if (exceptionType.getKind() == TypeKind.TYPEVAR) { + throwsClauseRequired = true; + break; + } + } + if (!throwsClauseRequired && !generic(e)) { + final TypeMirror returnType = e.getReturnType(); + if (returnType.getKind() != TypeKind.TYPEVAR && typeArguments(returnType).isEmpty()) { + boolean signatureRequired = false; + for (final VariableElement p : e.getParameters()) { + final TypeMirror parameterType = p.asType(); + if (parameterType.getKind() == TypeKind.TYPEVAR || !typeArguments(parameterType).isEmpty()) { + signatureRequired = true; + break; + } + } + if (!signatureRequired) { + return null; + } + } + } + final StringBuilder sb = new StringBuilder(); + methodSignature(e, sb, throwsClauseRequired); + return sb.toString(); + } else { + throw new IllegalArgumentException("e: " + e + "; kind: " + e.getKind()); + } + } finally { + CompletionLock.release(); + } + } + + private static final void methodSignature(final ExecutableElement e, final StringBuilder sb, final boolean throwsClauseRequired) { + CompletionLock.acquire(); + try { + if (e.getKind().isExecutable()) { + final TypeMirror returnType = e.getReturnType(); + typeParameters(e.getTypeParameters(), sb); + sb.append('('); + parameterSignatures(e.getParameters(), sb); + sb.append(')'); + if (returnType.getKind() == TypeKind.VOID) { + sb.append('V'); + } else { + typeSignature(returnType, sb); + } + if (throwsClauseRequired) { + throwsSignatures(e.getThrownTypes(), sb); + } + } else { + throw new IllegalArgumentException("e: " + e + "; kind: " + e.getKind()); + } + } finally { + CompletionLock.release(); + } + } + + private static final String fieldSignature(final Element e) { + CompletionLock.acquire(); + try { + return switch (e.getKind()) { + case ENUM_CONSTANT, FIELD, LOCAL_VARIABLE, PARAMETER, RECORD_COMPONENT -> { + final TypeMirror t = e.asType(); + if (t.getKind() != TypeKind.TYPEVAR && typeArguments(t).isEmpty()) { + // TODO: is this sufficient? Or do we, for example, have to examine the type's supertypes to see if *they* + // "use" a parameterized type? Maybe we have to look at the enclosing type too? But if so, why only here, and + // why not the same sort of thing for the return type of a method (see above)? + yield null; + } + final StringBuilder sb = new StringBuilder(); + fieldSignature(e, sb); + yield sb.toString(); + } + default -> throw new IllegalArgumentException("e: " + e + "; kind: " + e.getKind()); + }; + } finally { + CompletionLock.release(); + } + } + + private static final void fieldSignature(final Element e, final StringBuilder sb) { + CompletionLock.acquire(); + try { + switch (e.getKind()) { + case ENUM_CONSTANT, FIELD, LOCAL_VARIABLE, PARAMETER, RECORD_COMPONENT -> typeSignature(e.asType(), sb); + default -> throw new IllegalArgumentException("e: " + e); + } + } finally { + CompletionLock.release(); + } + } + + private static final void parameterSignatures(final List ps, final StringBuilder sb) { + if (ps.isEmpty()) { + return; + } + CompletionLock.acquire(); + try { + for (final VariableElement p : ps) { + if (p.getKind() != ElementKind.PARAMETER) { + throw new IllegalArgumentException("ps: " + ps); + } + typeSignature(p.asType(), sb); + } + } finally { + CompletionLock.release(); + } + } + + private static final void throwsSignatures(final List ts, final StringBuilder sb) { + if (ts.isEmpty()) { + return; + } + CompletionLock.acquire(); + try { + for (final TypeMirror t : ts) { + sb.append(switch (t.getKind()) { + case DECLARED, TYPEVAR -> "^"; + default -> throw new IllegalArgumentException("ts: " + ts); + }); + typeSignature(t, sb); + } + } finally { + CompletionLock.release(); + } + } + + private static final void typeParameters(final List tps, final StringBuilder sb) { + if (tps.isEmpty()) { + return; + } + sb.append('<'); + CompletionLock.acquire(); + try { + for (final TypeParameterElement tp : tps) { + switch (tp.getKind()) { + case TYPE_PARAMETER -> typeParameter(tp, sb); + default -> throw new IllegalArgumentException("tps: " + tps); + } + } + } finally { + CompletionLock.release(); + } + sb.append('>'); + } + + private static final void typeParameter(final TypeParameterElement e, final StringBuilder sb) { + CompletionLock.acquire(); + try { + if (e.getKind() != ElementKind.TYPE_PARAMETER) { + throw new IllegalArgumentException("e: " + e); + } + final List bounds = e.getBounds(); + sb.append(e.getSimpleName()); + if (bounds.isEmpty()) { + sb.append(":java.lang.Object"); + } else { + sb.append(':'); + classBound(bounds.get(0), sb); + } + interfaceBounds(bounds.subList(1, bounds.size()), sb); + } finally { + CompletionLock.release(); + } + } + + private static final void classBound(final TypeMirror t, final StringBuilder sb) { + CompletionLock.acquire(); + try { + if (t.getKind() != TypeKind.DECLARED) { + throw new IllegalArgumentException("t: " + t); + } + typeSignature(t, sb); + } finally { + CompletionLock.release(); + } + } + + private static final void interfaceBounds(final List ts, final StringBuilder sb) { + if (ts.isEmpty()) { + return; + } + CompletionLock.acquire(); + try { + for (final TypeMirror t : ts) { + interfaceBound(t, sb); + } + } finally { + CompletionLock.release(); + } + } + + @SuppressWarnings("fallthrough") + private static final void interfaceBound(final TypeMirror t, final StringBuilder sb) { + CompletionLock.acquire(); + try { + switch (t.getKind()) { + case DECLARED: + if (((DeclaredType)t).asElement().getKind().isInterface()) { + sb.append(':'); + typeSignature(t, sb); + return; + } + // fall through + default: + throw new IllegalArgumentException("t: " + t); + } + } finally { + CompletionLock.release(); + } + } + + private static final void superclassSignature(final TypeMirror t, final StringBuilder sb) { + classTypeSignature(t, sb); + } + + private static final void superinterfaceSignatures(final List ts, final StringBuilder sb) { + if (ts.isEmpty()) { + return; + } + CompletionLock.acquire(); + try { + for (final TypeMirror t : ts) { + superinterfaceSignature(t, sb); + } + } finally { + CompletionLock.release(); + } + } + + @SuppressWarnings("fallthrough") + private static final void superinterfaceSignature(final TypeMirror t, final StringBuilder sb) { + CompletionLock.acquire(); + try { + switch (t.getKind()) { + case DECLARED: + if (((DeclaredType)t).asElement().getKind().isInterface()) { + classTypeSignature(t, sb); + return; + } + // fall through + default: + throw new IllegalArgumentException("t: " + t); + } + } finally { + CompletionLock.release(); + } + } + + public static final String typeSignature(final TypeMirror t) { + final StringBuilder sb = new StringBuilder(); + typeSignature(t, sb); + return sb.toString(); + } + + private static final void typeSignature(final TypeMirror t, final StringBuilder sb) { + CompletionLock.acquire(); + try { + switch (t.getKind()) { + case ARRAY -> { + sb.append("["); + typeSignature(((ArrayType)t).getComponentType(), sb); // recursive + } + case BOOLEAN -> sb.append("Z"); + case BYTE -> sb.append("B"); + case CHAR -> sb.append("C"); + case DECLARED -> classTypeSignature((DeclaredType)t, sb); + case DOUBLE -> sb.append("D"); + case FLOAT -> sb.append("F"); + case INT -> sb.append("I"); + case LONG -> sb.append("J"); + case SHORT -> sb.append("S"); + case TYPEVAR -> sb.append("T").append(((TypeVariable)t).asElement().getSimpleName()).append(';'); + default -> throw new IllegalArgumentException("t: " + t); + } + } finally { + CompletionLock.release(); + } + } + + private static final void classTypeSignature(final TypeMirror t, final StringBuilder sb) { + CompletionLock.acquire(); + try { + switch (t.getKind()) { + case NONE: + return; + case DECLARED: + break; + default: + throw new IllegalArgumentException("t: " + t); + } + final DeclaredType dt = (DeclaredType)t; + + // Build a deque of elements from the package to the (possibly inner or nested) class. + final Deque dq = new ArrayDeque<>(); + Element e = dt.asElement(); + while (e != null && e.getKind() != ElementKind.MODULE) { + dq.push(e); + e = e.getEnclosingElement(); + } + + sb.append("L"); + + final Iterator i = dq.iterator(); + while (i.hasNext()) { + e = i.next(); + switch (e.getKind()) { + case PACKAGE: + // java.lang becomes java/lang + sb.append(((PackageElement)e).getQualifiedName().toString().replace('.', '/')); + assert i.hasNext(); + sb.append('/'); + break; + case ANNOTATION_TYPE: + case CLASS: + case ENUM: + case INTERFACE: + case RECORD: + // Outer.Inner remains Outer.Inner (i.e. not Outer$Inner or Outer/Inner) + sb.append(e.getSimpleName()); + if (i.hasNext()) { + sb.append('.'); + } + break; + default: + // note that a method could fall in here; we just skip it + break; + } + i.remove(); + } + assert dq.isEmpty(); + + // Now for the type arguments + final List typeArguments = dt.getTypeArguments(); + if (!typeArguments.isEmpty()) { + sb.append('<'); + for (final TypeMirror ta : typeArguments) { + sb.append(switch (ta.getKind()) { + case WILDCARD -> { + final WildcardType w = (WildcardType)ta; + final TypeMirror superBound = w.getSuperBound(); + if (superBound == null) { + final TypeMirror extendsBound = w.getExtendsBound(); + if (extendsBound == null) { + yield "*"; // I guess? + } else { + yield "+" + typeSignature(extendsBound); + } + } else { + yield "-" + typeSignature(superBound); + } + } + default -> typeSignature(ta); + }); + } + sb.append('>'); + } + + sb.append(';'); + } finally { + CompletionLock.release(); + } + } + + public static final String descriptor(final TypeMirror t) { + final StringBuilder sb = new StringBuilder(); + descriptor(t, sb); + return sb.toString(); + } + + private static final void descriptor(final TypeMirror t, final StringBuilder sb) { + CompletionLock.acquire(); + try { + switch (t.getKind()) { + case ARRAY -> { + sb.append("["); + descriptor(((ArrayType)t).getComponentType(), sb); + } + case BOOLEAN -> sb.append("Z"); // yes, really + case BYTE -> sb.append("B"); + case CHAR -> sb.append("C"); + case DECLARED -> sb.append("L").append(jvmBinaryName((TypeElement)((DeclaredType)t).asElement())).append(";"); // basically an erasure + case DOUBLE -> sb.append("D"); + case EXECUTABLE -> descriptor((ExecutableType)t, sb); + case FLOAT -> sb.append("F"); + case INT -> sb.append("I"); + case LONG -> sb.append("J"); // yes, really + case ERROR, INTERSECTION, MODULE, NONE, NULL, OTHER, PACKAGE, UNION, WILDCARD -> throw new IllegalArgumentException("t: " + t); + case SHORT -> sb.append("S"); + case TYPEVAR -> descriptor(erasure(t), sb); + case VOID -> sb.append("V"); + } + } finally { + CompletionLock.release(); + } + } + + private static final void descriptor(final ExecutableType t, final StringBuilder sb) { + CompletionLock.acquire(); + try { + if (t.getKind() != TypeKind.EXECUTABLE) { + throw new IllegalArgumentException("t: " + t); + } + sb.append('('); + for (final TypeMirror pt : t.getParameterTypes()) { + descriptor(pt, sb); + } + sb.append(')'); + descriptor(t.getReturnType(), sb); + } finally { + CompletionLock.release(); + } + } + + public static final String jvmBinaryName(final TypeElement te) { + CompletionLock.acquire(); + try { + if (!te.getKind().isDeclaredType()) { + throw new IllegalArgumentException("te: " + te); + } + return binaryName(te).toString().replace('.', '/'); + } finally { + CompletionLock.release(); + } + } + + + /* + * End of JVMS productions. + */ + + + public static final List directSupertypes(TypeMirror t) { + t = unwrap(t); + final Types types = pe().getTypeUtils(); + final List rv; + CompletionLock.acquire(); + try { + rv = types.directSupertypes(t); + } finally { + CompletionLock.release(); + } + return wrapTypes(rv); + } + + public static final boolean subsignature(ExecutableType e, ExecutableType f) { + e = unwrap(e); + f = unwrap(f); + final Types types = pe().getTypeUtils(); + CompletionLock.acquire(); + try { + return types.isSubsignature(e, f); + } finally { + CompletionLock.release(); + } + } + + public static final TypeMirror erasure(TypeMirror t) { + t = unwrap(t); + final Types types = pe().getTypeUtils(); + // JavacTypes#erasure(TypeMirror) calls TypeMirror#getKind(). + CompletionLock.acquire(); + try { + t = types.erasure(t); + assert t != null; + } finally { + CompletionLock.release(); + } + return wrap(t); + } + + public static final TypeAndElementSource typeAndElementSource() { + return ConstableTypeAndElementSource.INSTANCE; + } + + public static final long modifiers(final Element e) { + // modifiers is declared long because there are javac-specific modifiers that *might* be used later + long modifiers; + CompletionLock.acquire(); + try { + modifiers = modifiers(e.getModifiers()); + switch (e.getKind()) { + case ANNOTATION_TYPE: + modifiers |= (long)ACC_ANNOTATION; + // (abstract will have already been set) + assert (modifiers & (long)ACC_ABSTRACT) != 0; + modifiers |= (long)ACC_INTERFACE; + break; + case CONSTRUCTOR: + // if (canonicalConstructor(e)) { + // modifiers |= JAVAC_RECORD; // See RECORD case + // if (compactConstructor(e)) { + // modifiers |= JAVAC_COMPACT_RECORD_CONSTRUCTOR; + // } + // } + break; + case ENUM: + case ENUM_CONSTANT: + // Weirdly, if you're an enum *type* or an enum *constant* the same flag is set, I guess? + modifiers |= (long)ACC_ENUM; + break; + case INTERFACE: + // (abstract will have already been set) + assert (modifiers & (long)ACC_ABSTRACT) != 0; + modifiers |= (long)ACC_INTERFACE; + break; + case METHOD: + if (bridge(e)) { + modifiers |= (long)ACC_BRIDGE; + } + if (((ExecutableElement)e).isVarArgs()) { + modifiers |= (long)ACC_VARARGS; + } + break; + case MODULE: + // TODO: there are various ACC_OPEN, ACC_STATIC_PHASE etc. modifiers that I'm not entirely sure where to put them + break; + case RECORD: + // ASM uses Opcodes.ACC_RECORD; Byte buddy (among possibly other libraries) uses this to test recordness (maybe not the best idea) + modifiers |= ASM_RECORD; + break; + case RECORD_COMPONENT: + // "Flag to indicate that a class is a record. The flag is also used to mark fields that are part of the state + // vector of a record [record components] and to mark the canonical constructor" + // modifiers |= JAVAC_RECORD; + break; + case TYPE_PARAMETER: + // "Flag that marks formal parameters." + // modifiers |= JAVAC_PARAMETER; + break; + default: + break; + } + switch (origin(e)) { + case SYNTHETIC: + modifiers |= (long)ACC_SYNTHETIC; + break; + case MANDATED: + modifiers |= (long)ACC_MANDATED; + break; + case EXPLICIT: + default: + break; + } + + } finally { + CompletionLock.release(); + } + return modifiers; + } + + public static final long modifiers(final Set ms) { + long modifiers = 0L; + for (final Modifier m : ms) { + modifiers |= modifierMasks.get(m).longValue(); + } + return modifiers; + } + + public static final Set modifiers(final long modifiers) { + final EnumSet s = EnumSet.noneOf(Modifier.class); + for (final Entry e : modifierMasks.entrySet()) { + if ((modifiers & e.getValue().longValue()) != 0) { + s.add(e.getKey()); + } + } + return Collections.unmodifiableSet(s); + } + + public static final ModuleElement moduleElement(final Class c) { + return moduleElement(c.getModule()); + } + + public static final ModuleElement moduleElement(final Module module) { + if (module.isNamed()) { + final ModuleElement rv = moduleElement(module.getName()); + assert rv != null : "null moduleElement for " + module.getName(); + return rv; + } + return moduleElement(""); + } + + public static final ModuleElement moduleElement(final CharSequence moduleName) { + Objects.requireNonNull(moduleName, "moduleName"); + final Elements elements = pe().getElementUtils(); + final ModuleElement rv; + // Not absolutely clear this causes completion but... + CompletionLock.acquire(); + try { + rv = elements.getModuleElement(moduleName); + if (rv == null) { + // Every so often this will return null. It shouldn't. + if (LOGGER.isLoggable(DEBUG)) { + LOGGER.log(DEBUG, "null ModuleElement for module name " + moduleName); + try { + LOGGER.log(DEBUG, "default module: " + getDefaultModuleMethod.invoke(modulesField.get(elements))); + } catch (final ReflectiveOperationException x) { + if (LOGGER.isLoggable(DEBUG)) { + LOGGER.log(DEBUG, x.getMessage(), x); + } + } + } + } + } finally { + CompletionLock.release(); + } + return rv == null ? null : wrap(rv); + } + + public static final ModuleElement moduleOf(Element e) { + e = unwrap(e); + final Elements elements = pe().getElementUtils(); + final ModuleElement rv; + // This doesn't seem to cause completion, but better safe than sorry. + CompletionLock.acquire(); + try { + rv = elements.getModuleOf(e); + if (rv == null) { + if (LOGGER.isLoggable(DEBUG)) { + LOGGER.log(DEBUG, "null ModuleElement for Element " + e); + try { + LOGGER.log(DEBUG, "default module: " + getDefaultModuleMethod.invoke(modulesField.get(e))); + } catch (final ReflectiveOperationException x) { + if (LOGGER.isLoggable(DEBUG)) { + LOGGER.log(DEBUG, x.getMessage(), x); + } + } + } + } + } finally { + CompletionLock.release(); + } + return wrap(rv); + } + + public static final Name name(final CharSequence name) { + final String s; + if (name instanceof String) { + s = (String)name; + } else { + CompletionLock.acquire(); + try { + // Name.toString() is not thread-safe. + s = name.toString(); + } finally { + CompletionLock.release(); + } + } + return name(s); + } + + public static final Name name(final String name) { + return pe().getElementUtils().getName(Objects.requireNonNull(name, "name")); + } + + public static final Elements.Origin origin(Element e) { + e = unwrap(e); + final Elements elements = pe().getElementUtils(); + final Elements.Origin rv; + CompletionLock.acquire(); + try { + rv = elements.getOrigin(e); + } finally { + CompletionLock.release(); + } + return rv; + } + + public static final PackageElement packageElement(final Class c) { + return packageElement(c.getModule(), c.getPackage()); + } + + public static final PackageElement packageElement(final Package pkg) { + return packageElement(pkg.getName()); + } + + public static final PackageElement packageElement(final CharSequence fullyQualifiedName) { + Objects.requireNonNull(fullyQualifiedName, "fullyQualifiedName"); + final Elements elements = pe().getElementUtils(); + // JavacElements#getPackageElement() may end up calling JavacElements#nameToSymbol(ModuleSymbol, String, Class), + // which calls complete() in certain code paths. + final PackageElement rv; + CompletionLock.acquire(); + try { + rv = elements.getPackageElement(fullyQualifiedName); + } finally { + CompletionLock.release(); + } + return rv == null ? null : wrap(rv); + } + + public static final PackageElement packageElement(final Module module, final Package pkg) { + return packageElement(moduleElement(module), pkg); + } + + public static final PackageElement packageElement(final ModuleElement moduleElement, final Package pkg) { + return packageElement(moduleElement, pkg.getName()); + } + + public static final PackageElement packageElement(final ModuleElement moduleElement, final CharSequence fullyQualifiedName) { + final Elements elements = pe().getElementUtils(); + final PackageElement rv; + CompletionLock.acquire(); + try { + rv = elements.getPackageElement(moduleElement, fullyQualifiedName); + } finally { + CompletionLock.release(); + } + return rv == null ? null : wrap(rv); + } + + public static final PackageElement packageOf(final Element e) { + // This does NOT appear to cause completion. + final PackageElement rv = pe().getElementUtils().getPackageOf(unwrap(e)); + return rv == null ? null : wrap(rv); + } + + public static final ArrayType arrayType(final Class arrayClass) { + if (!arrayClass.isArray()) { + throw new IllegalArgumentException("arrayClass: " + arrayClass); + } + return arrayTypeOf(type(arrayClass.getComponentType())); + } + + public static final ArrayType arrayType(final GenericArrayType g) { + return arrayTypeOf(type(g.getGenericComponentType())); + } + + // Called by describeConstable(). + public static final ArrayType arrayTypeOf(TypeMirror componentType) { + componentType = unwrap(componentType); + final Types types = pe().getTypeUtils(); + final ArrayType rv; + // JavacTypes#getArrayType(TypeMirror) calls getKind() on the component type. + CompletionLock.acquire(); + try { + rv = types.getArrayType(componentType); + } finally { + CompletionLock.release(); + } + return wrap(rv); + } + + public static final DeclaredType declaredType(final Type t) { + return switch (t) { + case Class c -> declaredType(c); + case ParameterizedType p -> declaredType(p); + default -> throw new IllegalArgumentException("t: " + t); + }; + } + + public static final DeclaredType declaredType(final Class c) { + if (c.isPrimitive() || c.isArray() || c.isLocalClass() || c.isAnonymousClass()) { + throw new IllegalArgumentException("c: " + c); + } + final Class ec = c.getEnclosingClass(); + return declaredType(ec == null ? null : declaredType(ec), typeElement(c)); + } + + public static final DeclaredType declaredType(final ParameterizedType pt) { + return + declaredType(pt.getOwnerType() == null ? null : declaredType(pt.getOwnerType()), + typeElement(pt.getRawType()), + typeArray(pt.getActualTypeArguments())); + } + + public static final DeclaredType declaredType(final CharSequence canonicalName) { + return declaredType(typeElement(canonicalName)); + } + + public static final DeclaredType declaredType(final Type ownerType, + final Type rawType, + final Type... typeArguments) { + return declaredType(ownerType == null ? null : declaredType(ownerType), typeElement(rawType), typeArray(typeArguments)); + } + + public static final DeclaredType declaredType(TypeElement typeElement, TypeMirror... typeArguments) { + typeElement = unwrap(typeElement); + typeArguments = unwrap(typeArguments); + final Types types = pe().getTypeUtils(); + final DeclaredType rv; + CompletionLock.acquire(); + try { + rv = types.getDeclaredType(typeElement, typeArguments); + } finally { + CompletionLock.release(); + } + return wrap(rv); + } + + public static final DeclaredType declaredType(DeclaredType containingType, + TypeElement typeElement, + TypeMirror... typeArguments) { + if (containingType != null) { + containingType = unwrap(containingType); + } + typeElement = unwrap(typeElement); + typeArguments = unwrap(typeArguments); + final Types types = pe().getTypeUtils(); + final DeclaredType rv; + CompletionLock.acquire(); + try { + // java.lang.NullPointerException: Cannot invoke "javax.lang.model.type.TypeMirror.toString()" because "t" is null + // at jdk.compiler/com.sun.tools.javac.model.JavacTypes.getDeclaredType0(JavacTypes.java:272) + // at jdk.compiler/com.sun.tools.javac.model.JavacTypes.getDeclaredType(JavacTypes.java:241) + // at jdk.compiler/com.sun.tools.javac.model.JavacTypes.getDeclaredType(JavacTypes.java:249) + // at org.microbean.lang@0.0.1-SNAPSHOT/org.microbean.lang.Lang.declaredType(Lang.java:1381) + rv = types.getDeclaredType(containingType, typeElement, typeArguments); + } finally { + CompletionLock.release(); + } + return wrap(rv); + } + + public static final List typeArguments(final TypeMirror t) { + if (Objects.requireNonNull(t, "t") instanceof DeclaredType dt) { + CompletionLock.acquire(); + try { + switch (t.getKind()) { + case DECLARED: + return dt.getTypeArguments(); + default: + break; + } + } finally { + CompletionLock.release(); + } + } + return List.of(); + } + + public static final ExecutableElement executableElement(final Executable e) { + Objects.requireNonNull(e, "e"); + return switch (e) { + case Constructor c -> executableElement(c); + case Method m -> executableElement(m); + default -> throw new IllegalArgumentException("e: " + e); + }; + } + + public static final ExecutableElement executableElement(final Constructor c) { + return + executableElement(typeElement(c.getDeclaringClass()), typeArray(c.getParameterTypes())); // deliberate erasure + } + + public static final ExecutableElement executableElement(final Method m) { + return + executableElement(typeElement(m.getDeclaringClass()), m.getName(), typeArray(m.getParameterTypes())); // deliberate erasure + } + + public static final ExecutableElement executableElement(TypeElement declaringClass, + final List parameterTypes) { + declaringClass = unwrap(declaringClass); + final Types types = pe().getTypeUtils(); + ExecutableElement rv = null; + final int parameterTypesSize = parameterTypes == null ? 0 : parameterTypes.size(); + CompletionLock.acquire(); + try { + CONSTRUCTOR_LOOP: + for (final ExecutableElement c : (Iterable)constructorsIn(declaringClass.getEnclosedElements())) { + final List parameterElements = c.getParameters(); + if (parameterTypesSize == parameterElements.size()) { + for (int i = 0; i < parameterTypesSize; i++) { + if (!types.isSameType(types.erasure(unwrap(parameterTypes.get(i))), + types.erasure(parameterElements.get(i).asType()))) { + continue CONSTRUCTOR_LOOP; + } + } + rv = c; + break; + } + } + } finally { + CompletionLock.release(); + } + return rv == null ? null : wrap(rv); + } + + public static final ExecutableElement executableElement(TypeElement declaringClass, + final TypeMirror... parameterTypes) { + declaringClass = unwrap(declaringClass); + final Types types = pe().getTypeUtils(); + ExecutableElement rv = null; + final int parameterTypesSize = parameterTypes == null ? 0 : parameterTypes.length; + CompletionLock.acquire(); + try { + CONSTRUCTOR_LOOP: + for (final ExecutableElement c : (Iterable)constructorsIn(declaringClass.getEnclosedElements())) { + final List parameterElements = c.getParameters(); + if (parameterTypesSize == parameterElements.size()) { + for (int i = 0; i < parameterTypesSize; i++) { + if (!types.isSameType(types.erasure(unwrap(parameterTypes[i])), + types.erasure(parameterElements.get(i).asType()))) { + continue CONSTRUCTOR_LOOP; + } + } + rv = c; + break; + } + } + } finally { + CompletionLock.release(); + } + return rv == null ? null : wrap(rv); + } + + public static final ExecutableElement executableElement(TypeElement declaringClass, + final CharSequence name, + final List parameterTypes) { + Objects.requireNonNull(name, "name"); + declaringClass = unwrap(declaringClass); + final Types types = pe().getTypeUtils(); + ExecutableElement rv = null; + final int parameterTypesSize = parameterTypes == null ? 0 : parameterTypes.size(); + CompletionLock.acquire(); + try { + METHOD_LOOP: + for (final ExecutableElement m : (Iterable)methodsIn(declaringClass.getEnclosedElements())) { + if (m.getSimpleName().contentEquals(name)) { + final List parameterElements = m.getParameters(); + if (parameterTypesSize == parameterElements.size()) { + for (int i = 0; i < parameterTypesSize; i++) { + if (!types.isSameType(types.erasure(unwrap(parameterTypes.get(i))), + types.erasure(parameterElements.get(i).asType()))) { + continue METHOD_LOOP; + } + } + rv = m; + break; + } + } + } + } finally { + CompletionLock.release(); + } + return rv == null ? null : wrap(rv); + } + + public static final ExecutableElement executableElement(TypeElement declaringClass, + final CharSequence name, + final TypeMirror... parameterTypes) { + Objects.requireNonNull(name, "name"); + declaringClass = unwrap(declaringClass); + final Types types = pe().getTypeUtils(); + ExecutableElement rv = null; + final int parameterTypesSize = parameterTypes == null ? 0 : parameterTypes.length; + CompletionLock.acquire(); + try { + METHOD_LOOP: + for (final ExecutableElement m : (Iterable)methodsIn(declaringClass.getEnclosedElements())) { + if (m.getSimpleName().contentEquals(name)) { + final List parameterElements = m.getParameters(); + if (parameterTypesSize == parameterElements.size()) { + for (int i = 0; i < parameterTypesSize; i++) { + if (!types.isSameType(types.erasure(unwrap(parameterTypes[i])), + types.erasure(parameterElements.get(i).asType()))) { + continue METHOD_LOOP; + } + } + rv = m; + break; + } + } + } + } finally { + CompletionLock.release(); + } + return rv == null ? null : wrap(rv); + } + + public static final ExecutableType executableType(final Executable e) { + return (ExecutableType)executableElement(e).asType(); + } + + public static final NoType noType(final TypeKind k) { + return pe().getTypeUtils().getNoType(k); + } + + // Called by describeConstable + public static final NullType nullType() { + return pe().getTypeUtils().getNullType(); + } + + public static final PrimitiveType primitiveType(final Class c) { + if (!c.isPrimitive()) { + throw new IllegalArgumentException("c: " + c); + } + return primitiveType(TypeKind.valueOf(c.getName().toUpperCase())); + } + + // Called by describeConstable(). + public static final PrimitiveType primitiveType(final TypeKind k) { + return pe().getTypeUtils().getPrimitiveType(k); + } + + public static final boolean sameType(TypeMirror t, TypeMirror s) { + if (t == s) { + // Optimization + return true; + } + t = unwrap(t); + s = unwrap(s); + final Types types = pe().getTypeUtils(); + CompletionLock.acquire(); + try { + // Internally JavacTypes calls getKind() on each type, causing symbol completion + return types.isSameType(t, s); + } finally { + CompletionLock.release(); + } + } + + public static final TypeElement typeElement(final Type t) { + return switch (t) { + case null -> throw new NullPointerException("t"); + case Class c -> typeElement(c); + case ParameterizedType pt -> typeElement(pt.getRawType()); + default -> throw new IllegalArgumentException("t: " + t); + }; + } + + public static final TypeElement typeElement(final Class c) { + final ModuleElement me = moduleElement(c.getModule()); + return me == null ? typeElement(c.getCanonicalName()) : typeElement(me, c.getCanonicalName()); + } + + public static final TypeElement typeElement(final CharSequence canonicalName) { + Objects.requireNonNull(canonicalName, "canonicalName"); + final Elements elements = pe().getElementUtils(); + final TypeElement rv; + CompletionLock.acquire(); + try { + rv = elements.getTypeElement(canonicalName); + if (rv == null) { + if (LOGGER.isLoggable(DEBUG)) { + LOGGER.log(DEBUG, "null TypeElement for canonicalName " + canonicalName); + } + return null; + } else if (!rv.getKind().isDeclaredType() && LOGGER.isLoggable(WARNING)) { + LOGGER.log(WARNING, "rv.getKind(): " + rv.getKind() + "; rv: " + rv); + } + } finally { + CompletionLock.release(); + } + return wrap(rv); + } + + public static final TypeElement typeElement(final Module module, final CharSequence canonicalName) { + return module == null ? typeElement(canonicalName) : typeElement(moduleElement(module), canonicalName); + } + + public static final TypeElement typeElement(ModuleElement moduleElement, final CharSequence canonicalName) { + if (moduleElement == null) { + // TODO: in certain cases while running parallel unit tests even with all the synchronization in this class, a + // ModuleElement can be null, and can get described as a DynamicConstant that is + // java.lang.constant.ConstantDescs.NULL, which will then be hydrated back to null, and passed to this + // method. This should, of course, never happen. Finding out what is responsible is a long-running pain in my ass. + // + // One possible culprit may be the Elements#getModuleOf(Element) method, which does this (see + // com.sun.tools.javac.model.JavacElements): + // + // Symbol sym = cast(Symbol.class, e); + // if (modules.getDefaultModule() == syms.noModule) + // return null; // <--- NOTE + // return (sym.kind == MDL) ? ((ModuleElement) e) + // : (sym.owner.kind == MDL) ? (ModuleElement) sym.owner + // : sym.packge().modle; + // + // modules is an instance of com.sun.tools.javac.comp.Modules. It only sets defaultModule to syms.noModule if + // modules are not allowed by the current source level, which would seem to be impossible in this case. + final Elements elements = pe().getElementUtils(); + String message = "moduleElement"; + CompletionLock.acquire(); + try { + message += "; canonicalName: " + canonicalName + "; defaultModule: " + getDefaultModuleMethod.invoke(modulesField.get(elements)); + } catch (final ReflectiveOperationException x) { + x.printStackTrace(); + } finally { + CompletionLock.release(); + } + throw new NullPointerException(message); + } + Objects.requireNonNull(canonicalName, "canonicalName"); + moduleElement = unwrap(moduleElement); + final Elements elements = pe().getElementUtils(); + final TypeElement rv; + CompletionLock.acquire(); + try { + rv = elements.getTypeElement(moduleElement, canonicalName); + if (rv == null) { + if (LOGGER.isLoggable(DEBUG)) { + LOGGER.log(DEBUG, "null TypeElement for ModuleElement " + moduleElement + " and canonicalName " + canonicalName); + } + return null; + } + } finally { + CompletionLock.release(); + } + return wrap(rv); + } + + public static final Parameterizable parameterizable(final GenericDeclaration gd) { + Objects.requireNonNull(gd, "gd"); + return switch (gd) { + case Executable e -> executableElement(e); + case Class c -> typeElement(c); + default -> throw new IllegalArgumentException("gd: " + gd); + }; + } + + public static final TypeParameterElement typeParameterElement(final java.lang.reflect.TypeVariable t) { + return typeParameterElement(t.getGenericDeclaration(), t.getName()); + } + + public static final TypeParameterElement typeParameterElement(GenericDeclaration gd, final String name) { + Objects.requireNonNull(gd, "gd"); + Objects.requireNonNull(name, "name"); + while (gd != null) { + final java.lang.reflect.TypeVariable[] typeParameters = gd.getTypeParameters(); + for (int i = 0; i < typeParameters.length; i++) { + if (typeParameters[i].getName().equals(name)) { + return parameterizable(gd).getTypeParameters().get(i); + } + } + gd = gd instanceof Executable e ? e.getDeclaringClass() : ((Class)gd).getEnclosingClass(); + } + return null; + } + + public static final TypeVariable typeVariable(final java.lang.reflect.TypeVariable t) { + final TypeParameterElement e = typeParameterElement(t); + return e == null ? null : (TypeVariable)e.asType(); + } + + public static final TypeVariable typeVariable(final GenericDeclaration gd, final String name) { + final TypeParameterElement e = typeParameterElement(gd, name); + return e == null ? null : (TypeVariable)e.asType(); + } + + public static final VariableElement variableElement(final Field f) { + Objects.requireNonNull(f, "f"); + CompletionLock.acquire(); + try { + // Conveniently, fieldsIn() doesn't use internal javac constructs so we can just skip the unwrapping. + for (final VariableElement ve : (Iterable)fieldsIn(typeElement(f.getDeclaringClass()).getEnclosedElements())) { + if (ve.getSimpleName().contentEquals(f.getName())) { + return ve; + } + } + } finally { + CompletionLock.release(); + } + return null; + } + + public static final WildcardType wildcardType() { + return wildcardType(null, null); + } + + public static final WildcardType wildcardType(final java.lang.reflect.WildcardType t) { + final Type[] lowerBounds = t.getLowerBounds(); + final Type lowerBound = lowerBounds.length <= 0 ? null : lowerBounds[0]; + final Type upperBound = t.getUpperBounds()[0]; + return wildcardType(upperBound == null ? null : type(upperBound), lowerBound == null ? null : type(lowerBound)); + } + + public static final WildcardType wildcardType(TypeMirror extendsBound, TypeMirror superBound) { + extendsBound = extendsBound == null ? null : unwrap(extendsBound); + superBound = superBound == null ? null : unwrap(superBound); + final Types types = pe().getTypeUtils(); + final WildcardType rv; + CompletionLock.acquire(); + try { + // JavacTypes#getWildcardType() can call getKind() on bounds etc. which triggers symbol completion + rv = types.getWildcardType(extendsBound, superBound); + } finally { + CompletionLock.release(); + } + return wrap(rv); + } + + public static final boolean assignable(TypeMirror payload, TypeMirror receiver) { + payload = unwrap(payload); + receiver = unwrap(receiver); + final Types types = pe().getTypeUtils(); + CompletionLock.acquire(); + try { + return types.isAssignable(payload, receiver); + } finally { + CompletionLock.release(); + } + } + + public static final boolean subtype(TypeMirror payload, TypeMirror receiver) { + payload = unwrap(payload); + receiver = unwrap(receiver); + final Types types = pe().getTypeUtils(); + CompletionLock.acquire(); + try { + return types.isSubtype(payload, receiver); + } finally { + CompletionLock.release(); + } + } + + public static final TypeMirror type(final Type t) { + Objects.requireNonNull(t, "t"); + return switch (t) { + case Class c when c == void.class -> noType(TypeKind.VOID); + case Class c when c.isArray() -> arrayType(c); + case Class c when c.isPrimitive() -> primitiveType(c); + case Class c -> { + final Class ec = c.getEnclosingClass(); + yield declaredType(ec == null ? null : declaredType(typeElement(ec)), typeElement(c)); + } + case ParameterizedType pt -> declaredType((DeclaredType)type(pt.getOwnerType()), typeElement((Class)pt.getRawType()), typeArray(pt.getActualTypeArguments())); + case GenericArrayType g -> arrayType(g); + case java.lang.reflect.TypeVariable tv -> typeVariable(tv); + case java.lang.reflect.WildcardType w -> wildcardType(w); + default -> throw new IllegalArgumentException("t: " + t); + }; + } + + public static final TypeMirror type(final Field f) { + return variableElement(f).asType(); + } + + public static final Equality sameTypeEquality() { + return SameTypeEquality.INSTANCE; + } + + + /* + * Private static methods. + */ + + + private static final TypeMirror[] typeArray(final Type[] ts) { + if (ts.length <= 0) { + return EMPTY_TYPEMIRROR_ARRAY; + } + final TypeMirror[] rv = new TypeMirror[ts.length]; + for (int i = 0; i < ts.length; i++) { + rv[i] = type(ts[i]); + } + return rv; + } + + private static final TypeMirror[] typeArray(final List ts) { + if (ts.isEmpty()) { + return EMPTY_TYPEMIRROR_ARRAY; + } + final TypeMirror[] rv = new TypeMirror[ts.size()]; + for (int i = 0; i < ts.size(); i++) { + rv[i] = type(ts.get(i)); + } + return rv; + } + + private static final List typeList(final Type[] ts) { + if (ts.length <= 0) { + return List.of(); + } + final List rv = new ArrayList<>(ts.length); + for (final Type t : ts) { + rv.add(type(t)); + } + return Collections.unmodifiableList(rv); + } + + static final List typeList(final Collection ts) { + if (ts.isEmpty()) { + return List.of(); + } + final List rv = new ArrayList<>(ts.size()); + for (final Type t : ts) { + rv.add(type(t)); + } + return Collections.unmodifiableList(rv); + } + + static final ProcessingEnvironment pe() { + ProcessingEnvironment pe = Lang.pe; // volatile read + if (pe == null) { + try { + initLatch.await(); + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + } + pe = Lang.pe; // volatile read + if (pe == null) { + throw new IllegalStateException(); + } + } + return pe; + } + + // Called once, ever, by the static initializer. Idempotent. + private static final void initialize() { + if (pe != null) { // volatile read + return; + } + if (LOGGER.isLoggable(DEBUG)) { + LOGGER.log(DEBUG, "Initializing"); + } + new BlockingCompilationTaskInvocationThread("Lang", initLatch).start(); + } + + @SuppressWarnings("unchecked") + public static final T unwrap(final T t) { + return (T)DelegatingTypeMirror.unwrap(Objects.requireNonNull(t, "t")); + } + + public static final TypeMirror[] unwrap(final TypeMirror[] ts) { + return DelegatingTypeMirror.unwrap(Objects.requireNonNull(ts, "ts")); + } + + public static final E unwrap(final E e) { + return DelegatingElement.unwrap(Objects.requireNonNull(e, "e")); + } + + @SuppressWarnings("unchecked") + public static final T wrap(final T t) { + return (T)DelegatingTypeMirror.of(Objects.requireNonNull(t, "t"), typeAndElementSource(), null); + } + + @SuppressWarnings("unchecked") + public static final E wrap(final E e) { + return (E)DelegatingElement.of(Objects.requireNonNull(e, "e"), typeAndElementSource(), null); + } + + public static final List wrapTypes(final Collection ts) { + return DelegatingTypeMirror.of(Objects.requireNonNull(ts, "ts"), typeAndElementSource(), null); + } + + public static final List wrapElements(final Collection es) { + return DelegatingElement.of(Objects.requireNonNull(es, "es"), typeAndElementSource(), null); + } + + + /* + * Inner and nested classes. + */ + + + public static final class SameTypeEquality extends Equality { + + public static final SameTypeEquality INSTANCE = new SameTypeEquality(); + + private SameTypeEquality() { + super(false); + } + + @Override + public final boolean equals(final Object o1, final Object o2) { + if (o1 == o2) { + return true; + } else if (o1 == null || o2 == null) { + return false; + } else if (o1 instanceof TypeMirror t1) { + return o2 instanceof TypeMirror t2 && Lang.sameType(t1, t2); + } else if (o2 instanceof TypeMirror) { + return false; + } else { + return super.equals(o1, o2); + } + } + + } + + public static final class ConstableTypeAndElementSource implements Constable, TypeAndElementSource { + + public static final ConstableTypeAndElementSource INSTANCE = new ConstableTypeAndElementSource(); + + private static final ClassDesc CD_ConstableTypeAndElementSource = ClassDesc.of(ConstableTypeAndElementSource.class.getName()); + + private ConstableTypeAndElementSource() { + super(); + } + + @Override + public final ArrayType arrayTypeOf(final TypeMirror componentType) { + return Lang.arrayTypeOf(componentType); + } + + // Note the counterintuitive parameter order. + @Override + public final boolean assignable(final TypeMirror payload, final TypeMirror receiver) { + return Lang.assignable(payload, receiver); + } + + @Override + public final TypeElement boxedClass(final PrimitiveType t) { + return Lang.boxedClass(t); + } + + @Override + public final DeclaredType declaredType(final TypeElement typeElement, final TypeMirror... typeArguments) { + return Lang.declaredType(typeElement, typeArguments); + } + + @Override + public final DeclaredType declaredType(final DeclaredType containingType, + final TypeElement typeElement, + final TypeMirror... typeArguments) { + return Lang.declaredType(containingType, typeElement, typeArguments); + } + + @Override + @SuppressWarnings("unchecked") + public final T erasure(final T t) { + return (T)Lang.erasure(t); + } + + @Override + public final NoType noType(final TypeKind k) { + return Lang.noType(k); + } + + @Override + public final PrimitiveType primitiveType(final TypeKind k) { + return Lang.primitiveType(k); + } + + @Override + public final boolean sameType(final TypeMirror t, final TypeMirror s) { + return Lang.sameType(t, s); + } + + @Override + public final TypeElement typeElement(final CharSequence moduleName, final CharSequence canonicalName) { + if (moduleName == null) { + return Lang.typeElement(canonicalName); + } + final ModuleElement m = Lang.moduleElement(moduleName); + if (m == null) { + if (LOGGER.isLoggable(DEBUG)) { + // moduleName might be a javax.lang.element.Name and the shared name table might be in effect + CompletionLock.acquire(); + try { + LOGGER.log(DEBUG, "null ModuleElement for module name " + moduleName); + } finally { + CompletionLock.release(); + } + } + } + return Lang.typeElement(m, canonicalName); + } + + @Override + public final TypeVariable typeVariable(final java.lang.reflect.TypeVariable t) { + return Lang.typeVariable(t); + } + + @Override + public final WildcardType wildcardType(final TypeMirror extendsBound, final TypeMirror superBound) { + return Lang.wildcardType(extendsBound, superBound); + } + + @Override + public final Optional> describeConstable() { + return + Optional.of(DynamicConstantDesc.of(BSM_INVOKE, + MethodHandleDesc.ofField(STATIC_GETTER, + CD_ConstableTypeAndElementSource, + "INSTANCE", + CD_ConstableTypeAndElementSource))); + } + + } + + private static final class BlockingCompilationTaskInvocationThread extends Thread { + + private static final Logger LOGGER = System.getLogger(BlockingCompilationTaskInvocationThread.class.getName()); + + private final CountDownLatch initLatch; + + private final CountDownLatch runningLatch; + + private BlockingCompilationTaskInvocationThread(final String name, + final CountDownLatch initLatch) { + super(name); + this.setDaemon(true); // critical; runningLatch is never counted down except in error cases + this.initLatch = Objects.requireNonNull(initLatch, "initLatch"); + this.runningLatch = new CountDownLatch(1); + } + + @Override + public final void run() { + if (LOGGER.isLoggable(DEBUG)) { + LOGGER.log(DEBUG, "CompilationTask invocation daemon thread running"); + } + try { + final JavaCompiler jc = ToolProvider.getSystemJavaCompiler(); + if (jc == null) { + runningLatch.countDown(); + initLatch.countDown(); + return; + } + + final List options = new ArrayList<>(); + options.add("-proc:only"); + options.add("-cp"); + options.add(System.getProperty("java.class.path")); + + final Set effectiveModulePathModules = effectiveModulePathModules(); + final Module unnamedModule = Lang.class.getClassLoader().getUnnamedModule(); + for (final Module m : effectiveModulePathModules) { + assert m.isNamed(); + if (m.canRead(unnamedModule)) { + // This is a (required) stupendous hack. + options.add("--add-reads"); + options.add(m.getName() + "=ALL-UNNAMED"); + } + } + + // See + // https://github.com/openjdk/jdk/blob/jdk-21%2B35/src/jdk.compiler/share/classes/com/sun/tools/javac/util/Names.java#L430-L436 + // in JDK 21; JDK 22+ changes things dramatically. + // + // This turns out to be rather important. The default name table in javac (21 and earlier) is shared and + // unsynchronized such that, given a Name, calling its toString() method may involve reading a shared byte array + // that is being updated in another thread. By contrast, the unshared name table creates Names whose contents + // are not shared, so toString() invocations on them are not problematic. + // + // In JDK 22+, there is a String-based name table that is used by default; see + // https://github.com/openjdk/jdk/pull/15470. However note that this is also not thread-safe: + // https://github.com/archiecobbs/jdk/blob/1dcafc70c2969093b0c59cf637a982697b05030b/src/jdk.compiler/share/classes/com/sun/tools/javac/util/StringNameTable.java#L65 + // + // TODO: It *seems* that the thread safety issues we sometimes see are due to the shared name table's Name + // implementation's toString() method, which can read a portion of the shared byte[] array in which all name + // content is stored at the same time that the same byte array is being updated. It does not appear to me that + // any of the other Name.Table implementations suffer from this, so the string table may be good enough. That + // is, except for the shared name table situation, any time you have a Name in your hand you should be able to + // call toString() on it without any problems. + if (Boolean.getBoolean("useSharedNameTable")) { + if (LOGGER.isLoggable(DEBUG)) { + LOGGER.log(DEBUG, "Using shared name table"); + } + } else { + options.add("-XDuseUnsharedTable"); // TODO: experimental + if (LOGGER.isLoggable(DEBUG)) { + LOGGER.log(DEBUG, "Using unshared name table"); + } + } + + if (Boolean.getBoolean(Lang.class.getName() + ".verbose")) { + options.add("-verbose"); + } + + final List classes = new ArrayList<>(); + classes.add("java.lang.annotation.RetentionPolicy"); // arbitrary, but loads the least amount of stuff up front + + // (Any "loading" is actually performed by, e.g. com.sun.tools.javac.jvm.ClassReader.fillIn(), not reflective + // machinery. Once a class has been loaded, com.sun.tools.javac.code.Symtab#getClass(ModuleSymbol, Name) will + // retrieve it from a HashMap.) + final CompilationTask task = + jc.getTask(null, // additionalOutputWriter + new ReadOnlyModularJavaFileManager(jc.getStandardFileManager(null, null, null), effectiveModulePathModules), // fileManager, + null, // diagnosticListener, + options, + classes, + null); // compilation units; null means we aren't actually compiling anything + + task.setProcessors(List.of(new P())); + + final Set systemModules = ModuleFinder.ofSystem().findAll(); + final Set modulePath = ModuleLayer.boot() + .configuration() + .modules() + .stream() + .map(ResolvedModule::reference) + .filter(Predicate.not(systemModules::contains)) + .map(mr -> mr.descriptor().name()) + .collect(Collectors.toUnmodifiableSet()); + + task.addModules(modulePath); + + if (LOGGER.isLoggable(DEBUG)) { + LOGGER.log(DEBUG, "CompilationTask options: " + options); + LOGGER.log(DEBUG, "CompilationTask modules: " + modulePath); + LOGGER.log(DEBUG, "Calling CompilationTask"); + } + + if (Boolean.FALSE.equals(task.call())) { // NOTE: runs the task; task blocks forever by design + if (LOGGER.isLoggable(WARNING)) { + LOGGER.log(WARNING, "CompilationTask called unuccessfully; releasing latches"); + } + runningLatch.countDown(); + initLatch.countDown(); + } + + } catch (final RuntimeException | Error e) { + e.printStackTrace(); + runningLatch.countDown(); + initLatch.countDown(); + throw e; + } finally { + pe = null; // volatile write + } + } + + private static final Set effectiveModulePathModules() { + final Set modules = new HashSet<>(ModuleLayer.boot().modules()); + // Remove system modules (e.g. java.base etc.). Whatever's left was on the module path. + modules.removeIf(m -> { + if (m.isNamed()) { + for (final ModuleReference mr : ModuleFinder.ofSystem().findAll()) { + if (mr.descriptor().name().equals(m.getName())) { + return true; + } + } + } + return false; + }); + return Collections.unmodifiableSet(modules); + } + + private final class P extends AbstractProcessor { + + private static final Logger LOGGER = System.getLogger(P.class.getName()); + + private P() { + super(); + if (LOGGER.isLoggable(DEBUG)) { + LOGGER.log(DEBUG, "Created"); + } + } + + @Override + public final void init(final ProcessingEnvironment pe) { + if (LOGGER.isLoggable(DEBUG)) { + LOGGER.log(DEBUG, "AbstractProcessor inititializing with " + pe); + } + Lang.pe = pe; // volatile write + initLatch.countDown(); + if (LOGGER.isLoggable(DEBUG)) { + LOGGER.log(DEBUG, "The " + Lang.class.getName() + " class is ready for use"); + } + // Note to future maintainers: you're going to desperately want to move this to the process() method, and you + // cannot. If you decide to doubt this message, at least comment this out so you don't lose it here. Don't say + // I didn't warn you. + try { + runningLatch.await(); + } catch (final InterruptedException e) { + Thread.currentThread().interrupt(); + } + } + + @Override // AbstractProcessor + public final Set getSupportedAnnotationTypes() { + return Set.of(); // we claim nothing, although it's moot because we're the only processor in existence + } + + @Override // AbstractProcessor + public final Set getSupportedOptions() { + return Set.of(); + } + + @Override // AbstractProcessor + public final SourceVersion getSupportedSourceVersion() { + return SourceVersion.latestSupported(); + } + + @Override // AbstractProcessor + public final boolean process(final Set annotations, final RoundEnvironment roundEnvironment) { + assert this.isInitialized(); + return false; // we don't claim anything, but we're the only processor in existence + } + + } + + } + + private static final class ReadOnlyModularJavaFileManager extends ForwardingJavaFileManager { + + private static final Logger LOGGER = System.getLogger(ReadOnlyModularJavaFileManager.class.getName()); + + private static final Set ALL_KINDS = EnumSet.allOf(JavaFileObject.Kind.class); + + private final Set modulePath; + + private final Map>> maps; + + private ReadOnlyModularJavaFileManager(final StandardJavaFileManager fm, final Set effectiveModulePathModules) { + super(fm); + this.maps = new ConcurrentHashMap<>(); + this.modulePath = effectiveModulePathModules.stream() + .map(m -> m.getLayer().configuration().findModule(m.getName()).orElseThrow().reference()) + .collect(Collectors.toUnmodifiableSet()); + } + + @Override + public final void close() throws IOException { + super.close(); + this.maps.clear(); + } + + @Override + public final ClassLoader getClassLoader(final Location location) { + assert !location.isModuleOrientedLocation(); + if (location instanceof ModuleLocation m) { + return ModuleLayer.boot().findLoader(m.moduleReference().descriptor().name()); + } + return super.getClassLoader(location); + } + + @Override + public final JavaFileObject getJavaFileForInput(final Location location, final String className, final JavaFileObject.Kind kind) throws IOException { + if (location instanceof ModuleLocation m) { + try (final ModuleReader mr = m.moduleReference().open()) { + return mr.find(className.replace('.', '/') + kind.extension) + .map(u -> new JavaFileRecord(kind, className, u)) + .orElse(null); + } catch (final IOException e) { + throw new UncheckedIOException(e.getMessage(), e); + } + } + return super.getJavaFileForInput(location, className, kind); + } + + @Override + public final boolean hasLocation(final Location location) { + return switch (location) { + case null -> false; + case StandardLocation s -> switch (s) { + case CLASS_PATH, MODULE_PATH, PATCH_MODULE_PATH, PLATFORM_CLASS_PATH, SYSTEM_MODULES, UPGRADE_MODULE_PATH -> { + assert !s.isOutputLocation(); + yield super.hasLocation(s); + } + case ANNOTATION_PROCESSOR_MODULE_PATH, ANNOTATION_PROCESSOR_PATH, CLASS_OUTPUT, MODULE_SOURCE_PATH, NATIVE_HEADER_OUTPUT, SOURCE_OUTPUT, SOURCE_PATH -> false; + }; + case ModuleLocation m -> true; + default -> !location.isOutputLocation() && super.hasLocation(location); + }; + } + + @Override + public final Iterable list(final Location location, final String packageName, final Set kinds, final boolean recurse) throws IOException { + if (location instanceof ModuleLocation m) { + final ModuleReference mref = m.moduleReference(); + if (recurse) { + // Don't cache anything; not really worth it + try (final ModuleReader reader = mref.open()) { + return list(reader, packageName, kinds, true); + } + } + final Map> m0 = this.maps.computeIfAbsent(mref, mr -> { + try (final ModuleReader reader = mr.open(); + final Stream ss = reader.list()) { + return + Collections.unmodifiableMap(ss.filter(s -> !s.endsWith("/")) + .collect(HashMap::new, + (map, s) -> { + // s is, e.g., "foo/Bar.class" + final int lastSlashIndex = s.lastIndexOf('/'); + assert lastSlashIndex != 0; + final String p0 = lastSlashIndex > 0 ? s.substring(0, lastSlashIndex).replace('/', '.') : ""; + // p0 is now "foo" + final List list = map.computeIfAbsent(p0, p1 -> new ArrayList<>()); + final JavaFileObject.Kind kind = kind(s); + try { + list.add(new JavaFileRecord(kind, + kind == JavaFileObject.Kind.CLASS || kind == JavaFileObject.Kind.SOURCE ? s.substring(0, s.length() - kind.extension.length()).replace('/', '.') : null, + reader.find(s).orElseThrow())); + } catch (final IOException ioException) { + throw new UncheckedIOException(ioException.getMessage(), ioException); + } + }, + Map::putAll)); + } catch (final IOException ioException) { + throw new UncheckedIOException(ioException.getMessage(), ioException); + } + }); + List unfilteredPackageContents = m0.get(packageName); + if (unfilteredPackageContents == null) { + return List.of(); + } + assert !unfilteredPackageContents.isEmpty(); + if (kinds.size() < ALL_KINDS.size()) { + unfilteredPackageContents = new ArrayList<>(unfilteredPackageContents); + unfilteredPackageContents.removeIf(f -> !kinds.contains(f.kind())); + } + return Collections.unmodifiableList(unfilteredPackageContents); + } + return super.list(location, packageName, kinds, recurse); + } + + @Override + public final Iterable> listLocationsForModules(final Location location) throws IOException { + return + location == StandardLocation.MODULE_PATH ? + Set.of(this.modulePath.stream().map(ModuleLocation::new).collect(Collectors.toUnmodifiableSet())) : + super.listLocationsForModules(location); + } + + @Override + public final String inferBinaryName(final Location location, final JavaFileObject file) { + return file instanceof JavaFileRecord f ? f.binaryName() : super.inferBinaryName(location, file); + } + + @Override + public final String inferModuleName(final Location location) throws IOException { + if (location instanceof ModuleLocation m) { + assert m.getName() != null : "m.getName() == null: " + m; + return m.getName(); + } + return super.inferModuleName(location); + } + + @Override + public final boolean contains(final Location location, final FileObject fo) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public final FileObject getFileForInput(final Location location, final String packageName, final String relativeName) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public final FileObject getFileForOutput(final Location location, final String packageName, final String relativeName, final FileObject sibling) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public final FileObject getFileForOutputForOriginatingFiles(final Location location, final String packageName, final String relativeName, final FileObject... originatingFiles) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public final JavaFileObject getJavaFileForOutput(final Location location, final String className, final JavaFileObject.Kind kind, final FileObject sibling) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public final JavaFileObject getJavaFileForOutputForOriginatingFiles(final Location location, final String className, final JavaFileObject.Kind kind, final FileObject... originatingFiles) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public final Location getLocationForModule(final Location location, final String moduleName) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public final Location getLocationForModule(final Location location, final JavaFileObject fileObject) throws IOException { + throw new UnsupportedOperationException(); + } + + @Override + public final boolean isSameFile(final FileObject a, final FileObject b) { + throw new UnsupportedOperationException(); + } + + + /* + * Static methods. + */ + + + private static final JavaFileObject.Kind kind(final String s) { + return kind(ALL_KINDS, s); + } + + private static final JavaFileObject.Kind kind(final Iterable kinds, final String s) { + for (final JavaFileObject.Kind kind : kinds) { + if (kind != JavaFileObject.Kind.OTHER && s.endsWith(kind.extension)) { + return kind; + } + } + return JavaFileObject.Kind.OTHER; + } + + private static final Iterable list(final ModuleReader mr, + final String packageName, + final Set kinds, + final boolean recurse) + throws IOException { + final String p = packageName.replace('.', '/'); + final int packagePrefixLength = p.length() + 1; + try (final Stream ss = mr.list()) { + return ss + .filter(s -> !s.endsWith("/") && s.startsWith(p) && isAKind(kinds, s) && (recurse || s.indexOf('/', packagePrefixLength) < 0)) + .map(s -> { + final JavaFileObject.Kind kind = kind(kinds, s); + try { + return + new JavaFileRecord(kind, + kind == JavaFileObject.Kind.CLASS || kind == JavaFileObject.Kind.SOURCE ? s.substring(0, s.length() - kind.extension.length()).replace('/', '.') : null, + mr.find(s).orElseThrow()); + } catch (final IOException ioException) { + throw new UncheckedIOException(ioException.getMessage(), ioException); + } + }) + .collect(Collectors.toUnmodifiableList()); + } + } + + private static final boolean isAKind(final Set kinds, final String moduleResource) { + for (final JavaFileObject.Kind k : kinds) { + if (moduleResource.endsWith(k.extension)) { + return true; + } + } + return false; + } + + private static final record ModuleLocation(ModuleReference moduleReference) implements Location { + + private ModuleLocation(final Module module) { + this(module.getLayer().configuration().findModule(module.getName()).orElseThrow().reference()); + } + + @Override + public final String getName() { + // Won't be null. + return this.moduleReference().descriptor().name(); + } + + @Override + public final boolean isModuleOrientedLocation() { + return false; + } + + @Override + public final boolean isOutputLocation() { + return false; + } + + } + + private static final record JavaFileRecord(JavaFileObject.Kind kind, String binaryName, URI uri) implements JavaFileObject { + + @Override + public final URI toUri() { + return this.uri(); + } + + @Override + public final NestingKind getNestingKind() { + return null; + } + + @Override + public final Modifier getAccessLevel() { + return null; + } + + @Override + public final long getLastModified() { + return 0L; + } + + @Override + public final Reader openReader(final boolean ignoreEncodingErrors) { + throw new UnsupportedOperationException(); + } + + @Override + public final OutputStream openOutputStream() { + throw new UnsupportedOperationException(); + } + + @Override + public final CharSequence getCharContent(final boolean ignoreEncodingErrors) { + throw new UnsupportedOperationException(); + } + + @Override + public final Writer openWriter() { + throw new UnsupportedOperationException(); + } + + @Override + public final JavaFileObject.Kind getKind() { + return this.kind(); + } + + private final String path() { + final String path = this.uri().getPath(); + if (path == null) { + // Probably a jar: URI + final String ssp = this.uri().getSchemeSpecificPart(); + return ssp.substring(ssp.lastIndexOf('!') + 1); + } + return path; + } + + @Override + public final String getName() { + return this.path(); + } + + @Override + public final boolean isNameCompatible(final String simpleName, final JavaFileObject.Kind kind) { + if (kind != this.kind()) { + return false; + } + final String basename = simpleName + kind.extension; + final String path = this.path(); + return path.equals(basename) || path.endsWith("/" + basename); + } + + @Override + public final boolean delete() { + return false; + } + + @Override + public final InputStream openInputStream() throws IOException { + return this.uri().toURL().openConnection().getInputStream(); + } + + } + + } + + +} diff --git a/lang/src/main/java/org/microbean/lang/Modeler.java b/lang/src/main/java/org/microbean/lang/Modeler.java new file mode 100644 index 00000000..deda7443 --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/Modeler.java @@ -0,0 +1,94 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2022–2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang; + +import java.util.HashMap; +import java.util.Map; + +import java.util.function.BiConsumer; +import java.util.function.Supplier; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; + +import javax.lang.model.type.TypeMirror; + +// NOT thread safe +public abstract class Modeler implements TypeAndElementSource { + + private final Map annotations; + + private final Map elements; + + private final Map types; + + protected Modeler() { + super(); + this.annotations = new HashMap<>(); + this.elements = new HashMap<>(); + this.types = new HashMap<>(); + } + + @SuppressWarnings("unchecked") + protected final A annotation(final K k, + final Supplier s, + final BiConsumer c) { + A r = (A)this.annotations.get(k); + if (r == null) { + final A a = s.get(); + r = (A)this.annotations.putIfAbsent(k, a); + if (r == null) { + c.accept(k, a); + return a; + } + } + return r; + } + + @SuppressWarnings("unchecked") + protected final E element(final K k, + final Supplier s, + final BiConsumer c) { + E r = (E)this.elements.get(k); + if (r == null) { + final E e = s.get(); + r = (E)this.elements.putIfAbsent(k, e); + if (r == null) { + c.accept(k, e); + return e; + } + } + return r; + } + + @SuppressWarnings("unchecked") + protected final T type(final K k, + final Supplier s, + final BiConsumer c) { + T r = (T)this.types.get(k); + if (r == null) { + final T t = s.get(); + r = (T)this.types.putIfAbsent(k, t); + if (r == null) { + c.accept(k, t); + return t; + } + } + return r; + } + +} diff --git a/lang/src/main/java/org/microbean/lang/NameElementComparator.java b/lang/src/main/java/org/microbean/lang/NameElementComparator.java new file mode 100644 index 00000000..d2ae0eb7 --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/NameElementComparator.java @@ -0,0 +1,41 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2022–2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.microbean.lang; + +import java.util.Comparator; + +import javax.lang.model.element.Element; +import javax.lang.model.element.QualifiedNameable; + +public final class NameElementComparator implements Comparator { + + public static final NameElementComparator INSTANCE = new NameElementComparator(); + + private NameElementComparator() { + super(); + } + + public final int compare(final Element e, final Element f) { + return + e == f ? 0 : + e == null ? 1 : + f == null ? -1 : + CharSequence.compare(name(e), name(f)); + } + + private static final CharSequence name(final Element e) { + return e instanceof QualifiedNameable qn ? qn.getQualifiedName() : e.getSimpleName(); + } + +} diff --git a/lang/src/main/java/org/microbean/lang/NameTypeMirrorComparator.java b/lang/src/main/java/org/microbean/lang/NameTypeMirrorComparator.java new file mode 100644 index 00000000..7c7d81ec --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/NameTypeMirrorComparator.java @@ -0,0 +1,133 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2022–2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.microbean.lang; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.StringJoiner; + +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.TypeParameterElement; + +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.IntersectionType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; +import javax.lang.model.type.WildcardType; + +// Totally ordering (I hope!) Comparator inconsistent with equals that uses names internally to compare types. The names +// used are deliberately undefined but incorporate fully qualified names where possible as well as type argument and +// bound information. +// +// This comparator isn't very useful on its own, although it does establish a total order. Typically you'll want to +// compose this as a last-ditch tiebreaker with ClassesFirstTypeMirrorComparator and +// SpecializationDepthTypeMirrorComparator. +public final class NameTypeMirrorComparator implements Comparator { + + public static final NameTypeMirrorComparator INSTANCE = new NameTypeMirrorComparator(); + + private NameTypeMirrorComparator() { + super(); + } + + @Override // Comparator + public final int compare(final TypeMirror t, final TypeMirror s) { + return + t == s ? 0 : + t == null ? 1 : + s == null ? -1 : + CharSequence.compare(this.name(t), this.name(s)); + } + + final CharSequence name(final TypeMirror t) { + return switch (t.getKind()) { + case ARRAY -> name(((ArrayType)t).getComponentType()) + "[]"; + case BOOLEAN -> "boolean"; + case BYTE -> "byte"; + case CHAR -> "char"; + case DECLARED -> { + final DeclaredType dt = (DeclaredType)t; + final TypeElement e = (TypeElement)dt.asElement(); + final List typeParameters = e.getTypeParameters(); + if (typeParameters.isEmpty()) { + yield e.getQualifiedName(); + } + final List typeArguments = dt.getTypeArguments(); + if (typeArguments.isEmpty()) { + yield e.getQualifiedName(); + } + final StringJoiner sj = new StringJoiner(", "); + for (final TypeMirror typeArgument : typeArguments) { + sj.add(name(typeArgument)); + } + yield e.getQualifiedName() + "<" + sj.toString() + ">"; + } + case DOUBLE -> "double"; + case FLOAT -> "float"; + case INT -> "int"; + case INTERSECTION -> { + final List bounds = ((IntersectionType)t).getBounds(); + if (bounds.size() <= 1) { + throw new IllegalArgumentException("t: " + t); + } + final List sortedBounds = new ArrayList<>(bounds); + // The bounds of an IntersectionType will always be DeclaredTypes. So all we have to do is put the non-interface + // ones first. + // + // TODO: technically we don't have to do this since this is just to get deterministic order based on names. + Collections.sort(sortedBounds, + new TestingTypeMirrorComparator(type -> + type.getKind() == TypeKind.DECLARED && + !((DeclaredType)type).asElement().getKind().isInterface()) + .thenComparing(this)); + final StringJoiner sj = new StringJoiner(" & "); + for (final TypeMirror bound : sortedBounds) { + sj.add(name(bound)); + } + yield sj.toString(); + } + case LONG -> "long"; + case SHORT -> "short"; + case TYPEVAR -> { + final TypeVariable tv = (TypeVariable)t; + final TypeMirror bound = tv.getUpperBound(); + yield + bound == null || bound.getKind() == TypeKind.NONE ? tv.asElement().getSimpleName() + " extends java.lang.Object" : + tv.asElement().getSimpleName() + " extends " + name(bound); + } + case VOID -> "void"; + case WILDCARD -> { + final WildcardType w = (WildcardType)t; + final TypeMirror extendsBound = w.getExtendsBound(); + final TypeMirror superBound = w.getSuperBound(); + if (superBound == null) { + if (extendsBound == null) { + yield "? extends java.lang.Object"; // we could have just said "?" but this handles the fact that they're interchangeable + } + yield "? extends " + name(extendsBound); + } else if (extendsBound == null) { + yield "? super " + name(superBound); + } + throw new IllegalArgumentException("t: " + t); + } + case ERROR -> throw new AssertionError("t.getKind() == TypeKind.ERROR; t: " + t); + default -> throw new IllegalArgumentException("t: " + t); + }; + } + +} diff --git a/lang/src/main/java/org/microbean/lang/SpecializationDepthElementComparator.java b/lang/src/main/java/org/microbean/lang/SpecializationDepthElementComparator.java new file mode 100644 index 00000000..2d3d2750 --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/SpecializationDepthElementComparator.java @@ -0,0 +1,38 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2022–2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.microbean.lang; + +import java.util.Comparator; +import java.util.Objects; + +import javax.lang.model.element.Element; + +public final class SpecializationDepthElementComparator implements Comparator { + + private final SpecializationDepthTypeMirrorComparator c; + + public SpecializationDepthElementComparator(final SpecializationDepthTypeMirrorComparator c) { + super(); + this.c = Objects.requireNonNull(c, "c"); + } + + public final int compare(final Element e, final Element f) { + return + e == f ? 0 : + e == null ? 1 : + f == null ? -1 : + this.c.compare(e.asType(), f.asType()); + } + +} diff --git a/lang/src/main/java/org/microbean/lang/SpecializationDepthTypeMirrorComparator.java b/lang/src/main/java/org/microbean/lang/SpecializationDepthTypeMirrorComparator.java new file mode 100644 index 00000000..847bb96e --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/SpecializationDepthTypeMirrorComparator.java @@ -0,0 +1,101 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.microbean.lang; + +import java.util.Comparator; +import java.util.Objects; + +import java.util.function.Function; + +import javax.lang.model.element.QualifiedNameable; + +import javax.lang.model.type.TypeMirror; + +import org.microbean.lang.type.DelegatingTypeMirror; + +public final class SpecializationDepthTypeMirrorComparator implements Comparator { + + private final TypeAndElementSource elementSource; + + private final Equality equality; + + private final Function> directSupertypes; + + public SpecializationDepthTypeMirrorComparator() { + this(Lang.typeAndElementSource(), Lang.sameTypeEquality(), Lang::directSupertypes); + } + + public SpecializationDepthTypeMirrorComparator(final Equality equality) { + this(Lang.typeAndElementSource(), equality, Lang::directSupertypes); + } + + public SpecializationDepthTypeMirrorComparator(final TypeAndElementSource elementSource, + final Equality equality) { + this(elementSource, equality, Lang::directSupertypes); + } + + public SpecializationDepthTypeMirrorComparator(final TypeAndElementSource elementSource, + final Equality equality, + final Function> directSupertypes) { + super(); + this.elementSource = Objects.requireNonNull(elementSource, "elementSource"); + this.equality = Objects.requireNonNull(equality, "equality"); + this.directSupertypes = Objects.requireNonNull(directSupertypes, "directSupertypes"); + } + + @Override // Comparator + public final int compare(final TypeMirror t, final TypeMirror s) { + return + t == s ? 0 : + t == null ? 1 : + s == null ? -1 : + // Note this comparison is "backwards" + Integer.signum(this.specializationDepth(DelegatingTypeMirror.of(s, this.elementSource, this.equality)) - + this.specializationDepth(DelegatingTypeMirror.of(t, this.elementSource, this.equality))); + } + + public final int specializationDepth(final TypeMirror t) { + return this.specializationDepth(DelegatingTypeMirror.of(t, this.elementSource, this.equality)); + } + + @SuppressWarnings("fallthrough") + private final int specializationDepth(final DelegatingTypeMirror t) { + // See + // https://github.com/openjdk/jdk/blob/2e340e855b760e381793107f2a4d74095bd40199/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L3570-L3615. + switch (t.getKind()) { + case DECLARED: + if (((QualifiedNameable)t.asElement()).getQualifiedName().contentEquals("java.lang.Object")) { + return 0; + } + // fall through + case ARRAY: + case INTERSECTION: + case TYPEVAR: + // My initial specialization depth is 0, although we know I will have at least one supertype (java.lang.Object) + // because we already handled java.lang.Object, which has no supertypes, above. + int sd = 0; + for (final TypeMirror s : this.directSupertypes.apply(t)) { + sd = Math.max(sd, this.specializationDepth(DelegatingTypeMirror.of(s, this.elementSource, this.equality))); + } + // My specialization depth is equal to the greatest one of my direct supertypes, plus one (representing me, a + // subtype). + return sd + 1; + case ERROR: + throw new AssertionError("t.getKind() == TypeKind.ERROR; t: " + t); + default: + throw new IllegalArgumentException("t: " + t + "; t.getKind(): " + t.getKind()); + } + } + +} diff --git a/lang/src/main/java/org/microbean/lang/TestingTypeMirrorComparator.java b/lang/src/main/java/org/microbean/lang/TestingTypeMirrorComparator.java new file mode 100644 index 00000000..c575709c --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/TestingTypeMirrorComparator.java @@ -0,0 +1,42 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2022–2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.microbean.lang; + +import java.util.Comparator; +import java.util.Objects; + +import java.util.function.Predicate; + +import javax.lang.model.type.TypeMirror; + +public class TestingTypeMirrorComparator implements Comparator { + + private final Predicate p; + + public TestingTypeMirrorComparator(final Predicate p) { + super(); + this.p = Objects.requireNonNull(p, "p"); + } + + @Override // Comparator + public final int compare(final TypeMirror t, final TypeMirror s) { + return + t == s ? 0 : + t == null ? 1 : + s == null ? -1 : + p.test(t) ? p.test(s) ? 0 : -1 : + p.test(s) ? 1 : 0; + } + +} diff --git a/lang/src/main/java/org/microbean/lang/TypeAndElementSource.java b/lang/src/main/java/org/microbean/lang/TypeAndElementSource.java new file mode 100644 index 00000000..aacbbfd2 --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/TypeAndElementSource.java @@ -0,0 +1,155 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.microbean.lang; + +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +import java.util.Objects; + +import javax.lang.model.element.TypeElement; + +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.NoType; +import javax.lang.model.type.PrimitiveType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; +import javax.lang.model.type.WildcardType; + +public interface TypeAndElementSource { + + public default ArrayType arrayType(final Class arrayClass) { + if (!arrayClass.isArray()) { + throw new IllegalArgumentException("arrayClass: " + arrayClass); + } + return arrayTypeOf(type(arrayClass.getComponentType())); + } + + public default ArrayType arrayType(final GenericArrayType g) { + return arrayTypeOf(type(g.getGenericComponentType())); + } + + public ArrayType arrayTypeOf(final TypeMirror componentType); + + public boolean assignable(final TypeMirror payload, final TypeMirror receiver); + + public TypeElement boxedClass(final PrimitiveType t); + + public default DeclaredType declaredType(final CharSequence canonicalName) { + return this.declaredType(this.typeElement(canonicalName)); + } + + public default DeclaredType declaredType(final CharSequence moduleName, final CharSequence canonicalName) { + return this.declaredType(this.typeElement(moduleName, canonicalName)); + } + + public default DeclaredType declaredType(final Class c) { + return this.declaredType(c.getEnclosingClass() == null ? null : this.declaredType(c.getEnclosingClass()), this.typeElement(c)); + } + + public DeclaredType declaredType(final DeclaredType enclosingType, + final TypeElement typeElement, + final TypeMirror... typeArguments); + + public default DeclaredType declaredType(final ParameterizedType pt) { + return + this.declaredType(pt.getOwnerType() == null ? null : this.declaredType(pt.getOwnerType()), + this.typeElement(pt.getRawType()), + typeArray(pt.getActualTypeArguments())); + } + + public default DeclaredType declaredType(final Type t) { + return switch (t) { + case Class c -> declaredType(c); + case ParameterizedType p -> declaredType(p); + default -> throw new IllegalArgumentException("t: " + t); + }; + } + + public DeclaredType declaredType(final TypeElement typeElement, final TypeMirror... typeArguments); + + public T erasure(final T t); + + public NoType noType(final TypeKind k); + + public default PrimitiveType primitiveType(final Class c) { + return primitiveType(TypeKind.valueOf(c.getName().toUpperCase())); + } + + public PrimitiveType primitiveType(final TypeKind k); + + public boolean sameType(final TypeMirror t, final TypeMirror s); + + public default TypeMirror type(final Type t) { + return switch (t) { + case Class c when c == void.class -> noType(TypeKind.VOID); + case Class c when c.isArray() -> arrayType(c); + case Class c when c.isPrimitive() -> primitiveType(c); + case Class c -> declaredType(c.getEnclosingClass() == null ? null : declaredType(typeElement(c.getEnclosingClass())), typeElement(c)); + case ParameterizedType pt -> declaredType(pt.getOwnerType() == null ? null : (DeclaredType)type(pt.getOwnerType()), typeElement((Class)pt.getRawType()), typeArray(pt.getActualTypeArguments())); + case GenericArrayType g -> arrayType(g); + case java.lang.reflect.TypeVariable tv -> typeVariable(tv); + case java.lang.reflect.WildcardType w -> wildcardType(w); + default -> throw new IllegalArgumentException("t: " + t); + }; + } + + public default TypeMirror[] typeArray(final Type[] ts) { + if (ts.length <= 0) { + return new TypeMirror[0]; + } + final TypeMirror[] rv = new TypeMirror[ts.length]; + for (int i = 0; i < ts.length; i++) { + rv[i] = type(ts[i]); + } + return rv; + } + + public default TypeElement typeElement(final CharSequence canonicalName) { + return this.typeElement(null, canonicalName); + } + + public TypeElement typeElement(final CharSequence moduleName, final CharSequence canonicalName); + + public default TypeElement typeElement(final Class c) { + final Module m = c.getModule(); + final TypeElement e = this.typeElement(m.isNamed() ? m.getName() : "", c.getCanonicalName()); + assert e != null : "null type element for module " + m.getName() + " and " + c.getCanonicalName(); + return e; + } + + public default TypeElement typeElement(final Type t) { + Objects.requireNonNull(t); + return switch (t) { + case Class c -> typeElement(c); + case ParameterizedType pt -> typeElement(pt.getRawType()); + default -> throw new IllegalArgumentException("t: " + t); + }; + } + + public TypeVariable typeVariable(final java.lang.reflect.TypeVariable t); + + public WildcardType wildcardType(TypeMirror extendsBound, TypeMirror superBound); + + public default WildcardType wildcardType(final java.lang.reflect.WildcardType t) { + final Type[] lowerBounds = t.getLowerBounds(); + final Type lowerBound = lowerBounds.length <= 0 ? null : lowerBounds[0]; + final Type upperBound = t.getUpperBounds()[0]; + return wildcardType(type(upperBound), type(lowerBound)); + } + +} diff --git a/lang/src/main/java/org/microbean/lang/element/AnnotationMirror.java b/lang/src/main/java/org/microbean/lang/element/AnnotationMirror.java new file mode 100644 index 00000000..79019ba9 --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/element/AnnotationMirror.java @@ -0,0 +1,133 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.element; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.ExecutableElement; + +import javax.lang.model.type.DeclaredType; + +import org.microbean.lang.Equality; + +public class AnnotationMirror implements javax.lang.model.element.AnnotationMirror { + + + /* + * Instance fields. + */ + + + private DeclaredType annotationType; + + private final Map elementValues; + + private final Map unmodifiableElementValues; + + + /* + * Constructors. + */ + + + public AnnotationMirror() { + super(); + this.elementValues = new HashMap<>(); + this.unmodifiableElementValues = Collections.unmodifiableMap(this.elementValues); + } + + + /* + * Instance methods. + */ + + + @Override // AnnotationMirror + public final DeclaredType getAnnotationType() { + return this.annotationType; + } + + public final void setAnnotationType(final DeclaredType annotationType) { + final DeclaredType old = this.getAnnotationType(); + if (old == null) { + if (annotationType != null) { + this.annotationType = validateAnnotationType(annotationType); + } + } else if (old != annotationType) { + throw new IllegalStateException("old: " + old + "; annotationType: " + annotationType); + } + } + + @Override // AnnotationMirror + public final Map getElementValues() { + return this.unmodifiableElementValues; + } + + public final void putElementValue(final ExecutableElement ee, final AnnotationValue av) { + this.elementValues.put(ee, av); + } + + public final void setElementValues(final Map evs) { + this.elementValues.putAll(evs); + } + + @Override // Object + public int hashCode() { + return Equality.hashCode(this, true); + } + + @Override + public boolean equals(final Object other) { + if (this == other) { + return true; + } else if (other instanceof AnnotationMirror her) { // instanceof is on purpose + return Equality.equals(this, her, true); + } else { + return false; + } + } + + + /* + * Static methods. + */ + + + private static final DeclaredType validateAnnotationType(final DeclaredType annotationType) { + switch (annotationType.getKind()) { + case DECLARED: + return annotationType; + default: + throw new IllegalArgumentException("annotationType: " + annotationType); + } + } + + + public static final AnnotationMirror of(final javax.lang.model.element.AnnotationMirror am) { + if (am instanceof AnnotationMirror mam) { + return mam; + } + final AnnotationMirror r = new AnnotationMirror(); + r.setAnnotationType(am.getAnnotationType()); + r.setElementValues(am.getElementValues()); + return r; + } + +} diff --git a/lang/src/main/java/org/microbean/lang/element/AnnotationValue.java b/lang/src/main/java/org/microbean/lang/element/AnnotationValue.java new file mode 100644 index 00000000..bf913f45 --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/element/AnnotationValue.java @@ -0,0 +1,92 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.element; + +import java.util.List; +import java.util.Objects; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValueVisitor; +import javax.lang.model.element.VariableElement; + +import javax.lang.model.type.TypeMirror; + +import org.microbean.lang.Equality; + +public final class AnnotationValue implements javax.lang.model.element.AnnotationValue { + + private final Object value; + + public AnnotationValue(final Object value) { + super(); + this.value = Objects.requireNonNull(value, "value"); + } + + @Override // AnnotationValue + @SuppressWarnings("unchecked") + public final R accept(final AnnotationValueVisitor v, final P p) { + return switch (this.getValue()) { + case String s -> v.visitString(s, p); + case Boolean b -> v.visitBoolean(b, p); + case Integer i -> v.visitInt(i, p); + case VariableElement e -> v.visitEnumConstant(e, p); + case javax.lang.model.element.AnnotationValue a -> v.visit(a, p); + case AnnotationMirror a -> v.visitAnnotation(a, p); + case List list -> v.visitArray((List)list, p); + case TypeMirror t -> v.visitType(t, p); + case Byte b -> v.visitByte(b, p); + case Character c -> v.visitChar(c, p); + case Double d -> v.visitDouble(d, p); + case Float f -> v.visitFloat(f, p); + case Long l -> v.visitLong(l, p); + case Short s -> v.visitShort(s, p); + default -> v.visitUnknown(this, p); + }; + } + + @Override // AnnotationValue + public final Object getValue() { + return this.value; + } + + @Override // Object + public final int hashCode() { + return Equality.hashCode(this, true); + } + + @Override // Object + public final boolean equals(final Object other) { + if (this == other) { + return true; + } else if (other instanceof javax.lang.model.element.AnnotationValue her) { // instanceof is on purpose + return Equality.equals(this, her, true); + } else { + return false; + } + } + + + /* + * Static methods. + */ + + + public static final AnnotationValue of(final Object value) { + return new AnnotationValue(value); + } + +} diff --git a/lang/src/main/java/org/microbean/lang/element/DelegatingElement.java b/lang/src/main/java/org/microbean/lang/element/DelegatingElement.java new file mode 100644 index 00000000..f8835b0a --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/element/DelegatingElement.java @@ -0,0 +1,419 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2022–2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.microbean.lang.element; + +import java.lang.annotation.Annotation; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ElementVisitor; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.ModuleElement; +import javax.lang.model.element.ModuleElement.Directive; +import javax.lang.model.element.Name; +import javax.lang.model.element.NestingKind; +import javax.lang.model.element.PackageElement; +import javax.lang.model.element.Parameterizable; +import javax.lang.model.element.QualifiedNameable; +import javax.lang.model.element.RecordComponentElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.TypeParameterElement; +import javax.lang.model.element.VariableElement; + +import javax.lang.model.type.TypeMirror; + +import org.microbean.lang.CompletionLock; +import org.microbean.lang.TypeAndElementSource; +import org.microbean.lang.Equality; + +import org.microbean.lang.type.DelegatingTypeMirror; +import org.microbean.lang.type.NoType; + +public final class DelegatingElement + implements ExecutableElement, + ModuleElement, + PackageElement, + Parameterizable, + RecordComponentElement, + TypeElement, + TypeParameterElement, + VariableElement { + + + /* + * Instance fields. + */ + + + private final Element delegate; + + private final TypeAndElementSource elementSource; + + private final Equality ehc; + + + /* + * Constructors. + */ + + + private DelegatingElement(final Element delegate, + final TypeAndElementSource elementSource, + final Equality ehc) { + super(); + this.delegate = unwrap(Objects.requireNonNull(delegate, "delegate")); + this.elementSource = Objects.requireNonNull(elementSource, "elementSource"); + this.ehc = ehc == null ? new Equality(true) : ehc; + } + + + /* + * Instance methods. + */ + + + @Override // Element + public final R accept(final ElementVisitor v, final P p) { + return switch (this.getKind()) { + case ANNOTATION_TYPE, CLASS, ENUM, INTERFACE, RECORD -> v.visitType(this, p); + case TYPE_PARAMETER -> v.visitTypeParameter(this, p); + case BINDING_VARIABLE, ENUM_CONSTANT, EXCEPTION_PARAMETER, FIELD, LOCAL_VARIABLE, PARAMETER, RESOURCE_VARIABLE -> v.visitVariable(this, p); + case RECORD_COMPONENT -> v.visitRecordComponent(this, p); + case CONSTRUCTOR, INSTANCE_INIT, METHOD, STATIC_INIT -> v.visitExecutable(this, p); + case PACKAGE -> v.visitPackage(this, p); + case MODULE -> v.visitModule(this, p); + case OTHER -> v.visitUnknown(this, p); + }; + } + + @Override // Element + public final TypeMirror asType() { + return DelegatingTypeMirror.of(this.delegate.asType(), this.elementSource, this.ehc); + } + + public final Element delegate() { + return this.delegate; + } + + @Override // RecordComponentElement + public final ExecutableElement getAccessor() { + return switch (this.getKind()) { + case RECORD_COMPONENT -> this.wrap(((RecordComponentElement)this.delegate).getAccessor()); + default -> null; + }; + } + + @Override // Element + public final A getAnnotation(final Class annotationType) { + return this.delegate.getAnnotation(annotationType); + } + + @Override // Element + public final List getAnnotationMirrors() { + CompletionLock.acquire(); + try { + // TODO: delegating annotation mirror? + return this.delegate.getAnnotationMirrors(); + } finally { + CompletionLock.release(); + } + } + + @Override // Element + public final A[] getAnnotationsByType(final Class annotationType) { + return this.delegate.getAnnotationsByType(annotationType); + } + + @Override // TypeParameterElement + public final List getBounds() { + return switch (this.getKind()) { + case TYPE_PARAMETER -> + DelegatingTypeMirror.of(((TypeParameterElement)this.delegate).getBounds(), this.elementSource, this.ehc); + default -> List.of(); + }; + } + + @Override // VariableElement + public final Object getConstantValue() { + return switch (this.getKind()) { + case BINDING_VARIABLE, ENUM_CONSTANT, EXCEPTION_PARAMETER, FIELD, LOCAL_VARIABLE, PARAMETER, RESOURCE_VARIABLE -> + ((VariableElement)this.delegate).getConstantValue(); + default -> null; + }; + } + + @Override // ExecutableElement + public final AnnotationValue getDefaultValue() { + return switch (this.getKind()) { + case ANNOTATION_TYPE -> + // TODO: delegating annotation value? could be a type mirror after all + ((ExecutableElement)this.delegate).getDefaultValue(); + default -> null; + }; + } + + @Override // ModuleElement + public final List getDirectives() { + return switch (this.getKind()) { + case MODULE -> ((ModuleElement)this.delegate).getDirectives(); + default -> List.of(); + }; + } + + @Override // Element + public final List getEnclosedElements() { + final List ee; + CompletionLock.acquire(); // CRITICAL! + try { + ee = this.delegate.getEnclosedElements(); + } finally { + CompletionLock.release(); + } + return this.wrap(ee); + } + + @Override // Element + public final Element getEnclosingElement() { + return this.wrap(this.delegate.getEnclosingElement()); + } + + @Override // TypeParameterElement + public final Element getGenericElement() { + return switch (this.getKind()) { + case TYPE_PARAMETER -> this.wrap(((TypeParameterElement)this.delegate).getGenericElement()); + default -> null; // illegal state + }; + } + + @Override // TypeElement + public final List getInterfaces() { + return switch (this.getKind()) { + case ANNOTATION_TYPE, CLASS, ENUM, INTERFACE, RECORD -> + DelegatingTypeMirror.of(((TypeElement)this.delegate).getInterfaces(), this.elementSource, this.ehc); + default -> List.of(); + }; + } + + @Override // Element + public final ElementKind getKind() { + CompletionLock.acquire(); // CRITICAL! + try { + return this.delegate.getKind(); + } finally { + CompletionLock.release(); + } + } + + @Override // Element + public final Set getModifiers() { + CompletionLock.acquire(); // CRITICAL; + try { + return this.delegate.getModifiers(); + } finally { + CompletionLock.release(); + } + } + + @Override // TypeElement + public final NestingKind getNestingKind() { + return switch (this.getKind()) { + case ANNOTATION_TYPE, CLASS, ENUM, INTERFACE, RECORD -> ((TypeElement)this.delegate).getNestingKind(); + default -> null; + }; + } + + @Override // ExecutableElement + public final List getParameters() { + return switch (this.getKind()) { + case CONSTRUCTOR, METHOD -> this.wrap(((ExecutableElement)this.delegate).getParameters()); + default -> List.of(); + }; + } + + @Override // ModuleElement, PackageElement, TypeElement + public final Name getQualifiedName() { + return switch (this.getKind()) { + // case ANNOTATION_TYPE, CLASS, ENUM, INTERFACE, MODULE, PACKAGE, RECORD -> ((QualifiedNameable)this.delegate).getQualifiedName(); + case ANNOTATION_TYPE, CLASS, ENUM, INTERFACE, MODULE, PACKAGE, RECORD -> + org.microbean.lang.element.Name.of(((QualifiedNameable)this.delegate).getQualifiedName()); + default -> org.microbean.lang.element.Name.of(); + }; + } + + @Override // ExecutableElement + public final TypeMirror getReceiverType() { + return + this.getKind().isExecutable() ? + DelegatingTypeMirror.of(this.asType(), this.elementSource, this.ehc).getReceiverType() : + null; + } + + @Override // TypeElement + public final List getRecordComponents() { + return switch (this.getKind()) { + case RECORD -> ((TypeElement)this.delegate).getRecordComponents(); + default -> List.of(); + }; + } + + @Override // ExecutableElement + public final TypeMirror getReturnType() { + return DelegatingTypeMirror.of(this.asType(), this.elementSource, this.ehc).getReturnType(); + } + + @Override // Element + public final Name getSimpleName() { + // return this.delegate.getSimpleName(); + return org.microbean.lang.element.Name.of(this.delegate.getSimpleName()); + } + + @Override // TypeElement + public final TypeMirror getSuperclass() { + return switch (this.getKind()) { + case ANNOTATION_TYPE, CLASS, ENUM, INTERFACE, RECORD -> + DelegatingTypeMirror.of(((TypeElement)this.delegate).getSuperclass(), this.elementSource, this.ehc); + default -> NoType.NONE; + }; + } + + @Override // ExecutableElement + public final List getThrownTypes() { + return + this.getKind().isExecutable() ? + DelegatingTypeMirror.of(((ExecutableElement)this.delegate).getThrownTypes(), this.elementSource, this.ehc) : + List.of(); + } + + @Override // ExecutableElement + public final List getTypeParameters() { + return switch (this.getKind()) { + case CLASS, CONSTRUCTOR, ENUM, INTERFACE, RECORD, METHOD -> this.wrap(((Parameterizable)this.delegate).getTypeParameters()); + default -> List.of(); + }; + } + + @Override // ExecutableElement + public final boolean isDefault() { + return switch (this.getKind()) { + case METHOD -> ((ExecutableElement)this.delegate).isDefault(); + default -> false; + }; + } + + @Override // ModuleElement + public final boolean isOpen() { + return switch (this.getKind()) { + case MODULE -> ((ModuleElement)this.delegate).isOpen(); + default -> false; + }; + } + + @Override // ModuleElement, PackageElement + public final boolean isUnnamed() { + return switch (this.getKind()) { + case MODULE -> ((ModuleElement)this.delegate).isUnnamed(); + case PACKAGE -> ((PackageElement)this.delegate).isUnnamed(); + default -> false; + }; + } + + @Override // ExecutableElement + public final boolean isVarArgs() { + return switch (this.getKind()) { + case CONSTRUCTOR, METHOD -> ((ExecutableElement)this.delegate).isVarArgs(); + default -> false; + }; + } + + @Override // Element + public final int hashCode() { + return this.ehc.hashCode(this); + } + + @Override // Element + public final boolean equals(final Object other) { + if (this == other) { + return true; + } else if (other instanceof Element e) { // instanceof on purpose + return this.ehc.equals(this, e); + } else { + return false; + } + } + + @Override // Element + public final String toString() { + return this.delegate.toString(); + } + + private final DelegatingElement wrap(final Element e) { + return of(e, this.elementSource, this.ehc); + } + + private final List wrap(final Collection es) { + return of(es, this.elementSource, this.ehc); + } + + + /* + * Static methods. + */ + + + public static final List of(final Collection es, final TypeAndElementSource elementSource) { + return of(es, elementSource, null); + } + + public static final List of(final Collection es, final TypeAndElementSource elementSource, final Equality ehc) { + final List newEs = new ArrayList<>(es.size()); + for (final Element e : es) { + newEs.add(of(e, elementSource, ehc)); + } + return Collections.unmodifiableList(newEs); + } + + public static final DelegatingElement of(final Element e, final TypeAndElementSource elementSource) { + return of(e, elementSource, null); + } + + public static final DelegatingElement of(final Element e, + final TypeAndElementSource elementSource, + final Equality ehc) { + return + e == null ? null : + e instanceof DelegatingElement d ? d : + new DelegatingElement(e, elementSource, ehc); + } + + @SuppressWarnings("unchecked") + public static final E unwrap(E e) { + while (e instanceof DelegatingElement de) { + e = (E)de.delegate(); + } + return e; + } + + private static final void doNothing() {} + +} diff --git a/lang/src/main/java/org/microbean/lang/element/Element.java b/lang/src/main/java/org/microbean/lang/element/Element.java new file mode 100644 index 00000000..ca9215fd --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/element/Element.java @@ -0,0 +1,276 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2022–2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.element; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ElementVisitor; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.Name; +import javax.lang.model.element.QualifiedNameable; + +import javax.lang.model.type.TypeMirror; + +import org.microbean.lang.AnnotatedConstruct; + +import org.microbean.lang.type.NoType; + +// NOT thread safe +public abstract sealed class Element + extends AnnotatedConstruct + implements javax.lang.model.element.Element, Encloseable, Encloser + permits ModuleElement, + PackageElement, + Parameterizable, + RecordComponentElement, + TypeParameterElement, + VariableElement { + + private final List enclosedElements; + + // Treat as effectively final, please. + private List unmodifiableEnclosedElements; + + private javax.lang.model.element.Element enclosingElement; + + private Runnable enclosedElementsGenerator; + + private final ElementKind kind; + + private final Set modifiers; + + private final Set unmodifiableModifiers; + + private Name simpleName; + + private TypeMirror type; + + protected Element(final ElementKind kind) { + super(); + this.kind = this.validateKind(kind); + this.modifiers = new LinkedHashSet<>(); + this.unmodifiableModifiers = Collections.unmodifiableSet(this.modifiers); + this.enclosedElements = new ArrayList<>(); + } + + @Override // Element + public R accept(final ElementVisitor v, final P p) { + return switch (this.getKind()) { + case ANNOTATION_TYPE, CLASS, ENUM, INTERFACE, RECORD -> v.visitType((javax.lang.model.element.TypeElement)this, p); + case TYPE_PARAMETER -> v.visitTypeParameter((javax.lang.model.element.TypeParameterElement)this, p); + case BINDING_VARIABLE, ENUM_CONSTANT, EXCEPTION_PARAMETER, FIELD, LOCAL_VARIABLE, PARAMETER, RESOURCE_VARIABLE -> v.visitVariable((javax.lang.model.element.VariableElement)this, p); + case RECORD_COMPONENT -> v.visitRecordComponent((javax.lang.model.element.RecordComponentElement)this, p); + case CONSTRUCTOR, INSTANCE_INIT, METHOD, STATIC_INIT -> v.visitExecutable((javax.lang.model.element.ExecutableElement)this, p); + case PACKAGE -> v.visitPackage((javax.lang.model.element.PackageElement)this, p); + case MODULE -> v.visitModule((javax.lang.model.element.ModuleElement)this, p); + case OTHER -> v.visitUnknown(this, p); + }; + } + + @Override // Element + public final TypeMirror asType() { + return this.type; + } + + public final void setType(final TypeMirror type) { + final TypeMirror old = this.asType(); + if (old == null) { + if (type != null) { + this.type = this.validateType(type); + } + } else if (old != type) { + throw new IllegalStateException("Type already set; element: " + this + "; old type: " + old + "; new type: " + type + "; old.equals(new)? " + old.equals(type)); + } + } + + protected TypeMirror validateType(final TypeMirror type) { + if (type == null) { + return NoType.NONE; + } + type.getKind(); // null check + return type; + } + + @Override // Element + public final List getEnclosedElements() { + if (this.unmodifiableEnclosedElements == null) { + this.unmodifiableEnclosedElements = Collections.unmodifiableList(this.enclosedElements); + if (this.enclosedElementsGenerator != null) { + final List existing = List.copyOf(this.unmodifiableEnclosedElements); + this.enclosedElementsGenerator.run(); + this.enclosedElements.removeIf(existing::contains); + this.enclosedElements.addAll(existing); + } + } + return this.unmodifiableEnclosedElements; + } + + public final void setEnclosedElementsGenerator(final Runnable f) { + if (this.enclosedElementsGenerator == null) { + this.enclosedElementsGenerator = Objects.requireNonNull(f, "f"); + } else if (this.enclosedElementsGenerator != f) { + throw new IllegalStateException(); + } + } + + // Deliberately not final to permit subclasses to override to throw UnsupportedOperationException. + @Override // Encloser + public void addEnclosedElement(final E e) { + this.validateEnclosedElement(e).setEnclosingElement(this); + this.enclosedElements.add(e); + } + + public final void addEnclosedElements(final Iterable es) { + for (final E e : es) { + this.addEnclosedElement(e); + } + } + + protected E validateEnclosedElement(final E e) { + if (!canEnclose(this.getKind(), e.getKind())) { + throw new IllegalArgumentException(this.getKind() + " cannot enclose " + e.getKind()); + } + return e; + } + + @Override // Element + public final ElementKind getKind() { + return this.kind; + } + + protected ElementKind validateKind(final ElementKind kind) { + return Objects.requireNonNull(kind, "kind"); + } + + @Override // Element + public final Set getModifiers() { + return this.unmodifiableModifiers; + } + + public final boolean addModifier(final Modifier modifier) { + return this.modifiers.add(this.validateModifier(modifier)); + } + + public final void addModifiers(final Iterable modifiers) { + for (final Modifier modifier : modifiers) { + this.addModifier(modifier); + } + } + + protected Modifier validateModifier(final Modifier modifier) { + return Objects.requireNonNull(modifier, "modifier"); + } + + @Override // Element + public final Name getSimpleName() { + return this.simpleName; + } + + public final void setSimpleName(final Name simpleName) { + final Name old = this.getSimpleName(); + if (old == null) { + if (simpleName != null) { + this.simpleName = this.validateSimpleName(simpleName); + } + } else if (old != simpleName) { + throw new IllegalStateException(); + } + } + + public final void setSimpleName(final String simpleName) { + this.setSimpleName(org.microbean.lang.element.Name.of(simpleName)); + } + + protected Name validateSimpleName(final Name n) { + final int length = n.length(); + switch (length) { + case 0: + // Anonymous class. Should we even be defining an element here? + break; + default: + if (n.charAt(0) == '.' || n.charAt(n.length() - 1) == '.') { + throw new IllegalArgumentException("n: " + n); + } + break; + } + return n; + } + + public boolean isUnnamed() { + return this.getSimpleName().length() <= 0; + } + + // Deliberately not final; ModuleElement and TypeParameterElement need to override this. + @Override // Element + public javax.lang.model.element.Element getEnclosingElement() { + return this.enclosingElement; + } + + // Deliberately not final; ModuleElement and TypeParameterElement need to override this. + @Override // Encloseable + public void setEnclosingElement(final javax.lang.model.element.Element enclosingElement) { + final Object old = this.getEnclosingElement(); + if (old == null) { + if (enclosingElement != null) { + if (enclosingElement != this) { + if (canEnclose(enclosingElement.getKind(), this.getKind())) { + this.enclosingElement = enclosingElement; + if (enclosingElement instanceof Encloser e) { + e.addEnclosedElement(this); + } + } else { + throw new IllegalArgumentException(enclosingElement.getKind() + " cannot enclose " + this.getKind()); + } + } else { + throw new IllegalArgumentException("enclosingElement: " + enclosingElement); + } + } + } else if (old != enclosingElement) { + throw new IllegalStateException("old != enclosingElement: " + old + " != " + enclosingElement); + } + } + + @Override + public String toString() { + final CharSequence n = this instanceof QualifiedNameable q ? q.getQualifiedName() : this.getSimpleName(); + return this.isUnnamed() ? "" : n == null ? "" : n.length() <= 0 ? "" : n.toString(); + } + + + /* + * Static methods. + */ + + + public static final boolean canEnclose(final ElementKind k1, final ElementKind k2) { + return switch (k1) { + case MODULE -> k2 == ElementKind.PACKAGE; + case PACKAGE -> switch (k2) { case ANNOTATION_TYPE, CLASS, ENUM, INTERFACE, RECORD -> true; default -> false; }; + case ANNOTATION_TYPE, CLASS, INTERFACE -> switch (k2) { case ANNOTATION_TYPE, CLASS, CONSTRUCTOR, ENUM, FIELD, INSTANCE_INIT, INTERFACE, METHOD, RECORD, STATIC_INIT -> true; default -> false; }; + case ENUM -> switch (k2) { case ANNOTATION_TYPE, CLASS, CONSTRUCTOR, ENUM, ENUM_CONSTANT, FIELD, INSTANCE_INIT, INTERFACE, METHOD, RECORD, STATIC_INIT -> true; default -> false; }; + case RECORD -> switch (k2) { case ANNOTATION_TYPE, CLASS, CONSTRUCTOR, ENUM, FIELD, INSTANCE_INIT, INTERFACE, METHOD, RECORD, RECORD_COMPONENT, STATIC_INIT -> true; default -> false; }; + default -> false; + }; + } + +} diff --git a/lang/src/main/java/org/microbean/lang/element/Encloseable.java b/lang/src/main/java/org/microbean/lang/element/Encloseable.java new file mode 100644 index 00000000..4aefc8b0 --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/element/Encloseable.java @@ -0,0 +1,40 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2022–2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.element; + +import javax.lang.model.element.Element; + +/** + * Something that can be enclosed by an {@link Element}. + * + * @author Laird Nelson + * + * @see #setEnclosingElement(Element) + * + * @see Element#getEnclosedElements() + */ +public interface Encloseable { + + /** + * Sets the {@link Element} that encloses this {@link Encloseable} to the supplied {@link Element}, which may be + * {@code null}. + * + * @param enclosingElement the {@link Element} that will enclose this {@link Encloseable}; may be {@code null} + */ + public void setEnclosingElement(final Element enclosingElement); + +} diff --git a/lang/src/main/java/org/microbean/lang/element/Encloser.java b/lang/src/main/java/org/microbean/lang/element/Encloser.java new file mode 100644 index 00000000..f8541e32 --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/element/Encloser.java @@ -0,0 +1,32 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.element; + +import javax.lang.model.element.Element; + +/** + * Something that can enclose {@link Element}s that are also {@link Encloseable}. + * + * @author Laird Nelson + * + * @see #addEnclosedElement(Element) + */ +public interface Encloser { + + public void addEnclosedElement(final E e); + +} diff --git a/lang/src/main/java/org/microbean/lang/element/ExecutableElement.java b/lang/src/main/java/org/microbean/lang/element/ExecutableElement.java new file mode 100644 index 00000000..afbcdf05 --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/element/ExecutableElement.java @@ -0,0 +1,266 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2022–2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.element; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; + +import javax.lang.model.element.AnnotationValue; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ElementVisitor; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.Name; +import javax.lang.model.element.TypeParameterElement; +import javax.lang.model.element.VariableElement; + +import javax.lang.model.type.ExecutableType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; + +public final class ExecutableElement extends Parameterizable implements javax.lang.model.element.ExecutableElement { + + private final List parameters; + + private final List unmodifiableParameters; + + private Boolean isDefault; + + private Boolean varArgs; + + private AnnotationValue defaultValue; + + public ExecutableElement(final ElementKind kind) { + super(kind); + this.parameters = new ArrayList<>(7); + this.unmodifiableParameters = Collections.unmodifiableList(this.parameters); + if (kind == ElementKind.CONSTRUCTOR) { + this.isDefault = false; + this.setSimpleName(""); + } + } + + public ExecutableElement(final boolean varArgs, + final boolean isDefault, + final AnnotationValue defaultValue) { + this(ElementKind.METHOD); + this.setDefault(isDefault); + this.setDefaultValue(defaultValue); + } + + public ExecutableElement(final ElementKind kind, + final boolean varArgs, + final boolean isDefault, + final AnnotationValue defaultValue) { + this(kind); + this.setVarArgs(varArgs); + this.setDefault(isDefault); + this.setDefaultValue(defaultValue); + } + + @Override // Element + public final R accept(final ElementVisitor v, final P p) { + return v.visitExecutable(this, p); + } + + @Override // Element + protected final TypeMirror validateType(final TypeMirror type) { + if (type.getKind() == TypeKind.EXECUTABLE && type instanceof ExecutableType) { + return type; + } + throw new IllegalArgumentException("type: " + type); + } + + @Override // ExecutableElement + public final boolean isDefault() { + final Boolean isDefault = this.isDefault; + return isDefault == null ? false : isDefault; + } + + public final void setDefault(final boolean isDefault) { + final Boolean old = this.isDefault; + if (old == null) { + this.isDefault = Boolean.valueOf(isDefault); + } else if (!old.booleanValue() == isDefault) { + throw new IllegalStateException(); + } + } + + @Override // ExecutableElement + public final boolean isVarArgs() { + final Boolean varArgs = this.varArgs; + return varArgs == null ? false : varArgs; + } + + public final void setVarArgs(final boolean varArgs) { + final Boolean old = this.varArgs; + if (old == null) { + this.varArgs = Boolean.valueOf(varArgs); + } else if (!old.booleanValue() == varArgs) { + throw new IllegalStateException(); + } + } + + @Override // ExecutableElement + public final AnnotationValue getDefaultValue() { + return this.defaultValue; + } + + public final void setDefaultValue(final AnnotationValue defaultValue) { + final Object old = this.getDefaultValue(); + if (old == null) { + if (defaultValue != null) { + this.defaultValue = validateDefaultValue(defaultValue); + } + } else if (old != defaultValue) { + throw new IllegalStateException(); + } + } + + @Override // ExecutableElement + public final List getParameters() { + return this.unmodifiableParameters; + } + + public void addParameter(final VariableElement p) { + this.parameters.add(this.validateParameter(p)); + } + + public void addParameters(final Iterable ps) { + for (final VariableElement p : ps) { + this.addParameter(p); + } + } + + private final VariableElement validateParameter(final VariableElement p) { + return Objects.requireNonNull(p, "p"); + } + + @Override // ExecutableElement + public final List getThrownTypes() { + return ((ExecutableType)this.asType()).getThrownTypes(); + } + + @Override // ExecutableElement + public final TypeMirror getReceiverType() { + return ((ExecutableType)this.asType()).getReceiverType(); + } + + @Override // ExecutableElement + public final TypeMirror getReturnType() { + return ((ExecutableType)this.asType()).getReturnType(); + } + + @Override + public final String toString() { + final StringBuilder sb = new StringBuilder(); + final List typeParameterElements = this.getTypeParameters(); + if (!typeParameterElements.isEmpty()) { + sb.append('<'); + final Iterator i = typeParameterElements.iterator(); + while (i.hasNext()) { + sb.append(i.next()); + if (i.hasNext()) { + sb.append(", "); + } + } + sb.append('>'); + } + final Name name = this.getSimpleName(); + if (name.contentEquals("")) { + final Element enclosingElement = this.getEnclosingElement(); + sb.append(enclosingElement == null ? "" : enclosingElement.getSimpleName()); + } else { + sb.append(name); + } + final List parameters = this.getParameters(); + if (!parameters.isEmpty()) { + sb.append('('); + final Iterator i = parameters.iterator(); + while (i.hasNext()) { + sb.append(i.next().asType()); + if (i.hasNext()) { + sb.append(','); + } + } + sb.append(')'); + } + return sb.toString(); + } + + @Override // Element + protected final Modifier validateModifier(final Modifier m) { + if (Objects.requireNonNull(m, "m") == Modifier.NON_SEALED || + m == Modifier.SEALED || + m == Modifier.STRICTFP || + m == Modifier.TRANSIENT || + m == Modifier.VOLATILE) { + throw new IllegalArgumentException("m: " + m); + } + return m; + } + + @Override // Element + protected final ElementKind validateKind(final ElementKind kind) { + switch (kind) { + case CONSTRUCTOR: + case INSTANCE_INIT: + case METHOD: + case STATIC_INIT: + return kind; + default: + throw new IllegalArgumentException("kind: " + kind); + } + } + + @Override // Element + protected final Name validateSimpleName(final Name name) { + switch (this.getKind()) { + case CONSTRUCTOR: + if (!name.contentEquals("")) { + throw new IllegalArgumentException("name: " + name); + } + break; + case STATIC_INIT: + if (!name.contentEquals("")) { + throw new IllegalArgumentException("name: " + name); + } + break; + case INSTANCE_INIT: + if (!name.isEmpty()) { + throw new IllegalArgumentException("name: " + name); + } + break; + case METHOD: + if (name.isEmpty() || name.contentEquals("") || name.contentEquals("")) { + throw new IllegalArgumentException("name: " + name); + } + break; + default: + throw new AssertionError(); + } + return name; + } + + private static final AnnotationValue validateDefaultValue(final AnnotationValue defaultValue) { + return defaultValue; + } + +} diff --git a/lang/src/main/java/org/microbean/lang/element/ModuleElement.java b/lang/src/main/java/org/microbean/lang/element/ModuleElement.java new file mode 100644 index 00000000..0a8b0e5b --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/element/ModuleElement.java @@ -0,0 +1,317 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2022–2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.element; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ElementVisitor; +import javax.lang.model.element.PackageElement; +import javax.lang.model.element.TypeElement; + +public final class ModuleElement extends org.microbean.lang.element.Element implements javax.lang.model.element.ModuleElement { + + + /* + * Instance fields. + */ + + + private final boolean open; + + private final List directives; + + private final List unmodifiableDirectives; + + + /* + * Constructors. + */ + + + public ModuleElement() { + this(false); + } + + public ModuleElement(final boolean open) { + super(ElementKind.MODULE); + this.open = open; + this.directives = new ArrayList<>(); + this.unmodifiableDirectives = Collections.unmodifiableList(this.directives); + } + + + /* + * Instance methods. + */ + + + @Override // Element + public final R accept(final ElementVisitor v, final P p) { + return v.visitModule(this, p); + } + + @Override // ModuleElement + public final boolean isOpen() { + return this.open; + } + + @Override // ModuleElement + public final List getDirectives() { + return this.unmodifiableDirectives; + } + + public final void addDirective(final Directive directive) { + this.directives.add(validateDirective(directive)); + } + + /** + * Returns {@code null} when invoked. + * + * @return {@code null} when invoked + * + * @see #setEnclosingElement(Element) + */ + @Override // ModuleElement + public final Element getEnclosingElement() { + return null; + } + + /** + * Throws an {@link UnsupportedOperationException} when invoked. + * + * @param e the enclosing {@link Element}; ignored + * + * @exception UnsupportedOperationException when invoked + * + * @see #getEnclosingElement() + */ + @Override // ModuleElement + public final void setEnclosingElement(final Element e) { + throw new UnsupportedOperationException(); + } + + @Override // QualifiedNameable + public final javax.lang.model.element.Name getQualifiedName() { + return this.getSimpleName(); + } + + + /* + * Static methods. + */ + + + private static final Directive validateDirective(final Directive directive) { + return Objects.requireNonNull(directive, "directive"); + } + + + /* + * Inner and nested classes. + */ + + + public static sealed class Directive + implements javax.lang.model.element.ModuleElement.Directive + permits ExportsDirective, OpensDirective, ProvidesDirective, RequiresDirective, UsesDirective { + + private final DirectiveKind kind; + + protected Directive(final DirectiveKind kind) { + super(); + this.kind = Objects.requireNonNull(kind, "kind"); + } + + @Override // Directive + public R accept(final DirectiveVisitor v, final P p) { + switch (this.getKind()) { + case EXPORTS: + return v.visitExports((ExportsDirective)this, p); + case OPENS: + return v.visitOpens((OpensDirective)this, p); + case PROVIDES: + return v.visitProvides((ProvidesDirective)this, p); + case REQUIRES: + return v.visitRequires((RequiresDirective)this, p); + case USES: + return v.visitUses((UsesDirective)this, p); + default: + return v.visitUnknown(this, p); + } + } + + @Override // Directive + public final DirectiveKind getKind() { + return this.kind; + } + + } + + public static final class ExportsDirective extends Directive implements javax.lang.model.element.ModuleElement.ExportsDirective { + + private final PackageElement pkg; + + private final List targetModules; + + public ExportsDirective(final PackageElement pkg, final List targetModules) { + super(DirectiveKind.EXPORTS); + this.pkg = Objects.requireNonNull(pkg, "pkg"); + this.targetModules = targetModules == null || targetModules.isEmpty() ? List.of() : List.copyOf(targetModules); + } + + @Override // Directive + public final R accept(final DirectiveVisitor v, final P p) { + return v.visitExports(this, p); + } + + @Override // ExportsDirective + public final PackageElement getPackage() { + return this.pkg; + } + + @Override // ExportsDirective + public final List getTargetModules() { + return this.targetModules; + } + + } + + public static final class OpensDirective extends Directive implements javax.lang.model.element.ModuleElement.OpensDirective { + + private final PackageElement pkg; + + private final List targetModules; + + public OpensDirective(final PackageElement pkg, final List targetModules) { + super(DirectiveKind.OPENS); + this.pkg = Objects.requireNonNull(pkg, "pkg"); + this.targetModules = targetModules == null || targetModules.isEmpty() ? List.of() : List.copyOf(targetModules); + } + + @Override // Directive + public final R accept(final DirectiveVisitor v, final P p) { + return v.visitOpens(this, p); + } + + @Override // OpensDirective + public final PackageElement getPackage() { + return this.pkg; + } + + @Override // OpensDirective + public final List getTargetModules() { + return this.targetModules; + } + + } + + public static final class ProvidesDirective extends Directive implements javax.lang.model.element.ModuleElement.ProvidesDirective { + + private final TypeElement service; + + private final List implementations; + + public ProvidesDirective(final TypeElement service, final List implementations) { + super(DirectiveKind.PROVIDES); + this.service = Objects.requireNonNull(service, "service"); + this.implementations = implementations == null || implementations.isEmpty() ? List.of() : List.copyOf(implementations); + } + + @Override // Directive + public final R accept(final DirectiveVisitor v, final P p) { + return v.visitProvides(this, p); + } + + @Override // ProvidesDirective + public final TypeElement getService() { + return this.service; + } + + @Override // ProvidesDirective + public final List getImplementations() { + return this.implementations; + } + + } + + public static final class RequiresDirective extends Directive implements javax.lang.model.element.ModuleElement.RequiresDirective { + + private final javax.lang.model.element.ModuleElement dependency; + + private final boolean isStatic; + + private final boolean transitive; + + public RequiresDirective(final javax.lang.model.element.ModuleElement dependency, + final boolean isStatic, + final boolean transitive) { + super(DirectiveKind.REQUIRES); + this.dependency = Objects.requireNonNull(dependency, "dependency"); + this.isStatic = isStatic; + this.transitive = transitive; + } + + @Override // Directive + public final R accept(final DirectiveVisitor v, final P p) { + return v.visitRequires(this, p); + } + + @Override // RequiresDirective + public final javax.lang.model.element.ModuleElement getDependency() { + return this.dependency; + } + + @Override // RequiresDirective + public final boolean isStatic() { + return this.isStatic; + } + + @Override // RequiresDirective + public final boolean isTransitive() { + return this.transitive; + } + + } + + public static final class UsesDirective extends Directive implements javax.lang.model.element.ModuleElement.UsesDirective { + + private final TypeElement service; + + public UsesDirective(final TypeElement service) { + super(DirectiveKind.USES); + this.service = Objects.requireNonNull(service, "service"); + } + + @Override // Directive + public final R accept(final DirectiveVisitor v, final P p) { + return v.visitUses(this, p); + } + + @Override // UsesDirective + public final TypeElement getService() { + return this.service; + } + + } + + +} diff --git a/lang/src/main/java/org/microbean/lang/element/Name.java b/lang/src/main/java/org/microbean/lang/element/Name.java new file mode 100644 index 00000000..03644ea3 --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/element/Name.java @@ -0,0 +1,202 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2022–2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.element; + +import java.util.stream.IntStream; + +import org.microbean.lang.CompletionLock; + +public final class Name implements javax.lang.model.element.Name { + + private static final Name EMPTY = new Name(""); + + private final String content; + + private Name() { + super(); + this.content = ""; + } + + private Name(final String s) { + super(); + this.content = s == null ? "" : s; + } + + private Name(final javax.lang.model.element.Name name) { + super(); + this.content = switch (name) { + case null -> ""; + case Name n -> n.content; + default -> { + final String s; + CompletionLock.acquire(); + try { + s = name.toString(); + } finally { + CompletionLock.release(); + } + yield s == null ? "" : s; + }}; + } + + private Name(final CharSequence cs) { + super(); + this.content = switch (cs) { + case null -> ""; + case Name n -> n.content; + case javax.lang.model.element.Name n -> { + final String s; + CompletionLock.acquire(); + try { + s = n.toString(); + } finally { + CompletionLock.release(); + } + yield s == null ? "" : s; + } + default -> { + final String s = cs.toString(); + yield s == null ? "" : s; + } + }; + } + + private Name(final Name n) { + super(); + this.content = n == null ? "" : n.content; + } + + @Override // CharSequence + public final char charAt(final int index) { + return this.content.charAt(index); + } + + @Override // CharSequence + public final IntStream chars() { + return this.content.chars(); + } + + @Override // CharSequence + public final IntStream codePoints() { + return this.content.codePoints(); + } + + @Override // CharSequence + public final int length() { + return this.content.length(); + } + + @Override // CharSequence + public final Name subSequence(final int start, final int end) { + return of(this.content.subSequence(start, end)); + } + + @Override // Name + public final boolean contentEquals(final CharSequence cs) { + return switch (cs) { + case null -> false; + case Name n -> this.content.equals(n.content); + case javax.lang.model.element.Name n -> { + CompletionLock.acquire(); + try { + yield this.content.equals(n.toString()); + } finally { + CompletionLock.release(); + } + } + default -> this.content.equals(cs.toString()); + }; + } + + @Override // Object + public final int hashCode() { + return this.content.hashCode(); + } + + @Override // Object + public final boolean equals(final Object other) { + if (this == other) { + return true; + } else if (other != null && this.getClass() == other.getClass()) { + return this.content.equals(((Name)other).content); + } else { + return false; + } + } + + @Override // CharSequence + public final String toString() { + return this.content; + } + + + /* + * Static methods. + */ + + + public static final Name of() { + return EMPTY; + } + + public static final Name of(final CharSequence cs) { + return switch (cs) { + case null -> EMPTY; + case Name n -> n; + case javax.lang.model.element.Name n -> { + CompletionLock.acquire(); + try { + yield n.length() <= 0 ? EMPTY : new Name(n); + } finally { + CompletionLock.release(); + } + } + default -> cs.length() <= 0 ? EMPTY : new Name(cs); + }; + } + + public static final Name of(final Name n) { + return n == null ? EMPTY : n; + } + + public static final Name of(final String s) { + return s == null || s.isEmpty() ? EMPTY : new Name(s); + } + + public static final Name ofSimple(final CharSequence cs) { + if (cs == null) { + return EMPTY; + } + final String s; + if (cs instanceof javax.lang.model.element.Name) { + CompletionLock.acquire(); + try { + s = cs.toString(); + } finally { + CompletionLock.release(); + } + } else { + s = cs.toString(); + } + final int lastDotIndex = s.lastIndexOf('.'); + if (lastDotIndex > 0 && s.length() > 2) { + return of(s.substring(lastDotIndex + 1, s.length())); + } + return of(s); + } + +} diff --git a/lang/src/main/java/org/microbean/lang/element/PackageElement.java b/lang/src/main/java/org/microbean/lang/element/PackageElement.java new file mode 100644 index 00000000..3e720a22 --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/element/PackageElement.java @@ -0,0 +1,62 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2022–2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.element; + +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ElementVisitor; + +import javax.lang.model.type.NoType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; + +public final class PackageElement extends Element implements javax.lang.model.element.PackageElement { + + + /* + * Constructors. + */ + + + public PackageElement() { + super(ElementKind.PACKAGE); + } + + + /* + * Instance methods. + */ + + + @Override // Element + public final R accept(final ElementVisitor v, final P p) { + return v.visitPackage(this, p); + } + + @Override // QualifiedNameable + public final javax.lang.model.element.Name getQualifiedName() { + return this.getSimpleName(); + } + + @Override // Element + protected final TypeMirror validateType(final TypeMirror type) { + if (type.getKind() == TypeKind.PACKAGE && type instanceof NoType) { + return type; + } + throw new IllegalArgumentException("type: " + type); + } + +} diff --git a/lang/src/main/java/org/microbean/lang/element/Parameterizable.java b/lang/src/main/java/org/microbean/lang/element/Parameterizable.java new file mode 100644 index 00000000..4e5859b6 --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/element/Parameterizable.java @@ -0,0 +1,101 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2022–2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.element; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.TypeParameterElement; + +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.ExecutableType; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; + +public abstract sealed class Parameterizable extends Element implements javax.lang.model.element.Parameterizable + permits ExecutableElement, TypeElement { + + private final List typeParameters; + + private final List unmodifiableTypeParameters; + + protected Parameterizable(final ElementKind kind) { + super(kind); // no need to validate; sealed class; subclasses already validate + this.typeParameters = new ArrayList<>(5); + this.unmodifiableTypeParameters = Collections.unmodifiableList(this.typeParameters); + } + + @Override // Parameterizable + public List getTypeParameters() { + return this.unmodifiableTypeParameters; + } + + public final

void addTypeParameter(final P tp) { + this.typeParameters.add(validateAndEncloseTypeParameter(tp)); + } + + public final

void addTypeParameters(final Iterable tps) { + for (final P tp : tps) { + this.addTypeParameter(tp); + } + } + + private final

P validateAndEncloseTypeParameter(final P tp) { + switch (tp.getKind()) { + case TYPE_PARAMETER: + final TypeMirror t = tp.asType(); + switch (t.getKind()) { + case TYPEVAR: + tp.setEnclosingElement(this); // idempotent + assert tp.getGenericElement() == this; + assert tp.getEnclosingElement() == this; + assert tp.asType() == t; + assert t instanceof TypeVariable tv ? tv.asElement() == tp : true; + return tp; + default: + throw new IllegalArgumentException("typeParameter: " + tp + "; ((TypeVariable)tp.asType()).getKind(): " + t.getKind()); + } + default: + throw new IllegalArgumentException("typeParameter: " + tp); + } + } + + @Override // Element + public final boolean isUnnamed() { + return false; + } + + + /* + * Static methods. + */ + + + public static final List typeArguments(final TypeMirror t) { + switch (t.getKind()) { + case DECLARED: + return ((DeclaredType)t).getTypeArguments(); + case EXECUTABLE: + return ((ExecutableType)t).getTypeVariables(); + default: + return List.of(); + } + } + +} diff --git a/lang/src/main/java/org/microbean/lang/element/RecordComponentElement.java b/lang/src/main/java/org/microbean/lang/element/RecordComponentElement.java new file mode 100644 index 00000000..60a6bd37 --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/element/RecordComponentElement.java @@ -0,0 +1,129 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.element; + +import java.util.Set; + +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ElementVisitor; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; + +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; + +public final class RecordComponentElement extends Element implements javax.lang.model.element.RecordComponentElement { + + private ExecutableElement accessor; + + public RecordComponentElement() { + super(ElementKind.RECORD_COMPONENT); + // Record components are always public and nothing else. + this.addModifier(Modifier.PUBLIC); + } + + @Override // AbstractElement + public final R accept(final ElementVisitor v, final P p) { + return v.visitRecordComponent(this, p); + } + + @Override // Element + public final boolean isUnnamed() { + return false; + } + + @Override // RecordComponentElement + public final ExecutableElement getAccessor() { + return this.accessor; + } + + public final void setAccessor(final ExecutableElement e) { + final Object old = this.getAccessor(); + if (old == null) { + if (e != null) { + this.accessor = this.validateAccessor(e); + } + } else if (old != e) { + throw new IllegalStateException("e: " + e + "; old: " + old); + } + } + + @Override + protected final E validateEnclosedElement(final E e) { + throw new IllegalArgumentException("record components cannot enclose Elements"); + } + + @Override + protected final ElementKind validateKind(final ElementKind kind) { + switch (kind) { + case RECORD_COMPONENT: + return kind; + default: + throw new IllegalArgumentException("kind: " + kind); + } + } + + @Override // Element + protected final Modifier validateModifier(final Modifier modifier) { + switch (modifier) { + case PUBLIC: + return modifier; + default: + throw new IllegalArgumentException("modifier: " + modifier); + } + } + + @Override + protected final TypeMirror validateType(final TypeMirror type) { + switch (type.getKind()) { + case DECLARED: + return type; + default: + throw new IllegalArgumentException("type: " + type); + } + } + + private final ExecutableElement validateAccessor(final ExecutableElement e) { + switch (e.getKind()) { + case METHOD: + final Set modifiers = e.getModifiers(); + if (modifiers.contains(Modifier.ABSTRACT)) { + throw new IllegalArgumentException("abstract accessor: " + e); + } else if (e.isDefault() || modifiers.contains(Modifier.DEFAULT)) { + throw new IllegalArgumentException("default accessor: " + e); + } else if (modifiers.contains(Modifier.NATIVE)) { + throw new IllegalArgumentException("native accessor: " + e); + } else if (!modifiers.contains(Modifier.PUBLIC)) { + throw new IllegalArgumentException("non-public accessor: " + e); + } else if (modifiers.contains(Modifier.STATIC)) { + throw new IllegalArgumentException("static accessor: " + e); + } else if (e.asType().getKind() != TypeKind.EXECUTABLE || + !e.getParameters().isEmpty() || + !e.getThrownTypes().isEmpty() || + !e.getTypeParameters().isEmpty() || + !e.getReturnType().equals(this.asType()) || + !e.getSimpleName().equals(this.getSimpleName())) { + throw new IllegalArgumentException("invalid accessor: " + e); + } + // TODO: here or elsewhere: e must have Modifier.PUBLIC; e must not be static, etc. etc. + return e; + default: + throw new IllegalArgumentException("e: " + e); + } + } + +} diff --git a/lang/src/main/java/org/microbean/lang/element/TypeElement.java b/lang/src/main/java/org/microbean/lang/element/TypeElement.java new file mode 100644 index 00000000..80cd0489 --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/element/TypeElement.java @@ -0,0 +1,220 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2022–2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.element; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ElementVisitor; +import javax.lang.model.element.NestingKind; +import javax.lang.model.element.QualifiedNameable; +import javax.lang.model.element.RecordComponentElement; + +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeMirror; + +public final class TypeElement extends Parameterizable implements javax.lang.model.element.TypeElement { + + private final NestingKind nestingKind; + + private TypeMirror superclass; + + private final List interfaces; + + private final List unmodifiableInterfaces; + + private final List permittedSubclasses; + + private final List unmodifiablePermittedSubclasses; + + private final List recordComponents; + + private final List unmodifiableRecordComponents; + + public TypeElement(final ElementKind kind) { + this(kind, NestingKind.TOP_LEVEL); + } + + public TypeElement(final ElementKind kind, + final NestingKind nestingKind) { + super(kind); + this.nestingKind = nestingKind == null ? NestingKind.TOP_LEVEL : nestingKind; + this.interfaces = new ArrayList<>(5); + this.unmodifiableInterfaces = Collections.unmodifiableList(this.interfaces); + this.permittedSubclasses = new ArrayList<>(5); + this.unmodifiablePermittedSubclasses = Collections.unmodifiableList(this.permittedSubclasses); + if (kind == ElementKind.RECORD) { + this.recordComponents = new ArrayList<>(7); + this.unmodifiableRecordComponents = Collections.unmodifiableList(this.recordComponents); + } else { + this.recordComponents = List.of(); + this.unmodifiableRecordComponents = List.of(); + } + } + + @Override // TypeElement + public final R accept(final ElementVisitor v, final P p) { + return v.visitType(this, p); + } + + @Override // TypeElement + public final List getInterfaces() { + return this.unmodifiableInterfaces; + } + + @SuppressWarnings("fallthrough") + public final void addInterface(final TypeMirror i) { + switch (i.getKind()) { + case DECLARED: + switch (((DeclaredType)i).asElement().getKind()) { + case INTERFACE: + this.interfaces.add(i); + return; + default: + break; + } + default: + break; + } + throw new IllegalArgumentException("i: " + i); + } + + @Override // TypeElement + public final NestingKind getNestingKind() { + return this.nestingKind; + } + + @Override // TypeElement + public final javax.lang.model.element.Name getQualifiedName() { + final QualifiedNameable qn; + switch (this.getNestingKind()) { + case ANONYMOUS: + case LOCAL: + qn = null; + break; + case MEMBER: + case TOP_LEVEL: + qn = (QualifiedNameable)this.getEnclosingElement(); + break; + default: + throw new AssertionError(); + } + return qn == null ? this.getSimpleName() : Name.of(qn.getQualifiedName() + "." + this.getSimpleName()); + } + + @Override // TypeElement + public final TypeMirror getSuperclass() { + return this.superclass; + } + + public final void setSuperclass(final TypeMirror superclass) { + final Object old = this.getSuperclass(); + if (old == null) { + if (superclass != null) { + this.superclass = validateSuperclass(superclass); + } + } else if (old != superclass) { + throw new IllegalStateException(); + } + } + + @Override // TypeElement + public final List getPermittedSubclasses() { + return this.unmodifiablePermittedSubclasses; + } + + public final void addPermittedSubclass(final TypeMirror t) { + this.permittedSubclasses.add(this.validatePermittedSubclass(t)); + } + + public final void addPermittedSubclasses(final Iterable ts) { + for (final TypeMirror t : ts) { + this.addPermittedSubclass(t); + } + } + + protected TypeMirror validatePermittedSubclass(final TypeMirror t) { + return Objects.requireNonNull(t, "t"); + } + + @Override // TypeElement + public final List getRecordComponents() { + return this.unmodifiableRecordComponents; + } + + @Override + protected final E validateEnclosedElement(E e) { + e = super.validateEnclosedElement(e); + switch (e.getKind()) { + case RECORD_COMPONENT: + switch (e.asType().getKind()) { + case DECLARED: + return e; + default: + throw new IllegalArgumentException("e: " + e); + } + default: + // throw new IllegalArgumentException("e: " + e); + return e; + } + } + + @Override // Element + protected ElementKind validateKind(final ElementKind kind) { + switch (kind) { + case ANNOTATION_TYPE: + case CLASS: + case ENUM: + case INTERFACE: + case RECORD: + return kind; + default: + throw new IllegalArgumentException("kind: " + kind); + } + } + + @Override // Element + protected TypeMirror validateType(final TypeMirror type) { + switch (type.getKind()) { + case DECLARED: + case ERROR: + return type; + default: + throw new IllegalArgumentException("type: " + type); + } + } + + @Override + public final String toString() { + final CharSequence n = this.getQualifiedName(); + if (n == null) { + return ""; + } else if (this.isUnnamed() || n.length() <= 0) { + return ""; + } else { + return n.toString(); + } + } + + private static final T validateSuperclass(final T superclass) { + return superclass; + } + +} diff --git a/lang/src/main/java/org/microbean/lang/element/TypeParameterElement.java b/lang/src/main/java/org/microbean/lang/element/TypeParameterElement.java new file mode 100644 index 00000000..91f6d737 --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/element/TypeParameterElement.java @@ -0,0 +1,146 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.element; + +import java.util.List; + +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ElementVisitor; + +import javax.lang.model.type.IntersectionType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; + +import org.microbean.lang.type.DefineableType; + +public final class TypeParameterElement extends Element implements javax.lang.model.element.TypeParameterElement { + + private javax.lang.model.element.Element genericElement; + + public TypeParameterElement() { + super(ElementKind.TYPE_PARAMETER); + } + + public & TypeVariable> + TypeParameterElement(final javax.lang.model.element.Name simpleName, final T typeVariable) { + this(); + this.setSimpleName(simpleName); + typeVariable.setDefiningElement(this); + } + + @Override // Element + public final R accept(final ElementVisitor v, final P p) { + return v.visitTypeParameter(this, p); + } + + @Override // Element + protected final TypeMirror validateType(final TypeMirror type) { + if (type.getKind() == TypeKind.TYPEVAR && type instanceof TypeVariable) { + return type; + } + throw new IllegalArgumentException("type: " + type); + } + + @Override // Element + public final boolean isUnnamed() { + return false; + } + + @Override // TypeParameterElement + public final List getBounds() { + return boundsFrom((TypeVariable)this.asType()); + } + + @Override // Element + public final javax.lang.model.element.Element getEnclosingElement() { + return this.getGenericElement(); + } + + @Override // Element + public final void setEnclosingElement(final javax.lang.model.element.Element genericElement) { + this.setGenericElement(genericElement); + } + + @Override // TypeParameterElement + public final javax.lang.model.element.Element getGenericElement() { + return this.genericElement; + } + + public final void setGenericElement(final javax.lang.model.element.Element genericElement) { + final Object old = this.getGenericElement(); + if (old == null) { + if (genericElement != null) { + this.genericElement = validateGenericElement(genericElement); + } + } else if (old != genericElement) { + throw new IllegalStateException("old: " + old + "; genericElement: " + genericElement); + } + } + + @Override + public final String toString() { + return this.getSimpleName() + " " + this.asType(); + } + + private final javax.lang.model.element.Element validateGenericElement(final javax.lang.model.element.Element element) { + if (element == null) { + return null; + } else if (element == this) { + throw new IllegalArgumentException("element: " + element); + } + switch (element.getKind()) { + case CLASS: + case CONSTRUCTOR: + case INTERFACE: + case METHOD: + return element; + default: + throw new IllegalArgumentException("Not a valid generic element: " + element); + } + } + + + /* + * Static methods. + */ + + + private static final TypeVariable validateTypeVariable(final TypeVariable tv) { + switch (tv.getKind()) { + case TYPEVAR: + return tv; + default: + throw new IllegalArgumentException("tv: " + tv); + } + } + + private static final List boundsFrom(final TypeVariable typeVariable) { + final TypeMirror upperBound = typeVariable.getUpperBound(); + switch (upperBound.getKind()) { + case INTERSECTION: + return ((IntersectionType)upperBound).getBounds(); + case ARRAY: + case DECLARED: + case TYPEVAR: + return List.of(upperBound); + default: + throw new IllegalArgumentException("typeVariable: " + typeVariable); + } + } + +} diff --git a/lang/src/main/java/org/microbean/lang/element/VariableElement.java b/lang/src/main/java/org/microbean/lang/element/VariableElement.java new file mode 100644 index 00000000..e7994cc9 --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/element/VariableElement.java @@ -0,0 +1,116 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.element; + +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ElementVisitor; + +import javax.lang.model.type.TypeMirror; + +public final class VariableElement extends Element implements javax.lang.model.element.VariableElement { + + public Object constantValue; + + public VariableElement(final ElementKind kind) { + super(kind); + } + + public VariableElement(final ElementKind kind, final Object constantValue) { + super(kind); + if (constantValue != null) { + this.setConstantValue(constantValue); + } + } + + @Override // Element + public final R accept(final ElementVisitor v, final P p) { + return v.visitVariable(this, p); + } + + @Override // Element + public final boolean isUnnamed() { + return false; + } + + @Override // VariableElement + public final Object getConstantValue() { + return this.constantValue; + } + + public final void setConstantValue(final Object constantValue) { + final Object old = this.getConstantValue(); + if (old == null) { + if (constantValue != null) { + this.constantValue = validateConstantValue(constantValue); + } + } else if (old != constantValue) { + throw new IllegalStateException(); + } + } + + @Override + public final String toString() { + return this.getSimpleName().toString(); + } + + @Override + protected final ElementKind validateKind(final ElementKind kind) { + switch (kind) { + case BINDING_VARIABLE: + case ENUM: + case ENUM_CONSTANT: + case EXCEPTION_PARAMETER: + case FIELD: + case LOCAL_VARIABLE: + case PARAMETER: + case RESOURCE_VARIABLE: + return kind; + default: + throw new IllegalArgumentException("kind: " + kind); + } + } + + @Override + protected final TypeMirror validateType(final TypeMirror type) { + switch (type.getKind()) { + case ARRAY: + case DECLARED: + case INTERSECTION: + case BOOLEAN: + case BYTE: + case CHAR: + case DOUBLE: + case FLOAT: + case INT: + case LONG: + case SHORT: + case TYPEVAR: + case WILDCARD: + return type; + default: + throw new IllegalArgumentException("type: " + type); + } + } + + private final Object validateConstantValue(final Object constantValue) { + if (constantValue == this) { + throw new IllegalArgumentException("constantValue: " + constantValue); + } + return constantValue; + } + +} diff --git a/lang/src/main/java/org/microbean/lang/element/package-info.java b/lang/src/main/java/org/microbean/lang/element/package-info.java new file mode 100644 index 00000000..08060cd4 --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/element/package-info.java @@ -0,0 +1,20 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +/** + * Provides classes and interfaces related to the Java language model. + * + * @author Laird Nelson + */ +package org.microbean.lang.element; diff --git a/lang/src/main/java/org/microbean/lang/package-info.java b/lang/src/main/java/org/microbean/lang/package-info.java new file mode 100644 index 00000000..c3cb6b81 --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/package-info.java @@ -0,0 +1,24 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2022–2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +/** + * Provides classes and interfaces related to the Java language model. + * + * @author Laird Nelson + */ +package org.microbean.lang; diff --git a/lang/src/main/java/org/microbean/lang/type/ArrayType.java b/lang/src/main/java/org/microbean/lang/type/ArrayType.java new file mode 100644 index 00000000..5c0eda91 --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/type/ArrayType.java @@ -0,0 +1,90 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2022–2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.type; + +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeVisitor; + +public final class ArrayType extends TypeMirror implements javax.lang.model.type.ArrayType { + + private javax.lang.model.type.TypeMirror componentType; + + public ArrayType() { + super(TypeKind.ARRAY); + } + + public ArrayType(final javax.lang.model.type.TypeMirror componentType) { + this(); + this.setComponentType(componentType); + } + + + /* + * Instance methods. + */ + + + @Override // TypeMirror + public final R accept(final TypeVisitor v, final P p) { + return v.visitArray(this, p); + } + + @Override // ArrayType + public final javax.lang.model.type.TypeMirror getComponentType() { + return this.componentType; + } + + public final void setComponentType(final javax.lang.model.type.TypeMirror componentType) { + final javax.lang.model.type.TypeMirror old = this.getComponentType(); + if (old == null) { + if (componentType != null) { + this.componentType = this.validateComponentType(componentType); + } + } else if (old != componentType) { + throw new IllegalStateException(); + } + } + + public final String toString() { + return this.componentType + "[]"; + } + + private final javax.lang.model.type.TypeMirror validateComponentType(final javax.lang.model.type.TypeMirror componentType) { + if (componentType == this) { + throw new IllegalArgumentException("componentType: " + componentType); + } + switch (componentType.getKind()) { + case ARRAY: + case DECLARED: + // case INTERSECTION: + case BOOLEAN: + case BYTE: + case CHAR: + case DOUBLE: + case FLOAT: + case INT: + case LONG: + case SHORT: + case TYPEVAR: + case WILDCARD: + return componentType; + default: + throw new IllegalArgumentException("componentType: " + componentType); + } + } + +} diff --git a/lang/src/main/java/org/microbean/lang/type/Capture.java b/lang/src/main/java/org/microbean/lang/type/Capture.java new file mode 100644 index 00000000..90c1264a --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/type/Capture.java @@ -0,0 +1,134 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2022–2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.type; + +import javax.lang.model.element.ElementKind; + +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; +import javax.lang.model.type.TypeVisitor; +import javax.lang.model.type.WildcardType; + +import org.microbean.lang.element.Name; +import org.microbean.lang.element.TypeParameterElement; + +public final class Capture extends DefineableType implements TypeVariable { + + private TypeMirror upperBound; + + private TypeMirror lowerBound; + + private final WildcardType wildcardType; + + public Capture(final WildcardType w) { + this(Name.of(""), w); + } + + public Capture(final javax.lang.model.element.Name name, final WildcardType w) { + super(TypeKind.TYPEVAR); + this.wildcardType = validateWildcardType(w); + this.setDefiningElement(new TypeParameterElement(name, this)); + } + + @Override // DefineableType + protected final javax.lang.model.element.TypeParameterElement validateDefiningElement(final javax.lang.model.element.TypeParameterElement e) { + if (e.getKind() != ElementKind.TYPE_PARAMETER) { + throw new IllegalArgumentException("e: " + e); + } + final Object t = e.asType(); + if (t != null && this != t) { + throw new IllegalArgumentException("e: " + e + "; this (" + this + ") != e.asType() (" + t + ")"); + } + return e; + } + + @Override // TypeVariable + public final R accept(final TypeVisitor v, final P p) { + return v.visitTypeVariable(this, p); + } + + @Override + public final TypeMirror getLowerBound() { + return this.lowerBound; + } + + public final void setLowerBound(final TypeMirror t) { + final Object old = this.getLowerBound(); + if (old == null) { + if (t != null) { + this.lowerBound = validateLowerBound(t); + } + } else if (old != t) { + throw new IllegalStateException(); + } + } + + @Override // TypeVariable + public final TypeMirror getUpperBound() { + return this.upperBound; + } + + public final void setUpperBound(final TypeMirror t) { + final Object old = this.getUpperBound(); + if (old == null) { + if (t != null) { + this.upperBound = validateUpperBound(t); + } + } else if (old != t) { + throw new IllegalStateException(); + } + } + + public final WildcardType getWildcardType() { + return this.wildcardType; + } + + + /* + * Static methods. + */ + + + private static final TypeMirror validateUpperBound(final TypeMirror upperBound) { + switch (upperBound.getKind()) { + case DECLARED: + case INTERSECTION: + case TYPEVAR: + return upperBound; + default: + throw new IllegalArgumentException("upperBound: " + upperBound); + } + } + + private static final TypeMirror validateLowerBound(final TypeMirror lowerBound) { + return lowerBound; + } + + private static final WildcardType validateWildcardType(final WildcardType w) { + if (w != null) { + switch (w.getKind()) { + case WILDCARD: + break; + default: + throw new IllegalArgumentException("w: " + w); + } + } + return w; + } + +} diff --git a/lang/src/main/java/org/microbean/lang/type/DeclaredType.java b/lang/src/main/java/org/microbean/lang/type/DeclaredType.java new file mode 100644 index 00000000..c215ba8c --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/type/DeclaredType.java @@ -0,0 +1,206 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2022–2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.type; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import javax.lang.model.element.TypeElement; + +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVisitor; + +public sealed class DeclaredType extends DefineableType implements javax.lang.model.type.DeclaredType + permits ErrorType { + + + /* + * Instance fields. + */ + + + private TypeMirror enclosingType; + + // ArrayType, DeclaredType, ErrorType, TypeVariable, WildcardType + private final List typeArguments; + + private final List unmodifiableTypeArguments; + + // See + // https://github.com/openjdk/jdk/blob/jdk-20+11/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Type.java#L1197-L1200 + private Boolean erased; + + + /* + * Constructors. + */ + + + public DeclaredType() { + super(TypeKind.DECLARED); + this.typeArguments = new ArrayList<>(5); + this.unmodifiableTypeArguments = Collections.unmodifiableList(this.typeArguments); + } + + public DeclaredType(final boolean erased) { + this(); + this.setErased(erased); + } + + DeclaredType(final TypeKind kind, final boolean erased) { + super(kind); + this.typeArguments = new ArrayList<>(5); + this.unmodifiableTypeArguments = Collections.unmodifiableList(this.typeArguments); + this.setErased(erased); + } + + + /* + * Instance methods. + */ + + + @Override // TypeMirror + public R accept(final TypeVisitor v, final P p) { + return v.visitDeclared(this, p); + } + + @Override + protected TypeKind validateKind(final TypeKind kind) { + return switch (kind) { + case DECLARED -> kind; + default -> throw new IllegalArgumentException("kind: " + kind); + }; + } + + @Override // DefineableType + protected TypeElement validateDefiningElement(final TypeElement e) { + switch (e.getKind()) { + case ANNOTATION_TYPE: + case CLASS: + case ENUM: + case INTERFACE: + case RECORD: + final javax.lang.model.type.DeclaredType elementType = (javax.lang.model.type.DeclaredType)e.asType(); + if (elementType == null) { + throw new IllegalArgumentException("e: " + e + "; e.asType() == null"); + } else if (this != elementType) { + // We are a parameterized type, i.e. a type usage, i.e. the-type-denoted-by-Set + // vs. the-type-denoted-by-Set + final int size = this.getTypeArguments().size(); + if (size > 0 && size != elementType.getTypeArguments().size()) { + // We aren't a raw type (size > 0) and our type arguments aren't of the required size, so someone passed a bad + // defining element. + throw new IllegalArgumentException("e: " + e); + } + } + break; + default: + throw new IllegalArgumentException("e: " + e); + } + return e; + } + + public final boolean isErased() { + final Boolean erased = this.erased; + return erased == null ? false : erased.booleanValue(); + } + + public final void setErased(final boolean b) { + final Boolean old = this.erased; + if (old == null) { + this.erased = Boolean.valueOf(b); + } else if (!old.equals(Boolean.valueOf(b))) { + throw new IllegalStateException(); + } + } + + @Override // DeclaredType + public final TypeMirror getEnclosingType() { + return this.enclosingType; + } + + public final void setEnclosingType(final TypeMirror enclosingType) { + final Object old = this.getEnclosingType(); + if (old == null) { + if (enclosingType != null) { + this.enclosingType = this.validateEnclosingType(enclosingType); + } + } else if (old != enclosingType) { + throw new IllegalStateException(); + } + } + + private final TypeMirror validateEnclosingType(final TypeMirror t) { + if (t == null) { + return NoType.NONE; + } else if (t == this) { + throw new IllegalArgumentException("t: " + t); + } + switch (t.getKind()) { + case DECLARED: + case NONE: + return t; + default: + throw new IllegalArgumentException("t: " + t); + } + } + + @Override // DeclaredType + public final List getTypeArguments() { + return this.unmodifiableTypeArguments; + } + + public final void addTypeArgument(final TypeMirror t) { + this.typeArguments.add(this.validateTypeArgument(t)); + } + + public final void addTypeArguments(final Iterable ts) { + for (final TypeMirror t : ts) { + this.addTypeArgument(t); + } + } + + private final TypeMirror validateTypeArgument(final TypeMirror t) { + if (t == this) { + throw new IllegalArgumentException("t: " + t); + } else if (this.isErased()) { + throw new IllegalStateException("this.isErased()"); + } + + switch (t.getKind()) { + case ARRAY: + case DECLARED: + // case INTERSECTION: // JLS says reference types and wildcards only + case TYPEVAR: + case WILDCARD: + break; + default: + throw new IllegalArgumentException("t: " + t); + } + return t; + } + + @Override + public String toString() { + final Object element = this.asElement(); + return element == null ? "(undefined type; arguments: " + this.getTypeArguments() + ")" : element.toString(); + } + +} diff --git a/lang/src/main/java/org/microbean/lang/type/DefineableType.java b/lang/src/main/java/org/microbean/lang/type/DefineableType.java new file mode 100644 index 00000000..97031ff0 --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/type/DefineableType.java @@ -0,0 +1,69 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2022–2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.type; + +import javax.lang.model.element.Element; + +import javax.lang.model.type.TypeKind; + +public abstract sealed class DefineableType extends TypeMirror permits Capture, DeclaredType, TypeVariable { + + private E definingElement; + + protected DefineableType(final TypeKind kind) { + super(kind); + } + + public final E asElement() { + return this.definingElement; + } + + public final void setDefiningElement(final E definingElement) { + final E old = this.asElement(); + if (old == null) { + if (definingElement != null) { + this.definingElement = this.validateDefiningElement(definingElement); + } + } else if (old != definingElement) { + throw new IllegalStateException(); + } + } + + public final boolean isDefined() { + return this.asElement() != null; + } + + public final javax.lang.model.type.TypeMirror getElementType() { + final Element e = this.asElement(); + return e == null ? null : e.asType(); + } + + protected abstract E validateDefiningElement(final E e); + + @Override + protected TypeKind validateKind(final TypeKind kind) { + switch (kind) { + case DECLARED: + case ERROR: + case TYPEVAR: + return kind; + default: + throw new IllegalArgumentException("kind: " + kind); + } + } + +} diff --git a/lang/src/main/java/org/microbean/lang/type/DelegatingTypeMirror.java b/lang/src/main/java/org/microbean/lang/type/DelegatingTypeMirror.java new file mode 100644 index 00000000..a1cfcca0 --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/type/DelegatingTypeMirror.java @@ -0,0 +1,351 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.microbean.lang.type; + +import java.lang.annotation.Annotation; + +import java.lang.constant.ClassDesc; +import java.lang.constant.Constable; +import java.lang.constant.ConstantDesc; +import java.lang.constant.DynamicConstantDesc; +import java.lang.constant.MethodHandleDesc; +import java.lang.constant.MethodTypeDesc; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Optional; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; + +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.ErrorType; +import javax.lang.model.type.ExecutableType; +import javax.lang.model.type.IntersectionType; +import javax.lang.model.type.NullType; +import javax.lang.model.type.PrimitiveType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; +import javax.lang.model.type.TypeVisitor; +import javax.lang.model.type.UnionType; +import javax.lang.model.type.WildcardType; + +import org.microbean.lang.CompletionLock; +import org.microbean.lang.Lang; +import org.microbean.lang.TypeAndElementSource; +import org.microbean.lang.Equality; + +import org.microbean.lang.element.DelegatingElement; + +import org.microbean.lang.type.NoType; +import org.microbean.lang.type.Types; + +import static java.lang.constant.ConstantDescs.BSM_INVOKE; +import static java.lang.constant.DirectMethodHandleDesc.Kind.STATIC; + +public final class DelegatingTypeMirror + implements ArrayType, + Constable, + ErrorType, + ExecutableType, + IntersectionType, + javax.lang.model.type.NoType, + NullType, + PrimitiveType, + TypeVariable, + UnionType, + WildcardType { + + private static final ClassDesc CD_DelegatingTypeMirror = ClassDesc.of("org.microbean.lang.type.DelegatingTypeMirror"); + + private static final ClassDesc CD_TypeAndElementSource = ClassDesc.of("org.microbean.lang.TypeAndElementSource"); + + private static final ClassDesc CD_Equality = ClassDesc.of("org.microbean.lang.Equality"); + + private static final ClassDesc CD_TypeMirror = ClassDesc.of("javax.lang.model.type.TypeMirror"); + + private final TypeAndElementSource elementSource; + + private final TypeMirror delegate; + + private final Equality ehc; + + private DelegatingTypeMirror(final TypeMirror delegate, final TypeAndElementSource elementSource, final Equality ehc) { + super(); + this.delegate = unwrap(Objects.requireNonNull(delegate, "delegate")); + this.elementSource = elementSource == null ? Lang.typeAndElementSource() : elementSource; + this.ehc = ehc == null ? new Equality(true) : ehc; + } + + @Override // TypeMirror + public final R accept(final TypeVisitor v, final P p) { + return switch (this.getKind()) { + case ARRAY -> v.visitArray(this, p); + case DECLARED -> v.visitDeclared(this, p); + case ERROR -> v.visitError(this, p); + case EXECUTABLE -> v.visitExecutable(this, p); + case INTERSECTION -> v.visitIntersection(this, p); + case MODULE, NONE, PACKAGE, VOID -> v.visitNoType(this, p); + case NULL -> v.visitNull(this, p); + case BOOLEAN, BYTE, CHAR, DOUBLE, FLOAT, INT, LONG, SHORT -> v.visitPrimitive(this, p); + case TYPEVAR -> v.visitTypeVariable(this, p); + case UNION -> v.visitUnion(this, p); + case WILDCARD -> v.visitWildcard(this, p); + case OTHER -> v.visitUnknown(this, p); + }; + } + + @Override // Various + public final Element asElement() { + return switch (this.getKind()) { + case DECLARED -> DelegatingElement.of(((DeclaredType)this.delegate).asElement(), this.elementSource, this.ehc); + case TYPEVAR -> DelegatingElement.of(((TypeVariable)this.delegate).asElement(), this.elementSource, this.ehc); + default -> null; + }; + } + + public final TypeMirror delegate() { + return this.delegate; + } + + @Override // UnionType + public final List getAlternatives() { + return switch (this.getKind()) { + case UNION -> this.wrap(((UnionType)this.delegate).getAlternatives()); + default -> List.of(); + }; + } + + @Override // TypeMirror + public final A getAnnotation(final Class annotationType) { + return this.delegate.getAnnotation(annotationType); + } + + @Override // TypeMirror + public final List getAnnotationMirrors() { + // TODO: delegating annotation mirror? + return this.delegate.getAnnotationMirrors(); + } + + @Override // TypeMirror + public final A[] getAnnotationsByType(final Class annotationType) { + return this.delegate.getAnnotationsByType(annotationType); + } + + @Override // IntersectionType + public final List getBounds() { + return switch (this.getKind()) { + case INTERSECTION -> this.wrap(((IntersectionType)this.delegate).getBounds()); + default -> List.of(); + }; + } + + @Override // ArrayType + public final TypeMirror getComponentType() { + return switch (this.getKind()) { + case ARRAY -> this.wrap(((ArrayType)this.delegate).getComponentType()); + default -> NoType.NONE; + }; + } + + @Override // DeclaredType + public final TypeMirror getEnclosingType() { + return switch(this.getKind()) { + case DECLARED -> this.wrap(((DeclaredType)this.delegate).getEnclosingType()); + default -> NoType.NONE; + }; + } + + @Override // WildcardType + public final TypeMirror getExtendsBound() { + return switch (this.getKind()) { + case WILDCARD -> this.wrap(((WildcardType)this.delegate).getExtendsBound()); + default -> null; + }; + } + + @Override // TypeMirror + public final TypeKind getKind() { + CompletionLock.acquire(); // CRITICAL! + try { + return this.delegate.getKind(); + } finally { + CompletionLock.release(); + } + } + + @Override // TypeVariable + public final TypeMirror getLowerBound() { + return switch (this.getKind()) { + case TYPEVAR -> this.wrap(((TypeVariable)this.delegate).getLowerBound()); + default -> org.microbean.lang.type.NullType.INSTANCE; // bottom type, not NONE type + }; + } + + @Override // TypeVariable + public final TypeMirror getUpperBound() { + return switch (this.getKind()) { + case TYPEVAR -> this.wrap(((TypeVariable)this.delegate).getUpperBound()); + default -> this.wrap(this.elementSource.typeElement("java.lang.Object").asType()); + }; + } + + @Override // ExecutableType + public final List getParameterTypes() { + return switch (this.getKind()) { + case EXECUTABLE -> this.wrap(((ExecutableType)this.delegate).getParameterTypes()); + default -> List.of(); + }; + } + + @Override // ExecutableType + public final TypeMirror getReceiverType() { + return switch (this.getKind()) { + case EXECUTABLE -> this.wrap(((ExecutableType)this.delegate).getReceiverType()); + default -> null; + }; + } + + @Override // ExecutableType + public final TypeMirror getReturnType() { + return switch (this.getKind()) { + case EXECUTABLE -> this.wrap(((ExecutableType)this.delegate).getReturnType()); + default -> null; + }; + } + + @Override // WildcardType + public final TypeMirror getSuperBound() { + return switch (this.getKind()) { + case WILDCARD -> this.wrap(((WildcardType)this.delegate).getSuperBound()); + default -> null; + }; + } + + @Override // ExecutableType + public final List getThrownTypes() { + return switch (this.getKind()) { + case EXECUTABLE -> this.wrap(((ExecutableType)this.delegate).getThrownTypes()); + default -> List.of(); + }; + } + + @Override // DeclaredType + public final List getTypeArguments() { + return switch (this.getKind()) { + case DECLARED -> this.wrap(((DeclaredType)this.delegate).getTypeArguments()); + default -> List.of(); + }; + } + + @Override // ExecutableType + public final List getTypeVariables() { + return switch (this.getKind()) { + case EXECUTABLE -> this.wrap(((ExecutableType)this.delegate).getTypeVariables()); + default -> List.of(); + }; + } + + @Override // TypeMirror + public final int hashCode() { + return this.ehc.hashCode(this); + } + + @Override // TypeMirror + public final boolean equals(final Object other) { + return this.ehc.equals(this, other); + } + + @Override // TypeMirror + public final String toString() { + return this.delegate.toString(); + } + + @Override // Constable + public final Optional describeConstable() { + return Lang.describeConstable(this.delegate) + .flatMap(delegateDesc -> (this.elementSource instanceof Constable c ? c.describeConstable() : Optional.empty()) + .flatMap(elementSourceDesc -> this.ehc.describeConstable() + .map(equalityDesc -> DynamicConstantDesc.of(BSM_INVOKE, + MethodHandleDesc.ofMethod(STATIC, + CD_DelegatingTypeMirror, + "of", + MethodTypeDesc.of(CD_DelegatingTypeMirror, + CD_TypeMirror, + CD_TypeAndElementSource, + CD_Equality)), + delegateDesc, + elementSourceDesc, + equalityDesc)))); + } + + private final DelegatingTypeMirror wrap(final TypeMirror t) { + return of(t, this.elementSource, this.ehc); + } + + private final List wrap(final Collection ts) { + return of(ts, this.elementSource, this.ehc); + } + + + /* + * Static methods. + */ + + + public static final List of(final Collection ts, final TypeAndElementSource elementSource) { + return of(ts, elementSource, null); + } + + public static final List of(final Collection ts, final TypeAndElementSource elementSource, final Equality ehc) { + final List newTs = new ArrayList<>(ts.size()); + for (final TypeMirror t : ts) { + newTs.add(of(t, elementSource, ehc)); + } + return Collections.unmodifiableList(newTs); + } + + public static final DelegatingTypeMirror of(final TypeMirror t, final TypeAndElementSource elementSource) { + return of(t, elementSource, null); + } + + // Called by describeConstable + public static final DelegatingTypeMirror of(final TypeMirror t, final TypeAndElementSource elementSource, final Equality ehc) { + return + t == null ? null : + t instanceof DelegatingTypeMirror d ? d : + new DelegatingTypeMirror(t, elementSource, ehc); + } + + public static final TypeMirror unwrap(TypeMirror t) { + while (t instanceof DelegatingTypeMirror dt) { + t = dt.delegate(); + } + return t; + } + + public static final TypeMirror[] unwrap(final TypeMirror[] ts) { + final TypeMirror[] rv = new TypeMirror[ts.length]; + for (int i = 0; i < ts.length; i++) { + rv[i] = unwrap(ts[i]); + } + return rv; + } + +} diff --git a/lang/src/main/java/org/microbean/lang/type/ErrorType.java b/lang/src/main/java/org/microbean/lang/type/ErrorType.java new file mode 100644 index 00000000..9d119e49 --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/type/ErrorType.java @@ -0,0 +1,50 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2022–2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.type; + +import javax.lang.model.element.AnnotationMirror; + +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeVisitor; + +public final class ErrorType extends DeclaredType implements javax.lang.model.type.ErrorType { + + public ErrorType() { + super(TypeKind.ERROR, false); + } + + @Override // DeclaredType + public final R accept(final TypeVisitor v, final P p) { + return v.visitError(this, p); + } + + @Override // AnnotatedConstruct + public void addAnnotationMirror(final AnnotationMirror a) { + throw new UnsupportedOperationException(); + } + + @Override // DeclaredType + protected final TypeKind validateKind(final TypeKind kind) { + switch (kind) { + case ERROR: + return kind; + default: + throw new IllegalArgumentException("kind: " + kind); + } + } + +} diff --git a/lang/src/main/java/org/microbean/lang/type/ExecutableType.java b/lang/src/main/java/org/microbean/lang/type/ExecutableType.java new file mode 100644 index 00000000..68865baa --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/type/ExecutableType.java @@ -0,0 +1,200 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2022–2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.type; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import javax.lang.model.element.AnnotationMirror; + +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeVisitor; +import javax.lang.model.type.TypeVariable; + +public final class ExecutableType extends TypeMirror implements javax.lang.model.type.ExecutableType { + + private final List parameterTypes; + + private final List unmodifiableParameterTypes; + + private javax.lang.model.type.TypeMirror receiverType; + + private javax.lang.model.type.TypeMirror returnType; + + private final List thrownTypes; + + private final List unmodifiableThrownTypes; + + private final List typeVariables; + + private final List unmodifiableTypeVariables; + + public ExecutableType() { + super(TypeKind.EXECUTABLE); + this.parameterTypes = new ArrayList<>(5); + this.unmodifiableParameterTypes = Collections.unmodifiableList(this.parameterTypes); + this.thrownTypes = new ArrayList<>(5); + this.unmodifiableThrownTypes = Collections.unmodifiableList(this.thrownTypes); + this.typeVariables = new ArrayList<>(5); + this.unmodifiableTypeVariables = Collections.unmodifiableList(this.typeVariables); + } + + public ExecutableType(final javax.lang.model.type.ExecutableType t) { + this(t, t.getTypeVariables()); + } + + public ExecutableType(final javax.lang.model.type.ExecutableType t, final Iterable typeVariables) { + this(); + if (t.getKind() != TypeKind.EXECUTABLE) { + throw new IllegalArgumentException(); + } + this.addParameterTypes(t.getParameterTypes()); + this.setReceiverType(t.getReceiverType()); + this.setReturnType(t.getReturnType()); + this.addThrownTypes(t.getThrownTypes()); + this.addTypeVariables(typeVariables); + } + + @Override + public final void addAnnotationMirror(final AnnotationMirror a) { + throw new UnsupportedOperationException(); + } + + @Override // AnnotatedConstruct + public final List getAnnotationMirrors() { + return List.of(); + } + + @Override // TypeMirror + public final R accept(final TypeVisitor v, final P p) { + return v.visitExecutable(this, p); + } + + @Override // ExecutableType + public final List getParameterTypes() { + return this.unmodifiableParameterTypes; + } + + public final void addParameterType(final javax.lang.model.type.TypeMirror t) { + this.parameterTypes.add(validateParameterType(t)); + } + + public final void addParameterTypes(final Iterable ts) { + for (final javax.lang.model.type.TypeMirror t : ts) { + this.addParameterType(t); + } + } + + @Override // ExecutableType + public final List getThrownTypes() { + return this.unmodifiableThrownTypes; + } + + public final void addThrownType(final javax.lang.model.type.TypeMirror t) { + this.thrownTypes.add(validateThrownType(t)); + } + + public final void addThrownTypes(final Iterable ts) { + for (final javax.lang.model.type.TypeMirror t : ts) { + this.addThrownType(t); + } + } + + @Override // ExecutableType + public final List getTypeVariables() { + return this.unmodifiableTypeVariables; + } + + public final void addTypeVariable(final TypeVariable t) { + this.typeVariables.add(validateTypeVariable(t)); + } + + public final void addTypeVariables(final Iterable ts) { + for (final javax.lang.model.type.TypeVariable t : ts) { + this.addTypeVariable(t); + } + } + + @Override // ExecutableType + public final javax.lang.model.type.TypeMirror getReceiverType() { + final javax.lang.model.type.TypeMirror t = this.receiverType; + return t == null ? NoType.NONE : t; + } + + public final void setReceiverType(final javax.lang.model.type.TypeMirror t) { + final Object old = this.receiverType; + if (old == null) { + if (t != null) { + this.receiverType = validateReceiverType(t); + } + } else if (old != t) { + throw new IllegalStateException(); + } + } + + @Override // ExecutableType + public final javax.lang.model.type.TypeMirror getReturnType() { + final javax.lang.model.type.TypeMirror t = this.returnType; + return t == null ? NoType.VOID : t; + } + + public final void setReturnType(final javax.lang.model.type.TypeMirror t) { + final Object old = this.returnType; + if (old == null) { + if (t != null) { + this.returnType = validateReturnType(t); + } + } else if (old != t) { + throw new IllegalStateException(); + } + } + + private final javax.lang.model.type.TypeMirror validateParameterType(final javax.lang.model.type.TypeMirror t) { + if (t == this) { + throw new IllegalArgumentException("t: " + t); + } + return Objects.requireNonNull(t, "t"); + } + + private final javax.lang.model.type.TypeMirror validateReturnType(final javax.lang.model.type.TypeMirror t) { + if (t == this) { + throw new IllegalArgumentException("t: " + t); + } + return t == null ? NoType.VOID : t; + } + + private final javax.lang.model.type.TypeMirror validateReceiverType(final javax.lang.model.type.TypeMirror t) { + if (t == this) { + throw new IllegalArgumentException("t: " + t); + } + return t == null ? NoType.NONE : t; + } + + private final javax.lang.model.type.TypeMirror validateThrownType(final javax.lang.model.type.TypeMirror t) { + if (t == this) { + throw new IllegalArgumentException("t: " + t); + } + return Objects.requireNonNull(t, "t"); + } + + private static final TypeVariable validateTypeVariable(final TypeVariable t) { + return Objects.requireNonNull(t, "t"); + } + +} diff --git a/lang/src/main/java/org/microbean/lang/type/IntersectionType.java b/lang/src/main/java/org/microbean/lang/type/IntersectionType.java new file mode 100644 index 00000000..9a73b40c --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/type/IntersectionType.java @@ -0,0 +1,98 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2022–2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.type; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Objects; +import java.util.StringJoiner; + +import javax.lang.model.AnnotatedConstruct; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.ElementKind; + +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeVariable; +import javax.lang.model.type.TypeVisitor; + +public final class IntersectionType extends TypeMirror implements javax.lang.model.type.IntersectionType { + + private final List bounds; + + private final List unmodifiableBounds; + + public IntersectionType() { + super(TypeKind.INTERSECTION); + this.bounds = new ArrayList<>(5); + this.unmodifiableBounds = Collections.unmodifiableList(this.bounds); + } + + public IntersectionType(final List bounds) { + this(); + this.addBounds(bounds); + } + + @Override + public final void addAnnotationMirror(final AnnotationMirror a) { + throw new UnsupportedOperationException(); + } + + @Override // TypeMirror + public final R accept(final TypeVisitor v, final P p) { + return v.visitIntersection(this, p); + } + + @Override // IntersectionType + public final List getBounds() { + return this.unmodifiableBounds; + } + + public final void addBound(final javax.lang.model.type.TypeMirror bound) { + this.bounds.add(validateBound(bound, this.bounds.size())); + } + + public final void addBounds(final Iterable bounds) { + for (final javax.lang.model.type.TypeMirror bound : bounds) { + this.addBound(bound); + } + } + + @Override + public final String toString() { + final StringJoiner sj = new StringJoiner(" & "); + for (final javax.lang.model.type.TypeMirror bound : this.bounds) { + sj.add(bound.toString()); + } + return sj.toString(); + } + + private final javax.lang.model.type.TypeMirror validateBound(final javax.lang.model.type.TypeMirror bound, final int index) { + if (bound.getKind() != TypeKind.DECLARED || index > 0 && !((DeclaredType)bound).asElement().getKind().isInterface()) { + throw new IllegalArgumentException("bound: " + bound + "; index: " + index); + } + return bound; + } + + public static IntersectionType of(final List bounds) { + return new IntersectionType(bounds); + } + +} diff --git a/lang/src/main/java/org/microbean/lang/type/NoType.java b/lang/src/main/java/org/microbean/lang/type/NoType.java new file mode 100644 index 00000000..26309ba6 --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/type/NoType.java @@ -0,0 +1,110 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2022–2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.type; + +import java.util.List; + +import javax.lang.model.element.AnnotationMirror; + +import javax.lang.model.type.TypeKind; + +public final class NoType extends TypeMirror implements javax.lang.model.type.NoType { + + + /* + * Static fields. + */ + + + public static final NoType NONE = new NoType(javax.lang.model.type.TypeKind.NONE); + + public static final NoType MODULE = new NoType(javax.lang.model.type.TypeKind.MODULE); + + public static final NoType PACKAGE = new NoType(javax.lang.model.type.TypeKind.PACKAGE); + + public static final NoType VOID = new NoType(javax.lang.model.type.TypeKind.VOID); + + + /* + * Constructors. + */ + + + private NoType(final javax.lang.model.type.TypeKind kind) { + super(kind); + } + + + /* + * Instance methods + */ + + + @Override + public final void addAnnotationMirror(final AnnotationMirror a) { + throw new UnsupportedOperationException(); + } + + @Override // AnnotatedConstruct + public final List getAnnotationMirrors() { + return List.of(); + } + + protected final TypeKind validateKind(final TypeKind kind) { + switch (kind) { + case MODULE: + case NONE: + case PACKAGE: + case VOID: + return kind; + default: + throw new IllegalArgumentException("kind: " + kind); + } + } + + + /* + * Static methods. + */ + + + public static final NoType of(final TypeKind kind) { + if (kind == null) { + return NONE; + } + switch (kind) { + case MODULE: + return MODULE; + case NONE: + return NONE; + case PACKAGE: + return PACKAGE; + case VOID: + return VOID; + default: + throw new IllegalArgumentException("kind: " + kind); + } + } + + public static final NoType of(final javax.lang.model.type.TypeMirror t) { + if (t instanceof NoType noType) { + return noType; + } + return of(t.getKind()); + } + +} diff --git a/lang/src/main/java/org/microbean/lang/type/NullType.java b/lang/src/main/java/org/microbean/lang/type/NullType.java new file mode 100644 index 00000000..d0301354 --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/type/NullType.java @@ -0,0 +1,43 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2022–2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.type; + +import java.util.List; + +import javax.lang.model.element.AnnotationMirror; + +import javax.lang.model.type.TypeKind; + +public final class NullType extends TypeMirror implements javax.lang.model.type.NullType { + + public static final NullType INSTANCE = new NullType(); + + private NullType() { + super(TypeKind.NULL); + } + + @Override + public final void addAnnotationMirror(final AnnotationMirror a) { + throw new UnsupportedOperationException(); + } + + @Override // AnnotatedConstruct + public final List getAnnotationMirrors() { + return List.of(); + } + +} diff --git a/lang/src/main/java/org/microbean/lang/type/PrimitiveType.java b/lang/src/main/java/org/microbean/lang/type/PrimitiveType.java new file mode 100644 index 00000000..e6472c0d --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/type/PrimitiveType.java @@ -0,0 +1,167 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2022–2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.type; + +import javax.lang.model.element.AnnotationMirror; + +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeVisitor; + +public non-sealed class PrimitiveType extends TypeMirror implements javax.lang.model.type.PrimitiveType { + + + /* + * Static fields. + */ + + + public static final PrimitiveType BOOLEAN = new PrimitiveType(TypeKind.BOOLEAN) { + @Override + public void addAnnotationMirror(final AnnotationMirror a) { + throw new UnsupportedOperationException(); + } + }; + + public static final PrimitiveType BYTE = new PrimitiveType(TypeKind.BYTE) { + @Override + public void addAnnotationMirror(final AnnotationMirror a) { + throw new UnsupportedOperationException(); + } + }; + + public static final PrimitiveType CHAR = new PrimitiveType(TypeKind.CHAR) { + @Override + public void addAnnotationMirror(final AnnotationMirror a) { + throw new UnsupportedOperationException(); + } + }; + + public static final PrimitiveType DOUBLE = new PrimitiveType(TypeKind.DOUBLE) { + @Override + public void addAnnotationMirror(final AnnotationMirror a) { + throw new UnsupportedOperationException(); + } + }; + + public static final PrimitiveType FLOAT = new PrimitiveType(TypeKind.FLOAT) { + @Override + public void addAnnotationMirror(final AnnotationMirror a) { + throw new UnsupportedOperationException(); + } + }; + + public static final PrimitiveType INT = new PrimitiveType(TypeKind.INT) { + @Override + public void addAnnotationMirror(final AnnotationMirror a) { + throw new UnsupportedOperationException(); + } + }; + + public static final PrimitiveType LONG = new PrimitiveType(TypeKind.LONG) { + @Override + public void addAnnotationMirror(final AnnotationMirror a) { + throw new UnsupportedOperationException(); + } + }; + + public static final PrimitiveType SHORT = new PrimitiveType(TypeKind.SHORT) { + @Override + public void addAnnotationMirror(final AnnotationMirror a) { + throw new UnsupportedOperationException(); + } + }; + + + /* + * Constructors. + */ + + + public PrimitiveType(final TypeKind kind) { + super(kind); + } + + + /* + * Instance methods. + */ + + + @Override // TypeMirror + public final R accept(final TypeVisitor v, final P p) { + return v.visitPrimitive(this, p); + } + + @Override + public final String toString() { + return this.getKind().toString().toLowerCase(); + } + + @Override + protected final TypeKind validateKind(final TypeKind kind) { + switch (kind) { + case BOOLEAN: + case BYTE: + case CHAR: + case DOUBLE: + case FLOAT: + case INT: + case LONG: + case SHORT: + return kind; + default: + throw new IllegalArgumentException("kind: " + kind); + } + } + + + /* + * Static methods. + */ + + + public static PrimitiveType of(final javax.lang.model.type.TypeMirror t) { + if (t instanceof PrimitiveType p) { + return p; + } + return of(t.getKind()); + } + + public static PrimitiveType of(final TypeKind kind) { + switch (kind) { + case BOOLEAN: + return BOOLEAN; + case BYTE: + return BYTE; + case CHAR: + return CHAR; + case DOUBLE: + return DOUBLE; + case FLOAT: + return FLOAT; + case INT: + return INT; + case LONG: + return LONG; + case SHORT: + return SHORT; + default: + throw new IllegalArgumentException("kind: " + kind); + } + } + +} diff --git a/lang/src/main/java/org/microbean/lang/type/TypeMirror.java b/lang/src/main/java/org/microbean/lang/type/TypeMirror.java new file mode 100644 index 00000000..b68cfc0c --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/type/TypeMirror.java @@ -0,0 +1,89 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2022–2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.type; + +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import javax.lang.model.element.AnnotationMirror; + +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeVisitor; + +import org.microbean.lang.AnnotatedConstruct; + +public abstract sealed class TypeMirror + extends AnnotatedConstruct + implements javax.lang.model.type.TypeMirror + permits ArrayType, + DefineableType, + ExecutableType, + IntersectionType, + NoType, + NullType, + PrimitiveType, + UnionType, + WildcardType { + + private final javax.lang.model.type.TypeKind kind; + + + /* + * Constructors. + */ + + + protected TypeMirror(final javax.lang.model.type.TypeKind kind) { + super(); + this.kind = this.validateKind(kind); + } + + + /* + * Instance methods. + */ + + + @Override // TypeMirror + public final javax.lang.model.type.TypeKind getKind() { + return this.kind; + } + + @Override // TypeMirror + public R accept(final TypeVisitor v, final P p) { + return switch (this.getKind()) { + case ARRAY -> v.visitArray((javax.lang.model.type.ArrayType)this, p); + case DECLARED -> v.visitDeclared((javax.lang.model.type.DeclaredType)this, p); + case ERROR -> v.visitError((javax.lang.model.type.ErrorType)this, p); + case EXECUTABLE -> v.visitExecutable((javax.lang.model.type.ExecutableType)this, p); + case INTERSECTION -> v.visitIntersection((javax.lang.model.type.IntersectionType)this, p); + case MODULE, NONE, PACKAGE, VOID -> v.visitNoType((javax.lang.model.type.NoType)this, p); + case NULL -> v.visitNull((javax.lang.model.type.NullType)this, p); + case BOOLEAN, BYTE, CHAR, DOUBLE, FLOAT, INT, LONG, SHORT -> v.visitPrimitive((javax.lang.model.type.PrimitiveType)this, p); + case TYPEVAR -> v.visitTypeVariable((javax.lang.model.type.TypeVariable)this, p); + case UNION -> v.visitUnion((javax.lang.model.type.UnionType)this, p); + case WILDCARD -> v.visitWildcard((javax.lang.model.type.WildcardType)this, p); + case OTHER -> v.visitUnknown(this, p); + }; + } + + protected TypeKind validateKind(final TypeKind kind) { + return Objects.requireNonNull(kind, "kind"); + } + +} diff --git a/lang/src/main/java/org/microbean/lang/type/TypeVariable.java b/lang/src/main/java/org/microbean/lang/type/TypeVariable.java new file mode 100644 index 00000000..d14c266d --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/type/TypeVariable.java @@ -0,0 +1,137 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2022–2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.type; + +import java.util.List; +import java.util.Objects; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.TypeParameterElement; + +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVisitor; + +import org.microbean.lang.TypeAndElementSource; + +public non-sealed class TypeVariable extends DefineableType implements javax.lang.model.type.TypeVariable { + + private final TypeAndElementSource elementSource; + + private TypeMirror upperBound; + + private TypeMirror lowerBound; + + public TypeVariable(final TypeAndElementSource elementSource) { + super(TypeKind.TYPEVAR); + this.elementSource = Objects.requireNonNull(elementSource, "elementSource"); + } + + public TypeVariable(final TypeAndElementSource elementSource, final TypeMirror upperBound) { + this(elementSource); + this.setUpperBound(upperBound); + } + + public TypeVariable(final TypeAndElementSource elementSource, final TypeMirror upperBound, final TypeMirror lowerBound) { + this(elementSource); + this.setUpperBound(upperBound); + this.setLowerBound(lowerBound); + } + + @Override // TypeVariable + public final TypeMirror getLowerBound() { + final TypeMirror t = this.lowerBound; + return t == null ? NullType.INSTANCE : t; // this is correct; can't just return null + } + + public final void setLowerBound(final TypeMirror lowerBound) { + final Object old = this.lowerBound; + if (old == null) { + if (lowerBound != null) { + this.lowerBound = validateLowerBound(lowerBound); + } + } else if (old != lowerBound) { + throw new IllegalStateException(); + } + } + + @Override // TypeVariable + public final TypeMirror getUpperBound() { + final TypeMirror t = this.upperBound; + return t == null ? this.elementSource.typeElement("java.lang.Object").asType() : t; // this is correct; can't just return null + } + + public final void setUpperBound(final TypeMirror upperBound) { + final Object old = this.upperBound; + if (old == null) { + if (upperBound != null) { + this.upperBound = validateUpperBound(upperBound); + } + } else if (old != upperBound) { + throw new IllegalStateException(); + } + } + + @Override // DefineableType + protected TypeParameterElement validateDefiningElement(final TypeParameterElement e) { + if (e.getKind() != ElementKind.TYPE_PARAMETER) { + throw new IllegalArgumentException("e: " + e); + } + final Object t = e.asType(); + if (t != null && this != t) { + throw new IllegalArgumentException("e: " + e + "; this (" + this + ") != e.asType() (" + t + ")"); + } + return e; + } + + @Override // TypeVariable + public R accept(final TypeVisitor v, final P p) { + return v.visitTypeVariable(this, p); + } + + @Override // Object + public String toString() { + return super.toString() + " extends " + this.getUpperBound(); + } + + private final TypeMirror validateUpperBound(final TypeMirror upperBound) { + switch (upperBound.getKind()) { + case DECLARED: + case INTERSECTION: + return upperBound; + case TYPEVAR: + if (upperBound == this) { + throw new IllegalArgumentException("upperBound: " + upperBound); + } + return upperBound; + default: + throw new IllegalArgumentException("upperBound: " + upperBound); + } + } + + + /* + * Static methods. + */ + + + private static final TypeMirror validateLowerBound(final TypeMirror lowerBound) { + return lowerBound == null ? NullType.INSTANCE : lowerBound; + } + +} diff --git a/lang/src/main/java/org/microbean/lang/type/Types.java b/lang/src/main/java/org/microbean/lang/type/Types.java new file mode 100644 index 00000000..5bf1dd23 --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/type/Types.java @@ -0,0 +1,598 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2022–2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.type; + +import java.lang.annotation.Annotation; + +import java.lang.ref.Reference; +import java.lang.ref.WeakReference; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.Set; +import java.util.WeakHashMap; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ElementVisitor; + +import javax.lang.model.type.TypeKind; + +import org.microbean.lang.TypeAndElementSource; + +public final class Types { + + private static final Annotation[] EMPTY_ANNOTATION_ARRAY = new Annotation[0]; + + // @see #asElement(TypeMirror, boolean) + // @GuardedBy("itself") + private static final WeakHashMap syntheticElements = new WeakHashMap<>(); + + private final TypeAndElementSource es; + + public Types(final TypeAndElementSource elementSource) { + super(); + this.es = Objects.requireNonNull(elementSource, "elementSource"); + } + + public final javax.lang.model.type.TypeMirror extendsBound(final javax.lang.model.type.TypeMirror t) { + // javac's "wildUpperBound". + // See + // https://github.com/openjdk/jdk/blob/jdk-20+12/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L130-L143 + switch (t.getKind()) { + case WILDCARD: + final javax.lang.model.type.WildcardType w = (javax.lang.model.type.WildcardType)t; + final javax.lang.model.type.TypeMirror superBound = w.getSuperBound(); + if (superBound == null) { + // Unbounded or upper-bounded. + final javax.lang.model.type.TypeMirror extendsBound = w.getExtendsBound(); + if (extendsBound == null) { + // Unbounded, so upper bound is Object. + return this.es.typeElement("java.lang.Object").asType(); + } else { + // Upper-bounded. + assert + extendsBound.getKind() == TypeKind.ARRAY || + extendsBound.getKind() == TypeKind.DECLARED || + extendsBound.getKind() == TypeKind.TYPEVAR : + "extendsBound kind: " + extendsBound.getKind(); + return extendsBound; + } + } else { + // See + // https://github.com/openjdk/jdk/blob/jdk-20+12/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L138. + // A (javac) WildcardType's bound field is NOT the same as its type field. The lang model only exposes the type + // field. + // + // Consider some context like this: + // + // interface Foo {} + // + // And then a wildcard like this: + // + // Foo f; + // + // The (javac) WildcardType's bound field will be initialized to T extends Serializable. Its type field will be + // initialized to String. wildUpperBound(thisWildcardType) will return Serializable.class, not Object.class. + // + // The lang model makes this impossible, because bound is not exposed, and getSuperBound() doesn't do anything + // fancy to return it. + // + // Dan Smith writes: + // + // "It turns out 'bound' is used to represent the *corresponding type parameter* of the wildcard, where + // additional bounds can be found (for things like capture conversion), not anything about the wildcard itself." + // + // And: + // + // "Honestly, it's a little sketchy that the compiler internals are doing this at all. I'm not totally sure + // that, for example, uses of 'wildUpperBound' aren't violating the language spec somewhere. For lang.model, no, + // the 'bound' field is not at all part of the specified API, so shouldn't have any impact on API behavior. + // + // "(The right thing to do, per the language spec, to incorporate the corresponding type parameter bounds, is to + // perform capture on the wildcard's enclosing parameterized type, and then work with the resulting capture type + // variables.)" + // + // So bound gets set to T extends Serializable. There is no way to extract T extends Serializable from a + // javax.lang.model.type.WildcardType, and without that ability we have no other information, so we must return + // Object.class. + return this.es.typeElement("java.lang.Object").asType(); + } + default: + return t; + } + } + + public final javax.lang.model.type.TypeMirror superBound(final javax.lang.model.type.TypeMirror t) { + // See + // https://github.com/openjdk/jdk/blob/jdk-20+12/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L157-L167 + return switch (t.getKind()) { + case WILDCARD -> { + final javax.lang.model.type.TypeMirror superBound = ((javax.lang.model.type.WildcardType)t).getSuperBound(); + yield superBound == null ? org.microbean.lang.type.NullType.INSTANCE : superBound; + } + default -> t; + }; + } + + public final javax.lang.model.type.TypeMirror box(final javax.lang.model.type.TypeMirror t) { + return t == null ? null : switch (t.getKind()) { + case BOOLEAN -> this.es.typeElement("java.lang.Boolean").asType(); + case BYTE -> this.es.typeElement("java.lang.Byte").asType(); + case CHAR -> this.es.typeElement("java.lang.Character").asType(); + case DOUBLE -> this.es.typeElement("java.lang.Double").asType(); + case FLOAT -> this.es.typeElement("java.lang.Float").asType(); + case INT -> this.es.typeElement("java.lang.Integer").asType(); + case LONG -> this.es.typeElement("java.lang.Long").asType(); + case SHORT -> this.es.typeElement("java.lang.Short").asType(); + case VOID -> this.es.typeElement("java.lang.Void").asType(); + default -> t; + }; + } + + public final javax.lang.model.type.TypeMirror unbox(final javax.lang.model.type.TypeMirror t) { + return t == null ? null : switch (t.getKind()) { + case DECLARED -> switch (((javax.lang.model.element.QualifiedNameable)((javax.lang.model.type.DeclaredType)t).asElement()).getQualifiedName().toString()) { + case "java.lang.Boolean" -> org.microbean.lang.type.PrimitiveType.BOOLEAN; + case "java.lang.Byte" -> org.microbean.lang.type.PrimitiveType.BYTE; + case "java.lang.Character" -> org.microbean.lang.type.PrimitiveType.CHAR; + case "java.lang.Double" -> org.microbean.lang.type.PrimitiveType.DOUBLE; + case "java.lang.Float" -> org.microbean.lang.type.PrimitiveType.FLOAT; + case "java.lang.Integer" -> org.microbean.lang.type.PrimitiveType.INT; + case "java.lang.Long" -> org.microbean.lang.type.PrimitiveType.LONG; + case "java.lang.Short" -> org.microbean.lang.type.PrimitiveType.SHORT; + case "java.lang.Void" -> org.microbean.lang.type.NoType.VOID; + default -> t; + }; + default -> t; + }; + } + + // Not visitor-based in javac + public static final List allTypeArguments(final javax.lang.model.type.TypeMirror t) { + return t == null ? List.of() : switch (t.getKind()) { + case ARRAY -> allTypeArguments(((javax.lang.model.type.ArrayType)t).getComponentType()); // RECURSIVE + case DECLARED -> allTypeArguments((javax.lang.model.type.DeclaredType)t); + default -> List.of(); + }; + } + + // com.foo.Foo.Baz will yield [Bar, String] when handed the DeclaredType denoted by Baz + public static final List allTypeArguments(final javax.lang.model.type.DeclaredType t) { + if (t == null) { + return List.of(); + } else if (t.getKind() != TypeKind.DECLARED) { + throw new IllegalArgumentException("t: " + t); + } + final List enclosingTypeTypeArguments = allTypeArguments(t.getEnclosingType()); // RECURSIVE + final List typeArguments = t.getTypeArguments(); + if (enclosingTypeTypeArguments.isEmpty()) { + return typeArguments.isEmpty() ? List.of() : typeArguments; + } else if (typeArguments.isEmpty()) { + return enclosingTypeTypeArguments; + } else { + final List list = new ArrayList<>(enclosingTypeTypeArguments.size() + typeArguments.size()); + list.addAll(enclosingTypeTypeArguments); + list.addAll(typeArguments); + return Collections.unmodifiableList(list); + } + } + + public static final javax.lang.model.element.Element asElement(final javax.lang.model.type.TypeMirror t, final boolean generateSyntheticElements) { + // TypeMirror#asElement() says: + // + // "Returns the element corresponding to a type. The type may be a DeclaredType or TypeVariable. Returns null if + // the type is not one with a corresponding element." + // + // This does not correspond at *all* to the innards of javac, where nearly every type has an associated element, + // even where it makes no sense. For example, error types, intersection types, executable types (!), primitive + // types and wildcard types (which aren't even types!) all have elements somehow, but these are not in the lang + // model. + // + // Although, see + // https://github.com/openjdk/jdk/blob/jdk-20%2B12/src/jdk.compiler/share/classes/com/sun/tools/javac/model/JavacTypes.java#L76, + // which *is* in the lang model and will return an element for an intersection type. What a mess. + // + // Much of javac's algorithmic behavior is based on most types having elements, even where the elements make no + // sense. In fact, the only types in javac that have no elements at all are: + // + // * no type + // * void type + // * null type + // * unknown type + // + // Symbols in javac do not override their equals()/hashCode() methods, so no two symbols are ever the same. + // + // We blend all these facts together and set up synthetic elements for types that, in the lang model, don't have + // them, but do have them behind the scenes in javac. We use a WeakHashMap to associate TypeMirror instances with + // their synthetic elements. + if (t == null) { + return null; + } + switch (t.getKind()) { + + case DECLARED: + return ((javax.lang.model.type.DeclaredType)t).asElement(); + + case TYPEVAR: + return ((javax.lang.model.type.TypeVariable)t).asElement(); + + case ARRAY: + if (generateSyntheticElements) { + synchronized (syntheticElements) { + // The compiler uses exactly one synthetic element for all + // array types. + return syntheticElements.computeIfAbsent(t, arrayType -> SyntheticArrayElement.INSTANCE); + } + } + return null; + + case EXECUTABLE: + // This is really problematic. There *is* an ExecutableElement in the lang model, and an ExecutableType, but they + // aren't related in the way that, say, DeclaredType and TypeElement are. javac seems to use a singleton synthetic + // ClassType (!) for all method symbols. I'm not sure what to do here. I'm going to leave it null for now. + /* + if (generateSyntheticElements) { + synchronized (syntheticElements) { + return syntheticElements.computeIfAbsent(t, s -> SyntheticExecutableElement.INSTANCE); + } + } + */ + return null; + + case WILDCARD: + if (generateSyntheticElements) { + synchronized (syntheticElements) { + // The compiler users exactly one synthetic element for all wildcard types. + return syntheticElements.computeIfAbsent(t, wildcardType -> SyntheticWildcardElement.INSTANCE); + } + } + return null; + + case BOOLEAN: + if (generateSyntheticElements) { + synchronized (syntheticElements) { + // The compiler users exactly one synthetic element for a given kind of primitive type. + return syntheticElements.computeIfAbsent(t, booleanType -> SyntheticPrimitiveElement.BOOLEAN); + } + } + return null; + + case BYTE: + if (generateSyntheticElements) { + synchronized (syntheticElements) { + // The compiler users exactly one synthetic element for a given kind of primitive type. + return syntheticElements.computeIfAbsent(t, byteType -> SyntheticPrimitiveElement.BYTE); + } + } + return null; + + case CHAR: + if (generateSyntheticElements) { + synchronized (syntheticElements) { + // The compiler users exactly one synthetic element for a given kind of primitive type. + return syntheticElements.computeIfAbsent(t, charType -> SyntheticPrimitiveElement.CHAR); + } + } + return null; + + case DOUBLE: + if (generateSyntheticElements) { + synchronized (syntheticElements) { + // The compiler users exactly one synthetic element for a given kind of primitive type. + return syntheticElements.computeIfAbsent(t, doubleType -> SyntheticPrimitiveElement.DOUBLE); + } + } + return null; + + case FLOAT: + if (generateSyntheticElements) { + synchronized (syntheticElements) { + // The compiler users exactly one synthetic element for a given kind of primitive type. + return syntheticElements.computeIfAbsent(t, floatType -> SyntheticPrimitiveElement.FLOAT); + } + } + return null; + + case INT: + if (generateSyntheticElements) { + synchronized (syntheticElements) { + // The compiler users exactly one synthetic element for a given kind of primitive type. + return syntheticElements.computeIfAbsent(t, intType -> SyntheticPrimitiveElement.INT); + } + } + return null; + + case LONG: + if (generateSyntheticElements) { + synchronized (syntheticElements) { + // The compiler users exactly one synthetic element for a given kind of primitive type. + return syntheticElements.computeIfAbsent(t, longType -> SyntheticPrimitiveElement.LONG); + } + } + return null; + + case SHORT: + if (generateSyntheticElements) { + synchronized (syntheticElements) { + // The compiler users exactly one synthetic element for a given kind of primitive type. + return syntheticElements.computeIfAbsent(t, shortType -> SyntheticPrimitiveElement.SHORT); + } + } + return null; + + case INTERSECTION: + case MODULE: + case PACKAGE: + if (generateSyntheticElements) { + synchronized (syntheticElements) { + // The compiler uses one instance of a bogus element for each instance of one of these types. + return syntheticElements.computeIfAbsent(t, SyntheticElement::new); + } + } + return null; + + case OTHER: + case NONE: + case NULL: + case VOID: + return null; + + case ERROR: + case UNION: + default: + throw new IllegalArgumentException("t: " + t); + } + } + + // Return the javax.lang.model.type.TypeMirror representing the declaration whose type may currently be being used. + // E.g. given a type denoted by List, return the type denoted by List (from List's usage of List) + // + // If it is passed something funny, it just returns what it was passed instead. The compiler does this a lot and I + // think it's confusing. + // + // I don't like this name. + @SuppressWarnings("unchecked") + public static final T typeDeclaration(final T t) { + final javax.lang.model.element.Element e = asElement(t, false /* don't generate synthetic elements */); + return e == null ? t : (T)e.asType(); + } + + public static final boolean hasTypeArguments(final javax.lang.model.type.TypeMirror t) { + // This is modeled after javac's allparams() method. javac frequently confuses type parameters and type arguments + // in its terminology. This implementation could probably be made more efficient. See + // https://github.com/openjdk/jdk/blob/67ecd30327086c5d7628c4156f8d9dcccb0f4d09/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Type.java#L1137 + return switch (t.getKind()) { + case ARRAY, DECLARED -> !allTypeArguments(t).isEmpty(); + default -> false; + }; + } + + public static final boolean isInterface(final javax.lang.model.element.Element e) { + return e.getKind().isInterface(); + } + + public static final boolean isInterface(final javax.lang.model.type.TypeMirror t) { + return t.getKind() == TypeKind.DECLARED && ((javax.lang.model.type.DeclaredType)t).asElement().getKind().isInterface(); + } + + public static final boolean isStatic(final javax.lang.model.element.Element e) { + return e.getModifiers().contains(javax.lang.model.element.Modifier.STATIC); + } + + // See + // https://github.com/openjdk/jdk/blob/67ecd30327086c5d7628c4156f8d9dcccb0f4d09/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Type.java#L1154-L1164 + public final boolean raw(final javax.lang.model.type.TypeMirror t) { + return t == null ? false : switch (t.getKind()) { + case ARRAY -> raw(((ArrayType)t).getComponentType()); + case DECLARED -> { + final javax.lang.model.type.TypeMirror typeDeclaration = typeDeclaration(t); + yield + t != typeDeclaration && // t is a parameterized type, i.e. a type usage, and + hasTypeArguments(typeDeclaration) && // the type it parameterizes has type arguments (type variables declared by type parameters) and + !hasTypeArguments(t); // t does not supply type arguments + } + default -> false; + }; + } + + public static final javax.lang.model.type.WildcardType unboundedWildcardType() { + return new org.microbean.lang.type.WildcardType(); + } + + public static final javax.lang.model.type.WildcardType unboundedWildcardType(final List annotationMirrors) { + final org.microbean.lang.type.WildcardType t = new org.microbean.lang.type.WildcardType(); + t.addAnnotationMirrors(annotationMirrors); + return t; + } + + public static final javax.lang.model.type.WildcardType upperBoundedWildcardType(final javax.lang.model.type.TypeMirror upperBound, + final List annotationMirrors) { + final org.microbean.lang.type.WildcardType t = new org.microbean.lang.type.WildcardType(upperBound); + t.addAnnotationMirrors(annotationMirrors); + return t; + } + + + /* + * Inner and nested classes. + */ + + + private abstract static class AbstractSyntheticElement implements javax.lang.model.element.Element { + + private final javax.lang.model.type.TypeMirror type; + + private final javax.lang.model.element.Name name; + + private AbstractSyntheticElement(final javax.lang.model.type.TypeMirror type, final javax.lang.model.element.Name name) { + super(); + this.type = type; + this.name = name; + } + + @Override + public R accept(final ElementVisitor v, final P p) { + return v.visitUnknown(this, p); + } + + @Override + public javax.lang.model.type.TypeMirror asType() { + return this.type; + } + + @Override + public final A getAnnotation(final Class annotationType) { + return null; + } + + @Override + public final List getAnnotationMirrors() { + return List.of(); + } + + @Override + @SuppressWarnings("unchecked") + public final A[] getAnnotationsByType(final Class annotationType) { + return (A[])EMPTY_ANNOTATION_ARRAY; + } + + @Override + public final List getEnclosedElements() { + return List.of(); + } + + @Override + public final javax.lang.model.element.Element getEnclosingElement() { + return null; + } + + @Override + public ElementKind getKind() { + return ElementKind.OTHER; + } + + @Override + public Set getModifiers() { + return Set.of(); + } + + @Override + public javax.lang.model.element.Name getSimpleName() { + return this.name; + } + + } + + private static final class SyntheticArrayElement extends AbstractSyntheticElement { + + private static final SyntheticArrayElement INSTANCE = new SyntheticArrayElement(); + + private SyntheticArrayElement() { + super(new org.microbean.lang.type.DeclaredType(), org.microbean.lang.element.Name.of("Array")); // emulate javac + } + + @Override + public final ElementKind getKind() { + return ElementKind.CLASS; // emulate javac + } + + } + + private static final class SyntheticElement extends AbstractSyntheticElement { + + private final Reference type; + + private SyntheticElement(final javax.lang.model.type.TypeMirror t) { + super(null, generateName(t)); + this.type = new WeakReference<>(t); + } + + @Override + public final javax.lang.model.type.TypeMirror asType() { + final javax.lang.model.type.TypeMirror t = this.type.get(); + return t == null ? org.microbean.lang.type.NoType.NONE : t; + } + + private static final javax.lang.model.element.Name generateName(final javax.lang.model.type.TypeMirror t) { + return org.microbean.lang.element.Name.of(); // TODO if it turns out to be important + } + + } + + private static final class SyntheticPrimitiveElement extends AbstractSyntheticElement { + + + /* + * Static fields. + */ + + + private static final SyntheticPrimitiveElement BOOLEAN = new SyntheticPrimitiveElement(org.microbean.lang.element.Name.of("boolean"), org.microbean.lang.type.PrimitiveType.BOOLEAN); + + private static final SyntheticPrimitiveElement BYTE = new SyntheticPrimitiveElement(org.microbean.lang.element.Name.of("byte"), org.microbean.lang.type.PrimitiveType.BYTE); + + private static final SyntheticPrimitiveElement CHAR = new SyntheticPrimitiveElement(org.microbean.lang.element.Name.of("char"), org.microbean.lang.type.PrimitiveType.CHAR); + + private static final SyntheticPrimitiveElement DOUBLE = new SyntheticPrimitiveElement(org.microbean.lang.element.Name.of("double"), org.microbean.lang.type.PrimitiveType.DOUBLE); + + private static final SyntheticPrimitiveElement FLOAT = new SyntheticPrimitiveElement(org.microbean.lang.element.Name.of("float"), org.microbean.lang.type.PrimitiveType.FLOAT); + + private static final SyntheticPrimitiveElement INT = new SyntheticPrimitiveElement(org.microbean.lang.element.Name.of("int"), org.microbean.lang.type.PrimitiveType.INT); + + private static final SyntheticPrimitiveElement LONG = new SyntheticPrimitiveElement(org.microbean.lang.element.Name.of("long"), org.microbean.lang.type.PrimitiveType.LONG); + + private static final SyntheticPrimitiveElement SHORT = new SyntheticPrimitiveElement(org.microbean.lang.element.Name.of("short"), org.microbean.lang.type.PrimitiveType.SHORT); + + + /* + * Constructors. + */ + + + private SyntheticPrimitiveElement(final javax.lang.model.element.Name name, final javax.lang.model.type.PrimitiveType type) { + super(validateType(type), name); + } + + private static final javax.lang.model.type.TypeMirror validateType(final javax.lang.model.type.TypeMirror t) { + if (!t.getKind().isPrimitive()) { + throw new IllegalArgumentException("t: " + t); + } + return t; + } + + } + + private static final class SyntheticWildcardElement extends AbstractSyntheticElement { + + private static final SyntheticWildcardElement INSTANCE = new SyntheticWildcardElement(); + + private SyntheticWildcardElement() { + super(new org.microbean.lang.type.DeclaredType(), + org.microbean.lang.element.Name.of("Bound")); // emulate javac + } + + @Override + public final ElementKind getKind() { + return ElementKind.CLASS; // emulate javac + } + + } + +} diff --git a/lang/src/main/java/org/microbean/lang/type/UnionType.java b/lang/src/main/java/org/microbean/lang/type/UnionType.java new file mode 100644 index 00000000..948d5ba8 --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/type/UnionType.java @@ -0,0 +1,86 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2022–2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.type; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import javax.lang.model.element.AnnotationMirror; + +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeVisitor; + +public non-sealed class UnionType extends TypeMirror implements javax.lang.model.type.UnionType { + + // I don't even know if this is legal. + public static final UnionType EMPTY = new UnionType() { + @Override + public final void addAlternative(final TypeMirror t) { + throw new UnsupportedOperationException(); + } + }; + + private final List alternatives; + + private final List unmodifiableAlternatives; + + public UnionType() { + super(TypeKind.UNION); + this.alternatives = new ArrayList<>(5); + this.unmodifiableAlternatives = Collections.unmodifiableList(this.alternatives); + } + + @Override + public final void addAnnotationMirror(final AnnotationMirror a) { + throw new UnsupportedOperationException(); + } + + @Override // AnnotatedConstruct + public final List getAnnotationMirrors() { + return List.of(); + } + + @Override // TypeMirror + public R accept(final TypeVisitor v, final P p) { + return v.visitUnion(this, p); + } + + @Override + public final List getAlternatives() { + return this.unmodifiableAlternatives; + } + + public void addAlternative(final TypeMirror t) { + this.alternatives.add(validateAlternative(t)); + } + + public final void addAlternatives(final Iterable ts) { + for (final TypeMirror t : ts) { + this.addAlternative(t); + } + } + + private final TypeMirror validateAlternative(final TypeMirror t) { + if (Objects.requireNonNull(t, "t") == this) { + throw new IllegalArgumentException("t: " + t); + } + return t; + } + +} diff --git a/lang/src/main/java/org/microbean/lang/type/WildcardType.java b/lang/src/main/java/org/microbean/lang/type/WildcardType.java new file mode 100644 index 00000000..1c37c18b --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/type/WildcardType.java @@ -0,0 +1,167 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2022–2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.type; + +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeVisitor; + +public final class WildcardType extends TypeMirror implements javax.lang.model.type.WildcardType { + + + /* + * Instance fields. + */ + + + private javax.lang.model.type.TypeMirror extendsBound; + + private javax.lang.model.type.TypeMirror superBound; + + + /* + * Constructors. + */ + + + public WildcardType() { + super(TypeKind.WILDCARD); + } + + public WildcardType(final javax.lang.model.type.TypeMirror extendsBound) { + this(); + this.setExtendsBound(extendsBound); + } + + public WildcardType(final javax.lang.model.type.TypeMirror extendsBound, final javax.lang.model.type.TypeMirror superBound) { + this(); + this.setExtendsBound(extendsBound); + this.setSuperBound(superBound); + } + + + /* + * Instance methods. + */ + + + @Override // TypeMirror + public final R accept(final TypeVisitor v, final P p) { + return v.visitWildcard(this, p); + } + + @Override // WildcardType + public final javax.lang.model.type.TypeMirror getExtendsBound() { + return this.extendsBound; + } + + public final void setExtendsBound(final javax.lang.model.type.TypeMirror t) { + final Object old = this.getExtendsBound(); + if (old == null) { + if (t != null) { + this.extendsBound = validateExtendsBound(t); + } + } else if (old != t) { + throw new IllegalStateException(); + } + } + + @Override // WildcardType + public final javax.lang.model.type.TypeMirror getSuperBound() { + return this.superBound; + } + + public final void setSuperBound(final javax.lang.model.type.TypeMirror t) { + final Object old = this.getSuperBound(); + if (old == null) { + if (t != null) { + this.superBound = validateSuperBound(t); + } + } else if (old != t) { + throw new IllegalStateException(); + } + } + + private final javax.lang.model.type.TypeMirror validateExtendsBound(final javax.lang.model.type.TypeMirror extendsBound) { + if (extendsBound == this) { + throw new IllegalArgumentException("extendsBound: " + extendsBound); + } + if (extendsBound != null) { + switch (extendsBound.getKind()) { + // See + // https://docs.oracle.com/javase/specs/jls/se18/html/jls-4.html#jls-WildcardBounds + // and + // https://docs.oracle.com/javase/specs/jls/se18/html/jls-4.html#jls-ReferenceType + case ARRAY: + case DECLARED: + case TYPEVAR: + break; + default: + throw new IllegalArgumentException("extendsBound: " + extendsBound); + } + } + return extendsBound; + } + + private final javax.lang.model.type.TypeMirror validateSuperBound(final javax.lang.model.type.TypeMirror superBound) { + if (superBound == this) { + throw new IllegalArgumentException("superBound: " + superBound); + } + if (superBound != null) { + switch (superBound.getKind()) { + // See + // https://docs.oracle.com/javase/specs/jls/se18/html/jls-4.html#jls-WildcardBounds + // and + // https://docs.oracle.com/javase/specs/jls/se18/html/jls-4.html#jls-ReferenceType + case ARRAY: + case DECLARED: + case TYPEVAR: + break; + default: + throw new IllegalArgumentException("superBound: " + superBound); + } + } + return superBound; + } + + public static WildcardType upperBoundedWildcardType(final javax.lang.model.type.TypeMirror extendsBound) { + return new WildcardType(extendsBound, null); + } + + public static WildcardType lowerBoundedWildcardType(final javax.lang.model.type.TypeMirror superBound) { + return new WildcardType(null, superBound); + } + + public static WildcardType unboundedWildcardType() { + return new WildcardType(null, null); + } + + public static WildcardType of(final javax.lang.model.type.TypeMirror extendsBound, + final javax.lang.model.type.TypeMirror superBound) { + if (extendsBound == null) { + if (superBound == null) { + return unboundedWildcardType(); + } else { + return lowerBoundedWildcardType(superBound); + } + } else if (superBound == null) { + return upperBoundedWildcardType(extendsBound); + } else { + throw new IllegalArgumentException("extendsBound: " + extendsBound + "; superBound: " + superBound); + } + } + +} diff --git a/lang/src/main/java/org/microbean/lang/type/package-info.java b/lang/src/main/java/org/microbean/lang/type/package-info.java new file mode 100644 index 00000000..16a50be5 --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/type/package-info.java @@ -0,0 +1,20 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ + +/** + * Provides classes and interfaces related to the Java language model. + * + * @author Laird Nelson + */ +package org.microbean.lang.type; diff --git a/lang/src/main/java/org/microbean/lang/visitor/AdaptingVisitor.java b/lang/src/main/java/org/microbean/lang/visitor/AdaptingVisitor.java new file mode 100644 index 00000000..66502c36 --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/visitor/AdaptingVisitor.java @@ -0,0 +1,219 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.visitor; + +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; +import javax.lang.model.type.WildcardType; + +import javax.lang.model.util.SimpleTypeVisitor14; + +import org.microbean.lang.TypeAndElementSource; + +import org.microbean.lang.element.DelegatingElement; + +import org.microbean.lang.type.Types; + +import static org.microbean.lang.type.Types.allTypeArguments; +import static org.microbean.lang.type.Types.asElement; + +/** + * Does something adapting-like. + * + *

Usage: call {@link #adapt(DeclaredType, DeclaredType)}, not {@link #visit(TypeMirror)}.

+ */ +// Not thread safe. +// Basically done. +// See https://github.com/openjdk/jdk/blob/jdk-20+13/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L4590-L4690 +final class AdaptingVisitor extends SimpleTypeVisitor14 { + + /* + * Ported mostly slavishly from the compiler. Some thoughts: + * + * The compiler code is really bad, but presumably battle-tested. I'm guessing that a list of "from" and a list of + * "to" rather than a Map was done on purpose. I cannot begin to think of what the purpose is. + */ + + // The compiler's implementation mutates this list. + private final List from; + + // The compiler's implementation mutates this list. + private final List to; + + private final TypeAndElementSource elementSource; + + private final Types types; + + private final Map mapping; + + private final SameTypeVisitor sameTypeVisitor; + + private final SubtypeVisitor subtypeVisitor; + + private final Set cache; + + AdaptingVisitor(final TypeAndElementSource elementSource, + final Types types, + final SameTypeVisitor sameTypeVisitor, + final SubtypeVisitor subtypeVisitor, + final List from, // mutated + final List to) { // mutated + super(); + this.elementSource = Objects.requireNonNull(elementSource, "elementSource"); + this.types = Objects.requireNonNull(types, "types"); + this.sameTypeVisitor = Objects.requireNonNull(sameTypeVisitor, "sameTypeVisitor"); + this.subtypeVisitor = Objects.requireNonNull(subtypeVisitor, "subtypeVisitor"); + this.mapping = new HashMap<>(); + this.cache = new HashSet<>(); + this.from = Objects.requireNonNull(from, "from"); + this.to = Objects.requireNonNull(to, "to"); + } + + final void adapt(final DeclaredType source, final DeclaredType target) { + this.visitDeclared(source, target); + final int fromSize = this.from.size(); + for (int i = 0; i < fromSize; i++) { + final TypeMirror val = this.mapping.get(DelegatingElement.of(asElement(this.from.get(i), true), this.elementSource)); + if (this.to.get(i) != val) { + this.to.set(i, val); + } + } + } + + final void adaptSelf(final DeclaredType target) { + this.adapt((DeclaredType)target.asElement().asType(), target); + } + + @Override + public final Void visitArray(final ArrayType source, final TypeMirror target) { + assert source.getKind() == TypeKind.ARRAY; + if (target.getKind() == TypeKind.ARRAY) { + this.adaptRecursive(source.getComponentType(), ((ArrayType)target).getComponentType()); + } + return null; + } + + @Override + public final Void visitDeclared(final DeclaredType source, final TypeMirror target) { + assert source.getKind() == TypeKind.DECLARED; + if (target.getKind() == TypeKind.DECLARED) { + this.adaptRecursive(allTypeArguments(source), allTypeArguments(target)); + } + return null; + } + + @Override + public final Void visitTypeVariable(final TypeVariable source, final TypeMirror target) { + assert source.getKind() == TypeKind.TYPEVAR; + final DelegatingElement sourceElement = DelegatingElement.of(source.asElement(), this.elementSource); + TypeMirror val = this.mapping.get(sourceElement); + if (val == null) { + val = target; + this.from.add(source); + this.to.add(target); + } else if (val.getKind() == TypeKind.WILDCARD && target.getKind() == TypeKind.WILDCARD) { + final WildcardType valWc = (WildcardType)val; + final TypeMirror valSuperBound = valWc.getSuperBound(); + final TypeMirror valExtendsBound = valWc.getExtendsBound(); + final WildcardType targetWc = (WildcardType)target; + final TypeMirror targetSuperBound = targetWc.getSuperBound(); + final TypeMirror targetExtendsBound = targetWc.getExtendsBound(); + if (valSuperBound == null) { + if (valExtendsBound == null) { + // valWc is lower-bounded (and upper-bounded) + if (targetExtendsBound == null && + this.subtypeVisitor.withCapture(true).visit(this.types.superBound(val), this.types.superBound(target))) { + // targetWc is lower-bounded (and maybe unbounded) + val = target; + } + } else if (targetSuperBound == null && + !this.subtypeVisitor.withCapture(true).visit(this.types.extendsBound(val), this.types.extendsBound(target))) { + // valWc is upper-bounded + // targetWc is upper-bounded (and maybe unbounded) + val = target; + } + } else if (valExtendsBound == null) { + // valWc is lower-bounded + if (targetExtendsBound == null && + this.subtypeVisitor.withCapture(true).visit(this.types.superBound(val), this.types.superBound(target))) { + // targetWc is lower-bounded (and maybe unbounded) + val = target; + } + } else { + throw new IllegalStateException("val: " + val); + } + } else if (!this.sameTypeVisitor.visit(val, target)) { + throw new IllegalStateException(); + } + this.mapping.put(sourceElement, val); + return null; + } + + @Override + public final Void visitWildcard(final WildcardType source, final TypeMirror target) { + assert source.getKind() == TypeKind.WILDCARD; + final TypeMirror extendsBound = source.getExtendsBound(); + final TypeMirror superBound = source.getSuperBound(); + if (extendsBound == null) { + if (superBound == null) { + this.adaptRecursive(this.types.extendsBound(source), this.types.extendsBound(target)); + } else { + this.adaptRecursive(this.types.superBound(source), this.types.superBound(target)); + } + } else if (superBound == null) { + this.adaptRecursive(this.types.extendsBound(source), this.types.extendsBound(target)); + } else { + throw new IllegalArgumentException("source: " + source); + } + return null; + } + + private final void adaptRecursive(final TypeMirror source, final TypeMirror target) { + final TypeMirrorPair pair = new TypeMirrorPair(this.types, this.sameTypeVisitor, source, target); + if (this.cache.add(pair)) { + try { + this.visit(source, target); + } finally { + this.cache.remove(pair); + } + } + } + + private final void adaptRecursive(final Collection source, final Collection target) { + if (source.size() == target.size()) { + final Iterator sourceIterator = source.iterator(); + final Iterator targetIterator = target.iterator(); + while (sourceIterator.hasNext()) { + assert targetIterator.hasNext(); + this.adaptRecursive(sourceIterator.next(), targetIterator.next()); + } + } + } + +} diff --git a/lang/src/main/java/org/microbean/lang/visitor/AsSuperVisitor.java b/lang/src/main/java/org/microbean/lang/visitor/AsSuperVisitor.java new file mode 100644 index 00000000..725fbbb2 --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/visitor/AsSuperVisitor.java @@ -0,0 +1,209 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.microbean.lang.visitor; + +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +import javax.lang.model.element.Element; + +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.ErrorType; +import javax.lang.model.type.IntersectionType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; + +import javax.lang.model.util.SimpleTypeVisitor14; + +import org.microbean.lang.TypeAndElementSource; +import org.microbean.lang.Equality; + +import org.microbean.lang.element.DelegatingElement; + +import org.microbean.lang.type.Types; + +import static org.microbean.lang.type.Types.asElement; + +/** + * A {@link SimpleTypeVisitor14} that, given a {@link TypeMirror} and an {@link Element}, attempts to find a supertype + * of that {@link TypeMirror} whose defining element is equal to (normally is identical to) the supplied {@link + * Element}. + * + *

For example, given a type denoted by {@code List}, and a {@link javax.lang.model.element.TypeElement} + * denoted by {@code Collection}, the result of visitation will be the type denoted by {@code Collection}.

+ * + *

{@code javac} does odd things with this and arrays and it is not clear that its documentation matches its + * code. Consequently I don't have a lot of faith in the {@link visitArray(ArrayType, Element)} method as of this + * writing.

+ * + *

The compiler's {@code asSuper} method documentation says, in part:

+ * + *
Return the (most specific) base type of {@code t} that starts with the given symbol. If none exists, + * return null.
+ */ +/*
Some examples:
+ *
+ * (Enum, Comparable) => Comparable
+ * (c.s.s.d.AttributeTree.ValueKind, Enum) => Enum
+ * (c.s.s.t.ExpressionTree, c.s.s.t.Tree) => c.s.s.t.Tree
+ * (j.u.List, Iterable) =>
+ *     Iterable
+ */
+// Basically done
+//
+// https://github.com/openjdk/jdk/blob/jdk-20+13/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L2165-L2221
+public final class AsSuperVisitor extends SimpleTypeVisitor14 {
+
+  private final Set seenTypes; // in the compiler, the field is called seenTypes but stores Symbols (Elements).
+
+  private final TypeAndElementSource elementSource;
+
+  private final Equality equality;
+
+  private final Types types;
+
+  private final SupertypeVisitor supertypeVisitor;
+
+  private SubtypeVisitor subtypeVisitor;
+
+  public AsSuperVisitor(final TypeAndElementSource elementSource,
+                        final Equality equality,
+                        final Types types,
+                        final SupertypeVisitor supertypeVisitor) {
+    super();
+    this.seenTypes = new HashSet<>();
+    this.elementSource = Objects.requireNonNull(elementSource, "elementSource");
+    this.equality = equality == null ? new Equality(true) : equality;
+    this.types = Objects.requireNonNull(types, "types");
+    this.supertypeVisitor = Objects.requireNonNull(supertypeVisitor, "supertypeVisitor");
+  }
+
+  public final AsSuperVisitor withSupertypeVisitor(final SupertypeVisitor supertypeVisitor) {
+    if (supertypeVisitor == this.supertypeVisitor) {
+      return this;
+    }
+    return new AsSuperVisitor(this.elementSource, this.equality, this.types, supertypeVisitor);
+  }
+
+  public final AsSuperVisitor withSubtypeVisitor(final SubtypeVisitor subtypeVisitor) {
+    if (subtypeVisitor == this.subtypeVisitor) {
+      return this;
+    }
+    final AsSuperVisitor v = new AsSuperVisitor(this.elementSource, this.equality, this.types, this.supertypeVisitor);
+    v.setSubtypeVisitor(subtypeVisitor);
+    return v;
+  }
+
+  final void setSubtypeVisitor(final SubtypeVisitor subtypeVisitor) {
+    if (subtypeVisitor.asSuperVisitor() != this) {
+      throw new IllegalArgumentException("subtypeVisitor");
+    } else if (subtypeVisitor != this.subtypeVisitor) {
+      this.subtypeVisitor = subtypeVisitor.withCapture(true);
+    }
+  }
+
+  // This is extraordinarily weird. In javac, all (non-generic? all?) array types have, as their synthetic element, one
+  // synthetic element that is a ClassSymbol named "Array". So the type denoted by, say, byte[] and the type denoted by
+  // Object[] both return exactly the same ClassSymbol reference from their asElement() method/sym field. See
+  // Symtab.java line 483ish. (I guess that ClassSymbol corresponds in some way to "[]".)
+  //
+  // Now, what type does that ClassSymbol have?  It is built like this:
+  //
+  //   arrayClass = new ClassSymbol(PUBLIC|ACYCLIC, names.Array, noSymbol);
+  //
+  // The first argument is a bunch of flags. OK. The second is, as we've seen, equal to, simply, "Array".  The third is
+  // its owner/enclosing element, which is nothing.
+  //
+  // This three-argument constructor delegates to the canonical four-argument constructor. What's missing? The type.  So
+  // when you build a ClassSymbol from the three-argument constructor, you effectively supply "new
+  // ClassType(Type.noType, null, null)" as its type argument.  If I'm reading this right and translating properly to
+  // the javax.lang.model hierarchy, that Element's asType() method will return a ClassType/DeclaredType (!) with no
+  // enclosing type and no type arguments.
+  @Override
+  public final TypeMirror visitArray(final ArrayType t, final Element element) {
+    assert t.getKind() == TypeKind.ARRAY;
+    final TypeMirror s = element.asType();
+    return this.subtypeVisitor.visit(t, s) ? s : null;
+  }
+
+  @Override
+  public final TypeMirror visitDeclared(final DeclaredType t, final Element element) {
+    assert t.getKind() == TypeKind.DECLARED;
+    return this.visitDeclaredOrIntersection(t, element);
+  }
+
+  private final TypeMirror visitDeclaredOrIntersection(final TypeMirror t, final Element element) {
+    assert t.getKind() == TypeKind.DECLARED || t.getKind() == TypeKind.INTERSECTION;
+    Objects.requireNonNull(element, "element");
+    final Element te = asElement(t, true /* yes, generate synthetic elements a la javac */);
+    if (te == null) {
+      return null;
+    } else if (this.equality.equals(te, element)) {
+      return t;
+    }
+    // TODO: may be able to get away with identity instead of DelegatingElement
+    final DelegatingElement c = DelegatingElement.of(te, this.elementSource);
+    if (!this.seenTypes.add(c)) { // javac calls it seenTypes but it stores Symbols/Elements
+      return null;
+    }
+    try {
+      final TypeMirror st = this.supertypeVisitor.visit(t);
+      switch (st.getKind()) {
+      case DECLARED:
+      case INTERSECTION:
+      case TYPEVAR:
+        final TypeMirror x = this.visit(st, element);
+        if (x != null) {
+          return x;
+        }
+        break;
+      default:
+        break;
+      }
+      if (element.getKind().isInterface()) {
+        for (final TypeMirror iface : this.supertypeVisitor.interfacesVisitor().visit(t)) {
+          final TypeMirror x = this.visit(iface, element);
+          if (x != null) {
+            return x;
+          }
+        }
+      }
+    } finally {
+      this.seenTypes.remove(c);
+    }
+    return null;
+  }
+
+  @Override
+  public final TypeMirror visitError(final ErrorType t, final Element element) {
+    assert t.getKind() == TypeKind.ERROR;
+    return t;
+  }
+
+  @Override
+  public final TypeMirror visitIntersection(final IntersectionType t, final Element element) {
+    assert t.getKind() == TypeKind.INTERSECTION;
+    return this.visitDeclaredOrIntersection(t, element);
+  }
+
+  @Override
+  public final TypeMirror visitTypeVariable(final TypeVariable t, final Element element) {
+    assert t.getKind() == TypeKind.TYPEVAR;
+    return this.equality.equals(t.asElement(), element) ? t : this.visit(t.getUpperBound(), element);
+  }
+
+}
diff --git a/lang/src/main/java/org/microbean/lang/visitor/AssignableVisitor.java b/lang/src/main/java/org/microbean/lang/visitor/AssignableVisitor.java
new file mode 100644
index 00000000..b3de6181
--- /dev/null
+++ b/lang/src/main/java/org/microbean/lang/visitor/AssignableVisitor.java
@@ -0,0 +1,60 @@
+/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
+ *
+ * Copyright © 2023 microBean™.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied.  See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+package org.microbean.lang.visitor;
+
+import java.util.Objects;
+
+import javax.lang.model.type.PrimitiveType;
+import javax.lang.model.type.TypeMirror;
+
+import javax.lang.model.util.SimpleTypeVisitor14;
+
+import org.microbean.lang.type.Types;
+
+public final class AssignableVisitor extends SimpleTypeVisitor14 {
+
+  private final Types types;
+
+  private final ConvertibleVisitor convertibleVisitor;
+
+  public AssignableVisitor(final Types types,
+                           final ConvertibleVisitor convertibleVisitor) {
+    super();
+    this.types = Objects.requireNonNull(types, "types");
+    this.convertibleVisitor = Objects.requireNonNull(convertibleVisitor, "convertibleVisitor");
+  }
+
+  public final AssignableVisitor withConvertibleVisitor(final ConvertibleVisitor convertibleVisitor) {
+    if (convertibleVisitor == this.convertibleVisitor) {
+      return this;
+    }
+    return new AssignableVisitor(this.types, convertibleVisitor);
+  }
+
+  @Override
+  protected final Boolean defaultAction(final TypeMirror t, final TypeMirror s) {
+    return this.convertibleVisitor.visit(t, s);
+  }
+
+  @Override
+  public final Boolean visitPrimitive(final PrimitiveType t, final TypeMirror s) {
+    // TODO: check out Types#isAssignable(), particularly the isSubRangeOf(INT) && t.constValue() != null part to see if
+    // it is actually possible to implement this
+    return super.visitPrimitive(t, s);
+  }
+
+}
diff --git a/lang/src/main/java/org/microbean/lang/visitor/BoundingClassVisitor.java b/lang/src/main/java/org/microbean/lang/visitor/BoundingClassVisitor.java
new file mode 100644
index 00000000..5fe2f736
--- /dev/null
+++ b/lang/src/main/java/org/microbean/lang/visitor/BoundingClassVisitor.java
@@ -0,0 +1,66 @@
+/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
+ *
+ * Copyright © 2023 microBean™.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
+ * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations under the License.
+ */
+package org.microbean.lang.visitor;
+
+import java.util.Objects;
+
+import javax.lang.model.element.TypeElement;
+
+import javax.lang.model.type.DeclaredType;
+import javax.lang.model.type.TypeKind;
+import javax.lang.model.type.TypeMirror;
+import javax.lang.model.type.TypeVariable;
+
+import javax.lang.model.util.SimpleTypeVisitor14;
+
+// A mostly slavish port of
+// https://github.com/openjdk/jdk/blob/jdk-21%2B22/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L2746-L2780.
+// To be quite honest I'm not sure how this ever has any effect at all.
+public final class BoundingClassVisitor extends SimpleTypeVisitor14 {
+
+  private final SupertypeVisitor supertypeVisitor;
+
+  public BoundingClassVisitor(final SupertypeVisitor supertypeVisitor) {
+    super();
+    this.supertypeVisitor = Objects.requireNonNull(supertypeVisitor, "supertypeVisitor");
+  }
+
+  @Override // SimpleTypeVisitor14
+  protected final TypeMirror defaultAction(final TypeMirror t, final Void x) {
+    return t;
+  }
+
+  @Override
+  public final DeclaredType visitDeclared(final DeclaredType t, final Void x) {
+    assert t.getKind() == TypeKind.DECLARED;
+    final TypeMirror enclosingType = t.getEnclosingType();
+    final TypeMirror visitedEnclosingType = this.visit(enclosingType);
+    if (enclosingType == visitedEnclosingType) {
+      return t;
+    }
+    final org.microbean.lang.type.DeclaredType dt = new org.microbean.lang.type.DeclaredType();
+    dt.addTypeArguments(t.getTypeArguments());
+    dt.addAnnotationMirrors(t.getAnnotationMirrors());
+    dt.setDefiningElement((TypeElement)t.asElement());
+    dt.setEnclosingType(visitedEnclosingType);
+    return dt;
+  }
+
+  @Override
+  public final TypeMirror visitTypeVariable(final TypeVariable t, final Void x) {
+    assert t.getKind() == TypeKind.TYPEVAR;
+    return this.visit(this.supertypeVisitor.visit(t));
+  }
+
+}
diff --git a/lang/src/main/java/org/microbean/lang/visitor/CaptureVisitor.java b/lang/src/main/java/org/microbean/lang/visitor/CaptureVisitor.java
new file mode 100644
index 00000000..9018a77d
--- /dev/null
+++ b/lang/src/main/java/org/microbean/lang/visitor/CaptureVisitor.java
@@ -0,0 +1,284 @@
+/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
+ *
+ * Copyright © 2023 microBean™.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied.  See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+package org.microbean.lang.visitor;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Objects;
+
+import javax.lang.model.element.Element;
+import javax.lang.model.element.TypeElement;
+
+import javax.lang.model.type.DeclaredType;
+import javax.lang.model.type.TypeKind;
+import javax.lang.model.type.TypeMirror;
+import javax.lang.model.type.TypeVariable;
+import javax.lang.model.type.WildcardType;
+
+import javax.lang.model.util.SimpleTypeVisitor14;
+
+import org.microbean.lang.TypeAndElementSource;
+import org.microbean.lang.Equality;
+
+import org.microbean.lang.type.Capture;
+import org.microbean.lang.type.Types;
+
+import static org.microbean.lang.type.Types.allTypeArguments;
+import static org.microbean.lang.type.Types.isInterface;
+
+// Basically done
+// javac's capture implementation is not visitor-based.
+// https://github.com/openjdk/jdk/blob/jdk-20+14/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L4388-L4456
+public final class CaptureVisitor extends SimpleTypeVisitor14 {
+
+  private final TypeAndElementSource elementSource;
+
+  private final Equality equality;
+
+  private final Types types;
+
+  private final SupertypeVisitor supertypeVisitor;
+
+  private SubtypeVisitor subtypeVisitor;
+
+  private final MemberTypeVisitor memberTypeVisitor;
+
+  private TypeClosureVisitor typeClosureVisitor;
+
+  public CaptureVisitor(final TypeAndElementSource elementSource,
+                        final Equality equality,
+                        final Types types,
+                        final SupertypeVisitor supertypeVisitor, // used by internal SubstituteVisitor
+                        final MemberTypeVisitor memberTypeVisitor) {
+    super();
+    this.elementSource = Objects.requireNonNull(elementSource, "elementSource");
+    this.equality = equality == null ? new Equality(true) : equality;
+    this.types = Objects.requireNonNull(types, "types");
+    this.supertypeVisitor = Objects.requireNonNull(supertypeVisitor, "supertypeVisitor");
+    this.memberTypeVisitor = Objects.requireNonNull(memberTypeVisitor, "memberTypeVisitor");
+  }
+
+  final void setSubtypeVisitor(final SubtypeVisitor v) {
+    if (v.captureVisitor() != this) {
+      throw new IllegalArgumentException("v");
+    } else if (this.subtypeVisitor != v) {
+      this.subtypeVisitor = v;
+    }
+  }
+
+  public final void setTypeClosureVisitor(final TypeClosureVisitor v) {
+    if (this.typeClosureVisitor == null || this.typeClosureVisitor != v) {
+      this.typeClosureVisitor = Objects.requireNonNull(v, "v");
+    }
+  }
+
+  @Override
+  protected final TypeMirror defaultAction(final TypeMirror t, final Void x) {
+    return t;
+  }
+
+  @Override
+  @SuppressWarnings("unchecked")
+  public final TypeMirror visitDeclared(DeclaredType t, final Void x) {
+    assert t.getKind() == TypeKind.DECLARED;
+
+    // https://github.com/openjdk/jdk/blob/jdk-20+14/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L4392-L4398
+    TypeMirror enclosingType = t.getEnclosingType();
+    switch (enclosingType.getKind()) {
+    case DECLARED:
+      final TypeMirror capturedEnclosingType = this.visitDeclared((DeclaredType)enclosingType, null); // RECURSIVE
+      if (capturedEnclosingType != enclosingType) {
+        final Element element = t.asElement();
+        final TypeMirror memberType = this.memberTypeVisitor.visit(capturedEnclosingType, element);
+        t = (DeclaredType)new SubstituteVisitor(this.elementSource,
+                                                this.equality,
+                                                this.supertypeVisitor,
+                                                ((DeclaredType)element.asType()).getTypeArguments(),
+                                                t.getTypeArguments())
+          .visit(memberType, null);
+        assert t.getKind() == TypeKind.DECLARED;
+        enclosingType = t.getEnclosingType();
+      }
+      break;
+    case NONE:
+      break;
+    default:
+      throw new IllegalArgumentException("t: " + t);
+    }
+
+    // https://github.com/openjdk/jdk/blob/jdk-20+14/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L4399-L4401
+    if (allTypeArguments(t).isEmpty() || this.types.raw(t)) {
+      // (Suppose somehow t were an intersection type (which gets modeled as a ClassType as well in javac). t would then
+      // be considered to be raw following the rules of javac (not sure about the language specification; the two
+      // frequently diverge). So it is accurate for this visitor not to implement visitIntersection().)
+      return t;
+    }
+
+    // https://github.com/openjdk/jdk/blob/jdk-20+14/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L4403-L4406
+    final DeclaredType G = (DeclaredType)t.asElement().asType();
+    final List A = (List)G.getTypeArguments();
+    final List T = t.getTypeArguments(); // t.getKind() == TypeKind.DECLARED ? ((DeclaredType)t).getTypeArguments() : List.of();
+    final List S = withFreshCapturedTypeVariables(T);
+
+    // https://github.com/openjdk/jdk/blob/jdk-20+14/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L4408-L4449
+    assert A.size() == T.size();
+    assert A.size() == S.size();
+    boolean captured = false;
+    for (int i = 0; i < A.size(); i++) {
+      final TypeVariable currentAHead = A.get(i);
+      assert currentAHead.getKind() == TypeKind.TYPEVAR;
+      final TypeMirror currentSHead = S.get(i);
+      final TypeMirror currentTHead = T.get(i);
+      if (currentSHead != currentTHead) {
+        captured = true;
+        TypeMirror Ui = currentAHead.getUpperBound();
+        if (Ui == null) {
+          Ui = this.elementSource.typeElement("java.lang.Object").asType();
+        }
+        final Capture Si = (Capture)currentSHead;
+        final WildcardType Ti = (WildcardType)currentTHead;
+        Si.setLowerBound(Ti.getSuperBound());
+        final TypeMirror TiExtendsBound = Ti.getExtendsBound();
+        if (TiExtendsBound == null) {
+          Si.setUpperBound(new SubstituteVisitor(this.elementSource, this.equality, this.supertypeVisitor, A, S).visit(Ui));
+        } else {
+          // TiExtendsBound can be DECLARED, INTERSECTION or TYPEVAR
+          Si.setUpperBound(glb(TiExtendsBound,
+                               new SubstituteVisitor(this.elementSource, this.equality, this.supertypeVisitor, A, S).visit(Ui)));
+        }
+      }
+    }
+
+    // https://github.com/openjdk/jdk/blob/jdk-20+14/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L4451-L4455
+    if (captured) {
+      assert t.getKind() == TypeKind.DECLARED;
+      return syntheticDeclaredType(t, S);
+    }
+    return t;
+  }
+
+  // https://github.com/openjdk/jdk/blob/jdk-20+14/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L4102-L4156
+  private final TypeMirror glb(final TypeMirror t, final TypeMirror s) {
+    // TODO: technically I think t and s can only be either DECLARED, INTERSECTION or TYPEVAR, since that's all closures
+    // will accept. See also
+    // https://stackoverflow.com/questions/73683649/in-the-javac-source-code-why-does-closuretype-return-a-non-empty-list-for-non.
+    if (s == null) {
+      return t;
+    } else if (t.getKind().isPrimitive()) {
+      throw new IllegalArgumentException("t: " + t);
+    } else if (s.getKind().isPrimitive()) {
+      throw new IllegalArgumentException("s: " + s);
+    } else if (this.subtypeVisitor.withCapture(false).visit(t, s)) {
+      return t;
+    } else if (this.subtypeVisitor.withCapture(false).visit(s, t)) {
+      return s;
+    }
+
+    final TypeClosure tc = this.typeClosureVisitor.visit(t);
+    tc.union(this.typeClosureVisitor.visit(s));
+
+    final List minimumTypes = tc.toMinimumTypes(this.subtypeVisitor);
+    final int size = minimumTypes.size();
+    switch (size) {
+    case 0:
+      return this.elementSource.typeElement("java.lang.Object").asType();
+    case 1:
+      return minimumTypes.get(0);
+    }
+
+    boolean classes = false;
+    final Collection capturedTypeVariables = new ArrayList<>(size);
+    final Collection lowers = new ArrayList<>(size);
+    for (int i = 0; i < size; i++) {
+      final TypeMirror minimumType = minimumTypes.get(i);
+      if (!isInterface(minimumType)) {
+        if (!classes) {
+          classes = true;
+        }
+        final TypeMirror lower = capturedTypeVariableLowerBound(minimumType);
+        if (minimumType != lower && lower.getKind() != TypeKind.NULL) {
+          capturedTypeVariables.add(minimumType);
+          lowers.add(lower);
+        }
+      }
+    }
+    if (classes) {
+      if (lowers.isEmpty()) {
+        throw new IllegalArgumentException("t: " + t);
+      }
+      final List bounds = new ArrayList<>(minimumTypes);
+      bounds.removeIf(capturedTypeVariables::contains);
+      bounds.addAll(lowers);
+      return this.glb(bounds); // RECURSIVE, in a way
+    }
+    return org.microbean.lang.type.IntersectionType.of(minimumTypes);
+  }
+
+  // https://github.com/openjdk/jdk/blob/jdk-20+14/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L4092-L4100
+  private final TypeMirror glb(final List ts) {
+    if (ts.isEmpty()) {
+      return null;
+    }
+    TypeMirror t1 = ts.get(0);
+    for (int i = 1; i < ts.size(); i++) {
+      t1 = this.glb(t1, ts.get(i)); // RECURSIVE, in a way
+    }
+    return t1;
+  }
+
+  private static final TypeMirror capturedTypeVariableLowerBound(final TypeMirror capture) {
+    if (capture.getKind() == TypeKind.TYPEVAR && capture instanceof Capture c) {
+      final TypeMirror lowerBound = c.getLowerBound();
+      if (lowerBound == null) {
+        return org.microbean.lang.type.NullType.INSTANCE;
+      } else {
+        return lowerBound.getKind() == TypeKind.NULL ? lowerBound : capturedTypeVariableLowerBound(lowerBound); // RECURSIVE
+      }
+    }
+    return capture;
+  }
+
+  private static final List withFreshCapturedTypeVariables(final List typeArguments) {
+    if (typeArguments.isEmpty()) {
+      return List.of();
+    }
+    final List list = new ArrayList<>(typeArguments.size());
+    for (final TypeMirror typeArgument : typeArguments) {
+      switch (typeArgument.getKind()) {
+      case WILDCARD:
+        list.add(new Capture((WildcardType)typeArgument));
+        break;
+      default:
+        list.add(typeArgument);
+        break;
+      }
+    }
+    return Collections.unmodifiableList(list);
+  }
+
+  private static final DeclaredType syntheticDeclaredType(final DeclaredType canonicalType,
+                                                          final List typeArguments) {
+    final org.microbean.lang.type.DeclaredType t = new org.microbean.lang.type.DeclaredType();
+    t.setEnclosingType(canonicalType.getEnclosingType());
+    t.addTypeArguments(typeArguments);
+    t.setDefiningElement((TypeElement)canonicalType.asElement());
+    return t;
+  }
+
+}
diff --git a/lang/src/main/java/org/microbean/lang/visitor/ContainsTypeVisitor.java b/lang/src/main/java/org/microbean/lang/visitor/ContainsTypeVisitor.java
new file mode 100644
index 00000000..cf008c97
--- /dev/null
+++ b/lang/src/main/java/org/microbean/lang/visitor/ContainsTypeVisitor.java
@@ -0,0 +1,174 @@
+/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*-
+ *
+ * Copyright © 2023 microBean™.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied.  See the License for the specific language governing
+ * permissions and limitations under the License.
+ */
+package org.microbean.lang.visitor;
+
+import java.util.List;
+import java.util.Objects;
+
+import javax.lang.model.type.ErrorType;
+import javax.lang.model.type.TypeKind;
+import javax.lang.model.type.TypeMirror;
+import javax.lang.model.type.WildcardType;
+
+import javax.lang.model.util.SimpleTypeVisitor14;
+
+import org.microbean.lang.TypeAndElementSource;
+
+import org.microbean.lang.type.Capture;
+import org.microbean.lang.type.Types;
+
+// Basically done.
+/*
+ * 

From the documentation of {@code com.sun.tools.javac.code.Types#containsType(Type, Type)}:

+ * + *

Check if {@code t} contains {@code s}.

+ * + *

{@code T} contains {@code S} if:

+ * + *

{@code L(T) <: L(S) && U(S) <: U(T)}

+ * + *

This relation is only used by {@code ClassType.isSubtype()} [in fact this is not true], that is,

+ * + *

{@code C <: C if T contains S.}

+ * + *

Because of F-bounds [e.g. class Enum>] , this relation can lead to infinite recursion. Thus we + * must somehow break that recursion. Notice that containsType() is only called from ClassType.isSubtype() [not true]. + * Since the arguments have already been checked against their bounds [not true], we know:

+ * + *

{@code U(S) <: U(T) if T is "super" bound (U(T) *is* the bound)}

+ * + *

{@code L(T) <: L(S) if T is "extends" bound (L(T) is bottom)}

+ * + * @see Type Arguments of + * Parameterized Types + * + * @see F-bounded + * quantification + */ +// https://github.com/openjdk/jdk/blob/jdk-20+12/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L1562-L1611 +public final class ContainsTypeVisitor extends SimpleTypeVisitor14 { + + private final TypeAndElementSource elementSource; + + private final Types types; + + private SameTypeVisitor sameTypeVisitor; + + private SubtypeVisitor subtypeVisitor; + + public ContainsTypeVisitor(final TypeAndElementSource elementSource, final Types types) { + super(Boolean.FALSE /* default value */); + this.elementSource = Objects.requireNonNull(elementSource, "elementSource"); + this.types = Objects.requireNonNull(types, "types"); + } + + final void setSameTypeVisitor(final SameTypeVisitor v) { + if (v.containsTypeVisitor() != this) { + throw new IllegalArgumentException("v: " + v); + } else if (v != this.sameTypeVisitor) { + this.sameTypeVisitor = v; + } + } + + final void setSubtypeVisitor(final SubtypeVisitor v) { + if (v.containsTypeVisitor() != this) { + throw new IllegalArgumentException("v: " + v); + } else if (v != this.subtypeVisitor) { + this.subtypeVisitor = v; + } + } + + public final ContainsTypeVisitor withSameTypeVisitor(final SameTypeVisitor sameTypeVisitor) { + if (sameTypeVisitor == this.sameTypeVisitor) { + return this; + } + final ContainsTypeVisitor v = new ContainsTypeVisitor(this.elementSource, this.types); + v.setSameTypeVisitor(sameTypeVisitor); + return v; + } + + public final ContainsTypeVisitor withSubtypeVisitor(final SubtypeVisitor subtypeVisitor) { + if (subtypeVisitor == this.subtypeVisitor) { + return this; + } + final ContainsTypeVisitor v = new ContainsTypeVisitor(this.elementSource, this.types); + v.setSubtypeVisitor(this.subtypeVisitor); + return v; + } + + // https://github.com/openjdk/jdk/blob/jdk-20+12/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L1524-L1531 + final boolean visit(final List t, final List s) { + final int size = t.size(); + if (size <= 0 || size != s.size()) { + return false; + } + for (int i = 0; i < size; i++) { + if (!this.visit(t.get(i), s.get(i))) { + return false; + } + } + return true; + } + + @Override + protected final Boolean defaultAction(final TypeMirror t, final TypeMirror s) { + return this.sameTypeVisitor.visit(t, s); + } + + @Override + public final Boolean visitError(final ErrorType e, final TypeMirror s) { + assert e.getKind() == TypeKind.ERROR; + return Boolean.TRUE; + } + + // https://github.com/openjdk/jdk/blob/jdk-20+12/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L1585-L1596 + @Override + @SuppressWarnings("fallthrough") + public final Boolean visitWildcard(final WildcardType w, TypeMirror s) { + assert w.getKind() == TypeKind.WILDCARD; + switch (s.getKind()) { + case TYPEVAR: + // Return true if s is a Capture that captures w, which just means that the wildcard that s captures is the same + // one as w. + if (s instanceof Capture sct) { + s = sct.getWildcardType(); + } else { + return Boolean.FALSE; + } + // fall through + case WILDCARD: + if (this.sameTypeVisitor.visit(w, s)) { + return Boolean.TRUE; + } + // fall through + default: + final TypeMirror wSuperBound = w.getSuperBound(); + if (wSuperBound == null) { + // ? or ? extends Foo + // Upper/extends-bounded (and possibly unbounded, which is the same thing). + return this.subtypeVisitor.withCapture(false).visit(this.types.extendsBound(s), this.types.extendsBound(w)); + } else if (w.getExtendsBound() == null) { + // ? super Foo + // Lower/super-bounded. + return this.subtypeVisitor.withCapture(false).visit(wSuperBound, this.types.superBound(s)); + } else { + throw new IllegalArgumentException("w: " + w); + } + } + } + +} diff --git a/lang/src/main/java/org/microbean/lang/visitor/ConvertibleVisitor.java b/lang/src/main/java/org/microbean/lang/visitor/ConvertibleVisitor.java new file mode 100644 index 00000000..d5843e29 --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/visitor/ConvertibleVisitor.java @@ -0,0 +1,73 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.visitor; + +import java.util.Objects; + +import javax.lang.model.type.PrimitiveType; +import javax.lang.model.type.TypeMirror; + +import javax.lang.model.util.SimpleTypeVisitor14; + +import org.microbean.lang.type.Types; + +public final class ConvertibleVisitor extends SimpleTypeVisitor14 { + + private final Types types; + + private final SubtypeUncheckedVisitor subtypeUncheckedVisitor; + + private final SubtypeVisitor subtypeVisitor; + + public ConvertibleVisitor(final Types types, + final SubtypeUncheckedVisitor subtypeUncheckedVisitor, + final SubtypeVisitor subtypeVisitor) { + super(); + this.types = Objects.requireNonNull(types, "types"); + this.subtypeUncheckedVisitor = Objects.requireNonNull(subtypeUncheckedVisitor, "subtypeUncheckedVisitor").withCapture(true); + this.subtypeVisitor = Objects.requireNonNull(subtypeVisitor, "subtypeVisitor"); + } + + public final ConvertibleVisitor withSubtypeUncheckedVisitor(final SubtypeUncheckedVisitor subtypeUncheckedVisitor) { + if (subtypeUncheckedVisitor == this.subtypeUncheckedVisitor) { + return this; + } + return new ConvertibleVisitor(this.types, subtypeUncheckedVisitor, this.subtypeVisitor); + } + + public final ConvertibleVisitor withSubtypeVisitor(final SubtypeVisitor subtypeVisitor) { + if (subtypeVisitor == this.subtypeVisitor) { + return this; + } + return new ConvertibleVisitor(this.types, this.subtypeUncheckedVisitor, subtypeVisitor); + } + + @Override + protected final Boolean defaultAction(final TypeMirror t, final TypeMirror s) { + assert !t.getKind().isPrimitive(); + return + s.getKind().isPrimitive() ? this.subtypeVisitor.visit(this.types.unbox(t), s) : this.subtypeUncheckedVisitor.visit(t, s); + } + + @Override + public final Boolean visitPrimitive(final PrimitiveType t, final TypeMirror s) { + assert t.getKind().isPrimitive(); + return + s.getKind().isPrimitive() ? this.subtypeUncheckedVisitor.visit(t, s) : this.subtypeVisitor.visit(this.types.box(t), s); + } + +} diff --git a/lang/src/main/java/org/microbean/lang/visitor/EraseVisitor.java b/lang/src/main/java/org/microbean/lang/visitor/EraseVisitor.java new file mode 100644 index 00000000..5b01e3b8 --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/visitor/EraseVisitor.java @@ -0,0 +1,132 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.visitor; + +import java.util.Objects; + +import javax.lang.model.element.TypeElement; + +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; +import javax.lang.model.type.WildcardType; + +import org.microbean.lang.TypeAndElementSource; + +import org.microbean.lang.type.Types; + +// Basically done. +public final class EraseVisitor extends StructuralTypeMapping { + + private final Types types; + + public EraseVisitor(final TypeAndElementSource elementSource, final Types types) { + super(elementSource); + this.types = Objects.requireNonNull(types, "types"); + } + + // https://github.com/openjdk/jdk/blob/jdk-20+12/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L2442-L2459 + @Override // StructuralTypeMapping + public final DeclaredType visitDeclared(final DeclaredType t, final Boolean recurse) { + assert t.getKind() == TypeKind.DECLARED; + // In the compiler, there's a LOT going on here. + // + // Type erased = t.tsym.erasure(Types.this) + // + // Every Type in com.sun.tools.javac.code has a Symbol. (This includes nonsensical symbols.) Every Symbol has a + // Type. + // + // This all loosely corresponds with javax.lang.model.type.DeclaredType#asElement() and Element#asType() but there + // are no nonsensical Elements. + // + // A Type's Symbol holds what I'll call the element type, or element Type when I'm being exceptionally precise. As + // javac goes about its business, it can take a Type (with its Symbol, which references, in many cases, that very + // Type in a circular fashion), and decorate it with another Type. When this happens, the decorating Type's Symbol's + // Type will be the decorated "element Type". (Often, but not always, this usage will indicate that the decorating + // Type is a parameterized type, whose (declared) element type is the Type being parameterized/used.) + // + // A Symbol has various methods on it that can cache various bits of ancillary data related to its Type. + // Symbol#erasure(Types) is one such method. + // + // It looks like this: + // + // /** The symbol's erased type. + // */ + // public Type erasure(Types types) { + // if (erasure_field == null) + // erasure_field = types.erasure(type); // recurse is false + // return erasure_field; + // } + // + // BUT NOTE: ClassSymbol overrides it (see + // https://github.com/openjdk/jdk/blob/jdk-20+12/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Symbol.java#L1352-L1358): + // + // public Type erasure(Types type) { + // if (erasure_field == null) + // erasure_field = new ClassType(types.erasure(type.getEnclosingType()), + // List.nil(), this, + // type.getMetadata()); + // return erasure_field; + // } + // + // So when the field is null, the equivalent of this.visit(t.getEnclosingType(), false) is first called, erasing the + // enclosing type. Then a new ClassType is created, wrapping the original Symbol, which references the original + // (unerased) ClassType, which is now the element type, with no type arguments. + // + // In this toolkit, we don't want to get into the same "cache stuff in the symbol" business if we can at all help + // it. + final org.microbean.lang.type.DeclaredType erasedType; + if (t instanceof org.microbean.lang.type.DeclaredType dt && dt.isErased()) { + erasedType = dt; + } else { + final org.microbean.lang.type.DeclaredType dt = new org.microbean.lang.type.DeclaredType(true /* erased */); + dt.setEnclosingType(this.visit(t.getEnclosingType(), false)); + dt.setDefiningElement((TypeElement)t.asElement()); + assert this.types.raw(dt); + erasedType = dt; + } + // Commenting this out because it does not appear to be necessary + // in javac. + /* + if (Boolean.TRUE.equals(recurse)) { + // This is extremely weird. In the compiler, if recurse is true, then an ErasedClassType (subtype of ClassType) + // is created to decorate the already-decorated ClassType being erased. An ErasedClassType differs from a regular + // ClassType only in that it returns true from its hasErasedSuperclasses() method, whereas + // ClassType#hasErasedSuperclasses() returns isRaw(). Maybe this is just a performance optimization? + // + // ErasedClassType does not appear anywhere else in all of the JDK, so it seems to exist solely to return true + // from its hasErasedSuperclasses(). Note that isRaw() will also return true for erased classes. + erasedType = new DefaultDeclaredType(erasedType.getEnclosingType(), List.of(), true, List.of()); + } + */ + return erasedType; + } + + @Override // SimpleTypeVisitor6 + public final TypeMirror visitTypeVariable(final TypeVariable t, final Boolean recurse) { + assert t.getKind() == TypeKind.TYPEVAR; + return this.visit(t.getUpperBound(), recurse); + } + + @Override // StructuralTypeMapping + public final TypeMirror visitWildcard(final WildcardType t, final Boolean recurse) { + assert t.getKind() == TypeKind.WILDCARD; + return this.visit(this.types.extendsBound(t), recurse); + } + +} diff --git a/lang/src/main/java/org/microbean/lang/visitor/InterfacesVisitor.java b/lang/src/main/java/org/microbean/lang/visitor/InterfacesVisitor.java new file mode 100644 index 00000000..c4b45b2d --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/visitor/InterfacesVisitor.java @@ -0,0 +1,143 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.visitor; + +import java.util.List; +import java.util.Objects; + +import java.util.function.Predicate; + +import javax.lang.model.element.Element; +import javax.lang.model.element.TypeElement; + +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.IntersectionType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; + +import javax.lang.model.util.SimpleTypeVisitor14; + +import org.microbean.lang.TypeAndElementSource; +import org.microbean.lang.Equality; + +import org.microbean.lang.type.Types; + +import static org.microbean.lang.type.Types.allTypeArguments; +import static org.microbean.lang.type.Types.isInterface; + +// Basically done +public final class InterfacesVisitor extends SimpleTypeVisitor14, Void> { + + private final TypeAndElementSource elementSource; + + private final Equality equality; + + private final Types types; + + private final EraseVisitor eraseVisitor; + + private final SupertypeVisitor supertypeVisitor; + + private final Predicate filter; + + public InterfacesVisitor(final TypeAndElementSource elementSource, + final Equality equality, + final Types types, + final EraseVisitor eraseVisitor, + final SupertypeVisitor supertypeVisitor, // used only for substitute visitor implementations + final Predicate filter) { + super(List.of()); + this.filter = filter == null ? t -> true : filter; + this.equality = equality == null ? new Equality(true) : equality; + this.elementSource = Objects.requireNonNull(elementSource, "elementSource"); + this.types = Objects.requireNonNull(types, "types"); + this.eraseVisitor = Objects.requireNonNull(eraseVisitor, "eraseVisitor"); + this.supertypeVisitor = Objects.requireNonNull(supertypeVisitor, "supertypeVisitor"); + } + + public final InterfacesVisitor withFilter(final Predicate filter) { + return + filter == this.filter ? this : + new InterfacesVisitor(this.elementSource, this.equality, this.types, this.eraseVisitor, this.supertypeVisitor, filter); + } + + // https://github.com/openjdk/jdk/blob/jdk-20+12/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L2599-L2633 + @Override + public final List visitDeclared(final DeclaredType t, final Void x) { + assert t.getKind() == TypeKind.DECLARED; + final List returnValue; + final Element e = t.asElement(); + if (e == null) { + returnValue = List.of(); + } else { + returnValue = switch (e.getKind()) { + case ANNOTATION_TYPE: + case CLASS: + case ENUM: + case INTERFACE: + case RECORD: + final List interfaces = ((TypeElement)e).getInterfaces(); + if (this.types.raw(t)) { + yield this.eraseVisitor.visit(interfaces, true); + } + @SuppressWarnings("unchecked") + final List formals = (List)allTypeArguments(e.asType()); + if (formals.isEmpty()) { + yield interfaces; + } + assert this.supertypeVisitor.interfacesVisitor() == this; + yield + new SubstituteVisitor(this.elementSource, this.equality, this.supertypeVisitor, formals, allTypeArguments(t)) + .visit(interfaces, x); + default: + yield List.of(); + }; + } + return returnValue.isEmpty() ? returnValue : returnValue.stream().filter(this.filter).toList(); + } + + @Override + public final List visitIntersection(final IntersectionType t, final Void x) { + assert t.getKind() == TypeKind.INTERSECTION; + // Here the porting is a little trickier. It turns out that an intersection type caches its supertype and its + // interfaces at construction time, and there's only one place where intersection types are created. In the lang + // model, that means that an IntersectionType's bounds are its single non-interface supertype, if any, followed by + // its interfaces. So we will hand-tool this. + final List bounds = t.getBounds(); + final int size = bounds.size(); + final List returnValue = switch (size) { + case 0 -> List.of(); + case 1 -> isInterface(bounds.get(0)) ? bounds : List.of(); + default -> isInterface(bounds.get(0)) ? bounds : bounds.subList(1, size); + }; + return returnValue.isEmpty() ? returnValue : returnValue.stream().filter(this.filter).toList(); + } + + @Override + public final List visitTypeVariable(final TypeVariable t, final Void x) { + assert t.getKind() == TypeKind.TYPEVAR; + final TypeMirror upperBound = t.getUpperBound(); + final List returnValue = switch (upperBound.getKind()) { + case DECLARED -> ((DeclaredType)upperBound).asElement().getKind().isInterface() ? List.of(upperBound) : List.of(); + case INTERSECTION -> this.visit(upperBound); + default -> List.of(); + }; + return returnValue.isEmpty() ? returnValue : returnValue.stream().filter(this.filter).toList(); + } + +} diff --git a/lang/src/main/java/org/microbean/lang/visitor/MemberTypeVisitor.java b/lang/src/main/java/org/microbean/lang/visitor/MemberTypeVisitor.java new file mode 100644 index 00000000..60950a32 --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/visitor/MemberTypeVisitor.java @@ -0,0 +1,177 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.visitor; + +import java.util.List; +import java.util.Objects; + +import javax.lang.model.element.Element; + +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.ErrorType; +import javax.lang.model.type.IntersectionType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; +import javax.lang.model.type.WildcardType; + +import javax.lang.model.util.SimpleTypeVisitor14; + +import org.microbean.lang.TypeAndElementSource; +import org.microbean.lang.Equality; + +import org.microbean.lang.type.NoType; +import org.microbean.lang.type.Types; + +import static org.microbean.lang.type.Types.allTypeArguments; +import static org.microbean.lang.type.Types.isStatic; + +// Basically done +// https://github.com/openjdk/jdk/blob/jdk-20+13/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L2294-L2340 +public final class MemberTypeVisitor extends SimpleTypeVisitor14 { + + private final TypeAndElementSource elementSource; + + private final Equality equality; + + private final Types types; + + private final AsSuperVisitor asSuperVisitor; + + private final EraseVisitor eraseVisitor; + + private final SupertypeVisitor supertypeVisitor; + + public MemberTypeVisitor(final TypeAndElementSource elementSource, + final Equality equality, + final Types types, + final AsSuperVisitor asSuperVisitor, + final EraseVisitor eraseVisitor, + final SupertypeVisitor supertypeVisitor) { // used only by substitute visitor implementations + super(); + this.elementSource = Objects.requireNonNull(elementSource, "elementSource"); + this.equality = equality == null ? new Equality(true) : equality; + this.types = Objects.requireNonNull(types, "types"); + this.asSuperVisitor = Objects.requireNonNull(asSuperVisitor, "asSuperVisitor"); + this.eraseVisitor = Objects.requireNonNull(eraseVisitor, "eraseVisitor"); + this.supertypeVisitor = Objects.requireNonNull(supertypeVisitor, "supertypeVisitor"); + } + + public final MemberTypeVisitor withAsSuperVisitor(final AsSuperVisitor asSuperVisitor) { + if (asSuperVisitor == this.asSuperVisitor) { + return this; + } + return new MemberTypeVisitor(this.elementSource, this.equality, this.types, asSuperVisitor, this.eraseVisitor, this.supertypeVisitor); + } + + // Only affects substitution + public final MemberTypeVisitor withSupertypeVisitor(final SupertypeVisitor supertypeVisitor) { + if (supertypeVisitor == this.supertypeVisitor) { + return this; + } + return new MemberTypeVisitor(this.elementSource, this.equality, this.types, this.asSuperVisitor, this.eraseVisitor, supertypeVisitor); + } + + @Override + protected final TypeMirror defaultAction(final TypeMirror t, final Element e) { + return e.asType(); + } + + @Override + public final TypeMirror visitDeclared(final DeclaredType t, final Element e) { + assert t.getKind() == TypeKind.DECLARED; + return this.visitDeclaredOrIntersection(t, e); + } + + private final TypeMirror visitDeclaredOrIntersection(final TypeMirror t, final Element e) { + assert t.getKind() == TypeKind.DECLARED || t.getKind() == TypeKind.INTERSECTION; + if (!isStatic(e)) { + final Element enclosingElement = e.getEnclosingElement(); + final TypeMirror enclosingType = enclosingElement.asType(); + final List enclosingTypeTypeArguments = allTypeArguments(enclosingType); + if (!enclosingTypeTypeArguments.isEmpty()) { + assert enclosingType.getKind() == TypeKind.DECLARED; + assert enclosingType instanceof DeclaredType; + final TypeMirror baseType = this.asOuterSuper(t, enclosingElement); + if (baseType != null) { + final List baseTypeTypeArguments = allTypeArguments(baseType); + if (baseTypeTypeArguments.isEmpty()) { + // baseType is raw + return this.eraseVisitor.visit(e.asType()); + } else { + return new SubstituteVisitor(this.elementSource, + this.equality, + this.supertypeVisitor, + enclosingTypeTypeArguments, + baseTypeTypeArguments) + .visit(e.asType()); + } + } + } + } + return e.asType(); + } + + @Override + public final TypeMirror visitError(final ErrorType t, final Element e) { + assert t.getKind() == TypeKind.ERROR; + return t; + } + + @Override + public final TypeMirror visitIntersection(final IntersectionType t, final Element e) { + assert t.getKind() == TypeKind.INTERSECTION; + return this.visitDeclaredOrIntersection(t, e); + } + + @Override + public final TypeMirror visitTypeVariable(final TypeVariable t, final Element e) { + assert t.getKind() == TypeKind.TYPEVAR; + return this.visit(t.getUpperBound(), e); + } + + @Override + public final TypeMirror visitWildcard(final WildcardType t, final Element e) { + assert t.getKind() == TypeKind.WILDCARD; + // TODO: watch out for that silly javac wildUpperBound thing + return this.visit(this.types.extendsBound(t), e); + } + + // https://github.com/openjdk/jdk/blob/jdk-21%2B15/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L2217-L2242 + private final TypeMirror asOuterSuper(TypeMirror t, final Element element) { + switch (t.getKind()) { + case ARRAY: + case TYPEVAR: + return this.asSuperVisitor.visit(t, element); + case DECLARED: + case INTERSECTION: + do { + final TypeMirror s = this.asSuperVisitor.visit(t, element); + if (s != null) { + return s; + } + t = t.getKind() == TypeKind.DECLARED ? ((DeclaredType)t).getEnclosingType() : NoType.NONE; + } while (t.getKind() == TypeKind.DECLARED || t.getKind() == TypeKind.INTERSECTION); + return null; + case ERROR: + return t; + default: + return null; + } + } + +} diff --git a/lang/src/main/java/org/microbean/lang/visitor/PrecedesPredicate.java b/lang/src/main/java/org/microbean/lang/visitor/PrecedesPredicate.java new file mode 100644 index 00000000..a229463a --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/visitor/PrecedesPredicate.java @@ -0,0 +1,129 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.microbean.lang.visitor; + +import java.util.Objects; + +import java.util.function.BiPredicate; + +import javax.lang.model.element.Element; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.TypeParameterElement; + +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeMirror; + +import org.microbean.lang.Equality; + +// https://github.com/openjdk/jdk/blob/jdk-20+14/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Symbol.java#L829-L849 +public class PrecedesPredicate implements BiPredicate { + + private final SupertypeVisitor supertypeVisitor; + + private final SubtypeVisitor subtypeVisitor; + + private final Equality equality; + + public PrecedesPredicate(final SupertypeVisitor supertypeVisitor, + final SubtypeVisitor subtypeVisitor) { + this(new Equality(true), supertypeVisitor, subtypeVisitor); + } + + public PrecedesPredicate(final Equality equality, + final SupertypeVisitor supertypeVisitor, + final SubtypeVisitor subtypeVisitor) { + super(); + this.equality = equality == null ? new Equality(true) : equality; + this.supertypeVisitor = Objects.requireNonNull(supertypeVisitor, "supertypeVisitor"); + this.subtypeVisitor = Objects.requireNonNull(subtypeVisitor, "subtypeVisitor"); + } + + // Does e precede f? + @Override + public final boolean test(final Element e, final Element f) { + if (e == f) { + // Optimization + return false; + } + final TypeMirror t = e.asType(); + final TypeMirror s = f.asType(); + switch (t.getKind()) { + case DECLARED: + switch (s.getKind()) { + case DECLARED: + if (this.equality.equals(e, f)) { // in place of == + // Both are completely interchangeable DeclaredTypes; can't say which comes first. + return false; + } + final int rt = this.rank(t); + final int rs = this.rank(s); + // Use JDK 21+ semantics; see https://github.com/openjdk/jdk/commit/426025aab42d485541a899844b96c06570088771 + return rs < rt || (rs == rt && this.disambiguate((TypeElement)e, (TypeElement)f) < 0); + default: + // t is not a type variable but s is, so t does not precede s, because "Type variables always precede other + // kinds of symbols [sic]." s precedes t in fact, but that's not what's being tested. + return false; + } + case TYPEVAR: + switch (s.getKind()) { + case TYPEVAR: + if (this.equality.equals(e, f)) { // in place of == + // Both are completely interchangeable TypeVariables; can't say which comes first. + return false; + } + return this.subtypeVisitor.withCapture(true).visit(t, s); + default: + // https://github.com/openjdk/jdk/blob/jdk-20+14/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Symbol.java#L833: + // "Type variables always precede other kinds of symbols." (Note that a type variable is not a symbol; I think + // javac means "type variables always precede other kinds of types". Or it could be using "type variable" to + // mean "type parameter element".) + // + // So t is a type variable and s is not, so t precedes s. + return true; + } + default: + return false; + } + } + + // Disambiguate two unequal TypeElements with the same rank/specialization depth. + protected int disambiguate(final TypeElement e, final TypeElement f) { + return CharSequence.compare(e.getQualifiedName(), f.getQualifiedName()); + } + + // https://github.com/openjdk/jdk/blob/jdk-20+14/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L3576-L3621 + @SuppressWarnings("fallthrough") + final int rank(final TypeMirror t) { + switch (t.getKind()) { + case DECLARED: + if (((TypeElement)((DeclaredType)t).asElement()).getQualifiedName().contentEquals("java.lang.Object")) { + return 0; + } + // fall through + case INTERSECTION: + case TYPEVAR: + int r = this.rank(this.supertypeVisitor.visit(t)); // RECURSIVE + for (final TypeMirror iface : this.supertypeVisitor.interfacesVisitor().visit(t)) { + r = Math.max(r, this.rank(iface)); // RECURSIVE + } + return r + 1; + case ERROR: + case NONE: + return 0; + default: + throw new IllegalArgumentException("t: " + t); + } + } + +} diff --git a/lang/src/main/java/org/microbean/lang/visitor/SameTypeVisitor.java b/lang/src/main/java/org/microbean/lang/visitor/SameTypeVisitor.java new file mode 100644 index 00000000..281d71c3 --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/visitor/SameTypeVisitor.java @@ -0,0 +1,412 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.visitor; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.ErrorType; +import javax.lang.model.type.ExecutableType; +import javax.lang.model.type.IntersectionType; +import javax.lang.model.type.NoType; +import javax.lang.model.type.NullType; +import javax.lang.model.type.PrimitiveType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; +import javax.lang.model.type.WildcardType; + +import javax.lang.model.util.SimpleTypeVisitor14; + +import org.microbean.lang.TypeAndElementSource; +import org.microbean.lang.Equality; + +import org.microbean.lang.element.DelegatingElement; + +// Basically done +// isSameType() in javac's Types.java +public final class SameTypeVisitor extends SimpleTypeVisitor14 { + + final ContainsTypeVisitor containsTypeVisitor; + + private final TypeAndElementSource elementSource; + + private final Equality equality; + + private final SupertypeVisitor supertypeVisitor; + + private final boolean wildcardsComparable; + + public SameTypeVisitor(final TypeAndElementSource elementSource, + final ContainsTypeVisitor containsTypeVisitor, + final SupertypeVisitor supertypeVisitor, + final boolean wildcardsCompatible) { + this(elementSource, new Equality(false), containsTypeVisitor, supertypeVisitor, wildcardsCompatible); + } + + public SameTypeVisitor(final TypeAndElementSource elementSource, + final Equality equality, + final ContainsTypeVisitor containsTypeVisitor, + final SupertypeVisitor supertypeVisitor, // used in visitExecutable, visitIntersection, hasSameBounds (only called from visitExecutable) + final boolean wildcardsComparable) { + super(Boolean.FALSE); + this.elementSource = Objects.requireNonNull(elementSource, "elementSource"); + this.equality = equality == null ? new Equality(false) : equality; + this.wildcardsComparable = wildcardsComparable; + this.containsTypeVisitor = Objects.requireNonNull(containsTypeVisitor, "containsTypeVisitor"); + this.supertypeVisitor = Objects.requireNonNull(supertypeVisitor, "supertypeVisitor"); + containsTypeVisitor.setSameTypeVisitor(this); + } + + public final SameTypeVisitor withSupertypeVisitor(final SupertypeVisitor supertypeVisitor) { + if (supertypeVisitor == this.supertypeVisitor) { + return this; + } + return + new SameTypeVisitor(this.elementSource, this.equality, this.containsTypeVisitor(), supertypeVisitor, this.wildcardsComparable); + + } + + final ContainsTypeVisitor containsTypeVisitor() { + return this.containsTypeVisitor; + } + + @Override + protected final Boolean defaultAction(final TypeMirror t, final TypeMirror s) { + return t == s || this.equality.equals(t, s); + } + + @Override + public final Boolean visitArray(final ArrayType t, final TypeMirror s) { + assert t.getKind() == TypeKind.ARRAY; + return t == s || switch (s.getKind()) { + case ARRAY -> this.containsTypeEquivalent(t.getComponentType(), ((ArrayType)s).getComponentType()); + default -> Boolean.FALSE; + }; + } + + @Override + public final Boolean visitDeclared(final DeclaredType t, final TypeMirror s) { + assert t.getKind() == TypeKind.DECLARED; + return t == s || switch (s.getKind()) { + case DECLARED -> this.visitDeclared(t, (DeclaredType)s); + case INTERSECTION -> this.visitDeclared(t, (IntersectionType)s); // basically returns false + case WILDCARD -> this.visitDeclared(t, (WildcardType)s); + default -> Boolean.FALSE; + }; + } + + // https://github.com/openjdk/jdk/blob/jdk-20+16/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L1424-L1426 + private final boolean visitDeclared(final DeclaredType t, final DeclaredType s) { + assert t.getKind() == TypeKind.DECLARED && s.getKind() == TypeKind.DECLARED; + assert t != s; + return + this.equality.equals(t.asElement(), s.asElement()) && // yes, really extreme equality not identity/== + this.visit(t.getEnclosingType(), s.getEnclosingType()) && // RECURSIVE + this.containsTypeEquivalent(t.getTypeArguments(), s.getTypeArguments()); + } + + private final boolean visitDeclared(final DeclaredType t, final IntersectionType s) { + assert t.getKind() == TypeKind.DECLARED && s.getKind() == TypeKind.INTERSECTION; + // https://github.com/openjdk/jdk/blob/jdk-20+16/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L1424-L1426 + // + // javac falls back on some common code that starts with: + // + // return t.tsym == s.tsym + // && ... + // + // But the Symbol of an IntersectionClassType will never be the same as the Symbol of any other Type. That's + // because + // https://github.com/openjdk/jdk/blob/jdk-20+16/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L2497-L2504 + // is the only place where a new IntersectionClassType is created + // (https://github.com/openjdk/jdk/search?q=%22new+IntersectionClassType%22), and you can see that a new synthetic + // Symbol is created each time as well. + // + // Therefore we can just return false. + return false; + } + + private final boolean visitDeclared(final DeclaredType t, final WildcardType s) { + assert t.getKind() == TypeKind.DECLARED && s.getKind() == TypeKind.WILDCARD; + // https://github.com/openjdk/jdk/blob/jdk-20+16/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L1401-L1402 + final TypeMirror superBound = s.getSuperBound(); + return + superBound != null && + s.getExtendsBound() == null && + this.visitDeclared(t, this.elementSource.typeElement("java.lang.Object").asType()) && + this.visit(t, superBound); + } + + @Override + public final Boolean visitError(final ErrorType t, final TypeMirror s) { + assert t.getKind() == TypeKind.ERROR; + return Boolean.TRUE; + } + + @Override + public final Boolean visitExecutable(final ExecutableType t, final TypeMirror s) { + assert t.getKind() == TypeKind.EXECUTABLE; + return t == s || switch (s.getKind()) { + case EXECUTABLE -> this.visitExecutable(t, (ExecutableType)s); + default -> Boolean.FALSE; + }; + } + + private final boolean visitExecutable(final ExecutableType t, final ExecutableType s) { + assert t.getKind() == TypeKind.EXECUTABLE && s.getKind() == TypeKind.EXECUTABLE; + assert t != s; + + // In javac, an ExecutableType has its representation spread across a "ForAll" type and a "MethodType", where a + // ForAll type "has a" MethodType. The ForAll type is basically a decorator, decorating its MethodType with its + // type variables. + // + // javac's isSameTypeVisitor TypeRelation first checks to see if two ForAlls have the "same" type variables (see + // hasSameBounds(); javac cannot decide on a term to mean, roughly, equivalent, using "same" and "equivalent" and + // "equal" oftentimes to mean the same thing). + // + // Here we follow suit: + final List ttvs = t.getTypeVariables(); + final List stvs = s.getTypeVariables(); + if (!hasSameBounds(ttvs, stvs)) { + return false; + } + final ExecutableType substitutedS = + new SubstituteVisitor(this.elementSource, this.equality, this.supertypeVisitor, stvs, ttvs).visitExecutable(s, null); + if (s != substitutedS) { + return this.visitExecutable(t, substitutedS); // RECURSIVE + } + + // OK, we completed the "ForAll" part. Next, javac checks the MethodType "portion", which consists of the parameter + // types and the return type. We've already checked the type variables involved, so we know they're compatible. + return + containsTypeEquivalent(t.getParameterTypes(), s.getParameterTypes()) && this.visit(t.getReturnType(), s.getReturnType()); + } + + @Override + public final Boolean visitIntersection(final IntersectionType t, final TypeMirror s) { + assert t.getKind() == TypeKind.INTERSECTION; + if (t == s) { + return true; + } + switch (s.getKind()) { + case INTERSECTION: + return this.visitIntersection(t, (IntersectionType)s); + default: + // https://github.com/openjdk/jdk/blob/jdk-20+16/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L1424-L1426 + // + // javac falls back on some common code that starts with: + // + // return t.tsym == s.tsym + // && ... + // + // But the Symbol of an IntersectionClassType will never be the same as the Symbol of any other Type. That's + // because + // https://github.com/openjdk/jdk/blob/jdk-20+16/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L2497-L2504 + // is the only place where a new IntersectionClassType is created + // (https://github.com/openjdk/jdk/search?q=%22new+IntersectionClassType%22), and you can see that a new synthetic + // Symbol is created each time as well. + // + // Therefore we can just return false. + return false; + } + } + + // https://github.com/openjdk/jdk/blob/jdk-20+16/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L1404-L1423 + private final boolean visitIntersection(final IntersectionType t, final IntersectionType s) { + assert t.getKind() == TypeKind.INTERSECTION && s.getKind() == TypeKind.INTERSECTION; + assert t != s; + if (!this.visit(this.supertypeVisitor.visit(t), this.supertypeVisitor.visit(s))) { + return false; + } + final Map tMap = new HashMap<>(); + for (final TypeMirror ti : this.supertypeVisitor.interfacesVisitor().visitIntersection(t, null)) { + assert ti.getKind() == TypeKind.DECLARED; + tMap.put(DelegatingElement.of(((DeclaredType)t).asElement(), this.elementSource), ti); + } + for (final TypeMirror si : this.supertypeVisitor.interfacesVisitor().visitIntersection(s, null)) { + assert si.getKind() == TypeKind.DECLARED; + final TypeMirror ti = tMap.remove(((DeclaredType)si).asElement()); + if (ti == null || !this.visit(ti, si)) { + return false; + } + } + return tMap.isEmpty(); + } + + @Override + public final Boolean visitNoType(final NoType t, final TypeMirror s) { + final TypeKind tKind = t.getKind(); + assert + tKind == TypeKind.MODULE || + tKind == TypeKind.NONE || + tKind == TypeKind.PACKAGE || + tKind == TypeKind.VOID; + return t == s || switch (tKind) { + case NONE -> s.getKind() == TypeKind.NONE; + default -> this.equality.equals(t, s); + }; + } + + @Override + public final Boolean visitNull(final NullType t, final TypeMirror s) { + assert t.getKind() == TypeKind.NULL; + return t == s || switch (s.getKind()) { + case NULL -> Boolean.TRUE; + default -> this.equality.equals(t, s); + }; + } + + @Override + public final Boolean visitPrimitive(final PrimitiveType t, final TypeMirror s) { + assert t.getKind().isPrimitive(); + return t == s || this.equality.equals(t, s); + } + + @Override + public final Boolean visitTypeVariable(final TypeVariable t, final TypeMirror s) { + assert t.getKind() == TypeKind.TYPEVAR; + switch (s.getKind()) { + case TYPEVAR: + // https://github.com/openjdk/jdk/blob/jdk-20+16/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L1363-L1368 + return t == s; + case WILDCARD: + return this.visitTypeVariable(t, (WildcardType)s); + default: + return false; // really? + } + } + + private final boolean visitTypeVariable(final TypeVariable t, final WildcardType s) { + assert t.getKind() == TypeKind.TYPEVAR && s.getKind() == TypeKind.WILDCARD; + // https://github.com/openjdk/jdk/blob/jdk-20+16/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L1370-L1374 + return + s.getExtendsBound() == null && + s.getSuperBound() != null && + this.visit(t, this.elementSource.typeElement("java.lang.Object").asType()); + } + + @Override + public final Boolean visitWildcard(final WildcardType t, final TypeMirror s) { + assert t.getKind() == TypeKind.WILDCARD; + return s.getKind() == TypeKind.WILDCARD && this.visitWildcard(t, (WildcardType)s); + } + + private final boolean visitWildcard(final WildcardType t, final WildcardType s) { + assert t.getKind() == TypeKind.WILDCARD && s.getKind() == TypeKind.WILDCARD; + // See + // https://github.com/openjdk/jdk/blob/jdk-20+16/src/jdk.compiler/share/classes/com/sun/tools/javac/model/JavacTypes.java#L88-L90, + // and then + // https://github.com/openjdk/jdk/blob/jdk-20+16/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L1382-L1391. + if (this.wildcardsComparable) { + final TypeMirror tExtendsBound = t.getExtendsBound(); + final TypeMirror sExtendsBound = s.getExtendsBound(); + final TypeMirror tSuperBound = t.getSuperBound(); + final TypeMirror sSuperBound = s.getSuperBound(); + // return (t.kind == t2.kind || (t.isExtendsBound() && s.isExtendsBound())) && + // isSameType(t.type, t2.type); + if (tExtendsBound == null) { + if (sExtendsBound == null) { + if (tSuperBound == null) { + return sSuperBound == null; + } else if (sSuperBound == null) { + // t is super-bounded + // s is super-bounded and extends-bounded + return false; + } else { + // t is super-bounded + // s is super-bounded + return this.visit(tSuperBound, sSuperBound); + } + } else if (tSuperBound == null) { + if (sSuperBound == null) { + // t is super-bounded and extends-bounded + // s is extends-bounded + return this.visit(tExtendsBound, sExtendsBound); + } else { + throw new IllegalArgumentException("s: " + s); + } + } else if (sSuperBound == null) { + return false; + } else { + throw new IllegalArgumentException("s: " + s); + } + } else if (tSuperBound == null) { + if (sExtendsBound == null) { + return sSuperBound == null && this.visit(tExtendsBound, sExtendsBound); + } else if (sSuperBound == null) { + // t is extends-bounded + // s is extends-bounded + return this.visit(tExtendsBound, sExtendsBound); + } else { + throw new IllegalArgumentException("s: " + s); + } + } else { + throw new IllegalArgumentException("t: " + t); + } + } + return false; + } + + // https://github.com/openjdk/jdk/blob/jdk-20+16/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L3468-L3480 + private final boolean hasSameBounds(final ExecutableType t, final ExecutableType s) { + return hasSameBounds(t.getTypeVariables(), s.getTypeVariables()); + } + + private final boolean hasSameBounds(final List ts, final List ss) { + final int size = ts.size(); + if (size != ss.size()) { + return false; + } else if (size > 0) { + final SubstituteVisitor sv = new SubstituteVisitor(this.elementSource, this.equality, this.supertypeVisitor, ss, ts); + for (int i = 0; i < size; i++) { + final TypeVariable t = ts.get(i); + final TypeVariable s = ss.get(i); + if (!this.visit(t.getUpperBound(), sv.visit(s.getUpperBound())) || + !this.visit(t.getLowerBound(), sv.visit(s.getLowerBound()))) { // lower bounds only relevant for captures + return false; + } + } + } + return true; + } + + // When invoked, the Lists will be type arguments.or method parameter types. + private final boolean containsTypeEquivalent(final List ts, final List ss) { + final int size = ts.size(); + if (size != ss.size()) { + return false; + } + for (int i = 0; i < size; i++) { + if (!this.containsTypeEquivalent(ts.get(i), ss.get(i))) { + return false; + } + } + return true; + } + + // https://github.com/openjdk/jdk/blob/jdk-20+16/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L4562-L4565 + private final boolean containsTypeEquivalent(final TypeMirror t, final TypeMirror s) { + return + this.visit(t, s) || // either the arguments are basically the same, or... + this.containsTypeVisitor.visit(t, s) && this.containsTypeVisitor.visit(s, t); // ...t contains s and s contains t + } + +} diff --git a/lang/src/main/java/org/microbean/lang/visitor/StructuralTypeMapping.java b/lang/src/main/java/org/microbean/lang/visitor/StructuralTypeMapping.java new file mode 100644 index 00000000..2fc810c0 --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/visitor/StructuralTypeMapping.java @@ -0,0 +1,150 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.visitor; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.ExecutableType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.WildcardType; + +import javax.lang.model.util.SimpleTypeVisitor14; + +import org.microbean.lang.TypeAndElementSource; + +// See https://github.com/openjdk/jdk/blob/jdk-20+11/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Type.java#L244 +class StructuralTypeMapping extends SimpleTypeVisitor14 { + + protected final TypeAndElementSource elementSource; + + StructuralTypeMapping(final TypeAndElementSource elementSource) { + super(); + this.elementSource = Objects.requireNonNull(elementSource, "elementSource"); + } + + @Override // SimpleTypeVisitor6 + protected TypeMirror defaultAction(final TypeMirror t, final S ignored) { + return t; + } + + public final List visit(final List ts, final S s) { + if (ts.isEmpty()) { + return ts; + } + final List list = new ArrayList<>(ts.size()); + boolean changed = false; + for (final TypeMirror t : ts) { + final TypeMirror visitedT = this.visit(t, s); + list.add(visitedT); + if (!changed && visitedT != t) { + changed = true; + } + } + if (changed) { + return Collections.unmodifiableList(list); + } + return ts; + } + + @Override // SimpleTypeVisitor6 + public TypeMirror visitArray(final ArrayType t, final S s) { + assert t.getKind() == TypeKind.ARRAY; + final TypeMirror componentType = t.getComponentType(); + final TypeMirror visitedComponentType = this.visit(componentType, s); + if (componentType == visitedComponentType) { + return t; + } + return new org.microbean.lang.type.ArrayType(visitedComponentType); + } + + @Override // SimpleTypeVisitor6 + public TypeMirror visitDeclared(final DeclaredType t, final S s) { + assert t.getKind() == TypeKind.DECLARED; + final TypeMirror enclosingType = t.getEnclosingType(); + final TypeMirror visitedEnclosingType = this.visit(enclosingType, s); + final List typeArguments = t.getTypeArguments(); + final List visitedTypeArguments = this.visit(typeArguments, s); + if (enclosingType == visitedEnclosingType && typeArguments == visitedTypeArguments) { + return t; + } + final org.microbean.lang.type.DeclaredType r = new org.microbean.lang.type.DeclaredType(); + r.setEnclosingType(visitedEnclosingType); + r.addTypeArguments(visitedTypeArguments); + r.setDefiningElement((javax.lang.model.element.TypeElement)t.asElement()); + return r; + } + + // See https://github.com/openjdk/jdk/blob/jdk-20+11/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Type.java#L290-L313 + @Override // SimpleTypeVisitor6 + public TypeMirror visitExecutable(final ExecutableType t, final S s) { + assert t.getKind() == TypeKind.EXECUTABLE; + // The compiler does not visit an executable type's receiver type or type variables. We follow suit. + final List parameterTypes = t.getParameterTypes(); + final List visitedParameterTypes = this.visit(parameterTypes, s); + final TypeMirror returnType = t.getReturnType(); + final TypeMirror visitedReturnType = this.visit(returnType, s); + final List thrownTypes = t.getThrownTypes(); + final List visitedThrownTypes = this.visit(thrownTypes, s); + if (parameterTypes == visitedParameterTypes && + returnType == visitedReturnType && + thrownTypes == visitedThrownTypes) { + return t; + } + final org.microbean.lang.type.ExecutableType r = new org.microbean.lang.type.ExecutableType(); + r.addParameterTypes(visitedParameterTypes); + r.setReceiverType(t.getReceiverType()); + r.setReturnType(visitedReturnType); + r.addThrownTypes(visitedThrownTypes); + r.addTypeVariables(t.getTypeVariables()); + return r; + } + + @Override // SimpleTypeVisitor6 + public TypeMirror visitWildcard(final WildcardType t, final S s) { + assert t.getKind() == TypeKind.WILDCARD; + TypeMirror extendsBound = t.getExtendsBound(); + final TypeMirror superBound = t.getSuperBound(); + final TypeMirror visitedExtendsBound; + final TypeMirror visitedSuperBound; + if (extendsBound == null) { + if (superBound == null) { + extendsBound = this.elementSource.typeElement("java.lang.Object").asType(); + visitedExtendsBound = this.visit(extendsBound, s); + visitedSuperBound = null; + } else { + visitedExtendsBound = null; + visitedSuperBound = this.visit(superBound, s); + } + } else if (superBound == null) { + visitedExtendsBound = this.visit(extendsBound, s); + visitedSuperBound = null; + } else { + throw new IllegalArgumentException("t: " + t); + } + if (extendsBound == visitedExtendsBound && superBound == visitedSuperBound) { + return t; + } + return org.microbean.lang.type.WildcardType.of(visitedExtendsBound, visitedSuperBound); + } + +} diff --git a/lang/src/main/java/org/microbean/lang/visitor/SubstituteVisitor.java b/lang/src/main/java/org/microbean/lang/visitor/SubstituteVisitor.java new file mode 100644 index 00000000..cc2d9709 --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/visitor/SubstituteVisitor.java @@ -0,0 +1,322 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.visitor; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import java.util.function.BiPredicate; + +import javax.lang.model.element.TypeParameterElement; + +import javax.lang.model.type.ExecutableType; +import javax.lang.model.type.IntersectionType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; +import javax.lang.model.type.WildcardType; + +import org.microbean.lang.TypeAndElementSource; +import org.microbean.lang.Equality; + +final class SubstituteVisitor extends StructuralTypeMapping { + + + /* + * Instance fields. + */ + + + private final Equality equality; + + private final SupertypeVisitor supertypeVisitor; + + private final List from; + + private final List to; + + + /* + * Constructors. + */ + + + SubstituteVisitor(final TypeAndElementSource elementSource, + final Equality equality, + final SupertypeVisitor supertypeVisitor, // used only for intersection types + List from, + List to) { + super(elementSource); + this.equality = equality == null ? new Equality(false) : equality; + this.supertypeVisitor = Objects.requireNonNull(supertypeVisitor, "supertypeVisitor"); + // https://github.com/openjdk/jdk/blob/jdk-20+12/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L3321-L3322 + // "If lists have different length, discard leading elements of the longer list." + int fromSize = from.size(); + int toSize = to.size(); + if (fromSize != toSize) { + if (fromSize > toSize) { + from = from.subList(fromSize - toSize, fromSize); + fromSize = from.size(); + } + if (toSize > fromSize) { + to = to.subList(toSize - fromSize, toSize); + toSize = to.size(); + } + assert fromSize == toSize; + } + this.from = List.copyOf(from); + this.to = List.copyOf(to); + } + + + /* + * Instance methods. + */ + + + final SubstituteVisitor with(final List from, + final List to) { + if (from == this.from && to == this.to) { + return this; + } + return new SubstituteVisitor(this.elementSource, this.equality, this.supertypeVisitor, from, to); + } + + // https://github.com/openjdk/jdk/blob/jdk-20+12/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L3382-L3411 + @Override + public final ExecutableType visitExecutable(ExecutableType t, final Void x) { + assert t.getKind() == TypeKind.EXECUTABLE; + + /* + if (Type.containsAny(to, t.tvars)) { + //perform alpha-renaming of free-variables in 't' + //if 'to' types contain variables that are free in 't' + List freevars = newInstances(t.tvars); + t = new ForAll(freevars, + Types.this.subst(t.qtype, t.tvars, freevars)); + } + */ + + List typeVariables = t.getTypeVariables(); + if (anyMatches(this.to, typeVariables, this.equality::equals)) { + t = new org.microbean.lang.type.ExecutableType(t, newInstances(typeVariables)); + typeVariables = t.getTypeVariables(); + } + + // Now do substitution on the type variable bounds themselves. + final List visitedTypeVariables = this.visitUpperBoundsOf(typeVariables); + + // Visit the other "parts" of the method that are structurally relevant. + ExecutableType visitedT = (ExecutableType)super.visitExecutable(t, x); + + if (typeVariables == visitedTypeVariables) { + if (t == visitedT) { + return t; + } + } else { + visitedT = this.with(typeVariables, visitedTypeVariables).visitExecutable(visitedT, x); + } + return new org.microbean.lang.type.ExecutableType(visitedT, visitedTypeVariables); + } + + @Override + public final IntersectionType visitIntersection(final IntersectionType t, final Void x) { + assert t.getKind() == TypeKind.INTERSECTION; + // final TypeMirror supertype = this.supertypeVisitor.visit(t, x); // (Returns t.getBounds().get(0).) + final TypeMirror supertype = t.getBounds().get(0); + final TypeMirror visitedSupertype = this.visit(supertype, x); + final List interfaces = this.supertypeVisitor.interfacesVisitor().visit(t, x); + final List visitedInterfaces = this.visit(interfaces, x); + if (supertype == visitedSupertype && interfaces == visitedInterfaces) { + return t; + } + final List list = new ArrayList<>(visitedInterfaces.size() + 1); + list.add(visitedSupertype); + list.addAll(visitedInterfaces); + return new org.microbean.lang.type.IntersectionType(list); + } + + // https://github.com/openjdk/jdk/blob/jdk-20+12/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L3347-L3357 + @Override + public final TypeMirror visitTypeVariable(final TypeVariable tv, final Void x) { + assert tv.getKind() == TypeKind.TYPEVAR; + if (!(tv instanceof org.microbean.lang.type.Capture)) { + final int size = this.from.size(); + assert size == this.to.size(); + for (int i = 0; i < size; i++) { + final TypeMirror f = this.from.get(i); + assert f.getKind() == TypeKind.TYPEVAR; + assert f instanceof TypeVariable; + if (this.equality.equals(f, tv)) { + final TypeMirror t = this.to.get(i); + assert t.getKind() == TypeKind.TYPEVAR; + assert t instanceof TypeVariable; + return t; + } + } + } + return tv; + } + + // https://github.com/openjdk/jdk/blob/jdk-20+12/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L3373-L3380 + @Override + public final WildcardType visitWildcard(final WildcardType wt, final Void x) { + assert wt.getKind() == TypeKind.WILDCARD; + final TypeMirror extendsBound = wt.getExtendsBound(); + final TypeMirror superBound = wt.getSuperBound(); + if (superBound == null) { + if (extendsBound == null) { + // Unbounded. No need to do anything else. + return wt; + } + // Upper-bounded. + final TypeMirror visitedExtendsBound = this.visit(extendsBound); // RECURSIVE + if (extendsBound == visitedExtendsBound) { + return wt; + } + return org.microbean.lang.type.WildcardType.upperBoundedWildcardType(visitedExtendsBound); + } else if (extendsBound == null) { + // Lower-bounded. + final TypeMirror visitedSuperBound = this.visit(superBound); // RECURSIVE + if (superBound == visitedSuperBound) { + return wt; + } + return org.microbean.lang.type.WildcardType.lowerBoundedWildcardType(visitedSuperBound); + } else { + // Wildcards can only specify a single bound, either upper or lower. + throw new IllegalArgumentException("wt: " + wt); + } + } + + // A port/translation of Types#newInstances(List). "perform alpha-renaming of free-variables in 't' [free variables + // in tvs]" + // + // This is a slavish port for the most part. There are several cases where we could make things more efficient but the + // undocumented semantics might be lost. + // + // https://github.com/openjdk/jdk/blob/jdk-20+12/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L3489-L3507 + private final List newInstances(final List tvs) { + assert !tvs.isEmpty(); // shouldn't be called if so + + final List visitedTvs = new ArrayList<>(tvs); + final SubstituteVisitor visitor = this.with(tvs, visitedTvs); + for (int i = 0; i < visitedTvs.size(); i++) { + + // Get the existing type variable at position i. + final TypeVariable tv = visitedTvs.get(i); + + final TypeMirror upperBound = tv.getUpperBound(); + + // Compute a new upper bound for it. Note the usage of tvs and visitedTvs in the from and to slots of the visitor + // respectively. + final TypeMirror visitedUpperBound = visitor.visit(upperBound); // RECURSIVE + + // It *seems* that if upperBound and visitedUpperBound are identical we wouldn't have to do anything below. But + // the compiler's version of this method always creates a new instance, and the method is named newInstances, so + // we do too. + + // Create a new TypeVariable whose upper bound is the just-visited upper bound. + final org.microbean.lang.type.TypeVariable newTv = + new org.microbean.lang.type.TypeVariable(this.elementSource, visitedUpperBound, tv.getLowerBound()); + newTv.setDefiningElement((TypeParameterElement)tv.asElement()); + + // Replace the type variable at position i (the very one we're looking at) with the new, newly-bounded type + // variable. + visitedTvs.set(i, newTv); + } + return Collections.unmodifiableList(visitedTvs); + } + + // https://github.com/openjdk/jdk/blob/3cd3a83647297f525f5eab48ce688e024ca6b08c/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L3413-L3452 + private final List visitUpperBoundsOf(final List tvs) { + if (tvs.isEmpty()) { + return tvs; // preserve identity whenever possible + } + + // Phase 1 (it appears): effectively call visit() on the upper bound of all the type variables. If this didn't + // result in any changes, we're done. + final List visitedUpperBounds = new ArrayList<>(tvs.size()); // could be pointless if nothing changed + boolean changed = false; + for (final TypeVariable tv : tvs) { + final TypeMirror upperBound = tv.getUpperBound(); + final TypeMirror visitedUpperBound = this.visit(upperBound); + visitedUpperBounds.add(visitedUpperBound); // this could end up being pointless if changed is false; nothing for it + if (!changed && upperBound != visitedUpperBound) { + changed = true; + } + } + if (!changed) { + return tvs; // preserve identity whenever possible + } + + // Phase 2: Create a list of type variables temporarily without upper bounds. We will give them upper bounds in + // phase 3. + final List newTvs = new ArrayList<>(tvs.size()); + for (final TypeVariable tv : tvs) { + final org.microbean.lang.type.TypeVariable newTv = + new org.microbean.lang.type.TypeVariable(this.elementSource, null, tv.getLowerBound()); + newTv.setDefiningElement((TypeParameterElement)tv.asElement()); + newTvs.add(newTv); + } + + // (All Lists at this point are the same size.) + + // Phase 3: Perform substitution over the visited (phase 1) upper bounds of the caller-supplied type variables with + // the un-upper-bounded type variable "prototypes" created in phase 2. (This will result in type variables that + // have no upper bound yet, but we have them in a list, and so can doctor them in phase 4 below.) + final SubstituteVisitor visitor = this.with(tvs, newTvs); + for (int i = 0; i < visitedUpperBounds.size(); i++) { + visitedUpperBounds.set(i, visitor.visit(visitedUpperBounds.get(i))); + } + + // Phase 4: Replace the unspecified upper bounds (see phase 2) with the substituted bounds we calculated in phase 1 + // and doctored in phase 3. + for (int i = 0; i < newTvs.size(); i++) { + final org.microbean.lang.type.TypeVariable upperBoundlessTv = newTvs.get(i); + final org.microbean.lang.type.TypeVariable newTv = + new org.microbean.lang.type.TypeVariable(this.elementSource, visitedUpperBounds.get(i), upperBoundlessTv.getLowerBound()); + newTv.setDefiningElement(upperBoundlessTv.asElement()); + newTvs.set(i, newTv); + } + + return Collections.unmodifiableList(newTvs); + } + + + /* + * Static methods. + */ + + + // Does at least one t in ts "match" at least one s in ss according to p? + private static final boolean anyMatches(final Iterable ts, + final Collection ss, + final BiPredicate p) { + for (final TypeMirror t : ts) { + for (final TypeMirror s : ss) { + if (p.test(t, s)) { + return true; + } + } + } + return false; + } + +} diff --git a/lang/src/main/java/org/microbean/lang/visitor/SubtypeUncheckedVisitor.java b/lang/src/main/java/org/microbean/lang/visitor/SubtypeUncheckedVisitor.java new file mode 100644 index 00000000..3f455605 --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/visitor/SubtypeUncheckedVisitor.java @@ -0,0 +1,169 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.visitor; + +import java.util.Objects; + +import javax.lang.model.element.Element; + +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; + +import javax.lang.model.util.SimpleTypeVisitor14; + +import org.microbean.lang.type.Types; + +import static org.microbean.lang.type.Types.asElement; + +// The compiler has a couple of methods that test whether some type t is an "unchecked subtype" of some other type +// s. See +// https://github.com/openjdk/jdk/blob/jdk-21%2B22/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L1005-L1067. +// +// Deliberately not thread safe. +public final class SubtypeUncheckedVisitor extends SimpleTypeVisitor14 { + + private final Types types; + + private final SubtypeVisitor subtypeVisitor; + + private final AsSuperVisitor asSuperVisitor; + + private final SameTypeVisitor sameTypeVisitor; + + private final boolean capture; + + private SubtypeUncheckedVisitor withCaptureVariant; + + private SubtypeUncheckedVisitor withoutCaptureVariant; + + public SubtypeUncheckedVisitor(final Types types, + final SubtypeVisitor subtypeVisitor, + final AsSuperVisitor asSuperVisitor, + final SameTypeVisitor sameTypeVisitor, + final boolean capture /* true by default */) { + super(); + this.types = Objects.requireNonNull(types); + this.subtypeVisitor = subtypeVisitor.withCapture(capture); + this.asSuperVisitor = Objects.requireNonNull(asSuperVisitor, "asSuperVisitor"); + this.sameTypeVisitor = Objects.requireNonNull(sameTypeVisitor, "sameTypeVisitor"); + this.capture = capture; + if (capture) { + this.withCaptureVariant = this; + } else { + this.withoutCaptureVariant = this; + } + } + + public final SubtypeUncheckedVisitor withAsSuperVisitor(final AsSuperVisitor asSuperVisitor) { + if (asSuperVisitor == this.asSuperVisitor) { + return this; + } + return + new SubtypeUncheckedVisitor(this.types, + this.subtypeVisitor, + asSuperVisitor, + this.sameTypeVisitor, + this.capture); + } + + public final SubtypeUncheckedVisitor withSubtypeVisitor(final SubtypeVisitor subtypeVisitor) { + if (subtypeVisitor == this.subtypeVisitor) { + return this; + } + return + new SubtypeUncheckedVisitor(this.types, + subtypeVisitor, + this.asSuperVisitor, + this.sameTypeVisitor, + this.capture); + } + + final SubtypeUncheckedVisitor withCapture(final boolean capture) { + if (capture) { + if (this.withCaptureVariant == null) { + this.withCaptureVariant = new SubtypeUncheckedVisitor(this.types, this.subtypeVisitor, this.asSuperVisitor, this.sameTypeVisitor, true); + } + return this.withCaptureVariant; + } else if (this.withoutCaptureVariant == null) { + this.withoutCaptureVariant = new SubtypeUncheckedVisitor(this.types, this.subtypeVisitor, this.asSuperVisitor, this.sameTypeVisitor, false); + } + return this.withoutCaptureVariant; + } + + @Override + protected final Boolean defaultAction(final TypeMirror t, final TypeMirror s) { + if (this.subtypeVisitor.withCapture(this.capture).visit(t, s)) { + return Boolean.TRUE; + } + + if (this.types.raw(s)) { + return Boolean.FALSE; + } + + final Element e = asElement(s, true /* yes, generate synthetic elements */); + if (e == null) { + return Boolean.FALSE; + } + + final TypeMirror t2 = this.asSuperVisitor.visit(t, e); + return this.types.raw(t2); + } + + @Override + public final Boolean visitArray(final ArrayType t, final TypeMirror s) { + assert t.getKind() == TypeKind.ARRAY; + switch (s.getKind()) { + case ARRAY: + final TypeMirror tct = t.getComponentType(); + if (tct.getKind().isPrimitive()) { + return this.sameTypeVisitor.visit(tct, ((ArrayType)s).getComponentType()); + } else { + return this.withCapture(false).visit(tct, ((ArrayType)s).getComponentType()); + } + default: + break; + } + + if (this.subtypeVisitor.withCapture(capture).visit(t, s)) { + return Boolean.TRUE; + } + + if (this.types.raw(s)) { + return Boolean.FALSE; + } + + final Element e = asElement(s, true /* yes, generate synthetic elements */); + if (e == null) { + return Boolean.FALSE; + } + + final TypeMirror t2 = this.asSuperVisitor.visit(t, e); + return this.types.raw(t2); + } + + // See https://github.com/openjdk/jdk/blob/jdk-21%2B15/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L1029-L1032 + @Override + public final Boolean visitTypeVariable(final TypeVariable t, final TypeMirror s) { + assert t.getKind() == TypeKind.TYPEVAR; + return + this.subtypeVisitor.withCapture(capture).visit(t, s) || + this.withCapture(false).visit(t.getUpperBound(), s); + } + +} diff --git a/lang/src/main/java/org/microbean/lang/visitor/SubtypeVisitor.java b/lang/src/main/java/org/microbean/lang/visitor/SubtypeVisitor.java new file mode 100644 index 00000000..899307e9 --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/visitor/SubtypeVisitor.java @@ -0,0 +1,531 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.visitor; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import javax.lang.model.element.Element; +import javax.lang.model.element.Name; +import javax.lang.model.element.QualifiedNameable; + +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.ErrorType; +import javax.lang.model.type.IntersectionType; +import javax.lang.model.type.NoType; +import javax.lang.model.type.NullType; +import javax.lang.model.type.PrimitiveType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; +import javax.lang.model.type.WildcardType; + +import javax.lang.model.util.SimpleTypeVisitor14; + +import org.microbean.lang.TypeAndElementSource; +import org.microbean.lang.Equality; + +import org.microbean.lang.type.Types; + +import static org.microbean.lang.type.Types.allTypeArguments; +import static org.microbean.lang.type.Types.asElement; +import static org.microbean.lang.type.Types.unboundedWildcardType; +import static org.microbean.lang.type.Types.upperBoundedWildcardType; + +// Deliberately not thread safe. +// See https://github.com/openjdk/jdk/blob/jdk-20+13/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L1109-L1238 +public final class SubtypeVisitor extends SimpleTypeVisitor14 { + + private final TypeAndElementSource elementSource; + + private final Equality equality; + + private final Types types; + + private final boolean capture; + + private final Set cache; + + private final SupertypeVisitor supertypeVisitor; + + private final ContainsTypeVisitor containsTypeVisitor; + + private final SameTypeVisitor sameTypeVisitor; + + private final CaptureVisitor captureVisitor; + + private final AsSuperVisitor asSuperVisitor; + + private SubtypeVisitor withCaptureVariant; + + private SubtypeVisitor withoutCaptureVariant; + + public SubtypeVisitor(final TypeAndElementSource elementSource, + final Equality equality, + final Types types, + final AsSuperVisitor asSuperVisitor, + final SupertypeVisitor supertypeVisitor, // used only for intersection type cases; passed to substitute visitor + final SameTypeVisitor sameTypeVisitor, + final ContainsTypeVisitor containsTypeVisitor, + final CaptureVisitor captureVisitor, + final boolean capture) { + super(Boolean.FALSE); + this.cache = new HashSet<>(); + this.elementSource = Objects.requireNonNull(elementSource, "elementSource"); + this.equality = equality == null ? new Equality(true) : equality; + this.types = Objects.requireNonNull(types, "types"); + this.asSuperVisitor = Objects.requireNonNull(asSuperVisitor, "asSuperVisitor"); + this.supertypeVisitor = Objects.requireNonNull(supertypeVisitor, "supertypeVisitor"); + this.sameTypeVisitor = Objects.requireNonNull(sameTypeVisitor, "sameTypeVisitor"); + this.containsTypeVisitor = Objects.requireNonNull(containsTypeVisitor, "containsTypeVisitor"); + this.captureVisitor = Objects.requireNonNull(captureVisitor, "captureVisitor"); + + this.capture = capture; + if (capture) { + this.withCaptureVariant = this; + } else { + this.withoutCaptureVariant = this; + } + + asSuperVisitor.setSubtypeVisitor(this); + containsTypeVisitor.setSubtypeVisitor(this); + captureVisitor.setSubtypeVisitor(this); + } + + public final SubtypeVisitor withAsSuperVisitor(final AsSuperVisitor asSuperVisitor) { + if (asSuperVisitor == this.asSuperVisitor) { + return this; + } + return + new SubtypeVisitor(this.elementSource, + this.equality, + this.types, + asSuperVisitor, + this.supertypeVisitor, + this.sameTypeVisitor, + this.containsTypeVisitor(), + this.captureVisitor(), + this.capture); + } + + final SubtypeVisitor withCapture(final boolean capture) { + if (capture) { + if (this.withCaptureVariant == null) { + this.withCaptureVariant = + new SubtypeVisitor(this.elementSource, + this.equality, + this.types, + this.asSuperVisitor, + this.supertypeVisitor, + this.sameTypeVisitor, + this.containsTypeVisitor, + this.captureVisitor, + true); + } + return this.withCaptureVariant; + } else if (this.withoutCaptureVariant == null) { + this.withoutCaptureVariant = + new SubtypeVisitor(this.elementSource, + this.equality, + this.types, + this.asSuperVisitor, + this.supertypeVisitor, + this.sameTypeVisitor, + this.containsTypeVisitor, + this.captureVisitor, + false); + } + return this.withoutCaptureVariant; + } + + final AsSuperVisitor asSuperVisitor() { + return this.asSuperVisitor; + } + + final CaptureVisitor captureVisitor() { + return this.captureVisitor; + } + + final ContainsTypeVisitor containsTypeVisitor() { + return this.containsTypeVisitor; + } + + @Override + protected final Boolean defaultAction(final TypeMirror t, final TypeMirror s) { + if (this.equality.equals(t, Objects.requireNonNull(s, "s"))) { + // TODO: this is always equalsNoMetadata(), which means "equals minus annotations", which, in javac, means == + // except in the sole case of arrays, where the "elemtype"s are compared for equality. + // + // javac implements the subtype relation in a bizarre half-visitor, half-not-visitor setup, where equality and some + // edge cases are tested first, and then the visitor is applied if the test fails. This means the visitor javac uses + // is exceptionally weird. See for yourself: + // https://github.com/openjdk/jdk/blob/jdk-21%2B14/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L1068-L1105 + // + // Here, we try to fold this into the visitor itself. + return Boolean.TRUE; + } + return super.defaultAction(t, s); + } + + // Is t a subtype of s? + // + // See + // https://github.com/openjdk/jdk/blob/jdk-20+11/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L1200-L1217 + // + // See also: + // https://github.com/openjdk/jdk/blob/jdk-20+11/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L1082-L1107 + @Override + public final Boolean visitArray(final ArrayType t, final TypeMirror s) { + assert t.getKind() == TypeKind.ARRAY; + if (super.visitArray(t, s)) { + return Boolean.TRUE; + } + final TypeKind sKind = s.getKind(); + switch (sKind) { + case ARRAY: + // https://github.com/openjdk/jdk/blob/jdk-21%2B22/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L1199C29-L1204 + final TypeMirror tct = t.getComponentType(); + final TypeMirror sct = ((ArrayType)s).getComponentType(); + // If both are primitive arrays, then see if their component types are "the same". Otherwise return the visitation + // of the component types. + return tct.getKind().isPrimitive() ? this.sameTypeVisitor.visit(tct, sct) : this.withCapture(false).visit(tct, sct); + case DECLARED: + // https://github.com/openjdk/jdk/blob/jdk-21%2B22/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L1206-L1211 + final Name sName = ((QualifiedNameable)((DeclaredType)s).asElement()).getQualifiedName(); + return + sName.contentEquals("java.lang.Object") || + sName.contentEquals("java.lang.Cloneable") || + sName.contentEquals("java.io.Serializable"); + default: + return Boolean.FALSE; + } + } + + @Override + public final Boolean visitDeclared(final DeclaredType t, final TypeMirror s) { + assert t.getKind() == TypeKind.DECLARED; + if (super.visitDeclared(t, s)) { + // See + // https://github.com/openjdk/jdk/blob/jdk-21%2B15/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L1080-L1081 + // and defaultAction() above + return Boolean.TRUE; + } + return this.visitDeclaredOrIntersection(t, s); + } + + private final Boolean visitDeclaredOrIntersection(TypeMirror t, final TypeMirror s) { + assert t.getKind() == TypeKind.DECLARED || t.getKind() == TypeKind.INTERSECTION; + assert !this.equality.equals(t, s); // better have already checked it + + // From javac's isSubtype(Type t, Type s, boolean capture); see + // https://github.com/openjdk/jdk/blob/jdk-21%2B15/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L1085-L1091 + if (s.getKind() == TypeKind.INTERSECTION) { + if (!this.visit(t, this.supertypeVisitor.visit(s))) { + // Visiting the supertype of an intersection type is exactly visiting its first bound. How? Well, first, the + // direct supertypes of an intersection type are its bounds, and its bounds, it turns out, must be (partially) + // ordered from most specialized to least specialized, with classes preceding interfaces. So we visit its first + // bound, that is, its first direct supertype, which may be an interface or a non-interface type, but will be + // the most specialized/specific of all of its direct supertypes. + // + // Hypothesis unproven as of this writing: the first bound of an interfaces-only intersection type as created by + // javac will be java.lang.Object. + return Boolean.FALSE; + } + for (final TypeMirror i : this.supertypeVisitor.interfacesVisitor().visit(s)) { // TODO: really we should start with its second bound + if (!this.visit(t, i)) { + return Boolean.FALSE; + } + } + return Boolean.TRUE; + } + + if (t.getKind() == TypeKind.DECLARED) { + // TODO: + /* + // Generally, if 's' is a lower-bounded type variable, recur on lower bound; but + // for inference variables and intersections, we need to keep 's' + // (see JLS 4.10.2 for intersections and 18.2.3 for inference vars) + if (!t.hasTag(UNDETVAR) && !t.isCompound()) { // [ljnelson] we handled this above + // TODO: JDK-8039198, bounds checking sometimes passes in a wildcard as s + Type lower = cvarLowerBound(wildLowerBound(s)); + if (s != lower && !lower.hasTag(BOT)) + return isSubtype(capture ? capture(t) : t, lower, false); + } + */ + + // Capture t if necessary. Capturing only does anything on a parameterized type, so therefore only on a type where + // t.getKind() == TypeKind.DECLARED (notably not INTERSECTION). See + // https://github.com/openjdk/jdk/blob/jdk-21%2B15/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L1103. + if (this.capture) { + t = this.captureVisitor.visit(t); + assert t.getKind() == TypeKind.DECLARED; + } + } + + final Element sElement = asElement(s, true); /* yes, generate synthetic elements; if s is an array type, for example, some weirdo thing will be synthesized */ + assert sElement != null : "sElement == null; s: " + s; + final TypeMirror tsup = this.asSuperVisitor.visit(t, sElement); + if (tsup == null) { + return Boolean.FALSE; + } else if (tsup.getKind() != TypeKind.DECLARED) { + assert tsup.getKind() != TypeKind.INTERSECTION; + return this.withCapture(false).visit(tsup, s); + } else if (s.getKind() != TypeKind.DECLARED) { + assert s.getKind() != TypeKind.INTERSECTION; // already handled above + // The compiler ultimately does some logic that will ultimately return false if s is not a non-compound ClassType, + // i.e. if s is anything other than a DeclaredType. Handle that case early here. + return Boolean.FALSE; + } + + final DeclaredType tsupDt = (DeclaredType)tsup; + final DeclaredType sDt = (DeclaredType)s; + + if (this.equality.equals(tsupDt.asElement(), sDt.asElement())) { + // so far so good + if (allTypeArguments(sDt).isEmpty() || this.containsTypeRecursive(sDt, tsupDt)) { + // so far so good + } else { + // bzzt you lose + return Boolean.FALSE; + } + // so far so good + return this.withCapture(false).visit(tsupDt.getEnclosingType(), sDt.getEnclosingType()); + } else { + return Boolean.FALSE; + } + } + + @Override + public final Boolean visitError(final ErrorType t, final TypeMirror s) { + assert t.getKind() == TypeKind.ERROR; + return Boolean.TRUE; + } + + @Override + public final Boolean visitIntersection(final IntersectionType t, final TypeMirror s) { + assert t.getKind() == TypeKind.INTERSECTION; + if (super.visitIntersection(t, s)) { + return Boolean.TRUE; + } + // See + // https://github.com/openjdk/jdk/blob/jdk-20+13/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L1191-L1192 + // and + // https://github.com/openjdk/jdk/blob/jdk-20+13/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L1088-L1094 + return this.visitDeclaredOrIntersection(t, s); + } + + @Override + public final Boolean visitNoType(final NoType t, final TypeMirror s) { + // javac implements the subtype relation in a bizarre half-visitor, half-not-visitor setup, where equality and some + // edge cases are tested first, and then the visitor is applied if the test fails. This means the visitor javac uses + // is exceptionally weird. See for yourself: + // https://github.com/openjdk/jdk/blob/jdk-21%2B14/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L1068-L1105 + // + // So javac's isSubtype() will return true for two NoType.NONE instances, but its visitor will return false. But in + // practice, the visitor's comparison is never executed. We marry the two here. + return this.equality.equals(t, s); + } + + @Override + public final Boolean visitNull(final NullType t, final TypeMirror s) { + assert t.getKind() == TypeKind.NULL; + return switch (s.getKind()) { + case ARRAY, DECLARED, NULL, TYPEVAR -> Boolean.TRUE; + default -> Boolean.FALSE; + }; + } + + // See https://docs.oracle.com/javase/specs/jls/se19/html/jls-4.html#jls-4.10.1 + @Override + public final Boolean visitPrimitive(final PrimitiveType t, final TypeMirror s) { + final TypeKind tKind = t.getKind(); + final TypeKind sKind = s.getKind(); + switch (tKind) { + case BOOLEAN: + case DOUBLE: + return tKind == sKind; + case BYTE: + switch (sKind) { + case BYTE: + case DOUBLE: + case FLOAT: + case INT: + case LONG: + case SHORT: + return true; + default: + return false; + } + case CHAR: + switch (sKind) { + case CHAR: + case DOUBLE: + case FLOAT: + case INT: + case LONG: + return true; + default: + return false; + } + case FLOAT: + switch (sKind) { + case DOUBLE: + case FLOAT: + return true; + default: + return false; + } + case INT: + switch (sKind) { + case DOUBLE: + case FLOAT: + case INT: + case LONG: + return true; + default: + return false; + } + case LONG: + switch (sKind) { + case DOUBLE: + case FLOAT: + case LONG: + return true; + default: + return false; + } + case SHORT: + switch (sKind) { + case DOUBLE: + case FLOAT: + case INT: + case LONG: + case SHORT: + return true; + default: + return false; + } + default: + return false; + } + } + + @Override + public final Boolean visitTypeVariable(final TypeVariable t, final TypeMirror s) { + assert t.getKind() == TypeKind.TYPEVAR; + // javac implements the subtype relation in a bizarre half-visitor, half-not-visitor setup, where equality and some + // edge cases are tested first, and then the visitor is applied if the test fails. This means the visitor javac uses + // is exceptionally weird. See for yourself: + // https://github.com/openjdk/jdk/blob/jdk-21%2B14/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L1068-L1105 + // + // We try to marry javac's visitor-based subtype relation and its non-visitor-based isSubtype method, which is why + // you see the equality test here. + return this.equality.equals(t, s) || this.withCapture(false).visit(t.getUpperBound(), s); + } + + @Override + public final Boolean visitWildcard(final WildcardType t, final TypeMirror s) { + assert t.getKind() == TypeKind.WILDCARD; + // See + // https://github.com/openjdk/jdk/blob/jdk-20+13/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L1129 + // and + // https://bugs.java.com/bugdatabase/view_bug.do?bug_id=7034495 + // and + // https://bugs.java.com/bugdatabase/view_bug.do?bug_id=7034922 + return false; + } + + private final boolean containsTypeRecursive(final TypeMirror t, final TypeMirror s) { + if (t.getKind() == TypeKind.DECLARED && s.getKind() == TypeKind.DECLARED) { + final List tTypeArguments = ((DeclaredType)t).getTypeArguments(); + final DeclaredType dts = (DeclaredType)s; + List sTypeArguments = dts.getTypeArguments(); + if (tTypeArguments.isEmpty() || sTypeArguments.isEmpty()) { + return false; + } + final TypeMirrorPair pair = new TypeMirrorPair(this.types, this.sameTypeVisitor, t, s); + if (this.cache.add(pair)) { + try { + return this.containsTypeVisitor.visit(tTypeArguments, sTypeArguments); + } finally { + this.cache.remove(pair); + } + } + final TypeMirror rewrittenS = this.rewriteSupers(dts); + switch (rewrittenS.getKind()) { + case DECLARED: + sTypeArguments = ((DeclaredType)rewrittenS).getTypeArguments(); + return !sTypeArguments.isEmpty() && this.containsTypeVisitor.visit(tTypeArguments, sTypeArguments); + default: + return false; + } + } + return false; + } + + private final TypeMirror rewriteSupers(final TypeMirror t) { + // I guess t could be an ArrayType (i.e. generic array type) + if (!allTypeArguments(t).isEmpty()) { + final List from = new ArrayList<>(); + final List to = new ArrayList<>(); + new AdaptingVisitor(this.elementSource, this.types, this.sameTypeVisitor, this, from, to).adaptSelf((DeclaredType)t); + if (!from.isEmpty()) { + final List rewrite = new ArrayList<>(); + boolean changed = false; + for (final TypeMirror orig : to) { + TypeMirror s = this.rewriteSupers(orig); // RECURSIVE + switch (s.getKind()) { + case WILDCARD: + // TODO: I'm not sure this case is actually possible. Ported from the javac nevertheless. + // + // TODO: I need to check and make sure I'm not getting burned by "bound", which in javac may mean, for + // example, "the lower bound of the type parameter element that the wildcard parameterizes" + if (((WildcardType)s).getSuperBound() != null) { + // TODO: maybe need to somehow ensure this shows up as non-canonical/synthetic + s = unboundedWildcardType(s.getAnnotationMirrors()); + changed = true; + } + break; + default: + if (s != orig) { // Don't need Equality.equals() here + // TODO: maybe need to somehow ensure this shows up as non-canonical/synthetic + s = upperBoundedWildcardType(this.types.extendsBound(s), s.getAnnotationMirrors()); + changed = true; + } + break; + } + rewrite.add(s); + } + if (changed) { + // (If t is a DeclaredType or a TypeVariable, call asElement().asType() and visit that.) + return new SubstituteVisitor(this.elementSource, this.equality, this.supertypeVisitor, from, rewrite) + .visit(switch (t.getKind()) { + case DECLARED -> ((DeclaredType)t).asElement().asType(); + case TYPEVAR -> ((TypeVariable)t).asElement().asType(); + default -> t; + }); + } + } + } + return t; + } + +} diff --git a/lang/src/main/java/org/microbean/lang/visitor/SupertypeVisitor.java b/lang/src/main/java/org/microbean/lang/visitor/SupertypeVisitor.java new file mode 100644 index 00000000..9dc59919 --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/visitor/SupertypeVisitor.java @@ -0,0 +1,221 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.microbean.lang.visitor; + +import java.util.List; +import java.util.Objects; + +import java.util.function.Predicate; + +import javax.lang.model.element.QualifiedNameable; +import javax.lang.model.element.TypeElement; + +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.IntersectionType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; + +import javax.lang.model.util.SimpleTypeVisitor14; + +import org.microbean.lang.TypeAndElementSource; +import org.microbean.lang.Equality; + +import org.microbean.lang.type.NoType; +import org.microbean.lang.type.Types; + +// Returns the superclass (or parameterized superclass) of a type, if applicable. See #interfacesVisitor() for returning +// superinterfaces. +// +// Basically done +public final class SupertypeVisitor extends SimpleTypeVisitor14 { + + + /* + * Instance fields. + */ + + + private final TypeAndElementSource elementSource; + + private final Equality equality; + + private final Types types; + + private final EraseVisitor eraseVisitor; + + private final Predicate filter; + + private final InterfacesVisitor interfacesVisitor; // (created by this class) + + private final BoundingClassVisitor boundingClassVisitor; // (inner class) + + + /* + * Constructors. + */ + + + public SupertypeVisitor(final TypeAndElementSource elementSource, + final Types types, + final EraseVisitor eraseVisitor) { + this(elementSource, null, types, eraseVisitor, null); + } + + public SupertypeVisitor(final TypeAndElementSource elementSource, + final Types types, + final EraseVisitor eraseVisitor, + final Predicate filter) { + this(elementSource, null, types, eraseVisitor, filter); + } + + public SupertypeVisitor(final TypeAndElementSource elementSource, + final Equality equality, + final Types types, + final EraseVisitor eraseVisitor, + final Predicate filter) { + super(NoType.NONE); // default return value from the visit*() methods + this.filter = filter == null ? t -> true : filter; + this.equality = equality == null ? new Equality(true) : equality; + this.elementSource = Objects.requireNonNull(elementSource, "elementSource"); + this.types = Objects.requireNonNull(types, "types"); + this.eraseVisitor = Objects.requireNonNull(eraseVisitor, "eraseVisitor"); + this.interfacesVisitor = new InterfacesVisitor(elementSource, this.equality, types, eraseVisitor, this, filter); + this.boundingClassVisitor = new BoundingClassVisitor(this); + } + + + /* + * Instance methods. + */ + + public final SupertypeVisitor withFilter(final Predicate filter) { + return filter == this.filter ? this : + new SupertypeVisitor(this.elementSource, this.equality, this.types, this.eraseVisitor, filter); + } + + public final InterfacesVisitor interfacesVisitor() { + return this.interfacesVisitor; + } + + @Override // SimpleTypeVisitor14 + public final TypeMirror visitArray(final ArrayType t, final Void x) { + assert t.getKind() == TypeKind.ARRAY; + final TypeMirror componentType = t.getComponentType(); + final TypeMirror returnValue; + if (componentType.getKind().isPrimitive() || isJavaLangObjectType(componentType)) { + // e.g. int[] or Object[] + returnValue = this.topLevelArraySupertype(); + } else { + final org.microbean.lang.type.ArrayType a = new org.microbean.lang.type.ArrayType(this.visit(componentType)); + a.addAnnotationMirrors(t.getAnnotationMirrors()); + returnValue = a; + } + if (this.filter.test(returnValue)) { + return returnValue; + } + return this.visit(returnValue); + } + + private final IntersectionType topLevelArraySupertype() { + return + new org.microbean.lang.type.IntersectionType(List.of(this.elementSource.typeElement("java.lang.Object").asType(), + this.elementSource.typeElement("java.io.Serializable").asType(), + this.elementSource.typeElement("java.lang.Cloneable").asType())); + } + + // The compiler's code is borderline incomprehensible. + // https://github.com/openjdk/jdk/blob/jdk-20+11/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L2531-L2552 + @Override // SimpleTypeVisitor14 + @SuppressWarnings("unchecked") + public final TypeMirror visitDeclared(final DeclaredType t, final Void x) { + assert t.getKind() == TypeKind.DECLARED; + final TypeMirror returnValue; + final TypeElement element = (TypeElement)t.asElement(); + final TypeMirror supertype = element.getSuperclass(); + // TODO: if supertype is DefaultNoType.NONE...? as would happen when element is an interface? The compiler + // does some wonky caching initialization: + // https://github.com/openjdk/jdk/blob/jdk-20+11/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L2536-L2537 + // I wonder: does it sneakily set this field to Object.class? Or does it let it be TypeKind.NONE? + if (this.types.raw(t)) { + returnValue = this.eraseVisitor.visit(supertype, true); + } else { + + // Get the actual declared type. If, for example, t represents the type denoted by "List", then get the + // type denoted by "List". + final DeclaredType typeDeclaration = (DeclaredType)element.asType(); + + // The type arguments of such a declared type are always type variables (declared by type parameters). In the + // type denoted by "List", the sole type *argument* is the type denoted by (the type parameter element) "E", + // and the sole type parameter element that declares that type is the element "E" itself. + @SuppressWarnings("unchecked") + final List formals = (List)Types.allTypeArguments(typeDeclaration); + if (formals.isEmpty()) { + returnValue = supertype; + } else { + returnValue = + new SubstituteVisitor(this.elementSource, + this.equality, + this, + formals, + (List)Types.allTypeArguments(this.boundingClassVisitor.visit(t))) // TODO: is this cast really and truly OK? + .visit(supertype); + } + } + if (this.filter.test(returnValue)) { + return returnValue; + } + return this.visit(returnValue); + } + + @Override // SimpleTypeVisitor14 + public final TypeMirror visitIntersection(final IntersectionType t, final Void x) { + assert t.getKind() == TypeKind.INTERSECTION; + final TypeMirror returnValue = t.getBounds().get(0); // TODO: presumes first bound will be the supertype; see https://github.com/openjdk/jdk/blob/jdk-19+25/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Type.java#L1268 + if (this.filter.test(returnValue)) { + return returnValue; + } + return this.visit(returnValue); + } + + @Override // SimpleTypeVisitor14 + public final TypeMirror visitTypeVariable(final TypeVariable t, final Void x) { + assert t.getKind() == TypeKind.TYPEVAR; + final TypeMirror upperBound = t.getUpperBound(); + final TypeMirror returnValue = switch (upperBound.getKind()) { + case TYPEVAR -> upperBound; + case INTERSECTION -> this.visit(upperBound); + default -> Types.isInterface(upperBound) ? this.visit(upperBound) : upperBound; + }; + if (this.filter.test(returnValue)) { + return returnValue; + } + return this.visit(returnValue); + } + + + /* + * Static methods. + */ + + + private static final boolean isJavaLangObjectType(final TypeMirror t) { + return + t.getKind() == TypeKind.DECLARED && + ((DeclaredType)t).asElement() instanceof QualifiedNameable qn && + qn.getQualifiedName().contentEquals("java.lang.Object"); + } + +} diff --git a/lang/src/main/java/org/microbean/lang/visitor/TypeClosure.java b/lang/src/main/java/org/microbean/lang/visitor/TypeClosure.java new file mode 100644 index 00000000..96dfd92a --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/visitor/TypeClosure.java @@ -0,0 +1,252 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.visitor; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Deque; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import java.util.function.BiPredicate; + +import javax.lang.model.element.Element; + +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; + +import org.microbean.lang.TypeAndElementSource; +import org.microbean.lang.Equality; + +import org.microbean.lang.type.DelegatingTypeMirror; + +// A type closure list builder. +// NOT THREADSAFE +public final class TypeClosure { + + private final TypeAndElementSource elementSource; + + // DelegatingTypeMirror so things like list.contains(t) will work with arbitrary TypeMirror implementations + private final Deque deque; + + private final BiPredicate precedesPredicate; + + private final BiPredicate equalsPredicate; + + TypeClosure(final TypeAndElementSource elementSource, final SupertypeVisitor supertypeVisitor, final SubtypeVisitor subtypeVisitor) { + this(elementSource, new PrecedesPredicate(supertypeVisitor, subtypeVisitor), null); + } + + TypeClosure(final TypeAndElementSource elementSource, final BiPredicate precedesPredicate) { + this(elementSource, precedesPredicate, null); + } + + TypeClosure(final TypeAndElementSource elementSource, + final BiPredicate precedesPredicate, + final BiPredicate equalsPredicate) { + super(); + this.elementSource = Objects.requireNonNull(elementSource, "elementSource"); + this.deque = new ArrayDeque<>(10); + this.precedesPredicate = Objects.requireNonNull(precedesPredicate, "precedesPredicate"); + this.equalsPredicate = equalsPredicate == null ? Equality::equalsIncludingAnnotations : equalsPredicate; + } + + final void union(final TypeMirror t) { + // t must be DECLARED or TYPEVAR. TypeClosureVisitor deals with INTERSECTION before it gets here. + + final Element e; + switch (t.getKind()) { + case DECLARED: + e = ((DeclaredType)t).asElement(); + break; + case TYPEVAR: + e = ((TypeVariable)t).asElement(); + break; + default: + throw new IllegalArgumentException("t: " + t); // TODO: or just return + } + + final DelegatingTypeMirror head = this.deque.peekFirst(); + if (head == null) { + this.deque.addFirst(DelegatingTypeMirror.of(t, this.elementSource)); + return; + } + + final Element headE; + switch (head.getKind()) { + case DECLARED: + headE = ((DeclaredType)head).asElement(); + break; + case TYPEVAR: + headE = ((TypeVariable)head).asElement(); + break; + default: + throw new IllegalStateException(); + } + + if (!this.equalsPredicate.test(e, headE)) { + if (this.precedesPredicate.test(e, headE)) { + this.deque.addFirst(DelegatingTypeMirror.of(t, this.elementSource)); + } else if (this.deque.size() == 1) { + // No need to recurse and get fancy; just add last + this.deque.addLast(DelegatingTypeMirror.of(t, this.elementSource)); + } else { + this.deque.removeFirst(); // returns head + this.union(t); // RECURSIVE + this.deque.addFirst(head); + } + } + + } + + final void union(final TypeClosure tc) { + this.union(tc.toList()); + } + + private final void union(final List list) { + final int size = list.size(); + switch (size) { + case 0: + return; + case 1: + this.union(list.get(0)); + return; + default: + break; + } + + if (this.deque.isEmpty()) { + for (final TypeMirror t : list) { + switch (t.getKind()) { + case DECLARED: + case TYPEVAR: + this.deque.addLast(DelegatingTypeMirror.of(t, this.elementSource)); + break; + default: + throw new IllegalArgumentException("t: " + t); + } + } + return; + } + + final DelegatingTypeMirror head1 = this.deque.peekFirst(); + final Element head1E; + switch (head1.getKind()) { + case DECLARED: + head1E = ((DeclaredType)head1).asElement(); + break; + case TYPEVAR: + head1E = ((TypeVariable)head1).asElement(); + break; + default: + throw new IllegalStateException(); + } + + final TypeMirror head2 = list.get(0); + final Element head2E; + switch (head2.getKind()) { + case DECLARED: + head2E = ((DeclaredType)head2).asElement(); + break; + case TYPEVAR: + head2E = ((TypeVariable)head2).asElement(); + break; + default: + throw new IllegalArgumentException("list: " + list); + } + + if (this.equalsPredicate.test(head1E, head2E)) { + // Don't include head2. + this.deque.removeFirst(); // removes head1 + this.union(list.subList(1, size)); // RECURSIVE + this.deque.addFirst(head1); // put head1 back + } else if (this.precedesPredicate.test(head2E, head1E)) { + this.union(list.subList(1, size)); // RECURSIVE + this.deque.addFirst(DelegatingTypeMirror.of(head2, this.elementSource)); + } else { + this.deque.removeFirst(); // removes head1 + this.union(list); // RECURSIVE + this.deque.addFirst(head1); // put head1 back + } + } + + // Weird case. See + // https://github.com/openjdk/jdk/blob/jdk-20+14/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L3717-L3718. + final void prepend(final TypeVariable t) { + if (t.getKind() != TypeKind.TYPEVAR) { + throw new IllegalArgumentException("t: " + t); + } + this.deque.addFirst(DelegatingTypeMirror.of(t, this.elementSource)); + } + + // Port of javac's Types#closureMin(List) + // https://github.com/openjdk/jdk/blob/jdk-20+14/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L3915-L3945 + final List toMinimumTypes(final SubtypeVisitor subtypeVisitor) { + final ArrayList classes = new ArrayList<>(); + final List interfaces = new ArrayList<>(); + final Set toSkip = new HashSet<>(); + + final TypeMirror[] array = this.deque.toArray(new TypeMirror[0]); + final int size = array.length; + + for (int i = 0, next = 1; i < size; i++, next++) { + final TypeMirror current = array[i]; + boolean keep = !toSkip.contains(DelegatingTypeMirror.of(current, this.elementSource)); + if (keep && current.getKind() == TypeKind.TYPEVAR && next < size) { + for (int j = next; j < size; j++) { + if (subtypeVisitor.withCapture(false).visit(array[j], current)) { + // If there's a subtype of current "later" in the list, then there's no need to "keep" current; it will be + // implied by the subtype's supertype hierarchy. + keep = false; + break; + } + } + } + if (keep) { + if (current.getKind() == TypeKind.DECLARED && ((DeclaredType)current).asElement().getKind().isInterface()) { + interfaces.add(current); + } else { + classes.add(current); + } + if (next < size) { + for (int j = next; j < size; j++) { + final TypeMirror t = array[j]; + if (subtypeVisitor.withCapture(false).visit(current, t)) { + // As we're processing this.list, we can skip supertypes of current (t, here) because we know current is + // already more specialized than they are. + toSkip.add(DelegatingTypeMirror.of(t, this.elementSource)); + } + } + } + } + } + classes.addAll(interfaces); + classes.trimToSize(); + return Collections.unmodifiableList(classes); + } + + // Every element of the returned list will have a TypeKind that is either DECLARED or TYPEVAR. + public final List toList() { + return List.copyOf(this.deque); + } + +} diff --git a/lang/src/main/java/org/microbean/lang/visitor/TypeClosureVisitor.java b/lang/src/main/java/org/microbean/lang/visitor/TypeClosureVisitor.java new file mode 100644 index 00000000..59208de8 --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/visitor/TypeClosureVisitor.java @@ -0,0 +1,151 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.microbean.lang.visitor; + +import java.util.Map; +import java.util.Objects; +import java.util.WeakHashMap; + +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.IntersectionType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; + +import javax.lang.model.util.SimpleTypeVisitor14; + +import org.microbean.lang.TypeAndElementSource; + +public final class TypeClosureVisitor extends SimpleTypeVisitor14 { + + private final TypeAndElementSource elementSource; + + // @GuardedBy("itself") + private final WeakHashMap closureCache; + + private final SupertypeVisitor supertypeVisitor; + + private final PrecedesPredicate precedesPredicate; + + public TypeClosureVisitor(final TypeAndElementSource elementSource, + final SupertypeVisitor supertypeVisitor, + final PrecedesPredicate precedesPredicate) { + super(); + this.elementSource = Objects.requireNonNull(elementSource, "elementSource"); + this.supertypeVisitor = Objects.requireNonNull(supertypeVisitor, "supertypeVisitor"); + this.precedesPredicate = Objects.requireNonNull(precedesPredicate, "precedesPredicate"); + this.closureCache = new WeakHashMap<>(); + } + + @Override + protected final TypeClosure defaultAction(final TypeMirror t, final Void x) { + // Interestingly, javac's Types#closure(Type) method returns a single-element list containing t if t is not a class + // or interface type. Nevertheless the intention seems to be that only class or interface types should be supplied, + // so we enforce that here. + throw new IllegalArgumentException("t: " + t + "; t.getKind(): " + t.getKind() + (t instanceof DeclaredType dt ? "; element: " + dt.asElement() + "; element kind: " + dt.asElement().getKind() : "")); + } + + @Override + public final TypeClosure visitDeclared(final DeclaredType t, final Void x) { + assert t.getKind() == TypeKind.DECLARED; + return this.visitDeclaredOrIntersectionOrTypeVariable(t, x); + } + + @Override + public final TypeClosure visitIntersection(final IntersectionType t, final Void x) { + assert t.getKind() == TypeKind.INTERSECTION; + return this.visitDeclaredOrIntersectionOrTypeVariable(t, x); + } + + @Override + public final TypeClosure visitTypeVariable(final TypeVariable t, final Void x) { + assert t.getKind() == TypeKind.TYPEVAR; + return this.visitDeclaredOrIntersectionOrTypeVariable(t, x); + } + + private final TypeClosure visitDeclaredOrIntersectionOrTypeVariable(final TypeMirror t, final Void x) { + TypeClosure closure; + synchronized (this.closureCache) { + closure = this.closureCache.get(t); + } + if (closure == null) { + switch (t.getKind()) { + case INTERSECTION: + // The closure does not include the intersection type itself. Note that this little nugget effectively removes + // intersection types from the possible types that will ever be passed to TypeClosure#union(TypeMirror). + closure = this.visit(this.supertypeVisitor.visit(t)); + break; + + case DECLARED: + case TYPEVAR: + final TypeMirror st = this.supertypeVisitor.visit(t); + switch (st.getKind()) { + case DECLARED: + // (Yes, it is OK that INTERSECTION is not present as a case; a TypeVariable cannot have an IntersectionType + // as a supertype.) + closure = this.visit(st); + closure.union(t); // reflexive + break; + + case TYPEVAR: + // javac does this + // (https://github.com/openjdk/jdk/blob/jdk-20+14/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L3717-L3718): + // + // cl = closure(st).prepend(t); + // + // Note that there's no equality or "precedes" check in this one case. Is this a bug, a feature, or just an + // optimization? I don't know. I reproduce the behavior here, for better or for worse. This permits two equal + // type variables in the closure, which otherwise would be filtered out. + // + // (The only time a supertype can be a type variable is if the subtype is also a type variable.) + assert t.getKind() == TypeKind.TYPEVAR : "Expected " + TypeKind.TYPEVAR + "; got DECLARED; t: " + t + "; st: " + st; + closure = this.visit(st); + closure.prepend((TypeVariable)t); // reflexive + break; + + case NONE: + closure = new TypeClosure(this.elementSource, this.precedesPredicate); + closure.union(t); // reflexive + break; + + default: + // Every now and again, probably only under parallel testing scenarios: + /* + java.lang.IllegalArgumentException: t: T + at org.microbean.lang@0.0.1-SNAPSHOT/org.microbean.lang.visitor.TypeClosureVisitor.visitDeclaredOrIntersectionOrTypeVariable(TypeClosureVisitor.java:123) + at org.microbean.lang@0.0.1-SNAPSHOT/org.microbean.lang.visitor.TypeClosureVisitor.visitTypeVariable(TypeClosureVisitor.java:74) + at org.microbean.lang@0.0.1-SNAPSHOT/org.microbean.lang.visitor.TypeClosureVisitor.visitTypeVariable(TypeClosureVisitor.java:30) + at jdk.compiler/com.sun.tools.javac.code.Type$TypeVar.accept(Type.java:1737) + at java.compiler@20/javax.lang.model.util.AbstractTypeVisitor6.visit(AbstractTypeVisitor6.java:104) + at org.microbean.bean@0.0.1-SNAPSHOT/org.microbean.bean.ReferenceTypeList.closure(ReferenceTypeList.java:251) + at org.microbean.bean@0.0.1-SNAPSHOT/org.microbean.bean.ReferenceTypeList.closure(ReferenceTypeList.java:237) + */ + // Probably t.getKind() is ERROR, and this is a synchronization problem. + throw new IllegalArgumentException("t: " + t + "; t.getKind(): " + t.getKind()); + } + break; + default: + throw new IllegalArgumentException("t: " + t); + } + for (final TypeMirror iface : this.supertypeVisitor.interfacesVisitor().visit(t)) { + closure.union(this.visit(iface)); + } + synchronized (this.closureCache) { + closureCache.put(t, closure); + } + } + return closure; + } + +} diff --git a/lang/src/main/java/org/microbean/lang/visitor/TypeMirrorPair.java b/lang/src/main/java/org/microbean/lang/visitor/TypeMirrorPair.java new file mode 100644 index 00000000..4908ecca --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/visitor/TypeMirrorPair.java @@ -0,0 +1,238 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.visitor; + +import java.util.List; +import java.util.Objects; + +import javax.lang.model.element.Element; +import javax.lang.model.element.Name; + +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.ExecutableType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; +import javax.lang.model.type.WildcardType; + +import org.microbean.lang.type.Types; + +import static org.microbean.lang.type.Types.asElement; + +final class TypeMirrorPair { + + private final Types types; + + private final SameTypeVisitor sameTypeVisitor; + + private final TypeMirror t; + + private final TypeMirror s; + + TypeMirrorPair(final Types types, + final SameTypeVisitor sameTypeVisitor, + final TypeMirror t, + final TypeMirror s) { + super(); + this.types = Objects.requireNonNull(types, "types"); + this.sameTypeVisitor = Objects.requireNonNull(sameTypeVisitor, "sameTypeVisitor"); + this.t = Objects.requireNonNull(t, "t"); + this.s = Objects.requireNonNull(s, "s"); + } + + @Override + public final boolean equals(final Object other) { + if (this == other) { + return true; + } else if (other != null && this.getClass() == other.getClass()) { + final TypeMirrorPair her = (TypeMirrorPair)other; + return + this.sameTypeVisitor.visit(this.t, her.t) && + this.sameTypeVisitor.visit(this.s, her.s); + } else { + return false; + } + } + + @Override + public final int hashCode() { + return 127 * hashCode(this.t) + hashCode(this.s); + } + + private final int hashCode(final TypeMirror t) { + if (t == null) { + return 0; + } + final TypeKind tk = t.getKind(); + switch (tk) { + case ARRAY: + return this.hashCode((ArrayType)t); + case DECLARED: + case INTERSECTION: + // return this.hashCode0(t); + int result = 127 * hashCode(enclosingType(t)); + final Element e = asElement(t, true); + if (e != null) { + result += flatName(e).hashCode(); + } + for (final TypeMirror typeArgument : typeArguments(t)) { + result = 127 * result + hashCode(typeArgument); + } + return result; + case EXECUTABLE: + return this.hashCode((ExecutableType)t); + case TYPEVAR: + return this.hashCode((TypeVariable)t); + case WILDCARD: + return this.hashCode((WildcardType)t); + default: + return tk.ordinal(); + } + } + + private final int hashCode(final ArrayType t) { + assert t.getKind() == TypeKind.ARRAY; + return hashCode(t.getComponentType()) + 12; + } + + // Involved: + // * kind + // * parameter types + // * return type + private final int hashCode(final ExecutableType t) { + final TypeKind tk = t.getKind(); + assert tk == TypeKind.EXECUTABLE; + int result = tk.ordinal(); + for (final TypeMirror pt : t.getParameterTypes()) { + result = (result << 5) + hashCode(pt); + } + return (result << 5) + hashCode(t.getReturnType()); + } + + private final int hashCode(final TypeVariable t) { + assert t.getKind() == TypeKind.TYPEVAR; + // TODO: it sure would be nice if we could use Equality.hashCode(t) here; maybe we can? + return System.identityHashCode(t); + } + + // https://github.com/openjdk/jdk/blob/jdk-20+16/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L4204-L4212 + private final int hashCode(final WildcardType t) { + assert t.getKind() == TypeKind.WILDCARD; + // javac's Types#HashcodeVisitor starts with the hashcode of the wildcard's BoundKind + // (https://github.com/openjdk/jdk/blob/jdk-20+17/src/jdk.compiler/share/classes/com/sun/tools/javac/code/BoundKind.java). + // + // BoundKind is: + // + // public enum BoundKind { + // EXTENDS, + // SUPER, + // UNBOUND + // } + // + // ...so doesn't override the default enum hashcode calculation. Each constant in the enum therefore has a constant + // hashcode value of System.identityHashCode(CONSTANT). We'll use 0, 1 and 2 instead. + // + // Other odd things: + // + // * the wildcard type denoted by ? extends Object has a different hashcode according to Types#HashcodeVisitor than + // does ?. Is this deliberate? does it matter? not sure. + final TypeMirror superBound = t.getSuperBound(); + final TypeMirror extendsBound = t.getExtendsBound(); + final int result; + if (superBound == null) { + result = extendsBound == null ? 254 /* 2 (UNBOUND) * 127 */ : hashCode(extendsBound) /* 0 (EXTENDS) * 127 + hashCode(extendsBound) */; + } else if (extendsBound == null) { + result = 127 + hashCode(superBound); // i.e. 1 (SUPER) * 127 + hashCode(superBound) + } else { + throw new IllegalArgumentException("t: " + t); + } + return result; + } + + private static final TypeMirror enclosingType(final TypeMirror t) { + switch (t.getKind()) { + case DECLARED: + return ((DeclaredType)t).getEnclosingType(); + default: + return org.microbean.lang.type.NoType.NONE; + } + } + + private static final Name flatName(final Element e) { + /* + // form a fully qualified name from a name and an owner, after + // converting to flat representation + public static Name formFlatName(Name name, Symbol owner) { + if (owner == null || owner.kind.matches(KindSelector.VAL_MTH) || + (owner.kind == TYP && owner.type.hasTag(TYPEVAR)) + ) return name; + char sep = owner.kind == TYP ? '$' : '.'; + Name prefix = owner.flatName(); + if (prefix == null || prefix == prefix.table.names.empty) + return name; + else return prefix.append(sep, name); + } + */ + final Element enclosingElement = e.getEnclosingElement(); + if (enclosingElement == null) { + return e.getSimpleName(); + } + final char separator; + switch (enclosingElement.getKind()) { + case ANNOTATION_TYPE: + case CLASS: + case ENUM: + case INTERFACE: + case RECORD: + switch (enclosingElement.asType().getKind()) { + case TYPEVAR: + return e.getSimpleName(); + default: + separator = '$'; + break; + } + break; + case CONSTRUCTOR: + case METHOD: + case BINDING_VARIABLE: + case ENUM_CONSTANT: + case EXCEPTION_PARAMETER: + case FIELD: + case LOCAL_VARIABLE: + case PARAMETER: + case RESOURCE_VARIABLE: + return e.getSimpleName(); + default: + separator = '.'; + break; + } + final Name prefix = flatName(enclosingElement); // RECURSIVE + if (prefix == null || prefix.isEmpty()) { + return e.getSimpleName(); + } + return org.microbean.lang.element.Name.of(new StringBuilder(prefix).append(separator).append(e.getSimpleName()).toString()); + } + + private static final List typeArguments(final TypeMirror t) { + return switch (t.getKind()) { + case DECLARED -> ((DeclaredType)t).getTypeArguments(); + default -> List.of(); + }; + } + +} diff --git a/lang/src/main/java/org/microbean/lang/visitor/ValidatingVisitor.java b/lang/src/main/java/org/microbean/lang/visitor/ValidatingVisitor.java new file mode 100644 index 00000000..bfdd1bfd --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/visitor/ValidatingVisitor.java @@ -0,0 +1,342 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang.visitor; + +import java.util.Collection; +import java.util.Objects; + +import javax.lang.model.element.Element; + +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.ExecutableType; +import javax.lang.model.type.IntersectionType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; +import javax.lang.model.type.UnionType; +import javax.lang.model.type.WildcardType; + +import org.microbean.lang.TypeAndElementSource; + +public final class ValidatingVisitor extends StructuralTypeMapping { + + public ValidatingVisitor(final TypeAndElementSource elementSource) { + super(elementSource); + } + + @Override // SimpleTypeVisitor6 + public TypeMirror visitArray(final ArrayType t, final Void x) { + switch (t.getKind()) { + case ARRAY: + break; + default: + throw new IllegalArgumentException("t: " + t); + } + final TypeMirror componentType = t.getComponentType(); + if (componentType == null || componentType == t) { + throw new IllegalArgumentException("t: " + t); + } + switch (componentType.getKind()) { + case ARRAY: + case BOOLEAN: + case BYTE: + case CHAR: + case DECLARED: + case DOUBLE: + case FLOAT: + case INT: + case LONG: + case SHORT: + case TYPEVAR: + break; + default: + throw new IllegalArgumentException("t: " + t); + } + return super.visitArray(t, x); + } + + @Override // SimpleTypeVisitor6 + public TypeMirror visitDeclared(final DeclaredType t, final Void x) { + final TypeMirror et = t.getEnclosingType(); + if (et == null || et == t) { + throw new IllegalArgumentException("t: " + t); + } + final Iterable typeArguments = t.getTypeArguments(); + if (typeArguments == null) { + throw new IllegalArgumentException("t: " + t); + } + for (final TypeMirror typeArgument : typeArguments) { + if (typeArgument == null || typeArgument == t) { + throw new IllegalArgumentException("t: " + t); + } + switch (typeArgument.getKind()) { + case DECLARED: + case TYPEVAR: + case WILDCARD: // ...right? + break; + default: + throw new IllegalArgumentException("t: " + t); + } + } + final Element e = t.asElement(); + if (e == null) { + throw new IllegalArgumentException("t: " + t); + } + switch (e.getKind()) { + case ANNOTATION_TYPE: + case CLASS: + case ENUM: + case INTERFACE: + switch (t.getKind()) { + case DECLARED: + break; + default: + throw new IllegalArgumentException("t: " + t); + } + break; + default: + throw new IllegalArgumentException("t: " + t); + } + return super.visitDeclared(t, x); + } + + @Override // SimpleTypeVisitor6 + public TypeMirror visitExecutable(final ExecutableType t, final Void x) { + final Iterable pts = t.getParameterTypes(); + if (pts == null) { + throw new IllegalArgumentException("t: " + t); + } + for (final TypeMirror pt : pts) { + switch (pt.getKind()) { + case ARRAY: + case BOOLEAN: + case BYTE: + case CHAR: + case DECLARED: + case DOUBLE: + case FLOAT: + case INT: + case LONG: + case SHORT: + case TYPEVAR: + break; + default: + throw new IllegalArgumentException("t: " + t); + } + } + final TypeMirror receiverType = t.getReceiverType(); + if (receiverType == null) { + throw new IllegalArgumentException("t: " + t); + } + switch (receiverType.getKind()) { + case DECLARED: + case NONE: + break; + default: + throw new IllegalArgumentException("t: " + t); + } + final TypeMirror returnType = t.getReturnType(); + if (returnType == null) { + throw new IllegalArgumentException("t: " + t); + } + switch (returnType.getKind()) { + case ARRAY: + case BOOLEAN: + case BYTE: + case CHAR: + case DECLARED: + case DOUBLE: + case FLOAT: + case INT: + case LONG: + case SHORT: + case TYPEVAR: + case VOID: + break; + default: + throw new IllegalArgumentException("t: " + t); + } + final Iterable tts = t.getThrownTypes(); + if (tts == null) { + throw new IllegalArgumentException("t: " + t); + } + for (final TypeMirror tt : tts) { + switch (tt.getKind()) { + case DECLARED: + case TYPEVAR: + break; + default: + throw new IllegalArgumentException("t: " + t); + } + } + final Iterable tvs = t.getTypeVariables(); + if (tvs == null) { + throw new IllegalArgumentException("t: " + t); + } + for (final TypeVariable tv : tvs) { + switch (tv.getKind()) { + case TYPEVAR: + break; + default: + throw new IllegalArgumentException("t: " + t); + } + } + return super.visitExecutable(t, x); + } + + @Override // SimpleTypeVisitor6 + public TypeMirror visitIntersection(final IntersectionType t, final Void x) { + final Collection bounds = t.getBounds(); + if (bounds == null || bounds.isEmpty()) { + throw new IllegalArgumentException("t: " + t); + } + for (final TypeMirror bound : bounds) { + if (bound == null) { + throw new IllegalArgumentException("t: " + t); + } + switch (bound.getKind()) { + case DECLARED: + case TYPEVAR: + break; + default: + throw new IllegalArgumentException("t: " + t); + } + } + return super.visitIntersection(t, x); + } + + @Override // SimpleTypeVisitor6 + public TypeMirror visitTypeVariable(final TypeVariable t, final Void x) { + final Element e = t.asElement(); + if (e == null) { + throw new IllegalArgumentException("t: " + t); + } + switch (e.getKind()) { + case TYPE_PARAMETER: + break; + default: + throw new IllegalArgumentException("t: " + t); + } + final TypeMirror lowerBound = t.getLowerBound(); + if (lowerBound == null) { + throw new IllegalArgumentException("t: " + t); + } + switch (lowerBound.getKind()) { + case DECLARED: + case INTERSECTION: + case NULL: + break; + default: + throw new IllegalArgumentException("t: " + t); + } + final TypeMirror upperBound = t.getUpperBound(); + if (upperBound == null) { + throw new IllegalArgumentException("t: " + t); + } + switch (upperBound.getKind()) { + case DECLARED: + case INTERSECTION: + case TYPEVAR: // I guess? Should just erase to its bounds/intersection. + break; + default: + throw new IllegalArgumentException("t: " + t); + } + return super.visitTypeVariable(t, x); + } + + @Override // SimpleTypeVisitor6 + public TypeMirror visitUnion(final UnionType t, final Void x) { + final Collection alts = t.getAlternatives(); + if (alts == null || alts.isEmpty()) { + throw new IllegalArgumentException("t: " + t); + } + for (final TypeMirror alt : alts) { + if (alt == null) { + throw new IllegalArgumentException("t: " + t); + } + switch (alt.getKind()) { + case DECLARED: + case TYPEVAR: // maybe? + break; + default: + throw new IllegalArgumentException("t: " + t); + } + } + return super.visitUnion(t, x); + } + + @Override // SimpleTypeVisitor6 + public TypeMirror visitWildcard(final WildcardType t, final Void x) { + final TypeMirror extendsBound = t.getExtendsBound(); + final TypeMirror superBound = t.getSuperBound(); + if (extendsBound == null) { + if (superBound != null) { + switch (superBound.getKind()) { + case DECLARED: + case TYPEVAR: + break; + default: + throw new IllegalArgumentException("t: " + t); + } + } + } else if (superBound == null) { + if (extendsBound != null) { + switch (extendsBound.getKind()) { + case DECLARED: + case TYPEVAR: + break; + default: + throw new IllegalArgumentException("t: " + t); + } + } + } else { + throw new IllegalArgumentException("t: " + t); + } + return super.visitWildcard(t, x); + } + + private static final boolean isReference(final TypeKind k) { + return switch (Objects.requireNonNull(k, "k")) { + case + ARRAY, + DECLARED, + ERROR, + INTERSECTION, + TYPEVAR, + WILDCARD -> true; + case + BOOLEAN, + BYTE, + CHAR, + DOUBLE, + EXECUTABLE, + FLOAT, + INT, + LONG, + MODULE, + NONE, + NULL, + OTHER, + PACKAGE, + SHORT, + UNION, + VOID -> false; + }; + } + +} diff --git a/lang/src/main/java/org/microbean/lang/visitor/Visitors.java b/lang/src/main/java/org/microbean/lang/visitor/Visitors.java new file mode 100644 index 00000000..a8f96027 --- /dev/null +++ b/lang/src/main/java/org/microbean/lang/visitor/Visitors.java @@ -0,0 +1,199 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.microbean.lang.visitor; + +import java.util.function.Predicate; + +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVisitor; + +import org.microbean.lang.TypeAndElementSource; +import org.microbean.lang.Lang; + +import org.microbean.lang.type.Types; + +public final class Visitors { + + private final TypeAndElementSource elementSource; + + private final EraseVisitor eraseVisitor; + + private final SupertypeVisitor supertypeVisitor; + + private final BoundingClassVisitor boundingClassVisitor; + + private final AsSuperVisitor asSuperVisitor; + + private final MemberTypeVisitor memberTypeVisitor; + + private final ContainsTypeVisitor containsTypeVisitor; + + private final SameTypeVisitor sameTypeVisitor; + + private final CaptureVisitor captureVisitor; + + private final SubtypeVisitor subtypeVisitor; + + private final SubtypeUncheckedVisitor subtypeUncheckedVisitor; + + private final ConvertibleVisitor convertibleVisitor; + + private final PrecedesPredicate precedesPredicate; + + private final TypeClosureVisitor typeClosureVisitor; + + private final AssignableVisitor assignableVisitor; + + public Visitors(final TypeAndElementSource es) { + this(es, false, true, t -> true); + } + + public Visitors(final TypeAndElementSource es, + final boolean subtypeCapture /* false by default */, + final boolean wildcardsCompatible /* true by default */) { + this(es, subtypeCapture, wildcardsCompatible, t -> true); + } + + public Visitors(TypeAndElementSource es, + final boolean subtypeCapture /* false by default */, + final boolean wildcardsCompatible /* true by default */, + final Predicate supertypeFilter) { + super(); + if (es == null) { + es = Lang.typeAndElementSource(); + } + this.elementSource = es; + final Types types = new Types(es); + this.eraseVisitor = new EraseVisitor(es, types); + this.supertypeVisitor = new SupertypeVisitor(es, types, this.eraseVisitor, supertypeFilter); + this.boundingClassVisitor = new BoundingClassVisitor(this.supertypeVisitor); + this.asSuperVisitor = new AsSuperVisitor(es, null, types, this.supertypeVisitor); + this.memberTypeVisitor = + new MemberTypeVisitor(es, null, types, this.asSuperVisitor, this.eraseVisitor, this.supertypeVisitor); + + this.containsTypeVisitor = new ContainsTypeVisitor(es, types); + + this.sameTypeVisitor = new SameTypeVisitor(es, this.containsTypeVisitor, this.supertypeVisitor, wildcardsCompatible); + + this.captureVisitor = new CaptureVisitor(es, null, types, this.supertypeVisitor, this.memberTypeVisitor); + + this.subtypeVisitor = + new SubtypeVisitor(es, + null, + types, + this.asSuperVisitor, + this.supertypeVisitor, + this.sameTypeVisitor, + this.containsTypeVisitor, + this.captureVisitor, + subtypeCapture); + + this.subtypeUncheckedVisitor = new SubtypeUncheckedVisitor(types, this.subtypeVisitor, this.asSuperVisitor, this.sameTypeVisitor, subtypeCapture); + + this.convertibleVisitor = new ConvertibleVisitor(types, this.subtypeUncheckedVisitor, subtypeVisitor); + this.assignableVisitor = new AssignableVisitor(types, this.convertibleVisitor); + + final PrecedesPredicate precedesPredicate = new PrecedesPredicate(null, this.supertypeVisitor, this.subtypeVisitor); + this.precedesPredicate = precedesPredicate; + this.typeClosureVisitor = new TypeClosureVisitor(es, this.supertypeVisitor, precedesPredicate); + this.captureVisitor.setTypeClosureVisitor(this.typeClosureVisitor); + + assert this.initialized(); + } + + public final TypeAndElementSource typeAndElementSource() { + return this.elementSource; + } + + public final EraseVisitor eraseVisitor() { + return this.eraseVisitor; + } + + public final SupertypeVisitor supertypeVisitor() { + return this.supertypeVisitor; + } + + public final InterfacesVisitor interfacesVisitor() { + return this.supertypeVisitor.interfacesVisitor(); + } + + public final BoundingClassVisitor boundingClassVisitor() { + return this.boundingClassVisitor; + } + + public final AsSuperVisitor asSuperVisitor() { + return this.asSuperVisitor; + } + + public final MemberTypeVisitor memberTypeVisitor() { + return this.memberTypeVisitor; + } + + public final ContainsTypeVisitor containsTypeVisitor() { + return this.containsTypeVisitor; + } + + public final SameTypeVisitor sameTypeVisitor() { + return this.sameTypeVisitor; + } + + public final CaptureVisitor captureVisitor() { + return this.captureVisitor; + } + + public final SubtypeVisitor subtypeVisitor() { + return this.subtypeVisitor; + } + + public final SubtypeUncheckedVisitor subtypeUncheckedVisitor() { + return this.subtypeUncheckedVisitor; + } + + public final ConvertibleVisitor convertibleVisitor() { + return this.convertibleVisitor; + } + + public final AssignableVisitor assignableVisitor() { + return this.assignableVisitor; + } + + public final PrecedesPredicate precedesPredicate() { + return this.precedesPredicate; + } + + public final TypeClosureVisitor typeClosureVisitor() { + return this.typeClosureVisitor; + } + + private final boolean initialized() { + for (final java.lang.reflect.Field f : this.getClass().getDeclaredFields()) { + f.trySetAccessible(); + try { + final Object visitor = f.get(this); + for (final java.lang.reflect.Field ff : visitor.getClass().getDeclaredFields()) { + ff.trySetAccessible(); + if (TypeVisitor.class.isAssignableFrom(ff.getType())) { + if (ff.get(visitor) == null) { + throw new AssertionError(ff + " in " + f + " was null"); + } + } + } + } catch (final IllegalAccessException e) { + throw new AssertionError(e.getMessage(), e); + } + } + return true; + } + +} diff --git a/lang/src/main/javadoc/css/fonts.css b/lang/src/main/javadoc/css/fonts.css new file mode 100644 index 00000000..bd00dc52 --- /dev/null +++ b/lang/src/main/javadoc/css/fonts.css @@ -0,0 +1,62 @@ +@import url('https://fonts.googleapis.com/css2?2?family=Roboto+Mono:ital,wght@0,400;1,400&family=Roboto:ital,wght@0,400;0,700;1,400;1,700&family=Lobster&display=swap'); +body { + font-family: 'Roboto', 'DejaVu Sans', Arial, Helvetica, sans-serif; +} +button { + font-family: 'Roboto', 'DejaVu Sans', Arial, Helvetica, sans-serif; +} +h1, h2, h3 { + font-weight: 700 +} +input { + font-family: 'Roboto', 'DejaVu Sans', Arial, Helvetica, sans-serif; +} +code, tt, pre { + font-family: 'Roboto Mono', 'DejaVu Sans Mono', monospace; +} +.contentContainer .description dl dd, .contentContainer .details dl dd, .serializedFormContainer dl dd { + font-family: 'Roboto', 'DejaVu Sans', Arial, Helvetica, sans-serif; +} +div.block { + font-family: 'Roboto', 'DejaVu Sans', Arial, Helvetica, sans-serif; +} +div.table-tabs > button { + font-weight: 700 +} +dl.notes > dt { + font-family: 'Roboto', 'DejaVu Sans', Arial, Helvetica, sans-serif; + font-weight: 700 +} +dl.notes > dd { + font-family: 'Roboto', 'DejaVu Sans', Arial, Helvetica, sans-serif; +} +strong { + font-weight: 700 +} +.caption { + font-weight: 700 +} +.table-header { + font-weight: 700 +} +.col-first a:link, .col-first a:visited, .col-second a:link, .col-second a:visited, .col-first a:link, .col-first a:visited, .col-second a:link, .col-second a:visited, .col-constructor-name a:link, .col-constructor-name a:visited, .col-summary-item-name a:link, .col-summary-item-name a:visited, .constant-values-container a:link, .constant-values-container a:visited, .all-classes-container a:link, .all-classes-container a:visited, .all-packages-container a:link, .all-packages-container a:visited { + font-weight: 700 +} +.deprecated-label, .descfrm-type-label, .implementation-label, .member-name-label, .member-name-link, .module-label-in-package, .module-label-in-type, .override-specify-label, .package-label-in-type, .package-hierarchy-label, .type-name-label, .type-name-link, .search-tag-link, .preview-label { + font-weight: 700 +} +.module-signature, .package-signature, .type-signature, .member-signature { + font-family: 'Roboto Mono', 'DejaVu Sans Mono', monospace; +} +main a[href*="://"]::after { + all: unset; +} +.result-highlight { + font-weight: 700; +} +.ui-widget { + font-family: 'Roboto', 'DejaVu Sans', Arial, Helvetica, sans-serif !important; +} +.ui-autocomplete-category { + font-weight: 700; +} diff --git a/lang/src/site/markdown/index.md.vm b/lang/src/site/markdown/index.md.vm new file mode 100644 index 00000000..c79baf67 --- /dev/null +++ b/lang/src/site/markdown/index.md.vm @@ -0,0 +1 @@ +#include("../../../README.md") diff --git a/lang/src/site/markdown/type-variables-and-intersection-types.md b/lang/src/site/markdown/type-variables-and-intersection-types.md new file mode 100644 index 00000000..b0a7bd76 --- /dev/null +++ b/lang/src/site/markdown/type-variables-and-intersection-types.md @@ -0,0 +1,26 @@ +# Type Variables and Intersection Types and Interfaces + +An `IntersectionType` is just a way of representing the many bounds of a `TypeVariable` in a single type. This also +means that `IntersectionType`s must play by the many rules imposed by `TypeVariable`s, the only types that "use" them. + +All of an `IntersectionType`'s bounds will be `DeclaredType`s. Notably, none of them will be `ArrayType`s, +`IntersectionType`s, or `TypeVariable`s. Only its first may be a `DeclaredType` declared by a non-interface +`TypeElement`. + +In the Java language model, a `TypeVariable` has exactly one upper bound, which may be an `IntersectionType`. + +If its sole bound is not an `IntersectionType`, then it will be either a `DeclaredType`, or a `TypeVariable`. It will +never be an `ArrayType`. + +We can tell if a `DeclaredType` "is an interface" by seeing if its declaring `TypeElement` is an interface. (Strictly +speaking a `DeclaredType` is not itself an interface; only its declaring `TypeElement` can be an interface.) + +We can tell if an `IntersectionType` "is an interface" by seeing if its first bound, a `DeclaredType`, "is an interface" +by the rule above. (Strictly speaking, an `IntersectionType` cannot itself be an interface, of course; we're checking to +see if all of its bounds are interfaces or not, and we can do this by simply checking the first of its bounds.) + +Applying these rules together and recursively where necessary, we can therefore tell whether any given `TypeVariable` +"is an interface" if either (a) its sole bound is a `DeclaredType` or `TypeVariable` that "is an interface" or (b) its +sole bound is an `IntersectionType` whose first bound is a `DeclaredType` that "is an interface". (Strictly speaking, a +`TypeVariable` cannot itself be an interface, of course; we're checking to see if its bound "is an interface" according +to these rules.) diff --git a/lang/src/test/java/logging.properties b/lang/src/test/java/logging.properties new file mode 100644 index 00000000..638b0e75 --- /dev/null +++ b/lang/src/test/java/logging.properties @@ -0,0 +1,5 @@ +.level = INFO +handlers = java.util.logging.ConsoleHandler +java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter +java.util.logging.ConsoleHandler.level = FINER +org.microbean.lang.level = FINE diff --git a/lang/src/test/java/org/microbean/lang/AnnotationProcessingInterceptor.java b/lang/src/test/java/org/microbean/lang/AnnotationProcessingInterceptor.java new file mode 100644 index 00000000..0d6ab3da --- /dev/null +++ b/lang/src/test/java/org/microbean/lang/AnnotationProcessingInterceptor.java @@ -0,0 +1,242 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2022–2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang; + +import java.lang.reflect.Method; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import java.util.Set; + +import java.util.function.Supplier; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.Filer; +import javax.annotation.processing.Messager; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.RoundEnvironment; +import javax.annotation.processing.SupportedSourceVersion; + +import javax.lang.model.SourceVersion; + +import javax.lang.model.element.TypeElement; + +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; + +import javax.tools.JavaCompiler.CompilationTask; +import javax.tools.ToolProvider; + +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.api.extension.InvocationInterceptor; +import org.junit.jupiter.api.extension.InvocationInterceptor.Invocation; +import org.junit.jupiter.api.extension.ParameterContext; +import org.junit.jupiter.api.extension.ParameterResolutionException; +import org.junit.jupiter.api.extension.ParameterResolver; +import org.junit.jupiter.api.extension.ReflectiveInvocationContext; +import org.junit.jupiter.api.extension.TestExecutionExceptionHandler; + +import org.opentest4j.AssertionFailedError; +import org.opentest4j.IncompleteExecutionException; +import org.opentest4j.MultipleFailuresError; + +public final class AnnotationProcessingInterceptor implements InvocationInterceptor, ParameterResolver, TestExecutionExceptionHandler { + + @Deprecated // for use by JUnit Jupiter internals only + private AnnotationProcessingInterceptor() { + super(); + } + + /* + * Invocation order: + * + * 1. supportsParameter(ParameterContext, ExtensionContext) + * 2. resolveParameter(ParameterContext, ExtensionContext) + * 3. interceptTestMethod(Invocation, + * ReflectiveInvocationContext, + * ExtensionContext) + * 4. (handleTestExecutionException(ExtensionContext, Throwable)) + */ + + @Deprecated // for use by JUnit Jupiter internals only + @Override // ParameterResolver + public final boolean supportsParameter(final ParameterContext parameterContext, + final ExtensionContext extensionContext) { + return ProcessingEnvironment.class.isAssignableFrom(parameterContext.getParameter().getType()); + } + + @Deprecated // for use by JUnit Jupiter internals only + @Override // ParameterResolver + public final ForwardingProcessingEnvironment resolveParameter(final ParameterContext parameterContext, + final ExtensionContext extensionContext) { + return new ForwardingProcessingEnvironment(); + } + + @Deprecated // for use by JUnit Jupiter internals only + @Override // InvocationInterceptor + public final void interceptTestMethod(final Invocation invocation, + final ReflectiveInvocationContext invocationContext, + final ExtensionContext extensionContext) + throws Throwable { + final List arguments = invocationContext.getArguments(); + if (arguments.size() != 1) { + invocation.proceed(); + return; + } + final Object soleArgument = arguments.get(0); + if (!(soleArgument instanceof ForwardingProcessingEnvironment)) { + invocation.proceed(); + return; + } + + final CompilationTask task = ToolProvider.getSystemJavaCompiler() + .getTask(null, + null, + null, + List.of("-proc:only"), + List.of("java.lang.Object"), + null); + // SupportedSourceVersion targets types only. + final SupportedSourceVersion ssv = invocationContext.getTargetClass().getAnnotation(SupportedSourceVersion.class); + final SourceVersion sourceVersion = ssv == null ? SourceVersion.latest() : ssv.value(); + final ForwardingProcessingEnvironment fpe = (ForwardingProcessingEnvironment)soleArgument; + task.setProcessors(List.of(new AbstractProcessor() { + + @Override // AbstractProcessor + public final void init(final ProcessingEnvironment processingEnvironment) { + try { + fpe.delegate = processingEnvironment; + invocation.proceed(); + } catch (final RuntimeException | Error e) { + throw e; + } catch (final Exception e) { + throw new RuntimeException(e.getMessage(), e); + } catch (final Throwable t) { + throw new AssertionError(t.getMessage(), t); + } finally { + fpe.delegate = null; + } + } + + @Override // AbstractProcessor + public final SourceVersion getSupportedSourceVersion() { + return sourceVersion; + } + + @Override // AbstractProcessor + public final boolean process(final Set annotations, final RoundEnvironment roundEnvironment) { + return false; + } + + })); + task.call(); + } + + @Override // TestExecutionExceptionHandler + public final void handleTestExecutionException(final ExtensionContext extensionContext, + final Throwable throwable) + throws Throwable { + if ((throwable instanceof Error) || (throwable instanceof IncompleteExecutionException)) { + throw throwable; + } else if (throwable != null) { + handleTestExecutionException(extensionContext, throwable.getCause()); + throw throwable; + } + } + + + /* + * Inner and nested classes. + */ + + + private static final class ForwardingProcessingEnvironment implements ProcessingEnvironment { + + private volatile ProcessingEnvironment delegate; + + private ForwardingProcessingEnvironment() { + super(); + } + + @Override + public final Elements getElementUtils() { + return this.delegate.getElementUtils(); + } + + @Override + public final Filer getFiler() { + return this.delegate.getFiler(); + } + + @Override + public final Locale getLocale() { + return this.delegate.getLocale(); + } + + @Override + public final Messager getMessager() { + return this.delegate.getMessager(); + } + + @Override + public final Map getOptions() { + return this.delegate.getOptions(); + } + + @Override + public final SourceVersion getSourceVersion() { + return this.delegate.getSourceVersion(); + } + + @Override + public final Types getTypeUtils() { + return this.delegate.getTypeUtils(); + } + + @Override + public final boolean isPreviewEnabled() { + return this.delegate.isPreviewEnabled(); + } + + @Override + public final int hashCode() { + return this.delegate == null ? 0 : this.delegate.hashCode(); + } + + @Override + public final boolean equals(final Object other) { + if (other == this) { + return true; + } else if (other != null && this.getClass() == other.getClass()) { + return Objects.equals(this.delegate, ((ForwardingProcessingEnvironment)other).delegate); + } else { + return false; + } + } + + @Override + public final String toString() { + return this.delegate == null ? super.toString() : this.delegate.toString(); + } + + } + +} diff --git a/lang/src/test/java/org/microbean/lang/TestAsSuper.java b/lang/src/test/java/org/microbean/lang/TestAsSuper.java new file mode 100644 index 00000000..e18e42f9 --- /dev/null +++ b/lang/src/test/java/org/microbean/lang/TestAsSuper.java @@ -0,0 +1,163 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.microbean.lang; + +import com.sun.tools.javac.code.Symbol; +import com.sun.tools.javac.code.Symbol.ClassSymbol; +import com.sun.tools.javac.code.Type; +import com.sun.tools.javac.code.Type.ClassType; + +import com.sun.tools.javac.model.JavacTypes; + +import java.lang.reflect.Field; + +import javax.lang.model.element.Element; +import javax.lang.model.element.TypeElement; + +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeMirror; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.microbean.lang.element.DelegatingElement; + +import org.microbean.lang.type.DelegatingTypeMirror; + +import org.microbean.lang.visitor.AsSuperVisitor; +import org.microbean.lang.visitor.ContainsTypeVisitor; +import org.microbean.lang.visitor.EraseVisitor; +import org.microbean.lang.visitor.SameTypeVisitor; +import org.microbean.lang.visitor.SubtypeVisitor; +import org.microbean.lang.visitor.SupertypeVisitor; +import org.microbean.lang.visitor.Visitors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import static org.microbean.lang.Lang.unwrap; + +final class TestAsSuper { + + private com.sun.tools.javac.code.Types javacCodeTypes; + + private Visitors visitors; + + private SameTypeVisitor sameTypeVisitor; + + private AsSuperVisitor asSuperVisitor; + + private TestAsSuper() { + super(); + } + + @BeforeEach + final void setup() throws IllegalAccessException, NoSuchFieldException { + final Field f = JavacTypes.class.getDeclaredField("types"); + assertTrue(f.trySetAccessible()); + this.javacCodeTypes = (com.sun.tools.javac.code.Types)f.get(Lang.pe().getTypeUtils()); + this.visitors = new Visitors(Lang.typeAndElementSource()); + } + + @AfterEach + final void tearDown() { + + } + + @Test + final void testAsSuperStringObject() { + final TypeElement stringElement = Lang.typeElement("java.lang.String"); + assertTrue(stringElement instanceof DelegatingElement); + + final DeclaredType stringTypeDeclaration = (DeclaredType)stringElement.asType(); + assertTrue(stringTypeDeclaration instanceof DelegatingTypeMirror); + + final Element objectElement = Lang.typeElement("java.lang.Object"); + assertTrue(objectElement instanceof DelegatingElement); + + final DeclaredType objectTypeDeclaration = (DeclaredType)objectElement.asType(); + assertTrue(objectTypeDeclaration instanceof DelegatingTypeMirror); + + assertSame(unwrap(objectElement), unwrap(objectTypeDeclaration.asElement())); + assertSame(unwrap(objectElement), unwrap(org.microbean.lang.type.Types.asElement(objectTypeDeclaration, true))); + final DeclaredType stringTypeSuperclass = (DeclaredType)stringElement.getSuperclass(); + final Element stringTypeSuperclassElement = stringTypeSuperclass.asElement(); + assertSame(unwrap(objectElement), unwrap(stringTypeSuperclassElement)); + // WTF; this fails: + // assertSame(objectTypeDeclaration, stringTypeSuperclass); + assertTrue(this.javacCodeTypes.isSameType((Type)unwrap(objectTypeDeclaration), (Type)unwrap(stringTypeSuperclass))); + assertTrue(this.visitors.sameTypeVisitor().visit(objectTypeDeclaration, stringTypeSuperclass)); + assertAsSuper(objectTypeDeclaration, stringTypeDeclaration, objectElement); + } + + @Test + final void testGorp() { + // The element denoted by java.util.List. Its underlying type declaration is the type denoted by java.util.List. + final TypeElement listElement = Lang.typeElement("java.util.List"); + assertTrue(listElement instanceof DelegatingElement); + + // The type denoted by java.util.List + final DeclaredType listTypeDeclaration = (DeclaredType)listElement.asType(); + assertTrue(listTypeDeclaration instanceof DelegatingTypeMirror); + + // The type denoted by java.util.List, i.e. a raw type. + final DeclaredType rawListType = Lang.declaredType(listElement); // note: no varargs type arguments supplied + assertTrue(rawListType instanceof DelegatingTypeMirror); + assertTrue(((Type)unwrap(rawListType)).isRaw()); + + // The raw List type is not the same as the List type declaration. + assertNotSame(unwrap(rawListType), unwrap(listTypeDeclaration)); + + // The type denoted by java.util.List. + final DeclaredType listQuestionMarkType = Lang.declaredType(null, listElement, Lang.wildcardType()); + assertTrue(listQuestionMarkType instanceof DelegatingTypeMirror); + assertFalse(((Type)unwrap(listQuestionMarkType)).isRaw()); + + assertSame(unwrap(listElement), unwrap(listQuestionMarkType.asElement())); + assertSame(unwrap(listElement), unwrap(rawListType.asElement())); + + assertSame(unwrap(listElement), unwrap(org.microbean.lang.type.Types.asElement(rawListType, true))); + + // Why is the expected type rawListType? Because the asSuper visitor's visitClassType() method, line 2175 or so, + // says: + // + // if (t.tsym == sym) + // return t; + // + // So here if rawListType.asElement() == listElement, which it does, as we've proven above, asSuper() will return + // rawListType. + // + // So no matter what parameterization the supplied type represents, if the type it parameterizes (which is the type + // declared by the element) "belongs to" the supplied element, then the supplied parameterized type is simply + // returned. + assertAsSuper(rawListType, rawListType, listElement); + } + + private final void assertAsSuper(final TypeMirror expected, final TypeMirror t, final Element e) { + assertTrue(t instanceof DelegatingTypeMirror); + assertTrue(e instanceof DelegatingElement); + assertSameType(expected, this.javacCodeTypes.asSuper((Type)unwrap(t), (Symbol)unwrap(e))); + assertSameType(expected, this.visitors.asSuperVisitor().visit(t, e)); + } + + private final void assertSameType(final TypeMirror t, final TypeMirror s) { + assertTrue(this.javacCodeTypes.isSameType((Type)unwrap(t), (Type)unwrap(s))); + assertTrue(this.visitors.sameTypeVisitor().visit(t, s)); + } + +} diff --git a/lang/src/test/java/org/microbean/lang/TestAssignable.java b/lang/src/test/java/org/microbean/lang/TestAssignable.java new file mode 100644 index 00000000..3138acab --- /dev/null +++ b/lang/src/test/java/org/microbean/lang/TestAssignable.java @@ -0,0 +1,151 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.microbean.lang; + +import java.util.ArrayList; +import java.util.List; + +import javax.lang.model.element.TypeElement; + +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeMirror; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.microbean.lang.type.DelegatingTypeMirror; +import org.microbean.lang.type.Types; + +import org.microbean.lang.visitor.AssignableVisitor; +import org.microbean.lang.visitor.SubtypeVisitor; +import org.microbean.lang.visitor.Visitors; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import static org.microbean.lang.Lang.unwrap; + +final class TestAssignable { + + private TypeAndElementSource es; + + private Types types; + + private SubtypeVisitor subtypeVisitor; + + private AssignableVisitor assignableVisitor; + + private TestAssignable() { + super(); + } + + @BeforeEach + final void setup() { + this.es = Lang.typeAndElementSource(); + this.types = new Types(this.es); + final Visitors visitors = new Visitors(Lang.typeAndElementSource()); + this.subtypeVisitor = visitors.subtypeVisitor(); + this.assignableVisitor = visitors.assignableVisitor(); + } + + @AfterEach + final void tearDown() { + + } + + @Test + final void testStringAssignableToObject() { + final DeclaredType string = (DeclaredType)this.es.typeElement("java.base", "java.lang.String").asType(); + final DeclaredType object = (DeclaredType)this.es.typeElement("java.base", "java.lang.Object").asType(); + assertSubtype(string, object); + assertAssignable(string, object); + assertNotSubtype(object, string); + assertNotAssignable(object, string); + } + + @Test + final void testListStringAssignableToListQuestionMark() { + final DeclaredType listString = Lang.declaredType(null, this.es.typeElement("java.base", "java.util.List"), this.es.typeElement("java.base", "java.lang.String").asType()); + assertTrue(listString instanceof DelegatingTypeMirror); + final DeclaredType listQuestionMark = Lang.declaredType(null, this.es.typeElement("java.base", "java.util.List"), Lang.wildcardType()); + assertTrue(listQuestionMark instanceof DelegatingTypeMirror); + assertSubtype(listString, listQuestionMark); + assertAssignable(listString, listQuestionMark); + assertNotSubtype(listQuestionMark, listString); + assertNotAssignable(listQuestionMark, listString); + } + + @Test + final void testListStringAssignableToListString() { + final DeclaredType listString = Lang.declaredType(null, this.es.typeElement("java.base", "java.util.List"), this.es.typeElement("java.base", "java.lang.String").asType()); + assertSubtype(listString, listString); + assertAssignable(listString, listString); + } + + @Test + final void testCompilationAssignability() { + final List listQuestionMark1 = new ArrayList(); + @SuppressWarnings("rawtypes") + final List rawList = listQuestionMark1; // ok + List listQuestionMark2 = rawList; // also ok + listQuestionMark2 = listQuestionMark1; // also ok + } + + @Test + final void testRawListAssignableToListQuestionMark() { + final DeclaredType rawList = Lang.declaredType(this.es.typeElement("java.base", "java.util.List")); + assertTrue(((com.sun.tools.javac.code.Type)unwrap(rawList)).isRaw()); + assertTrue(this.types.raw(rawList)); + + final DeclaredType listQuestionMark = Lang.declaredType(null, this.es.typeElement("java.base", "java.util.List"), Lang.wildcardType()); + assertFalse(((com.sun.tools.javac.code.Type)unwrap(listQuestionMark)).isRaw()); + assertFalse(this.types.raw(listQuestionMark)); + + // List is a subtype of List + assertSubtype(listQuestionMark, rawList); + + // List is assignable to List, i.e. List x = (List)y; + assertAssignable(listQuestionMark, rawList); + + // List is NOT a subtype of List + assertNotSubtype(rawList, listQuestionMark); + + // NOTE: + // List IS assignable to List, i.e. List x = (List)y; + assertAssignable(rawList, listQuestionMark); + } + + private final void assertAssignable(final TypeMirror payload, final TypeMirror receiver) { + assertTrue(Lang.assignable(payload, receiver)); + assertTrue(this.assignableVisitor.visit(payload, receiver).booleanValue()); + } + + private final void assertNotAssignable(final TypeMirror payload, final TypeMirror receiver) { + assertFalse(Lang.assignable(payload, receiver)); + assertFalse(this.assignableVisitor.visit(payload, receiver).booleanValue()); + } + + private final void assertSubtype(final TypeMirror payload, final TypeMirror receiver) { + assertTrue(Lang.subtype(payload, receiver)); + assertTrue(this.subtypeVisitor.visit(payload, receiver).booleanValue()); + } + + private final void assertNotSubtype(final TypeMirror payload, final TypeMirror receiver) { + assertFalse(Lang.subtype(payload, receiver)); + assertFalse(this.subtypeVisitor.visit(payload, receiver).booleanValue()); + } + +} diff --git a/lang/src/test/java/org/microbean/lang/TestBoundingClassVisitor.java b/lang/src/test/java/org/microbean/lang/TestBoundingClassVisitor.java new file mode 100644 index 00000000..72923432 --- /dev/null +++ b/lang/src/test/java/org/microbean/lang/TestBoundingClassVisitor.java @@ -0,0 +1,142 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.microbean.lang; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; + +import java.util.List; + +import javax.lang.model.element.TypeElement; + +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeVariable; + +import com.sun.tools.javac.code.Type; + +import com.sun.tools.javac.model.JavacTypes; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.microbean.lang.visitor.Visitors; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import static org.microbean.lang.Lang.unwrap; + +final class TestBoundingClassVisitor { + + private Visitors visitors; + + private com.sun.tools.javac.code.Types javacCodeTypes; + + private TestBoundingClassVisitor() { + super(); + } + + @BeforeEach + final void setup() throws IllegalAccessException, NoSuchFieldException { + this.visitors = new Visitors(Lang.typeAndElementSource()); + final Field f = JavacTypes.class.getDeclaredField("types"); + assertTrue(f.trySetAccessible()); + this.javacCodeTypes = (com.sun.tools.javac.code.Types)f.get(Lang.pe().getTypeUtils()); + } + + @Test + final void testSimple() { + final DeclaredType t = Lang.declaredType(String.class); + assertSame(Lang.noType(TypeKind.NONE), unwrap(t.getEnclosingType())); + // classBound() doesn't do anything here, obviously: + assertSame(unwrap(t), this.javacCodeTypes.classBound((Type)unwrap(t))); + assertSame(t, this.visitors.boundingClassVisitor().visit(t)); + } + + @Test + final void testMildlyComplicated() { + final DataHolder.Inner inner = new DataHolder<>("hello").data(); + final DeclaredType outerDeclaredType = Lang.declaredType(null, Lang.typeElement(DataHolder.class), Lang.declaredType(String.class)); + final DeclaredType innerDeclaredType = Lang.declaredType(outerDeclaredType, Lang.typeElement(inner.getClass())); + assertSame(unwrap(outerDeclaredType), unwrap(innerDeclaredType.getEnclosingType())); + // classBound() doesn't do anything here: + assertSame(unwrap(outerDeclaredType), this.javacCodeTypes.classBound((Type)unwrap(outerDeclaredType))); + assertSame(outerDeclaredType, this.visitors.boundingClassVisitor().visit(outerDeclaredType)); + // classBound() doesn't do anything here either: + assertSame(unwrap(innerDeclaredType), this.javacCodeTypes.classBound((Type)unwrap(innerDeclaredType))); + assertSame(innerDeclaredType, this.visitors.boundingClassVisitor().visit(innerDeclaredType)); + } + + @Test + final > void testReallyComplicated() throws NoSuchMethodException { + final DataHolder d = new DataHolder<>("hello"); + this.testReallyComplicated0(d); + } + + private final > void testReallyComplicated0(final X x) throws NoSuchMethodException { + final Object inner = x.data(); + final DeclaredType outerDeclaredType = Lang.declaredType(null, Lang.typeElement(DataHolder.class), Lang.declaredType(String.class)); + final DeclaredType innerDeclaredType = Lang.declaredType(outerDeclaredType, Lang.typeElement(inner.getClass())); + // classBound() doesn't do anything here either: + assertSame(unwrap(innerDeclaredType), this.javacCodeTypes.classBound((Type)unwrap(innerDeclaredType))); + assertSame(innerDeclaredType, this.visitors.boundingClassVisitor().visit(innerDeclaredType)); + + final Method thisMethod = TestBoundingClassVisitor.class.getDeclaredMethod("testReallyComplicated0", DataHolder.class); + java.lang.reflect.TypeVariable xx = null; + for (final java.lang.reflect.TypeVariable tv : thisMethod.getTypeParameters()) { + if (tv.getName().equals("X")) { + xx = tv; + break; + } + } + final TypeVariable xxx = Lang.typeVariable(xx); + // Here's where classBound() does something: + assertNotSame(unwrap(xxx), this.javacCodeTypes.classBound((Type)unwrap(xxx))); + assertNotSame(xxx, this.visitors.boundingClassVisitor().visit(xxx)); + } + + private static class DataHolder { + + private final Inner data; + + private DataHolder(final T data) { + super(); + this.data = new Inner(data); + } + + Inner data() { + return this.data; + } + + class Inner { + + private final T data; + + private Inner(final T data) { + super(); + this.data = data; + } + + final T data() { + return this.data; + } + + } + + } + +} diff --git a/lang/src/test/java/org/microbean/lang/TestComparators.java b/lang/src/test/java/org/microbean/lang/TestComparators.java new file mode 100644 index 00000000..4ef31f21 --- /dev/null +++ b/lang/src/test/java/org/microbean/lang/TestComparators.java @@ -0,0 +1,73 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2022–2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.microbean.lang; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; + +import java.util.concurrent.ConcurrentMap; + +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; + +import static org.microbean.lang.Lang.declaredType; +import static org.microbean.lang.Lang.primitiveType; +import static org.microbean.lang.Lang.typeElement; +import static org.microbean.lang.Lang.typeVariable; +import static org.microbean.lang.Lang.wildcardType; + +final class TestComparators { + + private TestComparators() { + super(); + } + + @SuppressWarnings("rawtypes") + @Test + final , ? super ArrayList>> void spike() throws ReflectiveOperationException { + final List l = new ArrayList<>(); + final TypeVariable tv = typeVariable(this.getClass().getDeclaredMethod("spike"), "T"); + l.add(tv); + final TypeMirror listQuestionMarkExtendsString = declaredType(null, typeElement("java.util.List"), wildcardType(declaredType("java.lang.String"), null)); + l.add(listQuestionMarkExtendsString); + final TypeMirror list = declaredType(null, typeElement("java.util.List")); + l.add(list); + + // final Comparator c = ClassesThenInterfacesTypeMirrorComparator.INSTANCE + // .thenComparing(new SpecializationDepthTypeMirrorComparator()) + // .thenComparing(NameTypeMirrorComparator.INSTANCE); + + final Comparator c = + new TestingTypeMirrorComparator(t -> + t.getKind() == TypeKind.DECLARED && + !((DeclaredType)t).asElement().getKind().isInterface()) + .thenComparing(new TestingTypeMirrorComparator(t -> + t.getKind() == TypeKind.DECLARED && + ((DeclaredType)t).asElement().getKind().isInterface())) + .thenComparing(new SpecializationDepthTypeMirrorComparator()) + .thenComparing(NameTypeMirrorComparator.INSTANCE); + + Collections.sort(l, c); + } + +} diff --git a/lang/src/test/java/org/microbean/lang/TestConstableSemantics.java b/lang/src/test/java/org/microbean/lang/TestConstableSemantics.java new file mode 100644 index 00000000..8f8700a6 --- /dev/null +++ b/lang/src/test/java/org/microbean/lang/TestConstableSemantics.java @@ -0,0 +1,61 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.microbean.lang; + +import java.lang.constant.Constable; +import java.lang.constant.ConstantDesc; + +import java.lang.invoke.MethodHandles; + +import java.util.Optional; + +import javax.lang.model.element.ModuleElement; +import javax.lang.model.element.TypeElement; + +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeMirror; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +final class TestConstableSemantics { + + private TestConstableSemantics() { + super(); + } + + @Test + final void testDeclaredType() throws ReflectiveOperationException { + final ModuleElement javaBase = Lang.moduleElement("java.base"); + assertNotNull(javaBase); + final TypeElement javaLangString = Lang.typeElement(javaBase, "java.lang.String"); + assertNotNull(javaLangString); + final DeclaredType t = Lang.declaredType(null, javaLangString); + assertNotNull(t); + final Optional o = Lang.describeConstable(t); + assertTrue(o.isPresent()); + assertTrue(Lang.sameType(t, (TypeMirror)o.orElseThrow(AssertionError::new).resolveConstantDesc(MethodHandles.lookup()))); + } + + @Test + final void testConstableElementSource() throws ReflectiveOperationException { + final TypeAndElementSource es = Lang.typeAndElementSource(); + assertSame(es, ((Constable)es).describeConstable().orElseThrow().resolveConstantDesc(MethodHandles.lookup())); + } + +} diff --git a/lang/src/test/java/org/microbean/lang/TestDirectSupertypes.java b/lang/src/test/java/org/microbean/lang/TestDirectSupertypes.java new file mode 100644 index 00000000..d465ec3a --- /dev/null +++ b/lang/src/test/java/org/microbean/lang/TestDirectSupertypes.java @@ -0,0 +1,54 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2022–2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.microbean.lang; + +import java.util.List; + +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import static org.microbean.lang.Lang.directSupertypes; +import static org.microbean.lang.Lang.declaredType; +import static org.microbean.lang.Lang.sameType; +import static org.microbean.lang.Lang.unwrap; + +final class TestDirectSupertypes { + + private TestDirectSupertypes() { + super(); + } + + @Test + final void testDirectSupertypes() { + final DeclaredType object = declaredType("java.lang.Object"); + List dsts = directSupertypes(object); + assertEquals(List.of(), dsts); + dsts = directSupertypes(declaredType("java.lang.Cloneable")); + assertEquals(1, dsts.size()); + assertSame(unwrap(object), unwrap(dsts.get(0))); + + final DeclaredType list = declaredType("java.util.List"); + dsts = directSupertypes(list); + assertEquals(2, dsts.size()); + assertTrue(sameType(unwrap(object), unwrap(dsts.get(0)))); + } + +} diff --git a/lang/src/test/java/org/microbean/lang/TestErase.java b/lang/src/test/java/org/microbean/lang/TestErase.java new file mode 100644 index 00000000..18d58a40 --- /dev/null +++ b/lang/src/test/java/org/microbean/lang/TestErase.java @@ -0,0 +1,174 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2022–2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang; + +import java.lang.reflect.Field; + +import java.util.List; + +import javax.annotation.processing.ProcessingEnvironment; + +import javax.lang.model.element.TypeElement; + +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.NoType; +import javax.lang.model.type.PrimitiveType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; +import javax.lang.model.type.WildcardType; + +import com.sun.tools.javac.code.Type; + +import com.sun.tools.javac.model.JavacTypes; + +import org.junit.jupiter.api.Test; + +import org.junit.jupiter.api.extension.ExtendWith; + +import org.microbean.lang.type.Types; + +import org.microbean.lang.visitor.EraseVisitor; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +@ExtendWith(AnnotationProcessingInterceptor.class) +final class TestErase { + + private TestErase() { + super(); + } + + @Test + final void testErase(final ProcessingEnvironment env) { + final javax.lang.model.util.Elements elements = env.getElementUtils(); + final javax.lang.model.util.Types javacModelTypes = env.getTypeUtils(); + assertTrue(javacModelTypes instanceof JavacTypes); + com.sun.tools.javac.code.Types javacTypes = null; + try { + final Field f = JavacTypes.class.getDeclaredField("types"); + assertTrue(f.trySetAccessible()); + javacTypes = (com.sun.tools.javac.code.Types)f.get(javacModelTypes); + } catch (final ReflectiveOperationException reflectiveOperationException) { + fail(reflectiveOperationException); + } + assertNotNull(javacTypes); + + final TypeElement integerElement = elements.getTypeElement("java.lang.Integer"); + + final DeclaredType integerElementType = (DeclaredType)integerElement.asType(); + assertSame(TypeKind.DECLARED, integerElementType.getKind()); + + // Get the type denoted by the type expression + // Comparable. Interfaces must be returned in + // declaration order, and Comparable is the first one + // declared. + final DeclaredType comparableIntegerType = (DeclaredType)integerElement.getInterfaces().get(0); + assertSame(TypeKind.DECLARED, comparableIntegerType.getKind()); // ...which is kind of weird when you think about it + + List typeArguments = comparableIntegerType.getTypeArguments(); + + // Cannot rely on identity for some reason: + // assertSame(integerElementType, typeArguments.get(0)); + + // ...but the type representing java.lang.Integer best be equal to + // the type representing java.lang.Integer! + assertTrue(Equality.equalsIncludingAnnotations(integerElementType, typeArguments.get(0))); + + // Now test "official" erasure, via javax.lang.model.util.Types. + DeclaredType erasure = (DeclaredType)javacModelTypes.erasure(comparableIntegerType); + typeArguments = erasure.getTypeArguments(); + assertEquals(0, typeArguments.size()); + assertTrue(((Type)erasure).isRaw()); + + // Now do it with our stuff. + + final TypeAndElementSource tes = new TypeAndElementSource() { + @Override + public final ArrayType arrayTypeOf(final TypeMirror componentType) { + return javacModelTypes.getArrayType(componentType); + } + @Override + public boolean assignable(final TypeMirror payload, final TypeMirror receiver) { + return javacModelTypes.isAssignable(payload, receiver); + } + @Override + public final TypeElement boxedClass(final PrimitiveType t) { + return javacModelTypes.boxedClass(t); + } + @Override + public final DeclaredType declaredType(final TypeElement typeElement, final TypeMirror... arguments) { + return javacModelTypes.getDeclaredType(typeElement, arguments); + } + @Override + public final DeclaredType declaredType(final DeclaredType enclosingType, final TypeElement typeElement, final TypeMirror... arguments) { + return javacModelTypes.getDeclaredType(enclosingType, typeElement, arguments); + } + @Override + @SuppressWarnings("unchecked") + public final T erasure(final T t) { + return (T)javacModelTypes.erasure(t); + } + @Override + public final NoType noType(final TypeKind k) { + return javacModelTypes.getNoType(k); + } + @Override + public final PrimitiveType primitiveType(final TypeKind k) { + return javacModelTypes.getPrimitiveType(k); + } + @Override + public boolean sameType(final TypeMirror t, final TypeMirror s) { + return javacModelTypes.isSameType(t, s); + } + @Override + public final TypeElement typeElement(final CharSequence m, final CharSequence n) { + return elements.getTypeElement(elements.getModuleElement(m), n); + } + @Override + public final TypeVariable typeVariable(final java.lang.reflect.TypeVariable t) { + throw new UnsupportedOperationException(); // NOTE + } + @Override + public final WildcardType wildcardType(final TypeMirror extendsBound, final TypeMirror superBound) { + return javacModelTypes.getWildcardType(extendsBound, superBound); + } + }; + + final Types types = new Types(tes); + + // Make sure our stuff thinks the javac erasure is raw. + assertTrue(types.raw(erasure)); + + final EraseVisitor eraseVisitor = new EraseVisitor(tes, types); + erasure = (DeclaredType)eraseVisitor.visit(comparableIntegerType); + typeArguments = erasure.getTypeArguments(); + assertEquals(0, typeArguments.size()); + assertTrue(types.raw(erasure)); + + // Check that we do erasure of primitive types properly. + final TypeMirror intType = javacModelTypes.getPrimitiveType(TypeKind.INT); + assertSame(intType, eraseVisitor.visit(intType)); + + } + +} diff --git a/lang/src/test/java/org/microbean/lang/TestJavaLanguageModel.java b/lang/src/test/java/org/microbean/lang/TestJavaLanguageModel.java new file mode 100644 index 00000000..7e561dd0 --- /dev/null +++ b/lang/src/test/java/org/microbean/lang/TestJavaLanguageModel.java @@ -0,0 +1,230 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2022–2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Target; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import java.lang.reflect.Method; + +import java.util.EnumSet; +import java.util.List; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.NestingKind; +import javax.lang.model.element.RecordComponentElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.TypeParameterElement; + +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.ExecutableType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; + +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import org.microbean.lang.element.DelegatingElement; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import static org.microbean.lang.Lang.unwrap; + +final class TestJavaLanguageModel { + + private TestJavaLanguageModel() { + super(); + } + + @Test + final void testGetTypeElementCanonicalName() { + final TypeElement e = Lang.typeElement(Lang.moduleElement("java.base"), "java.util.AbstractMap.SimpleEntry"); + assertNotNull(e); + } + + @Test + final void testJavaLanguageModel() { + final TypeElement s = Lang.typeElement("java.lang.String"); + assertNotNull(s); + final TypeElement x = Lang.typeElement("java.util.logging.Level"); + assertNotNull(x); + final TypeElement r = Lang.typeElement("org.microbean.lang.TestJavaLanguageModel.Gloop"); + assertNotNull(r); + assertTrue(r instanceof DelegatingElement); + assertSame(ElementKind.RECORD, r.getKind()); + assertSame(TypeKind.DECLARED, r.asType().getKind()); + assertEquals(1, r.getRecordComponents().size()); + final RecordComponentElement name = r.getRecordComponents().get(0); + final DeclaredType t = (DeclaredType)name.asType(); + assertSame(TypeKind.DECLARED, t.getKind()); + final TypeElement e = (TypeElement)t.asElement(); + assertEquals("String", e.getSimpleName().toString()); + final ExecutableElement accessor = name.getAccessor(); + assertSame(ElementKind.METHOD, accessor.getKind()); + assertSame(TypeKind.EXECUTABLE, accessor.asType().getKind()); + assertEquals(EnumSet.of(Modifier.PUBLIC), name.getModifiers()); + assertEquals(EnumSet.of(Modifier.PUBLIC, Modifier.FINAL), accessor.getModifiers()); + } + + @Test + final void testTypeParameters() { + final TypeElement flob = Lang.typeElement("org.microbean.lang.TestJavaLanguageModel.Flob"); + final TypeParameterElement tp = (TypeParameterElement)flob.getTypeParameters().get(0); + assertEquals("T", tp.getSimpleName().toString()); + } + + @Test + final void testReturnTypeOfTopLevelClassConstructor() { + final TypeElement object = Lang.typeElement("java.lang.Object"); + ExecutableElement c = null; + ENCLOSED_ELEMENTS: + for (final Element e : object.getEnclosedElements()) { + switch (e.getKind()) { + case CONSTRUCTOR: + final ExecutableElement ee = (ExecutableElement)e; + if (ee.getParameters().isEmpty()) { + c = ee; + break ENCLOSED_ELEMENTS; + } + break; + default: + break; + } + } + assertSame(TypeKind.VOID, ((ExecutableType)c.asType()).getReturnType().getKind()); + } + + @Test + final void testReflectionBridge() throws NoSuchMethodException { + final TypeElement e = Lang.typeElement(List.class); + assertTrue(e.getQualifiedName().contentEquals("java.util.List")); + final Method m = this.getClass().getDeclaredMethod("listString"); + final ExecutableElement ee = Lang.executableElement(m); + assertTrue(ee.getSimpleName().contentEquals("listString")); + final java.lang.reflect.TypeVariable tv = List.class.getTypeParameters()[0]; + assertEquals("E", tv.getName()); + assertSame(List.class, tv.getGenericDeclaration()); + final TypeParameterElement tpe = Lang.typeParameterElement(tv); + assertTrue(tpe.getSimpleName().contentEquals("E")); + assertSame(unwrap(tpe.asType()), unwrap(Lang.typeVariable(tv))); + } + + private static List listString() { + return List.of(); + } + + @Test + final void testAnnotationsOnClassesInMethods() throws ClassNotFoundException { + final TypeElement e = Lang.typeElement(this.getClass().getName()); + assertNotNull(e); + ExecutableElement baker = null; + EES: + for (final Element ee : e.getEnclosedElements()) { + switch (ee.getKind()) { + case METHOD: + baker = (ExecutableElement)ee; + break EES; + } + } + + // As it turns out, there is no way in the javax.lang.model.* hierarchy to actually get a local class. + + // Local classes are not enclosed by their defining methods. + assertEquals(0, baker.getEnclosedElements().size()); + + for (final Element ee : e.getEnclosedElements()) { + if (ee.getKind() == ElementKind.CLASS) { + // Possibly surprising! Local classes are not present as enclosed elements of their enclosing class either. + assertNotEquals(NestingKind.LOCAL, ((TypeElement)ee).getNestingKind()); + } + } + + // You can't get Charlie directly: + assertNull(Lang.typeElement(this.getClass().getName() + "$1Charlie")); + + // But it does exist, and in the reflection model you can find out all sorts of things about it. + final Class c = Class.forName(this.getClass().getName() + "$1Charlie"); + assertSame(this.getClass(), c.getEnclosingClass()); + assertEquals("baker", c.getEnclosingMethod().getName()); + } + + @Test + final void testStrangeJUnitTestAnnotationCase1() { + // This is tricky and perhaps counterintuitive so pay attention. + // + // In Maven, JUnit is run on the classpath, so is part of the unnamed module. JUnit is actually modular but for + // whatever reason Surefire by default puts it on the classpath, not the module path. + // + // So that's why this test passes: + assertNull(Lang.moduleElement("org.junit.jupiter.api")); + + assertSame(ElementKind.ANNOTATION_TYPE, Lang.typeElement("org.junit.jupiter.api.Test").getKind()); + assertSame(TypeKind.DECLARED, Lang.typeElement("org.junit.jupiter.api.Test").asType().getKind()); + } + + // @Disabled // see https://mail.openjdk.org/pipermail/compiler-dev/2023-July/023750.html + @Test + final void testStrangeJUnitTestAnnotationCase2() throws ReflectiveOperationException { + final ExecutableElement ee = Lang.executableElement(this.getClass().getDeclaredMethod("testStrangeJUnitTestAnnotationCase2")); + final AnnotationMirror am = ee.getAnnotationMirrors().get(0); + final DeclaredType t = am.getAnnotationType(); + assertSame(TypeKind.DECLARED, t.getKind()); // fails + assertSame(ElementKind.ANNOTATION_TYPE, t.asElement().getKind()); // fails + } + + + /* + * Members for introspecting/analyzing in tests. + */ + + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE) + private @interface Able {} + + private static void baker() { + @Able + class Charlie {}; + } + + private static record Gloop(String name) { + + // Note final here; accessor is otherwise stock + public final String name() { + return this.name; + } + + } + + private static class Flob<@Borf T> {} + + @Retention(RetentionPolicy.RUNTIME) + @Target(ElementType.TYPE_PARAMETER) + private @interface Borf {} + +} diff --git a/lang/src/test/java/org/microbean/lang/TestMethodSymbolAndTypeUseAnnotations.java b/lang/src/test/java/org/microbean/lang/TestMethodSymbolAndTypeUseAnnotations.java new file mode 100644 index 00000000..cac30688 --- /dev/null +++ b/lang/src/test/java/org/microbean/lang/TestMethodSymbolAndTypeUseAnnotations.java @@ -0,0 +1,97 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2022–2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang; + +import com.sun.tools.javac.code.Attribute; +import com.sun.tools.javac.code.Symbol.MethodSymbol; +import com.sun.tools.javac.code.Symbol.VarSymbol; +import com.sun.tools.javac.code.SymbolMetadata; +import com.sun.tools.javac.code.TargetType; +import com.sun.tools.javac.code.TypeAnnotationPosition; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import java.util.List; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.VariableElement; + +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.ExecutableType; + +import javax.lang.model.util.Elements; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +final class TestMethodSymbolAndTypeUseAnnotations { + + private TestMethodSymbolAndTypeUseAnnotations() { + super(); + } + + @Test + final void testJavac() { + final Elements elements = Lang.pe().getElementUtils(); + final Element e = elements.getTypeElement("org.microbean.lang.TestMethodSymbolAndTypeUseAnnotations.B"); + final ExecutableElement c = (ExecutableElement)e.getEnclosedElements().get(1); + assertTrue(c.getSimpleName().contentEquals("c")); + final VariableElement s = (VariableElement)c.getParameters().get(0); + // final List typeAttributes = ((MethodSymbol)c).getRawTypeAttributes(); + final List typeAttributes = ((VarSymbol)s).owner.getRawTypeAttributes(); + assertEquals(1, typeAttributes.size()); + final Attribute.TypeCompound a = (Attribute.TypeCompound)typeAttributes.get(0); + final TypeAnnotationPosition position = a.getPosition(); + final TargetType targetType = position.type; + assertSame(TargetType.METHOD_FORMAL_PARAMETER, targetType); + } + + @Test + final void testReflection() throws NoSuchMethodException { + final java.lang.reflect.Method c = B.class.getDeclaredMethod("c", String.class); + final java.lang.reflect.AnnotatedType sType = c.getAnnotatedParameterTypes()[0]; + assertSame(A.class, sType.getAnnotations()[0].annotationType()); + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.TYPE, ElementType.TYPE_USE }) + public @interface A {} + + @A + public static final class B { + + public B() { + super(); + } + + public static final void c(@A String s) {} + + } + +} diff --git a/lang/src/test/java/org/microbean/lang/TestModuleRelatedIssues.java b/lang/src/test/java/org/microbean/lang/TestModuleRelatedIssues.java new file mode 100644 index 00000000..49b6593d --- /dev/null +++ b/lang/src/test/java/org/microbean/lang/TestModuleRelatedIssues.java @@ -0,0 +1,89 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.microbean.lang; + +import java.lang.constant.Constable; +import java.lang.constant.ConstantDesc; + +import java.lang.invoke.MethodHandles; + +import java.util.Optional; + +import javax.lang.model.element.ModuleElement; +import javax.lang.model.element.TypeElement; + +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeMirror; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +final class TestModuleRelatedIssues { + + private TestModuleRelatedIssues() { + super(); + } + + @Test + final void testModuleRelatedIssues() { + // Maven puts the (modular) junit-jupiter-api jar file on the classpath, not the module path, so there's no module + // by this name on the module paththat can be found. + assertNull(Lang.moduleElement("org.junit.jupiter.api")); + + // The unnamed module at runtime looks like this: + final Module m = Test.class.getModule(); + assertNotNull(m); + assertFalse(m.isNamed()); + assertNull(m.getName()); + + // Note the lack of ModuleElement parameter: + TypeElement e = Lang.typeElement("org.junit.jupiter.api.Test"); + assertNotNull(e); + + final ModuleElement unnamedModule = Lang.moduleOf(e); + assertTrue(unnamedModule.isUnnamed()); + + // The unnamed module can "see" Test. Note that the ModuleElement parameter is not necessarily the module that + // (indirectly) encloses Test! + e = Lang.typeElement(unnamedModule, "org.junit.jupiter.api.Test"); + assertNotNull(e); + assertEquals(unnamedModule, Lang.moduleOf(e)); + + // java.base does not read the unnamed module, so should not be able to "see" Test. That is, e is not null because + // org.junit.jupiter.api.Test does not "belong" to it, it is null because java.base does not read + // org.junit.jupiter.api.Test (because it does not (cannot, except with command-line hacks) read the unnamed + // module). + e = Lang.typeElement(Lang.moduleElement("java.base"), "org.junit.jupiter.api.Test"); + assertNull(e); + + assertThrows(NullPointerException.class, () -> Lang.typeElement((ModuleElement)null, "org.junit.jupiter.api.Test")); + + // org.microbean.lang reads the unnamed module (every module reads the unnamed module) so should be able to "see" + // Test. Note, to state the obvious, that Test is not enclosed by a package enclosed by org.microbean.lang; + // org.microbean.lang is the "viewing" or reading module. + e = Lang.typeElement(Lang.moduleElement("org.microbean.lang"), "org.junit.jupiter.api.Test"); + assertNotNull(e); + + + + } + +} diff --git a/lang/src/test/java/org/microbean/lang/TestMultithread.java b/lang/src/test/java/org/microbean/lang/TestMultithread.java new file mode 100644 index 00000000..55128e8f --- /dev/null +++ b/lang/src/test/java/org/microbean/lang/TestMultithread.java @@ -0,0 +1,57 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.microbean.lang; + +import java.util.ArrayDeque; +import java.util.Deque; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +final class TestMultithread { + + private TestMultithread() { + super(); + } + + @Test + final void testMultithread() { + final Deque dq = new ArrayDeque<>(); + frob("java.lang.invoke.TypeDescriptor$OfField", dq); + assertEquals(2, dq.size()); + assertEquals("java.lang.constant.ConstantDesc", dq.pop()); + assertEquals("java.lang.invoke.TypeDescriptor$OfField", dq.pop()); + } + + private static void frob(final String s, final Deque stack) { + final StringBuilder sb = new StringBuilder(); + for (char c : s.toCharArray()) { + switch (c) { + case '<': + stack.push(sb.toString()); + sb.setLength(0); + break; + case '>': + stack.push(sb.toString()); + sb.setLength(0); + break; + default: + sb.append(c); + break; + } + } + } + +} diff --git a/lang/src/test/java/org/microbean/lang/TestNameTypeMirrorComparator.java b/lang/src/test/java/org/microbean/lang/TestNameTypeMirrorComparator.java new file mode 100644 index 00000000..48cd4820 --- /dev/null +++ b/lang/src/test/java/org/microbean/lang/TestNameTypeMirrorComparator.java @@ -0,0 +1,69 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2022–2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.microbean.lang; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import java.util.concurrent.ConcurrentMap; + +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; + +import static org.microbean.lang.Lang.declaredType; +import static org.microbean.lang.Lang.primitiveType; +import static org.microbean.lang.Lang.typeElement; +import static org.microbean.lang.Lang.typeVariable; +import static org.microbean.lang.Lang.wildcardType; + +final class TestNameTypeMirrorComparator { + + private TestNameTypeMirrorComparator() { + super(); + } + + @Test + final void testName() { + final TypeMirror t = declaredType(null, typeElement("java.util.List"), wildcardType(declaredType("java.lang.String"), null)); + assertEquals("java.util.List", NameTypeMirrorComparator.INSTANCE.name(t)); + } + + @SuppressWarnings("rawtypes") + @Test + final , ? super ArrayList>> void spike() throws ReflectiveOperationException { + final List l = new ArrayList<>(); + final TypeMirror primitiveInt = primitiveType(TypeKind.INT); + l.add(primitiveInt); + final TypeVariable tv = typeVariable(this.getClass().getDeclaredMethod("spike"), "T"); + l.add(tv); + final TypeMirror listQuestionMarkExtendsString = declaredType(null, typeElement("java.util.List"), wildcardType(declaredType("java.lang.String"), null)); + l.add(listQuestionMarkExtendsString); + final TypeMirror list = declaredType(null, typeElement("java.util.List")); + l.add(list); + Collections.sort(l, NameTypeMirrorComparator.INSTANCE); + assertEquals(4, l.size()); + assertSame(tv, l.get(0)); + assertSame(primitiveInt, l.get(1)); + assertSame(list, l.get(2)); + assertSame(listQuestionMarkExtendsString, l.get(3)); + } + +} diff --git a/lang/src/test/java/org/microbean/lang/TestOddsAndEnds.java b/lang/src/test/java/org/microbean/lang/TestOddsAndEnds.java new file mode 100644 index 00000000..4d971863 --- /dev/null +++ b/lang/src/test/java/org/microbean/lang/TestOddsAndEnds.java @@ -0,0 +1,56 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2022–2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import javax.lang.model.element.TypeElement; + +import javax.lang.model.type.DeclaredType; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +final class TestOddsAndEnds { + + private TestOddsAndEnds() { + super(); + } + + @Test + final void testTypeUseAnnotationOnClass() { + final TypeElement b = Lang.typeElement("org.microbean.lang.TestOddsAndEnds.B"); + // B is actually a declaration annotation per the rules of the JLS! + assertEquals(1, b.getAnnotationMirrors().size()); + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.TYPE_USE }) // ...which includes TYPE and TYPE_PARAMETER + private @interface A {} + + @A + private static final class B {} + +} diff --git a/lang/src/test/java/org/microbean/lang/TestOuterInner.java b/lang/src/test/java/org/microbean/lang/TestOuterInner.java new file mode 100644 index 00000000..9f1bb9fa --- /dev/null +++ b/lang/src/test/java/org/microbean/lang/TestOuterInner.java @@ -0,0 +1,94 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.microbean.lang; + +import java.lang.reflect.Field; + +import javax.lang.model.element.TypeElement; + +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; + +import javax.lang.model.util.Elements; +import javax.lang.model.util.Types; + +import com.sun.tools.javac.model.JavacTypes; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +final class TestOuterInner { + + private com.sun.tools.javac.code.Types javacCodeTypes; + + private TestOuterInner() { + super(); + } + + @BeforeEach + final void setup() throws IllegalAccessException, NoSuchFieldException { + final Field f = JavacTypes.class.getDeclaredField("types"); + assertTrue(f.trySetAccessible()); + this.javacCodeTypes = (com.sun.tools.javac.code.Types)f.get(Lang.pe().getTypeUtils()); + } + + @Test + final void testOuterInner() { + final DataHolder outer = new DataHolder<>("hello"); + final DataHolder.Inner inner = outer.data(); + // Hmm. OK. + final TypeElement e = Lang.typeElement(DataHolder.class); + assertNotNull(e); + final DeclaredType outerDeclaredType = Lang.declaredType(null, e, Lang.declaredType(String.class)); + assertSame(TypeKind.DECLARED, outerDeclaredType.getKind()); + final DeclaredType innerDeclaredType = Lang.declaredType(outerDeclaredType, Lang.typeElement(inner.getClass())); + assertSame(TypeKind.DECLARED, innerDeclaredType.getKind()); + } + + private static class DataHolder { + + private final Inner data; + + private DataHolder(final T data) { + super(); + this.data = new Inner(data); + } + + private Inner data() { + return this.data; + } + + private class Inner { + + private final T data; + + private Inner(final T data) { + super(); + this.data = data; + } + + private final T data() { + return this.data; + } + + } + + } + + +} diff --git a/lang/src/test/java/org/microbean/lang/TestSameTypeVisitor.java b/lang/src/test/java/org/microbean/lang/TestSameTypeVisitor.java new file mode 100644 index 00000000..5c6fb35e --- /dev/null +++ b/lang/src/test/java/org/microbean/lang/TestSameTypeVisitor.java @@ -0,0 +1,126 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2022–2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang; + +import java.lang.reflect.Field; + +import javax.annotation.processing.ProcessingEnvironment; + +import javax.lang.model.element.TypeElement; + +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.NoType; +import javax.lang.model.type.PrimitiveType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; +import javax.lang.model.type.WildcardType; + +import com.sun.tools.javac.model.JavacTypes; + +import org.junit.jupiter.api.Test; + +import org.junit.jupiter.api.extension.ExtendWith; + +import org.microbean.lang.visitor.Visitors; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +@ExtendWith(AnnotationProcessingInterceptor.class) +final class TestSameTypeVisitor { + + private TestSameTypeVisitor() { + super(); + } + + @Test + final void testSameTypeVisitor(final ProcessingEnvironment env) { + final javax.lang.model.util.Elements elements = env.getElementUtils(); + final javax.lang.model.util.Types javacModelTypes = env.getTypeUtils(); + assertTrue(javacModelTypes instanceof JavacTypes); + com.sun.tools.javac.code.Types javacTypes = null; + try { + final Field f = JavacTypes.class.getDeclaredField("types"); + assertTrue(f.trySetAccessible()); + javacTypes = (com.sun.tools.javac.code.Types)f.get(javacModelTypes); + } catch (final ReflectiveOperationException reflectiveOperationException) { + fail(reflectiveOperationException); + } + assertNotNull(javacTypes); + + // Set up the fundamentals. + final TypeAndElementSource tes = new TypeAndElementSource() { + @Override + public final ArrayType arrayTypeOf(final TypeMirror componentType) { + return javacModelTypes.getArrayType(componentType); + } + @Override + public boolean assignable(final TypeMirror payload, final TypeMirror receiver) { + return javacModelTypes.isAssignable(payload, receiver); + } + @Override + public final TypeElement boxedClass(final PrimitiveType t) { + return javacModelTypes.boxedClass(t); + } + @Override + public final DeclaredType declaredType(final TypeElement typeElement, final TypeMirror... arguments) { + return javacModelTypes.getDeclaredType(typeElement, arguments); + } + @Override + public final DeclaredType declaredType(final DeclaredType enclosingType, final TypeElement typeElement, final TypeMirror... arguments) { + return javacModelTypes.getDeclaredType(enclosingType, typeElement, arguments); + } + @Override + @SuppressWarnings("unchecked") + public final T erasure(final T t) { + return (T)javacModelTypes.erasure(t); + } + @Override + public final NoType noType(final TypeKind k) { + return javacModelTypes.getNoType(k); + } + @Override + public final PrimitiveType primitiveType(final TypeKind k) { + return javacModelTypes.getPrimitiveType(k); + } + @Override + public boolean sameType(final TypeMirror t, final TypeMirror s) { + return javacModelTypes.isSameType(t, s); + } + @Override + public final TypeElement typeElement(final CharSequence m, final CharSequence n) { + return elements.getTypeElement(elements.getModuleElement(m), n); + } + @Override + public final TypeVariable typeVariable(final java.lang.reflect.TypeVariable t) { + throw new UnsupportedOperationException(); // NOTE + } + @Override + public final WildcardType wildcardType(final TypeMirror extendsBound, final TypeMirror superBound) { + return javacModelTypes.getWildcardType(extendsBound, superBound); + } + }; + final Visitors visitors = new Visitors(tes); + + // Should be ready to go. + + } + +} diff --git a/lang/src/test/java/org/microbean/lang/TestSupertypeVisitor.java b/lang/src/test/java/org/microbean/lang/TestSupertypeVisitor.java new file mode 100644 index 00000000..5f740089 --- /dev/null +++ b/lang/src/test/java/org/microbean/lang/TestSupertypeVisitor.java @@ -0,0 +1,158 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2022–2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang; + +import java.lang.reflect.Field; + +import java.util.List; + +import javax.annotation.processing.ProcessingEnvironment; + +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.TypeParameterElement; + +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.NoType; +import javax.lang.model.type.PrimitiveType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; +import javax.lang.model.type.WildcardType; + +import com.sun.tools.javac.code.Type; +import com.sun.tools.javac.code.Type.ClassType; + +import com.sun.tools.javac.model.JavacTypes; + +import org.junit.jupiter.api.Test; + +import org.junit.jupiter.api.extension.ExtendWith; + +import org.microbean.lang.type.Types; + +import org.microbean.lang.visitor.EraseVisitor; +import org.microbean.lang.visitor.SupertypeVisitor; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +@ExtendWith(AnnotationProcessingInterceptor.class) +final class TestSupertypeVisitor { + + private TestSupertypeVisitor() { + super(); + } + + @Test + final void testInteger(final ProcessingEnvironment env) { + final javax.lang.model.util.Elements elements = env.getElementUtils(); + final javax.lang.model.util.Types javacModelTypes = env.getTypeUtils(); + assertTrue(javacModelTypes instanceof JavacTypes); + com.sun.tools.javac.code.Types javacTypes = null; + try { + final Field f = JavacTypes.class.getDeclaredField("types"); + assertTrue(f.trySetAccessible()); + javacTypes = (com.sun.tools.javac.code.Types)f.get(javacModelTypes); + } catch (final ReflectiveOperationException reflectiveOperationException) { + fail(reflectiveOperationException); + } + assertNotNull(javacTypes); + + final TypeElement integerElement = elements.getTypeElement("java.lang.Integer"); + final DeclaredType integerElementType = (DeclaredType)integerElement.asType(); + assertSame(TypeKind.DECLARED, integerElementType.getKind()); + + final Type javacSupertype = javacTypes.supertype((Type)integerElementType); + + // Let's try it with our visitor. + + // Set up the fundamentals. + final TypeAndElementSource tes = new TypeAndElementSource() { + @Override + public final ArrayType arrayTypeOf(final TypeMirror componentType) { + return javacModelTypes.getArrayType(componentType); + } + @Override + public boolean assignable(final TypeMirror payload, final TypeMirror receiver) { + return javacModelTypes.isAssignable(payload, receiver); + } + @Override + public final TypeElement boxedClass(final PrimitiveType t) { + return javacModelTypes.boxedClass(t); + } + @Override + public final DeclaredType declaredType(final TypeElement typeElement, final TypeMirror... arguments) { + return javacModelTypes.getDeclaredType(typeElement, arguments); + } + @Override + public final DeclaredType declaredType(final DeclaredType enclosingType, final TypeElement typeElement, final TypeMirror... arguments) { + return javacModelTypes.getDeclaredType(enclosingType, typeElement, arguments); + } + @Override + @SuppressWarnings("unchecked") + public final T erasure(final T t) { + return (T)javacModelTypes.erasure(t); + } + @Override + public final NoType noType(final TypeKind k) { + return javacModelTypes.getNoType(k); + } + @Override + public final PrimitiveType primitiveType(final TypeKind k) { + return javacModelTypes.getPrimitiveType(k); + } + @Override + public boolean sameType(final TypeMirror t, final TypeMirror s) { + return javacModelTypes.isSameType(t, s); + } + @Override + public final TypeElement typeElement(final CharSequence m, final CharSequence n) { + return elements.getTypeElement(elements.getModuleElement(m), n); + } + @Override + public final TypeVariable typeVariable(final java.lang.reflect.TypeVariable t) { + throw new UnsupportedOperationException(); // NOTE + } + @Override + public final WildcardType wildcardType(final TypeMirror extendsBound, final TypeMirror superBound) { + return javacModelTypes.getWildcardType(extendsBound, superBound); + } + }; + final Types types = new Types(tes); + final EraseVisitor eraseVisitor = new EraseVisitor(tes, types); + final SupertypeVisitor supertypeVisitor = new SupertypeVisitor(tes, types, eraseVisitor); + + final TypeMirror supertype = supertypeVisitor.visit(integerElementType); + + assertSame(javacSupertype, supertype); + + // How about superinterfaces? + + final List javacInterfaces = javacTypes.interfaces((Type)integerElementType); + final List interfaces = supertypeVisitor.interfacesVisitor().visit(integerElementType); + assertEquals(javacInterfaces, interfaces); + + } + +} diff --git a/lang/src/test/java/org/microbean/lang/TestSupertypeVisitor2.java b/lang/src/test/java/org/microbean/lang/TestSupertypeVisitor2.java new file mode 100644 index 00000000..091a6e92 --- /dev/null +++ b/lang/src/test/java/org/microbean/lang/TestSupertypeVisitor2.java @@ -0,0 +1,126 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2022–2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang; + +import java.lang.reflect.Field; + +import java.util.List; + +import javax.lang.model.element.TypeElement; + +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; + +import com.sun.tools.javac.code.Type; + +import com.sun.tools.javac.model.JavacTypes; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.microbean.lang.type.Types; + +import org.microbean.lang.visitor.EraseVisitor; +import org.microbean.lang.visitor.SupertypeVisitor; +import org.microbean.lang.visitor.Visitors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import static org.microbean.lang.Lang.unwrap; + +final class TestSupertypeVisitor2 { + + private com.sun.tools.javac.code.Types javacCodeTypes; + + private Visitors visitors; + + private TestSupertypeVisitor2() { + super(); + } + + @BeforeEach + final void setup() throws IllegalAccessException, NoSuchFieldException { + // this.visitors = new Visitors((m, n) -> Lang.typeElement(Lang.moduleElement(m), n)); + this.visitors = new Visitors(Lang.typeAndElementSource()); + final Field f = JavacTypes.class.getDeclaredField("types"); + assertTrue(f.trySetAccessible()); + this.javacCodeTypes = (com.sun.tools.javac.code.Types)f.get(Lang.pe().getTypeUtils()); + } + + @Test + final void testInteger() { + final DeclaredType integerElementType = Lang.declaredType("java.lang.Integer"); + assertSame(TypeKind.DECLARED, integerElementType.getKind()); + + final SupertypeVisitor supertypeVisitor = this.visitors.supertypeVisitor(); + + assertSame(this.javacCodeTypes.supertype((Type)unwrap(integerElementType)), + unwrap(supertypeVisitor.visit(integerElementType))); + + // How about superinterfaces? + + final List javacInterfaces = this.javacCodeTypes.interfaces((Type)unwrap(integerElementType)); + final List interfaces = supertypeVisitor.interfacesVisitor().visit(integerElementType); + assertEquals(javacInterfaces.size(), interfaces.size()); + for (int i = 0; i < javacInterfaces.size(); i++) { + assertEquals(javacInterfaces.get(i), unwrap(interfaces.get(i))); + } + + } + + @Test + final void testSupertypeOfInterface() { + final DeclaredType serializableElementType = (DeclaredType)Lang.typeElement("java.io.Serializable").asType(); + assertSame(TypeKind.DECLARED, serializableElementType.getKind()); + assertSame(unwrap(Lang.declaredType("java.lang.Object")), this.javacCodeTypes.supertype((Type)unwrap(serializableElementType))); + } + + @Test + final void testSupertypeOfObject() { + assertSame(Lang.noType(TypeKind.NONE), this.javacCodeTypes.supertype((Type)unwrap(Lang.declaredType("java.lang.Object")))); + } + + @Test + final void testFilter() { + final SupertypeVisitor supertypeVisitor = this.visitors.supertypeVisitor().withFilter(t -> { + if (t.getKind() == TypeKind.DECLARED && ((TypeElement)((DeclaredType)t).asElement()).getQualifiedName().contentEquals("java.lang.Object")) { + return false; + } + return true; + }); + assertSame(Lang.noType(TypeKind.NONE), unwrap(supertypeVisitor.visit(Lang.declaredType("java.io.Serializable")))); + } + + @Test + final void testFilter2() { + assertSame(unwrap(Lang.declaredType("java.lang.Number")), this.javacCodeTypes.supertype((Type)unwrap(Lang.declaredType("java.lang.Integer")))); + final SupertypeVisitor supertypeVisitor = this.visitors.supertypeVisitor().withFilter(t -> { + if (t.getKind() == TypeKind.DECLARED && ((TypeElement)((DeclaredType)t).asElement()).getQualifiedName().contentEquals("java.lang.Number")) { + return false; + } + return true; + }); + assertSame(unwrap(Lang.declaredType("java.lang.Object")), supertypeVisitor.visit(unwrap(Lang.declaredType("java.lang.Integer")))); + } + +} diff --git a/lang/src/test/java/org/microbean/lang/TestTypeAndElementAssumptions.java b/lang/src/test/java/org/microbean/lang/TestTypeAndElementAssumptions.java new file mode 100644 index 00000000..0e991b99 --- /dev/null +++ b/lang/src/test/java/org/microbean/lang/TestTypeAndElementAssumptions.java @@ -0,0 +1,140 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2022–2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang; + +import java.lang.reflect.Field; + +import java.util.List; + +import javax.annotation.processing.ProcessingEnvironment; + +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.TypeParameterElement; + +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; + +import com.sun.tools.javac.code.Type.ClassType; + +import com.sun.tools.javac.model.JavacTypes; + +import org.junit.jupiter.api.Test; + +import org.junit.jupiter.api.extension.ExtendWith; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +@ExtendWith(AnnotationProcessingInterceptor.class) +final class TestTypeAndElementAssumptions { + + private TestTypeAndElementAssumptions() { + super(); + } + + @Test + final void testAssumptions(final ProcessingEnvironment env) { + final javax.lang.model.util.Elements elements = env.getElementUtils(); + final javax.lang.model.util.Types javacModelTypes = env.getTypeUtils(); + assertTrue(javacModelTypes instanceof JavacTypes); + + com.sun.tools.javac.code.Types javacTypes = null; + try { + final Field f = JavacTypes.class.getDeclaredField("types"); + assertTrue(f.trySetAccessible()); + javacTypes = (com.sun.tools.javac.code.Types)f.get(javacModelTypes); + } catch (final ReflectiveOperationException reflectiveOperationException) { + fail(reflectiveOperationException); + } + assertNotNull(javacTypes); + + // Here we have an element representing a declaration, i.e. "public interface Comparable". + final TypeElement comparableElement = elements.getTypeElement("java.lang.Comparable"); + assertEquals(ElementKind.INTERFACE, comparableElement.getKind()); + + // The element declares a type, of course. + final DeclaredType elementType = (DeclaredType)comparableElement.asType(); + assertEquals(TypeKind.DECLARED, elementType.getKind()); + assertTrue(elementType instanceof ClassType); + + // More exploring. + assertSame(comparableElement, ((ClassType)elementType).tsym); + assertSame(comparableElement, elementType.asElement()); + + // The declared type has one type argument. The sole type argument is definitionally a TypeVariable. + final List typeArguments = elementType.getTypeArguments(); + assertEquals(1, typeArguments.size()); + final TypeVariable soleTypeArgument = (TypeVariable)typeArguments.get(0); + assertEquals(TypeKind.TYPEVAR, soleTypeArgument.getKind()); + + // The element has a type parameter, which is an element itself. + final List typeParameters = comparableElement.getTypeParameters(); + assertEquals(1, typeParameters.size()); + final TypeParameterElement soleTypeParameter = (TypeParameterElement)typeParameters.get(0); + assertSame(soleTypeArgument.asElement(), soleTypeParameter); + + // The sole type parameter element declares a type, which is the type variable we discussed above. + assertSame(soleTypeArgument, soleTypeParameter.asType()); + assertTrue(soleTypeParameter.getEnclosedElements().isEmpty()); + assertSame(comparableElement, soleTypeParameter.getEnclosingElement()); + assertSame(comparableElement, soleTypeParameter.getGenericElement()); + + // Now let's look at a place where a related type is used. + final TypeElement stringElement = elements.getTypeElement("java.lang.String"); + final DeclaredType stringType = (DeclaredType)stringElement.asType(); + javacTypes.interfaces((com.sun.tools.javac.code.Type)stringType); + final TypeMirror comparableStringType = stringElement.getInterfaces().stream() + .filter(i -> + i.getKind() == TypeKind.DECLARED && + i instanceof DeclaredType d && + d.asElement().getSimpleName().contentEquals("Comparable")) + .findFirst() + .orElseThrow(); + assertNotSame(elementType, comparableStringType); + final TypeElement comparableStringElement = (TypeElement)((DeclaredType)comparableStringType).asElement(); + assertSame(comparableElement, comparableStringElement); + + // So that's a case where the Comparable-type-declared-by-the-Comparable-element is (clearly) not the same as + // the Comparable-type-used-by-the-String-element. + // + // Important takeaways: + // + // * When you have a TypeElement that represents...I don't know how to talk about this. When you have a TypeElement + // that represents the declaration of a type, its asType() method will return a DeclaredType. + // + // * The DeclaredType so returned will have type arguments. + // + // * The type arguments of that DeclaredType will always be TypeVariables. + // + // * Such a TypeVariable will always return the proper TypeParameterElement from its asElement() method (see below). + // + // * The TypeElement we're talking about will always have type parameters. + // + // * For any given TypeParameterElement, its asType() method will return a TypeVariable, namely a TypeVariable + // mentioned above. + + } + +} diff --git a/lang/src/test/java/org/microbean/lang/TestTypeClosure.java b/lang/src/test/java/org/microbean/lang/TestTypeClosure.java new file mode 100644 index 00000000..25f22661 --- /dev/null +++ b/lang/src/test/java/org/microbean/lang/TestTypeClosure.java @@ -0,0 +1,233 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2022–2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang; + +import java.lang.reflect.Field; + +import java.util.List; + +import javax.annotation.processing.ProcessingEnvironment; + +import javax.lang.model.element.TypeElement; + +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.NoType; +import javax.lang.model.type.PrimitiveType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.type.TypeVariable; +import javax.lang.model.type.WildcardType; + +import com.sun.tools.javac.code.Type; + +import com.sun.tools.javac.model.JavacTypes; + +import org.junit.jupiter.api.Test; + +import org.junit.jupiter.api.extension.ExtendWith; + +import org.microbean.lang.visitor.Visitors; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +@ExtendWith(AnnotationProcessingInterceptor.class) +final class TestTypeClosure { + + private TestTypeClosure() { + super(); + } + + @Test + final void testArray(final ProcessingEnvironment env) { + final javax.lang.model.util.Elements elements = env.getElementUtils(); + final javax.lang.model.util.Types javacModelTypes = env.getTypeUtils(); + assertTrue(javacModelTypes instanceof JavacTypes); + com.sun.tools.javac.code.Types javacTypes = null; + try { + final Field f = JavacTypes.class.getDeclaredField("types"); + assertTrue(f.trySetAccessible()); + javacTypes = (com.sun.tools.javac.code.Types)f.get(javacModelTypes); + } catch (final ReflectiveOperationException reflectiveOperationException) { + fail(reflectiveOperationException); + } + assertNotNull(javacTypes); + + final ArrayType integerArrayType = javacModelTypes.getArrayType(elements.getTypeElement("java.lang.Integer").asType()); + final List integerArrayClosure = javacTypes.closure((Type)integerArrayType); + + // This is weird and we don't support it. See + // https://stackoverflow.com/questions/73683649/in-the-javac-source-code-why-does-closuretype-return-a-non-empty-list-for-non. + assertEquals(1, integerArrayClosure.size()); + assertSame(integerArrayType, integerArrayClosure.get(0)); + + // Let's prove we don't support it. + final TypeAndElementSource tes = new TypeAndElementSource() { + @Override + public final ArrayType arrayTypeOf(final TypeMirror componentType) { + return javacModelTypes.getArrayType(componentType); + } + @Override + public boolean assignable(final TypeMirror payload, final TypeMirror receiver) { + return javacModelTypes.isAssignable(payload, receiver); + } + @Override + public final TypeElement boxedClass(final PrimitiveType t) { + return javacModelTypes.boxedClass(t); + } + @Override + public final DeclaredType declaredType(final TypeElement typeElement, final TypeMirror... arguments) { + return javacModelTypes.getDeclaredType(typeElement, arguments); + } + @Override + public final DeclaredType declaredType(final DeclaredType enclosingType, final TypeElement typeElement, final TypeMirror... arguments) { + return javacModelTypes.getDeclaredType(enclosingType, typeElement, arguments); + } + @Override + @SuppressWarnings("unchecked") + public final T erasure(final T t) { + return (T)javacModelTypes.erasure(t); + } + @Override + public final NoType noType(final TypeKind k) { + return javacModelTypes.getNoType(k); + } + @Override + public final PrimitiveType primitiveType(final TypeKind k) { + return javacModelTypes.getPrimitiveType(k); + } + @Override + public boolean sameType(final TypeMirror t, final TypeMirror s) { + return javacModelTypes.isSameType(t, s); + } + @Override + public final TypeElement typeElement(final CharSequence m, final CharSequence n) { + return elements.getTypeElement(elements.getModuleElement(m), n); + } + @Override + public final TypeVariable typeVariable(final java.lang.reflect.TypeVariable t) { + throw new UnsupportedOperationException(); // NOTE + } + @Override + public final WildcardType wildcardType(final TypeMirror extendsBound, final TypeMirror superBound) { + return javacModelTypes.getWildcardType(extendsBound, superBound); + } + }; + final Visitors visitors = new Visitors(tes); // (m, n) -> elements.getTypeElement(elements.getModuleElement(m), n)); + try { + visitors.typeClosureVisitor().visit(integerArrayType); + fail(); + } catch (final IllegalArgumentException expected) { + // OK + } + } + + @Test + final void testTypeClosure(final ProcessingEnvironment env) { + final javax.lang.model.util.Elements elements = env.getElementUtils(); + final javax.lang.model.util.Types javacModelTypes = env.getTypeUtils(); + assertTrue(javacModelTypes instanceof JavacTypes); + com.sun.tools.javac.code.Types javacTypes = null; + try { + final Field f = JavacTypes.class.getDeclaredField("types"); + assertTrue(f.trySetAccessible()); + javacTypes = (com.sun.tools.javac.code.Types)f.get(javacModelTypes); + } catch (final ReflectiveOperationException reflectiveOperationException) { + fail(reflectiveOperationException); + } + assertNotNull(javacTypes); + + final TypeElement integerElement = elements.getTypeElement("java.lang.Integer"); + final DeclaredType integerElementType = (DeclaredType)integerElement.asType(); + assertSame(TypeKind.DECLARED, integerElementType.getKind()); + + // https://github.com/openjdk/jdk/blob/jdk-20+14/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java#L3702-L3703 + // + // "…(that is, subclasses come first, arbitrary but fixed otherwise)." + + final List closure = javacTypes.closure((Type)integerElementType); + // 0: java.lang.Integer + // 1: java.lang.Number (superclass of java.lang.Integer) + // 2: java.io.Serializable (declared interface) + // 3: java.lang.Comparable (declared interface) + // 4: java.lang.constant.Constable (declared interface) + // 5: java.lang.constant.ConstantDesc (declared interface) + // 6: java.lang.Object (superclass of java.lang.Number) + assertEquals(7, closure.size()); + + // Let's try it with our visitor. + final TypeAndElementSource tes = new TypeAndElementSource() { + @Override + public final ArrayType arrayTypeOf(final TypeMirror componentType) { + return javacModelTypes.getArrayType(componentType); + } + @Override + public boolean assignable(final TypeMirror payload, final TypeMirror receiver) { + return javacModelTypes.isAssignable(payload, receiver); + } + @Override + public final TypeElement boxedClass(final PrimitiveType t) { + return javacModelTypes.boxedClass(t); + } + @Override + public final DeclaredType declaredType(final TypeElement typeElement, final TypeMirror... arguments) { + return javacModelTypes.getDeclaredType(typeElement, arguments); + } + @Override + public final DeclaredType declaredType(final DeclaredType enclosingType, final TypeElement typeElement, final TypeMirror... arguments) { + return javacModelTypes.getDeclaredType(enclosingType, typeElement, arguments); + } + @Override + @SuppressWarnings("unchecked") + public final T erasure(final T t) { + return (T)javacModelTypes.erasure(t); + } + @Override + public final NoType noType(final TypeKind k) { + return javacModelTypes.getNoType(k); + } + @Override + public final PrimitiveType primitiveType(final TypeKind k) { + return javacModelTypes.getPrimitiveType(k); + } + @Override + public boolean sameType(final TypeMirror t, final TypeMirror s) { + return javacModelTypes.isSameType(t, s); + } + @Override + public final TypeElement typeElement(final CharSequence m, final CharSequence n) { + return elements.getTypeElement(elements.getModuleElement(m), n); + } + @Override + public final TypeVariable typeVariable(final java.lang.reflect.TypeVariable t) { + throw new UnsupportedOperationException(); // NOTE + } + @Override + public final WildcardType wildcardType(final TypeMirror extendsBound, final TypeMirror superBound) { + return javacModelTypes.getWildcardType(extendsBound, superBound); + } + }; + final Visitors visitors = new Visitors(tes); // (m, n) -> elements.getTypeElement(elements.getModuleElement(m), n)); + final List list = visitors.typeClosureVisitor().visit(integerElementType).toList(); + assertEquals(7, list.size(), "Unexpected type closure list: " + list); + } + +} diff --git a/lang/src/test/java/org/microbean/lang/TestTypeParameters.java b/lang/src/test/java/org/microbean/lang/TestTypeParameters.java new file mode 100644 index 00000000..44a9d6b8 --- /dev/null +++ b/lang/src/test/java/org/microbean/lang/TestTypeParameters.java @@ -0,0 +1,74 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import java.util.List; + +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.TypeParameterElement; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +final class TestTypeParameters { + + private TestTypeParameters() { + super(); + } + + @Test + final void testTypeParameters() { + final TypeElement a = Lang.typeElement("org.microbean.lang.TestTypeParameters.A"); + final List typeParameters = a.getTypeParameters(); + final TypeParameterElement b = typeParameters.get(0); + assertEquals("B", b.getSimpleName().toString()); + + // I'm expecting @C to be an annotation on B-the-type-parameter and not on B-the-type-variable. + // I'm expecting @D to be an annotation on B-the-type-parameter and not on B-the-type-variable. + // I'm expecting @E to be an annotation on B-the-type-parameter and not on B-the-type-variable. + // I'm expecting @F to be an annotation on B-the-type-parameter and not on B-the-type-variable, but in fact it doesn't compile due to https://bugs.openjdk.org/browse/JDK-8303784. + assertEquals(3, b.getAnnotationMirrors().size()); + assertEquals(0, b.asType().getAnnotationMirrors().size()); + } + + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.TYPE_PARAMETER }) + private @interface C{} + + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.TYPE_USE }) // remember that this implies TYPE and TYPE_PARAMETER as well + private @interface D{} + + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.TYPE_PARAMETER, ElementType.TYPE_USE }) // effectively the same as just TYPE_USE + private @interface E{} + + // Not applicable in JDK 19 or 20 to type parameter declarations, but should be; see https://bugs.openjdk.org/browse/JDK-8303784. + // @Retention(RetentionPolicy.RUNTIME) + // private @interface F{} + + private static final class A<@C @D @E /* @F */ B> { + + } + +} diff --git a/lang/src/test/java/org/microbean/lang/TestTypeUsages.java b/lang/src/test/java/org/microbean/lang/TestTypeUsages.java new file mode 100644 index 00000000..6f23893f --- /dev/null +++ b/lang/src/test/java/org/microbean/lang/TestTypeUsages.java @@ -0,0 +1,161 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2022–2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + * implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +package org.microbean.lang; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import java.util.List; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.TypeParameterElement; +import javax.lang.model.element.VariableElement; + +import javax.lang.model.type.ArrayType; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.ExecutableType; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +final class TestTypeUsages { + + private TestTypeUsages() { + super(); + } + + @Test + final void testClassDeclarationDeclaredTypeHasNoAnnotations() { + final Element e = Lang.typeElement("org.microbean.lang.TestTypeUsages.B"); + assertEquals(1, e.getAnnotationMirrors().size()); // @A applies to B-the-element + assertEquals(0, e.asType().getAnnotationMirrors().size()); // @A does NOT apply to B-the-type-declaration + } + + @Disabled // see https://bugs.openjdk.org/browse/JDK-8225377 + @Test + final void testMethodParameterDeclaredTypeHasNoAnnotations() { + final Element e = Lang.typeElement("org.microbean.lang.TestTypeUsages.B"); + + final ExecutableElement c = (ExecutableElement)e.getEnclosedElements().get(1); + + // The c method in the B class. + assertTrue(c.getSimpleName().contentEquals("c")); + + // There aren't any annotations on it. + assertEquals(0, c.getAnnotationMirrors().size()); + + // The s parameter of type String. + final VariableElement s = (VariableElement)c.getParameters().get(0); + assertEquals("s", s.getSimpleName().toString()); + + // Prove that @A, which doesn't have PARAMETER in its @Target, really and truly does not annotate s-as-element. + assertEquals(0, s.getAnnotationMirrors().size()); + + // The type of s (the DeclaredType backing the s parameter). + final DeclaredType sAsType = (DeclaredType)s.asType(); + + // Perhaps surprisingly, @A doesn't annotate s-as-type. But that's a javac bug. See + // https://bugs.openjdk.org/browse/JDK-8225377. + assertFalse(sAsType.getAnnotationMirrors().isEmpty()); + } + + @Disabled // see https://docs.oracle.com/javase/specs/jls/se19/html/jls-9.html#jls-9.7.4 and https://bugs.openjdk.org/browse/JDK-8225377 + @Test + final void testArrayTypeUse() { + final Element e = Lang.typeElement("org.microbean.lang.TestTypeUsages.B"); + final ExecutableElement d = (ExecutableElement)e.getEnclosedElements().get(2); + + // The d method in the B class. + assertTrue(d.getSimpleName().contentEquals("d")); + + final ArrayType returnType = (ArrayType)d.getReturnType(); + final AnnotationMirror a = returnType.getAnnotationMirrors().get(0); // fails + } + + @Disabled // see https://bugs.openjdk.org/browse/JDK-8225377 + @Test + final void testTypeArgument() { + final TypeElement f = Lang.typeElement("org.microbean.lang.TestTypeUsages.F"); + final List tps = f.getTypeParameters(); + assertEquals(1, tps.size()); + final TypeElement g = Lang.typeElement("org.microbean.lang.TestTypeUsages.G"); + final DeclaredType supertype = (DeclaredType)g.getSuperclass(); + final List typeArguments = supertype.getTypeArguments(); + assertEquals(1, typeArguments.size()); + final DeclaredType string = (DeclaredType)typeArguments.get(0); + assertEquals(1, string.getAnnotationMirrors().size()); + } + + + /* + * Inner and nested classes. + */ + + + @Retention(RUNTIME) + // Note as an interesting curiosity that TYPE_USE implies ANNOTATION_TYPE, TYPE and TYPE_PARAMETER as well. See + // https://mail.openjdk.org/pipermail/compiler-dev/2023-February/022200.html. + @Target({ TYPE, TYPE_USE }) + public @interface A {} + + @Retention(RUNTIME) + // Note as an interesting curiosity that TYPE_USE implies ANNOTATION_TYPE, TYPE and TYPE_PARAMETER as well. See + // https://mail.openjdk.org/pipermail/compiler-dev/2023-February/022200.html. + @Target({ TYPE_USE }) + public @interface E {} + + @A + private static final class B { + + private B() { + super(); + } + + public static final void c(@A String s) {} + + // Yow. @A annotates the array type (of the type denoted by String[][]). @E annotaes the component type (denoted by + // String[]). + public static final String @A [] @E [] d() { + return null; + } + + } + + private static class F {} + + private static class G extends F<@E String> {} + + + +} diff --git a/lang/src/test/java/org/microbean/lang/TestTypeVariable.java b/lang/src/test/java/org/microbean/lang/TestTypeVariable.java new file mode 100644 index 00000000..fe4ba386 --- /dev/null +++ b/lang/src/test/java/org/microbean/lang/TestTypeVariable.java @@ -0,0 +1,94 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.microbean.lang; + +import java.lang.reflect.TypeVariable; + +import java.util.Arrays; +import java.util.Map; + +import java.util.concurrent.ConcurrentMap; + +import javax.lang.model.type.TypeMirror; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import static org.microbean.lang.Lang.declaredType; +import static org.microbean.lang.Lang.subtype; +import static org.microbean.lang.Lang.typeVariable; + +final class TestTypeVariable { + + private TestTypeVariable() { + super(); + } + + @Test + final > void testTypeVariable() throws ReflectiveOperationException { + final TypeVariable tv = this.getClass().getDeclaredMethod("testTypeVariable").getTypeParameters()[0]; + // Note that in all three cases the bounds are as listed, not sorted according to specificity. + assertEquals("[interface java.lang.Cloneable, java.util.concurrent.ConcurrentMap]", Arrays.asList(tv.getBounds()).toString()); + assertEquals("[java.lang.Cloneable, java.util.concurrent.ConcurrentMap]", Lang.typeParameterElement(tv).getBounds().toString()); + assertEquals("java.lang.Object&java.lang.Cloneable&java.util.concurrent.ConcurrentMap", Lang.typeVariable(tv).getUpperBound().toString()); + } + + // JLS §4.10.2: "The direct supertypes of a type variable are the types listed in its bound." + // + // So a type variable T and a type variable S may only participate in the subtype relationship if T extends S or S + // extends T; bounds have no effect. + @Test + final + , + S extends ConcurrentMap, + R extends T> + void testTypeVariableSubtyping() throws ReflectiveOperationException { + final TypeMirror t = typeVariable(this.getClass().getDeclaredMethod("testTypeVariableSubtyping"), "T"); + assertNotNull(t); + final TypeMirror s = typeVariable(this.getClass().getDeclaredMethod("testTypeVariableSubtyping"), "S"); + assertNotNull(s); + assertFalse(subtype(t, s)); + assertFalse(subtype(s, t)); + + final TypeMirror r = typeVariable(this.getClass().getDeclaredMethod("testTypeVariableSubtyping"), "R"); + assertNotNull(r); + assertTrue(subtype(r, t)); + } + + @Test + final void testSimpleTypeVariableSubtyping() throws ReflectiveOperationException { + final TypeMirror t = typeVariable(this.getClass().getDeclaredMethod("testSimpleTypeVariableSubtyping"), "T"); + assertNotNull(t); + final TypeMirror s = typeVariable(this.getClass().getDeclaredMethod("testSimpleTypeVariableSubtyping"), "S"); + assertNotNull(s); + assertFalse(subtype(t, s)); + assertFalse(subtype(s, t)); + + assertTrue(subtype(t, declaredType("java.lang.CharSequence"))); + } + + @Test + final void testUnadornedTypeVariable() throws ReflectiveOperationException { + final javax.lang.model.type.TypeVariable t = typeVariable(this.getClass().getDeclaredMethod("testUnadornedTypeVariable"), "T"); + assertNotNull(t); + assertNotNull(t.getUpperBound()); + assertNotNull(t.getLowerBound()); + } + +} diff --git a/lang/src/test/java/org/microbean/lang/visitor/TestTypeClosureVisitor.java b/lang/src/test/java/org/microbean/lang/visitor/TestTypeClosureVisitor.java new file mode 100644 index 00000000..a39a5ffd --- /dev/null +++ b/lang/src/test/java/org/microbean/lang/visitor/TestTypeClosureVisitor.java @@ -0,0 +1,265 @@ +/* -*- mode: Java; c-basic-offset: 2; indent-tabs-mode: nil; coding: utf-8-unix -*- + * + * Copyright © 2023 microBean™. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + */ +package org.microbean.lang.visitor; + +import java.lang.reflect.Field; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + +import java.util.List; + +import javax.lang.model.element.TypeElement; + +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeMirror; + +import com.sun.tools.javac.model.JavacTypes; + +import com.sun.tools.javac.code.Symbol; +import com.sun.tools.javac.code.Symbol.TypeSymbol; +import com.sun.tools.javac.code.Type; +import com.sun.tools.javac.code.TypeTag; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.microbean.lang.Equality; +import org.microbean.lang.Lang; + +import org.microbean.lang.element.DelegatingElement; + +import org.microbean.lang.type.DelegatingTypeMirror; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import static org.microbean.lang.Lang.arrayType; +import static org.microbean.lang.Lang.unwrap; + +final class TestTypeClosureVisitor { + + private static com.sun.tools.javac.code.Types javacCodeTypes; + + private Visitors visitors; + + private TestTypeClosureVisitor() { + super(); + } + + @BeforeAll + static void staticSetup() throws IllegalAccessException, InvocationTargetException, NoSuchFieldException, NoSuchMethodException { + final Field f = JavacTypes.class.getDeclaredField("types"); + assertTrue(f.trySetAccessible()); + final Method pe = Lang.class.getDeclaredMethod("pe"); + assertTrue(pe.trySetAccessible()); + javacCodeTypes = (com.sun.tools.javac.code.Types)f.get(((javax.annotation.processing.ProcessingEnvironment)pe.invoke(null)).getTypeUtils()); + } + + @BeforeEach + final void setup() { + this.visitors = new Visitors(Lang.typeAndElementSource()); + } + + @Test + final void testClosure() { + final TypeMirror s = Lang.declaredType("java.lang.String"); + assertTrue(s instanceof DelegatingTypeMirror); + + final List closure = javacCodeTypes.closure((Type)unwrap(s)); + final List visitorClosure = this.visitors.typeClosureVisitor().visit(s).toList(); + assertEquals(closure.size(), visitorClosure.size()); + assertTrue(sameType(Lang.declaredType("java.lang.Object"), closure.get(closure.size() - 1))); + assertTrue(sameType(Lang.declaredType("java.lang.Object"), closure.get(visitorClosure.size() - 1))); + assertTrue(contentsEqual(closure, visitorClosure)); + } + + private static final boolean contentsEqual(final Iterable t, final Iterable s) { + boolean ok = false; + LOOP: + for (final TypeMirror c : t) { + ok = false; + for (final TypeMirror vc : s) { + if (sameType(c, vc)) { + ok = true; + continue LOOP; + } + } + } + if (ok) { + ok = false; + LOOP: + for (final TypeMirror vc : s) { + ok = false; + for (final TypeMirror c : t) { + if (sameType(vc, c)) { + ok = true; + continue LOOP; + } + } + } + } + return ok; + } + + private static final boolean sameType(final TypeMirror t, final TypeMirror s) { + return Lang.sameType(t, s); + } + + @Test + final void testUnion() { + final TypeElement serializableElement = Lang.typeElement("java.io.Serializable"); + assertTrue(serializableElement instanceof DelegatingElement); + + final DeclaredType serializable = (DeclaredType)serializableElement.asType(); + assertTrue(serializable instanceof DelegatingTypeMirror); + + final TypeElement constantDescElement = Lang.typeElement("java.lang.constant.ConstantDesc"); + assertTrue(constantDescElement instanceof DelegatingElement); + + final DeclaredType constantDesc = (DeclaredType)constantDescElement.asType(); + assertTrue(constantDesc instanceof DelegatingTypeMirror); + + final List union = javacCodeTypes.union(com.sun.tools.javac.util.List.of((Type)unwrap(serializable)), + com.sun.tools.javac.util.List.of((Type)unwrap(constantDesc))); + + final PrecedesPredicate precedesPredicate = this.visitors.precedesPredicate(); + final TypeClosure typeClosure = new TypeClosure(Lang.typeAndElementSource(), precedesPredicate); + typeClosure.union(serializable); + typeClosure.union(constantDesc); + assertTrue(contentsEqual(union, typeClosure.toList())); + } + + @Test + final void testCharSequenceAndComparablePrecedence() { + + final TypeElement charSequenceElement = Lang.typeElement("java.lang.CharSequence"); + assertTrue(charSequenceElement instanceof DelegatingElement); + + final DeclaredType charSequence = (DeclaredType)charSequenceElement.asType(); + assertTrue(charSequence instanceof DelegatingTypeMirror); + + final TypeElement comparableElement = Lang.typeElement("java.lang.Comparable"); + assertTrue(comparableElement instanceof DelegatingElement); + + final DeclaredType comparableString = Lang.declaredType(null, comparableElement, Lang.declaredType("java.lang.String")); + assertTrue(comparableString instanceof DelegatingTypeMirror); + assertSame(unwrap(comparableElement), unwrap(comparableString.asElement())); + + final PrecedesPredicate precedesPredicate = this.visitors.precedesPredicate(); + + // This is wild. The ranks are the same across the board: + final int rank = javacCodeTypes.rank((Type)unwrap(charSequence)); + assertEquals(1, rank); + assertEquals(1, javacCodeTypes.rank((Type)unwrap(comparableString))); + assertEquals(1, precedesPredicate.rank(charSequence)); + assertEquals(1, precedesPredicate.rank(comparableString)); + + // The type tags are the same: + assertSame(TypeTag.CLASS, ((Type)unwrap(charSequence)).getTag()); + assertSame(TypeTag.CLASS, ((Type)unwrap(comparableString)).getTag()); + + // charSequenceName precedes comparableName: + final com.sun.tools.javac.util.Name charSequenceName = (com.sun.tools.javac.util.Name)DelegatingElement.unwrap(charSequenceElement).getQualifiedName(); + final com.sun.tools.javac.util.Name comparableName = (com.sun.tools.javac.util.Name)DelegatingElement.unwrap(comparableElement).getQualifiedName(); + assertTrue(charSequenceName.compareTo(comparableName) < 0); + assertTrue(CharSequence.compare(charSequenceName, comparableName) < 0); + + // ...but precedes doesn't work like this in JDK 20 and earlier. + // + // In JDK 20 precedes() does this after ensuring tags are both CLASS: + // + // return + // types.rank(that.type) < types.rank(this.type) || + // types.rank(that.type) == types.rank(this.type) && + // that.getQualifiedName().compareTo(this.getQualifiedName()) < 0; // <-- NOTE + // + // In JDK 21 it does this: + // + // return + // types.rank(that.type) < types.rank(this.type) || + // (types.rank(that.type) == types.rank(this.type) && + // this.getQualifiedName().compareTo(that.getQualifiedName()) < 0); // <-- NOTE + + final boolean precedes = ((TypeSymbol)unwrap(charSequenceElement)).precedes((TypeSymbol)unwrap(comparableElement), javacCodeTypes); + if (Runtime.version().feature() < 21) { + assertFalse(precedes); // See https://github.com/openjdk/jdk/commit/426025aab42d485541a899844b96c06570088771 + } else { + assertTrue(precedes); + } + + // We follow the JDK 21 approach: + assertTrue(precedesPredicate.test(charSequenceElement, comparableElement)); + + } + + @Test + final void testRankOfArrayClass() { + assertThrows(AssertionError.class, () -> javacCodeTypes.rank((Type)unwrap(arrayType(Integer[].class)))); + } + + @Test + final void testSerializableAndConstantDescPrecedence() { + final TypeElement serializableElement = Lang.typeElement("java.io.Serializable"); + assertTrue(serializableElement instanceof DelegatingElement); + + final DeclaredType serializable = (DeclaredType)serializableElement.asType(); + assertTrue(serializable instanceof DelegatingTypeMirror); + + final TypeElement constantDescElement = Lang.typeElement("java.lang.constant.ConstantDesc"); + assertTrue(constantDescElement instanceof DelegatingElement); + + final DeclaredType constantDesc = (DeclaredType)constantDescElement.asType(); + assertTrue(constantDesc instanceof DelegatingTypeMirror); + + final PrecedesPredicate precedesPredicate = this.visitors.precedesPredicate(); + + // Ranks are equal. + assertEquals(1, javacCodeTypes.rank((Type)unwrap(serializable))); + assertEquals(1, javacCodeTypes.rank((Type)unwrap(constantDesc))); + assertEquals(1, precedesPredicate.rank(serializable)); + assertEquals(1, precedesPredicate.rank(constantDesc)); + + // Both types have CLASS tag internally. + assertSame(TypeTag.CLASS, ((Type)unwrap(serializable)).getTag()); + assertSame(TypeTag.CLASS, ((Type)unwrap(constantDesc)).getTag()); + + final com.sun.tools.javac.util.Name serializableName = (com.sun.tools.javac.util.Name)DelegatingElement.unwrap(serializableElement).getQualifiedName(); + final com.sun.tools.javac.util.Name constantDescName = (com.sun.tools.javac.util.Name)DelegatingElement.unwrap(constantDescElement).getQualifiedName(); + + // Using standard CharSequence semantics, the CharSequence "java.io.Serializable" precedes the CharSequence + // "java.lang.constant.ConstantDesc". + assertTrue(CharSequence.compare(serializableName, constantDescName) < 0); + + if (Runtime.version().feature() < 21) { + // In JDK 20, the Name denoting "java.io.Serializable" DOES NOT precede the Name denoting + // "java.lang.constant.ConstantDesc". + assertTrue(serializableName.compareTo(constantDescName) > 0); // This is wild. See https://github.com/openjdk/jdk/commit/426025aab42d485541a899844b96c06570088771 + // And yet, thanks to the reverse comparison order in JDK 20: + assertTrue(((TypeSymbol)unwrap(serializableElement)).precedes((TypeSymbol)unwrap(constantDescElement), javacCodeTypes)); + } else { + // In JDK 21+, they fixed this. + assertTrue(serializableName.compareTo(constantDescName) < 0); + assertTrue(((TypeSymbol)unwrap(serializableElement)).precedes((TypeSymbol)unwrap(constantDescElement), javacCodeTypes)); + } + + // We follow the JDK 21 approach. + assertTrue(precedesPredicate.test(serializableElement, constantDescElement)); + } + +} diff --git a/pom.xml b/pom.xml new file mode 100644 index 00000000..51c8166a --- /dev/null +++ b/pom.xml @@ -0,0 +1,624 @@ + + + 4.0.0 + + org.microbean + microbean-lang-parent + 0.0.1-SNAPSHOT + pom + + microBean™ Lang: Parent + microBean™ Lang: Parent: The parent project of the microBean™ Lang project suite. + 2023 + https://microbean.github.io/microbean-lang + + + + The Apache License, Version 2.0 + repo + Apache License 2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + + + + + microBean™ + http://microbean.systems/ + + + + + ljnelson + Laird Nelson + ljnelson@gmail.com + https://about.me/lairdnelson + + architect + developer + + -8 + + + + + ${scm.url} + ${scm.url} + https://github.com/microbean/microbean-lang/ + HEAD + + + + Github + https://github.com/microbean/microbean-lang/issues + + + + + sonatype-oss-repository-hosting + + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + Github Pages + microBean™ Lang Site + https://microbean.github.io/microbean-lang/ + + + sonatype-oss-repository-hosting + https://oss.sonatype.org/content/repositories/snapshots + + + + + bytebuddy + jandex + lang + + + + + + + + + io.smallrye + jandex + 3.1.6 + jar + + + + net.bytebuddy + byte-buddy + 1.14.10 + jar + + + + org.hamcrest + hamcrest + 2.2 + jar + + + + org.junit.jupiter + junit-jupiter-api + 5.10.1 + jar + + + + org.junit.jupiter + junit-jupiter-engine + 5.10.1 + jar + + + + + + ${project.groupId} + microbean-lang + ${project.version} + jar + + + + ${project.groupId} + microbean-lang-bytebuddy + ${project.version} + jar + + + + ${project.groupId} + microbean-lang-jandex + ${project.version} + jar + + + + + + + + + + + maven-antrun-plugin + 3.1.0 + + + + maven-assembly-plugin + 3.4.2 + + + + maven-checkstyle-plugin + 3.3.1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + project.basedir=${project.basedir} + project.build.sourceEncoding=${project.build.sourceEncoding} + + + + + com.puppycrawl.tools + checkstyle + 10.12.6 + + + + + + maven-clean-plugin + 3.3.2 + + + + ${basedir} + + src/**/*~ + *~ + + + + + + + + maven-compiler-plugin + 3.12.0 + + + -parameters + -Xlint:all + -Xlint:-preview + -Xpkginfo:always + + + + + + maven-dependency-plugin + 3.6.1 + + + + maven-deploy-plugin + 3.1.1 + + + + maven-enforcer-plugin + 3.4.1 + + + + maven-gpg-plugin + 3.0.1 + + + + maven-install-plugin + 3.1.1 + + + + maven-jar-plugin + 3.3.0 + + + + maven-javadoc-plugin + 3.6.3 + + + -J-Dhttp.agent=maven-javadoc-plugin + + + --add-stylesheet + ${project.basedir}/src/main/javadoc/css/fonts.css + + true + + + + + maven-plugin-plugin + 3.10.2 + + + + maven-project-info-reports-plugin + 3.5.0 + + + + maven-release-plugin + 3.0.1 + + + + maven-resources-plugin + 3.3.1 + + + + maven-scm-plugin + 2.0.1 + + + + maven-scm-publish-plugin + 3.2.1 + + + + maven-site-plugin + 4.0.0-M13 + + + Attach site descriptor + + attach-descriptor + + package + + + Stage site + + stage + + post-site + + + + + + maven-source-plugin + 3.3.0 + + + attach-sources + + jar-no-fork + + + + + + + maven-surefire-plugin + 3.2.3 + + + + junit.jupiter.execution.parallel.enabled=false + junit.jupiter.execution.parallel.mode.default=concurrent + + + + ${project.basedir} + ${project.build.directory} + ${project.build.testOutputDirectory} + + + + + + org.ow2.asm + asm + 9.5 + + + + + + maven-toolchains-plugin + 3.1.0 + + + + com.github.spotbugs + spotbugs-maven-plugin + 4.8.2.0 + + + + org.codehaus.mojo + versions-maven-plugin + 2.16.2 + + + + io.smallrye + jandex-maven-plugin + 3.1.6 + + + + org.sonatype.plugins + nexus-staging-maven-plugin + 1.6.13 + true + + + + com.thoughtworks.xstream + xstream + 1.4.20 + + + + sonatype-oss-repository-hosting + ${nexusUrl} + ${autoReleaseAfterClose} + + + + + + + + + io.smallrye + jandex-maven-plugin + + + make-index + + + + + + org.sonatype.plugins + nexus-staging-maven-plugin + + + + + + + + true + 21 + 21 + + + ${project.organization.name}. All rights reserved.]]> + false + false + https://docs.oracle.com/en/java/javase/20/docs/api/,https://microbean.github.io/microbean-development-annotations/apidocs/,https://javadoc.io/doc/net.bytebuddy/byte-buddy/latest,https://javadoc.io/doc/io.smallrye/jandex/latest + + + deploy,post-site,scm-publish:publish-scm + + + deployment + [maven-release-plugin] [skip ci] + v@{project.version} + false + + scm:git:git@github.com:microbean/microbean-lang.git + + + ${project.reporting.outputDirectory} + ${project.scm.developerConnection} + gh-pages + + + true + false + ${project.build.directory}/staging + https://microbean.github.io/microbean-lang/ + + + false + false + + + + true + + https://oss.sonatype.org/ + 10 + + + UTF8 + UTF8 + + + + + + + maven-checkstyle-plugin + + + aggregate + false + + checkstyle-aggregate + + + + + + maven-javadoc-plugin + + + aggregate + false + + aggregate + + + + default + true + + javadoc + + + + + + + + + + deployment + + + + maven-gpg-plugin + + + sign-artifacts + verify + + sign + + + + + + maven-javadoc-plugin + + + attach-javadocs + + jar + + + + + + maven-source-plugin + + + + + + + diff --git a/src/main/javadoc/css/fonts.css b/src/main/javadoc/css/fonts.css new file mode 100644 index 00000000..bd00dc52 --- /dev/null +++ b/src/main/javadoc/css/fonts.css @@ -0,0 +1,62 @@ +@import url('https://fonts.googleapis.com/css2?2?family=Roboto+Mono:ital,wght@0,400;1,400&family=Roboto:ital,wght@0,400;0,700;1,400;1,700&family=Lobster&display=swap'); +body { + font-family: 'Roboto', 'DejaVu Sans', Arial, Helvetica, sans-serif; +} +button { + font-family: 'Roboto', 'DejaVu Sans', Arial, Helvetica, sans-serif; +} +h1, h2, h3 { + font-weight: 700 +} +input { + font-family: 'Roboto', 'DejaVu Sans', Arial, Helvetica, sans-serif; +} +code, tt, pre { + font-family: 'Roboto Mono', 'DejaVu Sans Mono', monospace; +} +.contentContainer .description dl dd, .contentContainer .details dl dd, .serializedFormContainer dl dd { + font-family: 'Roboto', 'DejaVu Sans', Arial, Helvetica, sans-serif; +} +div.block { + font-family: 'Roboto', 'DejaVu Sans', Arial, Helvetica, sans-serif; +} +div.table-tabs > button { + font-weight: 700 +} +dl.notes > dt { + font-family: 'Roboto', 'DejaVu Sans', Arial, Helvetica, sans-serif; + font-weight: 700 +} +dl.notes > dd { + font-family: 'Roboto', 'DejaVu Sans', Arial, Helvetica, sans-serif; +} +strong { + font-weight: 700 +} +.caption { + font-weight: 700 +} +.table-header { + font-weight: 700 +} +.col-first a:link, .col-first a:visited, .col-second a:link, .col-second a:visited, .col-first a:link, .col-first a:visited, .col-second a:link, .col-second a:visited, .col-constructor-name a:link, .col-constructor-name a:visited, .col-summary-item-name a:link, .col-summary-item-name a:visited, .constant-values-container a:link, .constant-values-container a:visited, .all-classes-container a:link, .all-classes-container a:visited, .all-packages-container a:link, .all-packages-container a:visited { + font-weight: 700 +} +.deprecated-label, .descfrm-type-label, .implementation-label, .member-name-label, .member-name-link, .module-label-in-package, .module-label-in-type, .override-specify-label, .package-label-in-type, .package-hierarchy-label, .type-name-label, .type-name-link, .search-tag-link, .preview-label { + font-weight: 700 +} +.module-signature, .package-signature, .type-signature, .member-signature { + font-family: 'Roboto Mono', 'DejaVu Sans Mono', monospace; +} +main a[href*="://"]::after { + all: unset; +} +.result-highlight { + font-weight: 700; +} +.ui-widget { + font-family: 'Roboto', 'DejaVu Sans', Arial, Helvetica, sans-serif !important; +} +.ui-autocomplete-category { + font-weight: 700; +} diff --git a/src/site/markdown/index.md.vm b/src/site/markdown/index.md.vm new file mode 100644 index 00000000..c79baf67 --- /dev/null +++ b/src/site/markdown/index.md.vm @@ -0,0 +1 @@ +#include("../../../README.md") diff --git a/src/site/site.xml b/src/site/site.xml new file mode 100644 index 00000000..03543cb0 --- /dev/null +++ b/src/site/site.xml @@ -0,0 +1,37 @@ + + + + + + + + + org.apache.maven.skins + maven-fluido-skin + 2.0.0-M8 + + + + + + + + + + + + + + + true + false + + + +