From 4ca37bad33a02bf28de72591d1fb728f0469e009 Mon Sep 17 00:00:00 2001 From: Kyle Wood Date: Mon, 16 Dec 2024 00:17:54 -0600 Subject: [PATCH] Implement full support for JVM type signatures This is a breaking change, so bump to version 3.0.0 --- .../main/kotlin/DownloadJavadocListFiles.kt | 51 ++ buildSrc/src/main/kotlin/HypoJavaExtension.kt | 4 + buildSrc/src/main/kotlin/HypoPatchSpec.kt | 8 + buildSrc/src/main/kotlin/PatchJavadocList.kt | 63 ++ buildSrc/src/main/kotlin/hypo-java.gradle.kts | 44 +- .../src/main/kotlin/hypo-module.gradle.kts | 2 +- buildSrc/src/main/kotlin/hypo-test.gradle.kts | 5 + gradle.properties | 2 +- gradle/libs.versions.toml | 23 +- gradle/wrapper/gradle-wrapper.jar | Bin 43583 -> 43764 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 9 +- gradlew.bat | 4 +- hypo-asm/build.gradle.kts | 33 +- hypo-asm/hypo-asm-hydrate/build.gradle.kts | 9 +- .../asm/hydrate/BridgeMethodHydrator.java | 6 +- .../hypo/asm/hydrate/LambdaCallHydrator.java | 11 +- .../hypo/asm/hydrate/LocalClassHydrator.java | 6 +- .../asm/hydrate/SuperConstructorHydrator.java | 21 +- .../src/main/java/module-info.java | 31 + hypo-asm/hypo-asm-test-data/build.gradle.kts | 15 +- .../java/scenario03/TestClass.java | 2 +- .../java/scenario04/TestClass.java | 1 + .../dev/denwav/hypo/asm/AsmClassData.java | 15 +- .../dev/denwav/hypo/asm/AsmFieldData.java | 29 +- .../dev/denwav/hypo/asm/AsmMethodData.java | 21 +- .../dev/denwav/hypo/asm/AsmOutputWriter.java | 4 +- .../java/dev/denwav/hypo/asm/HypoAsmUtil.java | 44 +- hypo-asm/src/main/java/module-info.java | 33 + .../hypo/asm/scenarios/Scenario03Test.java | 14 +- .../hypo/asm/scenarios/Scenario05Test.java | 18 +- .../hypo/asm/scenarios/Scenario06Test.java | 6 +- .../hypo/asm/scenarios/Scenario07Test.java | 6 +- .../hypo/asm/scenarios/Scenario08Test.java | 8 +- .../hypo/asm/scenarios/Scenario12Test.java | 12 +- hypo-core/build.gradle.kts | 11 +- hypo-core/src/main/java/module-info.java | 28 + hypo-hydrate/build.gradle.kts | 28 +- .../hypo-hydrate-test-data/build.gradle.kts | 12 + .../java/scenario01/TestClass.java | 8 + .../java/scenario01/TestSuperClass.java | 7 + .../java/scenario02/TestClass.java | 11 + .../java/scenario02/TestSuperClass.java | 10 + hypo-hydrate/src/main/java/module-info.java | 32 + .../denwav/hypo/hydrate/Scenario01Test.java | 53 ++ .../denwav/hypo/hydrate/Scenario02Test.java | 98 +++ hypo-mappings/build.gradle.kts | 9 +- .../dev/denwav/hypo/mappings/LorenzUtil.java | 49 +- .../changes/CopyConstructorMappingChange.java | 65 +- .../mappings/changes/MemberReference.java | 2 +- .../contributors/CopyRecordParameters.java | 8 +- hypo-meta/hypo-catalog/build.gradle.kts | 3 +- hypo-meta/hypo-platform/build.gradle.kts | 1 + hypo-model/build.gradle.kts | 12 +- .../hypo/model/AbstractClassDataProvider.java | 6 + .../denwav/hypo/model/ClassDataProvider.java | 26 +- .../dev/denwav/hypo/model/HypoModelUtil.java | 7 +- .../hypo/model/SystemClassProviderRoot.java | 3 +- .../hypo/model/data/AbstractClassData.java | 9 +- .../hypo/model/data/AbstractFieldData.java | 16 +- .../hypo/model/data/AbstractMethodData.java | 9 +- .../dev/denwav/hypo/model/data/ClassData.java | 15 +- .../dev/denwav/hypo/model/data/FieldData.java | 49 +- .../denwav/hypo/model/data/LazyClassData.java | 15 + .../denwav/hypo/model/data/LazyFieldData.java | 63 ++ .../hypo/model/data/LazyMethodData.java | 18 + .../denwav/hypo/model/data/MethodData.java | 46 +- .../hypo/model/data/MethodDescriptor.java | 254 -------- .../denwav/hypo/model/data/package-info.java | 2 +- .../hypo/model/data/types/ArrayType.java | 124 ---- .../hypo/model/data/types/ClassType.java | 86 --- .../denwav/hypo/model/data/types/JvmType.java | 68 --- .../hypo/model/data/types/PrimitiveType.java | 120 ---- hypo-model/src/main/java/module-info.java | 29 + .../hypo/model/data/MethodDescriptorTest.java | 79 --- hypo-test/build.gradle.kts | 2 + hypo-test/hypo-test-data/build.gradle.kts | 8 + .../java/scenario01/ChildClass01.java | 1 + .../java/scenario01/ChildClass02.java | 1 + .../java/scenario01/ParentClass.java | 1 + .../java/scenario02/ChildClass.java | 1 + .../java/scenario02/GrandChildClass.java | 1 + .../java/scenario02/ParentClass.java | 1 + .../java/scenario03/ChildClassByte.java | 1 + .../java/scenario03/ChildClassString.java | 1 + .../scenario03/GrandChildClassString.java | 1 + .../java/scenario03/ParentClass.java | 1 + .../java/scenario04/ChildClassByte.java | 1 + .../java/scenario04/ChildClassString.java | 1 + .../java/scenario04/ParentClass.java | 1 + .../java/scenario05/ChildClassByte.java | 1 + .../java/scenario05/ChildClassString.java | 1 + .../java/scenario05/ParentClass.java | 1 + .../src/scenario-05/java/scenario05/Str.java | 1 + .../java/scenario06/ChildClass.java | 1 + .../java/scenario06/GrandChildClass.java | 1 + .../java/scenario06/GreatGrandChildClass.java | 1 + .../java/scenario06/ParentClass.java | 1 + .../java/scenario07/ChildClass.java | 1 + .../java/scenario07/GrandChildClass.java | 1 + .../java/scenario07/GreatGrandChildClass.java | 1 + .../java/scenario07/ParentClass.java | 1 + .../java/scenario08/TestClass.java | 1 + .../java/scenario09/ChildClass.java | 1 + .../java/scenario09/ParentClass.java | 1 + .../java/scenario10/ChildClass.java | 1 + .../java/scenario10/ParentClass.java | 1 + .../java/scenario11/ChildClass.java | 1 + .../java/scenario11/GrandChildClass.java | 1 + .../java/scenario11/ParentClass.java | 1 + .../java/scenario12/BaseClass.java | 1 + .../java/scenario12/ChildClass.java | 1 + .../java/scenario12/TestClass.java | 1 + .../java/scenario13/TestClass.java | 1 + .../java/scenario13/TestInterface.java | 1 + .../hypo/test/framework/TestScenarioBase.java | 8 +- .../hypo/test/scenarios/Scenario01Test.java | 1 + .../hypo/test/scenarios/Scenario03Test.java | 1 + hypo-types/build.gradle.kts | 31 + .../dev/denwav/hypo/types/HypoTypesUtil.java | 65 ++ .../dev/denwav/hypo/types/PrimitiveType.java | 256 ++++++++ .../dev/denwav/hypo/types/TypeBindable.java | 79 +++ .../denwav/hypo/types/TypeRepresentable.java | 140 +++++ .../denwav/hypo/types/TypeVariableBinder.java | 69 +++ .../java/dev/denwav/hypo/types/VoidType.java | 79 +++ .../hypo/types/desc/ArrayTypeDescriptor.java | 124 ++++ .../hypo/types/desc/ClassTypeDescriptor.java | 93 +++ .../denwav/hypo/types/desc/Descriptor.java | 33 + .../hypo/types/desc/MethodDescriptor.java | 229 +++++++ .../hypo/types/desc/TypeDescriptor.java | 153 +++++ .../denwav/hypo/types/desc}/package-info.java | 15 +- .../dev/denwav/hypo/types/intern/Intern.java | 187 ++++++ .../denwav/hypo/types/intern/InternKey.java | 41 ++ .../hypo/types/intern/package-info.java | 29 + .../dev/denwav/hypo/types/kind/ArrayType.java | 44 ++ .../dev/denwav/hypo/types/kind/ClassType.java | 40 ++ .../denwav/hypo/types/kind/MethodType.java | 47 ++ .../dev/denwav/hypo/types/kind/ValueType.java | 31 + .../denwav/hypo/types/kind/package-info.java | 24 + .../dev/denwav/hypo/types/package-info.java | 30 + .../parsing/JvmTypeParseFailureException.java | 33 + .../hypo/types/parsing/JvmTypeParser.java | 569 ++++++++++++++++++ .../hypo/types/parsing/ParserState.java | 194 ++++++ .../hypo/types/parsing/package-info.java | 25 + .../types/pattern/ClassSignaturePatterns.java | 141 +++++ .../hypo/types/pattern/MethodPatterns.java | 263 ++++++++ .../types/pattern/TypeArgumentPatterns.java | 98 +++ .../hypo/types/pattern/TypeCapture.java | 101 ++++ .../denwav/hypo/types/pattern/TypeMatch.java | 95 +++ .../hypo/types/pattern/TypeMatchContext.java | 76 +++ .../types/pattern/TypeParameterPatterns.java | 141 +++++ .../hypo/types/pattern/TypePattern.java | 278 +++++++++ .../hypo/types/pattern/TypePatterns.java | 429 +++++++++++++ .../types/pattern/TypeVariablePatterns.java | 99 +++ .../hypo/types/sig/ArrayTypeSignature.java | 142 +++++ .../denwav/hypo/types/sig/ClassSignature.java | 244 ++++++++ .../hypo/types/sig/ClassTypeSignature.java | 282 +++++++++ .../hypo/types/sig/MethodSignature.java | 306 ++++++++++ .../types/sig/ReferenceTypeSignature.java | 41 ++ .../dev/denwav/hypo/types/sig/Signature.java | 34 ++ .../hypo/types/sig/ThrowsSignature.java | 41 ++ .../hypo/types/sig/TypeParameterHolder.java | 36 ++ .../denwav/hypo/types/sig/TypeSignature.java | 191 ++++++ .../denwav/hypo/types/sig/package-info.java | 92 +++ .../types/sig/param/BoundedTypeArgument.java | 145 +++++ .../hypo/types/sig/param/TypeArgument.java | 68 +++ .../hypo/types/sig/param/TypeParameter.java | 221 +++++++ .../hypo/types/sig/param/TypeVariable.java | 283 +++++++++ .../types/sig/param/WildcardArgument.java | 68 +++ .../hypo/types/sig/param/WildcardBound.java | 60 ++ .../hypo/types/sig/param/package-info.java | 23 + hypo-types/src/main/java/module-info.java | 41 ++ .../denwav/hypo/types/ClassSignatureTest.java | 209 +++++++ .../dev/denwav/hypo/types/InternTest.java | 66 ++ .../denwav/hypo/types/JvmReflectionTest.java | 114 ++++ .../hypo/types/MethodDescriptorTest.java | 103 ++++ .../hypo/types/MethodSignatureTest.java | 179 ++++++ .../MethodSignatureThreadContentionTest.java | 60 ++ .../dev/denwav/hypo/types/ParsingTest.java | 73 +++ .../dev/denwav/hypo/types/TestAllTypes.java | 217 +++++++ .../hypo/types/pattern/TypeCaptureTest.java | 35 ++ .../hypo/types/pattern/TypePatternTest.java | 391 ++++++++++++ settings.gradle.kts | 3 + types-export/build.gradle.kts | 82 +++ .../dev/denwav/hypo/typesexport/Main.java | 147 +++++ 185 files changed, 9476 insertions(+), 1013 deletions(-) create mode 100644 buildSrc/src/main/kotlin/DownloadJavadocListFiles.kt create mode 100644 buildSrc/src/main/kotlin/HypoPatchSpec.kt create mode 100644 buildSrc/src/main/kotlin/PatchJavadocList.kt create mode 100644 hypo-asm/hypo-asm-hydrate/src/main/java/module-info.java create mode 100644 hypo-asm/src/main/java/module-info.java create mode 100644 hypo-core/src/main/java/module-info.java create mode 100644 hypo-hydrate/hypo-hydrate-test-data/build.gradle.kts create mode 100644 hypo-hydrate/hypo-hydrate-test-data/src/scenario-01/java/scenario01/TestClass.java create mode 100644 hypo-hydrate/hypo-hydrate-test-data/src/scenario-01/java/scenario01/TestSuperClass.java create mode 100644 hypo-hydrate/hypo-hydrate-test-data/src/scenario-02/java/scenario02/TestClass.java create mode 100644 hypo-hydrate/hypo-hydrate-test-data/src/scenario-02/java/scenario02/TestSuperClass.java create mode 100644 hypo-hydrate/src/main/java/module-info.java create mode 100644 hypo-hydrate/src/test/java/dev/denwav/hypo/hydrate/Scenario01Test.java create mode 100644 hypo-hydrate/src/test/java/dev/denwav/hypo/hydrate/Scenario02Test.java create mode 100644 hypo-model/src/main/java/dev/denwav/hypo/model/data/LazyFieldData.java delete mode 100644 hypo-model/src/main/java/dev/denwav/hypo/model/data/MethodDescriptor.java delete mode 100644 hypo-model/src/main/java/dev/denwav/hypo/model/data/types/ArrayType.java delete mode 100644 hypo-model/src/main/java/dev/denwav/hypo/model/data/types/ClassType.java delete mode 100644 hypo-model/src/main/java/dev/denwav/hypo/model/data/types/JvmType.java delete mode 100644 hypo-model/src/main/java/dev/denwav/hypo/model/data/types/PrimitiveType.java create mode 100644 hypo-model/src/main/java/module-info.java delete mode 100644 hypo-model/src/test/java/dev/denwav/hypo/model/data/MethodDescriptorTest.java create mode 100644 hypo-types/build.gradle.kts create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/HypoTypesUtil.java create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/PrimitiveType.java create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/TypeBindable.java create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/TypeRepresentable.java create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/TypeVariableBinder.java create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/VoidType.java create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/desc/ArrayTypeDescriptor.java create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/desc/ClassTypeDescriptor.java create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/desc/Descriptor.java create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/desc/MethodDescriptor.java create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/desc/TypeDescriptor.java rename {hypo-model/src/main/java/dev/denwav/hypo/model/data/types => hypo-types/src/main/java/dev/denwav/hypo/types/desc}/package-info.java (59%) create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/intern/Intern.java create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/intern/InternKey.java create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/intern/package-info.java create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/kind/ArrayType.java create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/kind/ClassType.java create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/kind/MethodType.java create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/kind/ValueType.java create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/kind/package-info.java create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/package-info.java create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/parsing/JvmTypeParseFailureException.java create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/parsing/JvmTypeParser.java create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/parsing/ParserState.java create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/parsing/package-info.java create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/pattern/ClassSignaturePatterns.java create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/pattern/MethodPatterns.java create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/pattern/TypeArgumentPatterns.java create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/pattern/TypeCapture.java create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/pattern/TypeMatch.java create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/pattern/TypeMatchContext.java create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/pattern/TypeParameterPatterns.java create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/pattern/TypePattern.java create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/pattern/TypePatterns.java create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/pattern/TypeVariablePatterns.java create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/sig/ArrayTypeSignature.java create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/sig/ClassSignature.java create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/sig/ClassTypeSignature.java create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/sig/MethodSignature.java create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/sig/ReferenceTypeSignature.java create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/sig/Signature.java create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/sig/ThrowsSignature.java create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/sig/TypeParameterHolder.java create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/sig/TypeSignature.java create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/sig/package-info.java create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/sig/param/BoundedTypeArgument.java create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/sig/param/TypeArgument.java create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/sig/param/TypeParameter.java create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/sig/param/TypeVariable.java create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/sig/param/WildcardArgument.java create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/sig/param/WildcardBound.java create mode 100644 hypo-types/src/main/java/dev/denwav/hypo/types/sig/param/package-info.java create mode 100644 hypo-types/src/main/java/module-info.java create mode 100644 hypo-types/src/test/java/dev/denwav/hypo/types/ClassSignatureTest.java create mode 100644 hypo-types/src/test/java/dev/denwav/hypo/types/InternTest.java create mode 100644 hypo-types/src/test/java/dev/denwav/hypo/types/JvmReflectionTest.java create mode 100644 hypo-types/src/test/java/dev/denwav/hypo/types/MethodDescriptorTest.java create mode 100644 hypo-types/src/test/java/dev/denwav/hypo/types/MethodSignatureTest.java create mode 100644 hypo-types/src/test/java/dev/denwav/hypo/types/MethodSignatureThreadContentionTest.java create mode 100644 hypo-types/src/test/java/dev/denwav/hypo/types/ParsingTest.java create mode 100644 hypo-types/src/test/java/dev/denwav/hypo/types/TestAllTypes.java create mode 100644 hypo-types/src/test/java/dev/denwav/hypo/types/pattern/TypeCaptureTest.java create mode 100644 hypo-types/src/test/java/dev/denwav/hypo/types/pattern/TypePatternTest.java create mode 100644 types-export/build.gradle.kts create mode 100644 types-export/src/main/java/dev/denwav/hypo/typesexport/Main.java diff --git a/buildSrc/src/main/kotlin/DownloadJavadocListFiles.kt b/buildSrc/src/main/kotlin/DownloadJavadocListFiles.kt new file mode 100644 index 0000000..6911cf2 --- /dev/null +++ b/buildSrc/src/main/kotlin/DownloadJavadocListFiles.kt @@ -0,0 +1,51 @@ +import java.net.URI +import java.net.http.HttpClient +import java.net.http.HttpRequest +import java.net.http.HttpResponse +import kotlin.io.path.createDirectories +import kotlin.io.path.deleteIfExists +import org.gradle.api.DefaultTask +import org.gradle.api.artifacts.MinimalExternalModuleDependency +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.ListProperty +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.TaskAction + +abstract class DownloadJavadocListFiles : DefaultTask() { + + @get:Input + abstract val dependencies: ListProperty + + @get:OutputDirectory + abstract val output: DirectoryProperty + + @TaskAction + fun run() { + val client = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.ALWAYS).build() + + val outDir = output.get().asFile.toPath() + val base = "https://static.javadoc.io" + dependencies.get().forEach { m -> + val types = listOf("element", "package") + var response: HttpResponse<*>? = null + for (type in types) { + val filePath = "${m.module.group}/${m.module.name}/${m.versionConstraint}/$type-list" + val url = "$base/$filePath" + val outFile = outDir.resolve(filePath) + outFile.parent.createDirectories() + + val request = HttpRequest.newBuilder().GET().uri(URI.create(url)).build() + response = client.send(request, HttpResponse.BodyHandlers.ofFile(outFile)) + if (response.statusCode() == 200) { + break + } + + outFile.deleteIfExists() + } + if (response == null || response.statusCode() != 200) { + throw Exception("Failed: $response") + } + } + } +} diff --git a/buildSrc/src/main/kotlin/HypoJavaExtension.kt b/buildSrc/src/main/kotlin/HypoJavaExtension.kt index c5c27aa..4e1879b 100644 --- a/buildSrc/src/main/kotlin/HypoJavaExtension.kt +++ b/buildSrc/src/main/kotlin/HypoJavaExtension.kt @@ -1,7 +1,9 @@ +import org.gradle.api.NamedDomainObjectContainer import org.gradle.api.artifacts.MinimalExternalModuleDependency import org.gradle.api.artifacts.ProjectDependency import org.gradle.api.model.ObjectFactory import org.gradle.api.provider.ListProperty +import org.gradle.kotlin.dsl.domainObjectContainer import org.gradle.kotlin.dsl.listProperty open class HypoJavaExtension(objects: ObjectFactory) { @@ -10,4 +12,6 @@ open class HypoJavaExtension(objects: ObjectFactory) { val javadocLibs: ListProperty = objects.listProperty() val javadocProjects: ListProperty = objects.listProperty() + + val patchJavadocList: NamedDomainObjectContainer = objects.domainObjectContainer(HypoPatchSpec::class) } diff --git a/buildSrc/src/main/kotlin/HypoPatchSpec.kt b/buildSrc/src/main/kotlin/HypoPatchSpec.kt new file mode 100644 index 0000000..f4b71bb --- /dev/null +++ b/buildSrc/src/main/kotlin/HypoPatchSpec.kt @@ -0,0 +1,8 @@ +import org.gradle.api.Named +import org.gradle.api.artifacts.MinimalExternalModuleDependency +import org.gradle.api.provider.Property + +interface HypoPatchSpec : Named { + + val library: Property +} diff --git a/buildSrc/src/main/kotlin/PatchJavadocList.kt b/buildSrc/src/main/kotlin/PatchJavadocList.kt new file mode 100644 index 0000000..c60bd47 --- /dev/null +++ b/buildSrc/src/main/kotlin/PatchJavadocList.kt @@ -0,0 +1,63 @@ +import javax.inject.Inject +import kotlin.io.path.bufferedReader +import kotlin.io.path.bufferedWriter +import kotlin.io.path.deleteIfExists +import kotlin.io.path.isRegularFile +import kotlin.io.path.readText +import org.gradle.api.DefaultTask +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.FileSystemOperations +import org.gradle.api.provider.ListProperty +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.TaskAction + +abstract class PatchJavadocList : DefaultTask() { + + @get:InputDirectory + abstract val input: DirectoryProperty + + @get:Input + abstract val patches: ListProperty + + @get:OutputDirectory + abstract val output: DirectoryProperty + + @get:Inject + abstract val fs: FileSystemOperations + + @TaskAction + fun run() { + fs.sync { + from(input) + into(output) + } + + val outputDir = output.get().asFile.toPath() + patches.get().forEach { patch -> + val lib = patch.library.get() + + val packageListFile = outputDir.resolve("${lib.module.group}/${lib.module.name}/${lib.versionConstraint}/package-list") + val elementListFile = packageListFile.resolveSibling("element-list") + + if (elementListFile.isRegularFile()) { + val originalText = elementListFile.readText() + elementListFile.bufferedWriter().use { writer -> + writer.appendLine("module:${patch.name}") + writer.append(originalText) + } + } else { + elementListFile.bufferedWriter().use { writer -> + writer.appendLine("module:${patch.name}") + + packageListFile.bufferedReader().use { reader -> + reader.copyTo(writer) + } + } + } + + packageListFile.deleteIfExists() + } + } +} diff --git a/buildSrc/src/main/kotlin/hypo-java.gradle.kts b/buildSrc/src/main/kotlin/hypo-java.gradle.kts index 21afca6..ba75404 100644 --- a/buildSrc/src/main/kotlin/hypo-java.gradle.kts +++ b/buildSrc/src/main/kotlin/hypo-java.gradle.kts @@ -1,3 +1,5 @@ +import kotlin.io.path.absolutePathString + plugins { `java-library` } @@ -12,7 +14,11 @@ java { } tasks.withType().configureEach { - options.release = 11 + options.release = 21 +} + +hypoJava.patchJavadocList.register("org.jetbrains.annotations") { + library.set(lib("annotations")) } afterEvaluate { @@ -33,7 +39,27 @@ afterEvaluate { } } + // javadoc doesn't like that static.javadoc.io redirects, so we'll manually copy the + // {element,package}-list for it so it doesn't complain + val javadocElementList by tasks.registering(DownloadJavadocListFiles::class) { + dependencies.set(hypoJava.javadocLibs) + output.set(layout.buildDirectory.dir("javadocElementLists")) + } + + val elementLists = layout.buildDirectory.dir("javadocElementListsPatched") + val javadocElementListPatch by tasks.registering(PatchJavadocList::class) { + input.set(javadocElementList.flatMap { it.output }) + patches.set(hypoJava.patchJavadocList) + output.set(elementLists) + } + tasks.javadoc { + dependsOn(javadocElementListPatch) + + javadocTool = javaToolchains.javadocToolFor { + languageVersion = JavaLanguageVersion.of(21) + } + for (projDep in hypoJava.jdkVersionProjects.get()) { val proj = project(projDep.path) @@ -42,11 +68,16 @@ afterEvaluate { classpath += sources } - val base = "https://javadoc.io/doc" + val packageListDir = elementLists.get().asFile.toPath() hypoJava.javadocLibs.get().forEach { m -> - val url = "$base/${m.module.group}/${m.module.name}/${m.versionConstraint}" - opt.links(url) + val base = "https://static.javadoc.io" + val artifact = "${m.module.group}/${m.module.name}/${m.versionConstraint}" + val packageDir = packageListDir.resolve(artifact) + val url = "$base/$artifact" + + opt.linksOffline(url, packageDir.absolutePathString()) } + hypoJava.javadocProjects.get().forEach { p -> val javadocTask = project(p.path).tasks.javadoc dependsOn(javadocTask) @@ -54,10 +85,5 @@ afterEvaluate { val url = "$base/${p.group}/${p.name}/${p.version}" opt.linksOffline(url, javadocTask.get().destinationDir!!.absolutePath) } - - doLast { - // a lot of tools still require a package-list file instead of element-list - destinationDir!!.resolve("element-list").copyTo(destinationDir!!.resolve("package-list")) - } } } diff --git a/buildSrc/src/main/kotlin/hypo-module.gradle.kts b/buildSrc/src/main/kotlin/hypo-module.gradle.kts index 5dcf0a7..2e3db84 100644 --- a/buildSrc/src/main/kotlin/hypo-module.gradle.kts +++ b/buildSrc/src/main/kotlin/hypo-module.gradle.kts @@ -10,7 +10,7 @@ plugins { val hypoModule = extensions.create("hypoModule", HypoModuleExtension::class) tasks.withType().configureEach { - options.compilerArgs = listOf("-Xlint:all,-serial,-fallthrough", "-Werror") + options.compilerArgs = listOf("-Xlint:all,-serial,-fallthrough,-options", "-Werror") options.errorprone { disable("NonApiType", "LabelledBreakTarget") } diff --git a/buildSrc/src/main/kotlin/hypo-test.gradle.kts b/buildSrc/src/main/kotlin/hypo-test.gradle.kts index 1264d66..d51d97b 100644 --- a/buildSrc/src/main/kotlin/hypo-test.gradle.kts +++ b/buildSrc/src/main/kotlin/hypo-test.gradle.kts @@ -1,3 +1,5 @@ +import kotlin.math.max + plugins { `java-library` } @@ -5,8 +7,11 @@ plugins { dependencies { testImplementation(lib("junit-api")) testRuntimeOnly(lib("junit-runtime")) + testRuntimeOnly(lib("junit-launcher")) } tasks.test { useJUnitPlatform() + + maxParallelForks = max(Runtime.getRuntime().availableProcessors() / 2, 1) } diff --git a/gradle.properties b/gradle.properties index 7538ebb..11b436b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ group = dev.denwav.hypo -version = 2.4.1 +version = 3.0.0 org.gradle.parallel=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index df0b926..fcc2f83 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,19 +1,21 @@ [versions] -junit = "5.10.0" -errorprone = "2.21.0" -asm = "9.5" -slf4j = "2.0.7" -log4j = "2.20.0" +junit = "5.11.3" +junit-platform = "1.11.3" +errorprone = "2.36.0" +asm = "9.7.1" +slf4j = "2.0.16" +log4j = "2.24.3" [plugins] nexusPublish = "io.github.gradle-nexus.publish-plugin:2.0.0" [libraries] # Deps -annotations = "org.jetbrains:annotations:24.0.1" +annotations = "org.jetbrains:annotations:26.0.1" slf4j-api = { module = "org.slf4j:slf4j-api", version.ref = "slf4j" } slf4j-log4j2-impl = { module = "org.apache.logging.log4j:log4j-slf4j2-impl", version.ref = "log4j" } log4j-core = { module = "org.apache.logging.log4j:log4j-core", version.ref = "log4j" } +log4j-requirement = "biz.aQute.bnd:biz.aQute.bnd.annotation:7.1.0" asm-core = { module = "org.ow2.asm:asm", version.ref = "asm" } asm-tree = { module = "org.ow2.asm:asm-tree", version.ref = "asm" } @@ -22,19 +24,24 @@ asm-commons = { module = "org.ow2.asm:asm-commons", version.ref = "asm" } lorenz = "org.cadixdev:lorenz:0.5.8" bombe = "org.cadixdev:bombe:0.4.4" -jgrapht = "org.jgrapht:jgrapht-core:1.4.0" +jgrapht = "org.jgrapht:jgrapht-core:1.5.2" lorenzTiny = "org.quiltmc:lorenz-tiny:3.0.0" +staxUtils = "net.java.dev.stax-utils:stax-utils:20070216" + # Testing junit-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" } junit-runtime = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" } +junit-launcher = { module = "org.junit.platform:junit-platform-launcher", version.ref = "junit-platform" } + +guava = "com.google.guava:guava:33.4.7-jre" # Linting errorprone-core = { module = "com.google.errorprone:error_prone_core", version.ref = "errorprone" } errorprone-annotations = { module = "com.google.errorprone:error_prone_annotations", version.ref = "errorprone" } # Gradle -gradle-errorprone = "net.ltgt.errorprone:net.ltgt.errorprone.gradle.plugin:3.1.0" +gradle-errorprone = "net.ltgt.errorprone:net.ltgt.errorprone.gradle.plugin:4.1.0" gradle-licenser = "org.cadixdev.licenser:org.cadixdev.licenser.gradle.plugin:0.6.1" [bundles] diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index a4b76b9530d66f5e68d973ea569d8e19de379189..1b33c55baabb587c669f562ae36f953de2481846 100644 GIT binary patch delta 34943 zcmXuKV_+Rz)3%+)Y~1X)v28cDZQE*`9qyPrXx!Mg8{4+s*nWFo&-eXbzt+q-bFO1% zb$T* z+;w-h{ce+s>j$K)apmK~8t5)PdZP3^U%(^I<0#3(!6T+vfBowN0RfQ&0iMAo055!% z04}dC>M#Z2#PO7#|Fj;cQ$sH}E-n7nQM_V}mtmG_)(me#+~0gf?s@gam)iLoR#sr( zrR9fU_ofhp5j-5SLDQP{O+SuE)l8x9_(9@h%eY-t47J-KX-1(`hh#A6_Xs+4(pHhy zuZ1YS9axk`aYwXuq;YN>rYv|U`&U67f=tinhAD$+=o+MWXkx_;qIat_CS1o*=cIxs zIgeoK0TiIa7t`r%%feL8VieY63-Aakfi~qlE`d;ZOn8hFZFX|i^taCw6xbNLb2sOS z?PIeS%PgD)?bPB&LaQDF{PbxHrJQME<^cU5b!Hir(x32zy{YzNzE%sx;w=!C z_(A>eZXkQ1w@ASPXc|CWMNDP1kFQuMO>|1X;SHQS8w<@D;5C@L(3r^8qbbm$nTp%P z&I3Ey+ja9;ZiMbopUNc2txS9$Jf8UGS3*}Y3??(vZYLfm($WlpUGEUgQ52v@AD<~Y z#|B=mpCPt3QR%gX*c^SX>9dEqck79JX+gVPH87~q0-T;ota!lQWdt3C-wY1Ud}!j8 z*2x5$^dsTkXj}%PNKs1YzwK$-gu*lxq<&ko(qrQ_na(82lQ$ z7^0Pgg@Shn!UKTD4R}yGxefP2{8sZ~QZY)cj*SF6AlvE;^5oK=S}FEK(9qHuq|Cm! zx6ILQBsRu(=t1NRTecirX3Iv$-BkLxn^Zk|sV3^MJ1YKJxm>A+nk*r5h=>wW*J|pB zgDS%&VgnF~(sw)beMXXQ8{ncKX;A;_VLcq}Bw1EJj~-AdA=1IGrNHEh+BtIcoV+Te z_sCtBdKv(0wjY{3#hg9nf!*dpV5s7ZvNYEciEp2Rd5P#UudfqXysHiXo`pt27R?Rk zOAWL-dsa+raNw9^2NLZ#Wc^xI=E5Gwz~_<&*jqz0-AVd;EAvnm^&4Ca9bGzM_%(n{>je5hGNjCpZJ%5#Z3&4}f3I1P!6?)d65 z-~d}g{g!&`LkFK9$)f9KB?`oO{a0VXFm1`W{w5bAIC5CsyOV=q-Q7Z8YSmyo;$T?K za96q@djtok=r#TdUkd#%`|QlBywo>ifG69&;k%Ahfic6drRP;K{V8ea_t2qbY48uYWlB3Hf6hnqsCO?kYFhV+{i> zo&AE+)$%ag^)ijm!~gU78tD%tB63b_tbv9gfWzS&$r@i4q|PM+!hS+o+DpKfnnSe{ zewFbI3Jc0?=Vz}3>KmVj$qTWkoUS8@k63XRP2m^e50x-5PU<4X!I#q(zj@EyT9K_E z9P%@Sy6Mq`xD<-E!-<3@MLp2Dq8`x}F?@}V6E#A9v6xm%@x1U3>OoFY{fX5qpxngY z+=2HbnEErBv~!yl%f`Eq2%&K%JTwgN1y@FZ#=ai+TFMFlG?UV{M1#%uCi#Knkb_h| z&ivG$>~NQ4Ou2-gy=8JdRe8`nJDsqYYs?)(LJkJ}NHOj|3gZxVQJWWp>+`H?8$$J5 z*_)+tlyII%x#dId3w(oXo`YEm^-|tFNNj-0rbEuUc2-=pZDk7fxWUlw;|@M9s1 zmK9*C)1Q?F5@NPUJOYOAe`GHnYB%G37_sg3dxAttqLs6Bro)4z ziy8j%C7KKDNL8r#Oj6!IHx|N(?%Zvo31y4;*L1%_KJh$v$6XhFkw*E|fEu9`or?JD_ z13X4g92;TZm0jA0!2R5qPD$W^U z`5XK|Y^27y_Q%D>wWGtF=K00-N0;=svka>o`(;~dOS(eT0gwsP{=Rq+-e2Ajq?D<)zww5V36u6^Ta8YT4cDaw} zfuGnhr_5?)D*1+*q<3tVhg(AsKhR1Di=nsJzt_si+)uac_7zx_pl#t(dh816IM zvToHR%D)$!Zj4Q^$s8A%HLRYa>q9dpbh=*kcF7nkM0RhMIOGq^7Tgn|Fvs)A% zznI7nlbWoA2=rHHbUZ4PJMXf{T$@>W1Tt4lb|Or4L;O!oFj8Op8KEE`^x^*VSJ`9~ z;Pe~{V3x*-2c|jBrvSV8s+*Y3VqFKa@Napr#JAd}4l7;sgn|Q#M!(<|IX1<)z!AC3 zv<5YpN58Fs4NYi|ndYcb=jVO6Ztpwd={@3Yp6orUYe6EG#s{qhX+L^7zMK+@cX1hh?gbp56>jX*_Z|2u9 zb*glt!xK>j!LyLnFtxs&1SLkyiL%xbMqgxywI-U*XV%%qwa5oiufFerY!wn*GgMq` zZ6mFf8MukDPHVaCQk#oyg^dhl*9p@Jc+4Q9+0iv?{}=}+&=>n+q{o z#rEZ<&Ku65y+1eRHwcl3G7bR`e{&~^fGg|0))$uW?B@;_sWSls!ctnjH6ykmM8WJx};hvdXZ>YKLS($5`yBK38HULv}&PKRo9k zdFzj>`CDIUbq8GxeIJ?8=61G-XO?7dYZ;xqtlG?qr`wzbh7YyaD=>eup7bVH`q*N5 z)0&n)!*wW$G<3A&l$vJ^Z-%1^NF$n3iPgqr6Yn_SsAsFQw?9fj z&AvH|_-6zethC3^$mLF7mF$mTKT<_$kbV6jMK0f0UonRN_cY?yM6v&IosO?RN=h z{IqdUJvZd#@5qsr_1xVnaRr`ba-7MyU4<_XjIbr$PmPBYO6rLrxC`|5MN zD8ae4rTxau=7125zw|TQsJpqm`~hLs@w_iUd%eMY6IR9{(?;$f^?`&l?U%JfX%JyV z$IdA`V)5CkvPA0yljj4!Ja&Hjx`zIkg_ceQ;4)vhoyBeW$3D<_LDR~M-DPzQQ?&!L*PUNb^moIz|QXB=S z9^9NnEpF+>_Oh6+Xr55ZLJ7`V=H}@D<70NiNGH{~^QE-U)*Sg@O}M|%{Rcpn z{0nD@D%@8!dE*mndd2g!-q9;)jb=IUED<(Pxh`9B>V3z#f>82~&CVZASC?|;C-VKy zJU35T|3jd(p8F|#n@T~Wh2l1yURI=LC>Uj_!8i7-DE_IaSKIMAx`WMEq8kN%8sAx% zOQs~R1v12(=_ghVxzylsYZum-%8QmjM3-s2V!jY|w#ccP)}OSW?MWhNu@o-t0eTg{ zyy`}x+}GObZC(k>-upb2C6#S*NOfWbKEyReP%gay8MT!pJpsx4jwCu%>7%sY}1L6Vybj_P+;yP`YS92 z^o_G!Gr_NP!ixe7d&82H&achfi83L;le3Fs?u%E*xbeOKkJr7mp=)RXjZF;h*hR<= zP_cs1hjc}0JlHal=enmG&G8wsn%Sm$5Wcgs=Zc}}A%3i6_<4k_`-$k2E5f6QV{a$V zg3VZO36o^w5q`q2ASwJw#?n7pBJyGt3R<`Sd8d|52=h&`|CPq&1Cz&42rRCHNjDZL z$}Y*L+#N;!K2Ov){~fmQM8hVYzj3H@{yS>?q3QhhDHWfNAJ#q@qko|rhlaGG4Qrvh zmHpmg&7YvgRuI|i78-{)|wFx(R^_ z{ag(}Kbbbx=UW42sAu}kg3yB#96dJlOB{+or<(51ylVwpXII7Hrlztq!pefQ?6pQhqSb76y=sQx zOC-swAJaqnL_ok{74u_IHojFk;RSSFfjdLrfqq{syUxA$Ld6D2#TMX(Phf~dvSuuX zmN2xzjwZxWHmbvK2M#OhE#{`urOzs=>%ku}nxymK-dB~smas?Z(YM^>x#K)M@?<&L zeagMnj!XK4=Mid$NvJ+JfSjvc`4rX9mTo^+iFs0q7ntZ{gfU3oSAbK_yzW3WA^`6x zWgPSLXlEVvh!G^fOzZ-O{C_v;V6=;DE+ZqRT4mbCq}xeQ0o z98Cho%25r#!cT_ozTd~FK^@AB3OnrAAEDI4==}#I_v}iw0nhA{y99mFRG*1kxFkZP z+are- z8D|3WoYE>s0<=h)^)0>^up+nPeu}Sv-A($6t3AUedFczOLn;NW5_xM0tMvvrOSZ}) zA2YG1m4GxLAHZ5k>%}pHYtf-caXMGcYmH8ZPLX9VCew0;@Pi-8zkH^#}Cu$%FmKJb=!)Twj!PgBmY0+>VUsyyT}Jy>vMt zo<^5lmPo5Jt-=)z2-F{2{jB{CpW2JDj%~JnP*rq^=(okNQpH=}#{kqMUw{&=e-5;G z!FwJVQTDS7YGL&|=vJ+xhg{dMika2m2A#l@$PazLQ<6$GLC+>4B37`4aW3&MgENJ% z#*tOQsg{>zmcuSgU?peLA}!Rlu&K3LTc@drSBaI?91dK75;_`(V`NHjkMj``jwjJx zcm_!liUxn=^!~0|#{g2#AuX9%;GTBq&k+Jz!~Cc+r?S}y=Q1okG0PRIi3C3wgP8F| zO2jcmnVbGXp*Mu&e#a9Q5a}w7$sITx@)8b}sh(v9#V(H$3GLHF@k!Wh+)kNueq;+r zFtj+^b1TQe?R#Y8{m!7~e6%83hbPKoizd2LIg3yS5=X2HE^l4_|(2q#LB zeNv&njrS$?=zzG?0Min#kY+3A)H1uMfogMYSm|vT%3i<_d9X&~N*ZCL4iB@YaJuo; zq}-;EGx~T43kq-UHmTn!@sc z3bwcs$rp?~73h*uZl_ysD*WK3_PS1G3N^t3U=KoRm_Gz@C?M>+x9HRMk(cA4m&L`! z=Lb~4*9zt*SHJgsAMAcTy*!1W^B>4T_doWvNw7UwmyA=Wq&kE{*GVHp9Yk5goUO;k zVb_3ARrFPG;&>Jv@P&`z%}t!*M|2127pm{S)gs~f_ID^lOH@nIW9DgU$=FjqNW0pv z&GYdoxe@)RAWWx^j|$N}sj*p)_bFpk`Y=NilvsI(>!Z&KBo&I+wb*kM5Vvkkr#;q< z3CobbF+GJ#MxL?rMldP0@XiC~yQCR57=wW_<$j!SY*$5J+^v{Pn!1{&@R-lHCiK8@ z&O=XQ=V?hjM;h&qCitHmHKJ_$=`v%;jixnQrve^x9{ykWs(;!Q9mlr#{VYVE93oaW z&z+vBD}!tBghkriZy7gX7xJp8c}ajR4;JDu^0#RdQo2itM^~uc==~eBgwx5-m7vLj zP)vE#k%~*N$bT#^>(C1sohq+DwAC{U*z(D)qjgghKKSy#$dPih`R09rfbfI-FLE!` zn!tg71Wr(D7ZV*4R@GqG&7)2K*Zc6_CMJoGu#Yc>9D#{eyZ>u-mrWG@4Hk(je3lnH zu9qvXdq+!`5R1mlzWjV^jvaHl>-^Z+g^s5dy49yem$0$>341=EGuOY=W5PCFBTbNN^19iIQ57C3KcV}z~z#Rvngs#j;g2gswC(TLWlViYW}tB5T#g4 z%vDUYTo1@+&zE&`P%fXc^@prE5z;E@;; zKtpEFYftJq-c0sD6lKYoEQ;O1X4uFZZ;3gdgfAKqIc=Dj6>unXAdM}DD*@a5LHk~o zyJjW@aK;XG%qr<)7Rqh7NdUpnTR6jc;6{FKcK_v_#h{IO{mez>^^70DAWB5whqq!J zevvLUotE;I?IWWf!ieJ-Hx`TqY5)ND>K0NCb7IW40Jk*J* z^#m%kIA~Go2=R|y5zM|*ehJxyuX;lOQZkArKVbQV(XmidUH|8U^q`wP(7%F}=uG}U z2~&~CLebE`c%SCdeU(l&hryL~+Y)6I^d@|||6F15IAGo`G+CdVf zc+!EycZnQH)OBE zyTd8k{(_v9d2}osA$*>Q>Q&OB(7ShxA$}p8ChVnYlXl5My$HlVx@ATprrj0}6)ycK zcQy#bwOms1CnS+xd26}k?J;WI{HR_U+1T^I!$B^S=pJkT705QaMF88VJp!s%`?y9z8f$&Xw(A}3u_(n5G{!)yH&zN)S?c1$SZlo>XieJ zyEFa>_p9B*cY){ct8=dq>uQTf# zd4vB4)(ebwQHlSAu}(6GCe28H32pz^}l%Zqs;Yl|B=l2d9HrCcUf%wxLYs4CBqJ#{gz*u6V$>?9IT@uSf~2Rgk6CNw;C21ZbNkm>ZTc@2zeOSXVE^>i5!2>t%!1cI z{FZA`*o4=dTDG3&{v$3xVr%g;3d(!SFJU}w6x_Re(ohlni)I54Wg{t zWLK{A(}qEIH@pamgtr3serA{THlp_IR(gt0CFguk={|Ochh10)7UV4DcnO7fvL<=x z^WCMg_TI?U8(loaUnAe+Nc9I1JIO#_C`=kJG(&wy%Cr9vRFcY9^8{A3A>GuSW~Zk( zMA#t~0Dw?;3^Ue|lhSp4p%YvYmw-&3ey3}+{6Uhz?l1D|6nYNok6?4N_C!OSR=QtS z2X&QtWlkZshPo#-dXBOlSqh3D;#*_`hyohR>vl$W+QC>HPOs0zwHKN`?zIKqCTw&w&NUGNS|abulHe{D+{q z`WvLw?C4K97cd}6V6f2NtfIAO;=c>qi^+y4#oMjK?5Hy9$Tg1#S~Cxoo-Zdpnt2kG^n}`9)Df-Spvx&Oi+6xXT=N*0l|d`p!ZU ziQo9$y}PYIF~Zqh^?6QZ8YS*JtD^gynifSLMlVYRhBi*f-mJFS<>l%5sp5$V$p*X9?V-0r4bKYvo3n@XkCm4vO-_v? zOsLkR?)>ogb>Ys*m^2>*6%Db0!J?Qvpyd+ODlbslPci9r#W>d~%vcU7J_V;#Um1+` zG0>Q$TrOLUF0%a3g=PaCdQVoUUWXgk>($39-P;tusnMlJ=Dz}#S|E== zl6b3bbYaYguw3Bpv|O(YR2aBk?(jo+QqN*^6f0x+to-@2uj!nu6X{qLK>*PxM!i0C zZwrQ}prOw6Ghz?ApvM`!L3Dzc@6mp<2hO0y{_`lqtt!FcUmBG+PBwl?>0Mwu)Ey{L zU;A{ywkT}jCZpPKH4`_o0$#4*^L7=29%)~!L4*czG!bAva#7ZCDR|6@lBE&cyy5eE zlKHwzv7R9gKZTF<8}3*8uVtI)!HE%AZRD-iW!AJI7oY43@9Z$0^MO@Egj1c?o(BwF ziz1|k#WOgAG?^r1 z>+p=DK?cA-RLIvcdmwq$q?R;ina0SPj@;Mus}W_V2xHnYhOq~=sxzA`yTUOsJ`8`VOSTE=IZ!x`cZYqHbgPijF>J>N7( zqbNsHK50vkB1NI52gyb^PflpU0DRw{&v7Y}Hy2>pV@W2f1EOd2j;H?|WiV%2?Dk7u zS(NrEUDl81<}yY9J#OCwM)N?x&PB-%1{oD*`_ZLiBJ=16uR{n+Lk~!t(&9U#>ZfVd8Iqn&idGd>uo?L@sjm>c|Lk z12d3Y>N9U`342@xaHl&Q@oE5V-f$s`04q983f0#m_WF=X_A89W8C#{uCdTNUZ+))$ zakPyNU)?MDayCKxWh0(-v~1rd8FxocW=Dc6B1%N4^SgQj$?ZMoAMQ-35)IMgf&)M?c@}4QG7=DTq{nHc7yp=CZ z1dh~VkK%OTr23U1mJ*a-DxX0Psvh_13t^YcPl9t?_^$pPEhhwGp}s~f=GFR;4@;@f z@B;R1U6Df?yl#Y=BgYTlP&<|8K27||rx_?{s|L);GM3^{Nn8HZp zFqxiG6s3Nb;PW3O=u;(-o(*q!^2i)jHY%N@;O5Hder~_@$zh4xG#-7?#S^-&M~yc} zh5Y=ltLBnTzt;Y%YNqi2d1M1LOz?MJbZ|Nc6>x19&l_S*2Rgk$DhaP7Y-C)4_uPzf zQm)OY)$AFfE1(0SxkbbN4}CHnlU`RqYFGIE7S9ipx_Q0vkE5JRq4Uc%zV7$?y(x$y zV^)5zwjH~+4?xN z9s@x~w`C_cS}khfI14K4Xgn^iuBxkd^u}3cY=VZI@-8iWHolPtt?JD5lZ1V=@g6yR zj0>bd7Z(dw+@)v#r!xpZaAxgT?4Ton(h`0}fkfF!ZDSu{f*r#{ZRp^oOrO3iB|Fa- z;|+PpW5JKZxJ-kjHf`-7ohmnO=a)Xl9lhI8&$)g6R#6PBIN$QSC8kT=4zj?w&=`!qjkCvvz;ypOfR7P)w^ z-7LFhXd6GLrFa_vGLwR5MRvcV*(r!NhQ@}T-ikBGy!fHaiePD$iA{|Q1$kct2`qHz z6nAyERuqvM6i2^?g@w7W2LLr~3s?pBDk6ce8@CxV;b%4%-rXK-GOk+($sSNK;_FBku zm89B}tpzL-x{dPS-IAjwyL*t7N%7~2E)9OsWJJWHc|}BNa5Xwdx(j7i7AmZhs?#zi z5{y$uQdx?O8x3>+5MR05HwUa-YZa*|UVLOb`T)KHk|~Gmwx8MfBUtM|afuM$0wb7m zR+_lU9=W~Y$uNlxt&(@&1;6t!r69A|W%;k3-%SzLlBzc0 z`b?Jmo`8{LI=d|I3JDAa|iK*D6=I_3q?%xFSLg1 zI^!pA=K}l1joBBj8aa8XHp^;Lf`9xNa&Cv+twW&$_HAwZfHrVcNUrRccn_ z1+L!z$k@LK28nc1VB|Fbwm$wO;B~yEdww1EUn|s&{-Tu;@$d94BLL(OQYx|aCa|&2WPT{qJzbNU!ep>j){o5=6le6 z>~Amqs+mCuOR2)aB!#sK5fuui7LsO!Qzl)lz?Lm!QoQFWbNIkfdkrn|)YbSu8WwxZ zO{}a~wE2Cu)`a3X+KI#LHm(Mi+}bOB6@N~H2}Y)e*}w8_z^Sx`c?CWvu*2{K#yqGo zx!Cu*+8&tdw!eiKqZIQlJg5Cb^hZ^Zh~Mb0l(4m4hc1mP&>oTdt7eS-bEz8mU~oObme{^%56|ou~EPOSFBa7VpUZC z0gVc<@IUeo~q)&?o zU@=bz-qfWm)&0Qn@W_fc9{wx={&-#8>0xHJ-+Ijl#P&1qB-%*KUU*DCPkKCLzF*#t z0U_vrk1(&Vwy6Vm8@#Th3J5J%5ZWd)G0mifB3onY8dA&%g6Hir5gqMH|hnEBL0VVvl~aJjdljF$-X@a zMg=J-bI?2LGw-8mHVF7Jbsk1K4LgWi7U>~QovGT2*t^U&XF#iDs_E$~G+t;U;tZn_@73Y6x>vU%x` z6?l`$@U4JYYe#|GcI^f+rsy|MdB|`PQunKSKkja4IGtj9G6buN&ZSnYi|ieaf{k5q z@ABM@!S(A6Y}Sv~YJcB;9JeqsM|-fPIZZfOgc*FSzIpEdT=YYT(R(z{(~X&x%6ZM1 zY0(|PepBl4dK*@9n6@`rUMd)K^^0!^?U-1rrB*b?LEZe<5taFp!NoC^lc>}YUy?5FjT9tFmC+%%DYNa+L zWr)zMB%y_6L{S%;dk6bJPO!wmT=wPPK1b$%+ffWcO8;2T+7C28T?{!96{%d`0G~j3 z)6g<%$dC{vAKJ22nY)fnxlD>P_Xb&@>wrG+ZpfQ%RX=R2kd@bH3N*M8=BO zi|Z$Z5e`0NcU5&aN_DST8O@4v3vroq3t<_5hBX;d)*AJgWPb~p=qx4}^Ms6pgyY`) zu z^|u7XSP^~b1)*61r(}zd!JOny@$KviSp>L|jSR!u*1IgKwId5jmAi2`qe%u+XCTwU z;a62_a~Z}TqDJ?6lje5hblv1f1(6U@kWpc)z|&nRBV*UIieQR{Rru*|$L2SzxtL&| z7abeg@xniYhexYoN6zxY{nI^*xKW0Gz8D~}tE>O4iCkpWn8wt4?S`(Ftv?<8vIvbw z(FFd5`p4~#m<(3uv2+pv7uVC$R(iZuhnxFEY{o}BxPg2nYK zzOjuMR`}t3{8z#zfLXy||4JCt|1nv5VFjS#|JEhRLI>(-;Rh~J7gK{as*K1{IJ%7F zoZnXx&Y54ABfp9q!HDWAJlvFFdSC9}J*llUYXFDN8meEa<0}s z8M~X?%iKLB$*-a}G_$rTh;U{M0vc<}N#PVAE1vQdL#9a-`uH3*cbJZ~u9ag-fny$i z8aCs;3E85mgVK&vWM6}FH9o^WI#G!=%YOB#gT`1^VttnSVf4$YKja@-;zARB-`7v< z*imICw^KX73Gq-go6e?w^os0U0HSxH>60JLWhFbDeGT&Z$d3;9NWy;WvICuoZaKMi z=UvTpLDrtssbhiK&A3EuWf6!)>$sUlRcn5?Pk^OCtvApB=6suN42uKN-Xs7u7EjXh zG|>-1Rp>w1KB%sI*b5dGwFbuHNN=|})sR(dekHBL=>I~l@Nao%H=w0q==`3$zP>!I zmgoBoi7ylm<9Fw6s3&T%wJ%>VQmx(H)!iq?ABhdSzitwHlFNGcBW4sc&9DmTThb^qz`diS`xzQT# zhZff!yj2#rS>yfS5?}{inV5BfcZw zF5uh!Z8b#76;GcBDp7^zWtzQ%J;D}es(iWWWQNA{SvyhO`X8oyNL?j8Afn=x(zHct z7)3c%RKTPAyKS0gwVpGLqR2_%EowBpk>rW}MFfsR9>#2aOL!HKZtg$bAOe+#;;w?3*If zQk=HPWSlX7cF?h1PVE1D>LL{K&Ze4d!#Y2qN+^N-`~RG(O^Gjg~EsZbW^ipD9*+uf$K4Cq=H zxnYj(#+^eUa_1nRDkJJH|9$VB>+n4c)jji1MPz$dV4Ojf;)iYjgw#m+4puPdwgLSj zubNnwfz=z1DqFmy@X!!7D}kTo6yBjVFYT`CisjAgjS^cO%|(B2vzWb5PcrnxTK4xu zm?ZZkCy>+)-K8*)fo5JCWa@}^R!iI}a6OA*S&ibX6V zKk0=}K_M7m$#QEMW=_j=4tDXgH{_l5u?oFF?CXKmk73#~&>ha8CH{7jDKT2WoJ&sW zD1wk_C4Q6m{-YEWeAg*gP5`2Yl>4S@DAbob$M?&Gk2@2%+H*H2wu_)XL3fn{D8ljl zh41$!&_(kR($}4zJj3?zH-A0f2$4;9tH|N9XT48P;?coFH~9`z4S_35{xiUZC4&-3 zo3Yt|ee&RI&qBF zW$mPrwbqtHO$6De21%1=8zUX5=uMV*>#k-H>d5vP zz8OPyI|HLGKn`U2i>k8-dUX}5DJ(|Oy>)cK%QOwU>>~+Wn?bp?yFpx?yE;9q{;DTa$CFGK2S&xDNk$24GuzOgK{np ztsuRfjYmLjvhn$}jK3F_+!AtM`LVw=u&FUIGIU6>0@nqZq~REsb}_1w!VB5-wbS#J zYPBNKKJcnu^LTORcjX|sa8KU?rH5RRhfJ&l7@AtLVi|n8R7-?$+OVx!2BrQCD8{a)Kc#rtcWIC2(YYu=0edjgP9sFpp0=(eKUE2*>jc+n@q? zKTY!?h-S?Ms1kNuRAjowlnTQZF=#1S3XPx<()Wc1>r=QN?#W;6OL z2|Y0fxO0y=?Qi#F4?$+-Qpt&J>-JT?;d6ITN&7R`s4l(v17J7rOD3#Mu@anT`A z88>nZmkgV5o2{_IQ^TOFu9g}ImZrc~3yltx&sdaLvM=bAFpUK=XGx*;5U2#%A{^-G zEpT(GF(}NVJNzn$I*!S`&mA<1j#FEw4`lJ|^Ii?VA+!l%tC)`Q6kS&`LD*!rp)SSZ z!fOJa=BWFG0rWJE<~c2SnT{ykD23&sE?h7iTM20!s3!XMY*WJK_oA3FzU zScKW==wTvjelr=iu2>(0OLprW-Pv$m4wZ7v>;gB4M5m0(gOK>_@aIy}t&Y`H8crZ% zbo1L-*2^hdvzq`~_{<=PT=3jZ#UgMI*bQbOCzf~T53X2F9_QJ+KHwwQCpU%g4AGP z7i4m>KYOFyVXw`L5P#h};Q56X@OHZ-P-1qabm)G~GS>9sP0ToSI#43Q5iDCjG6r<1 zyJZa^U&>SXTW+bvJNB5oHW0xNpCGimZgaFJSb^??Uz1|jbXP-h<65N`CgZYX8jM3^ zSJ2tNSxr8>9)`mMi8nHw1aDz_?+ZRuMO@tou|Q9z11zdD#ka!jZfeXi(bGK&_vVQ^ z?b#6fYLRy70Mb9>3LcE``^rMcoxj~!hvBT%&cQK#L#nhF)C)iw(B$hY1fwak15v#J z-<0Kg=Zh1uk_^yGnO~&Hl|4?14*DFz9!$a(EAbT!5(<}0xUlYlC%`_JfofaWqfWNEfhlbLb2Ds@#m_oKXUJ0 zdSUbdO-BOnM!b2U2o3t3AQ&HGTzjL}LBTpwM2|gf3<(USB~4unKD6^_G>?@N%R2V zE+a}P6(vB@x|W>|ol!d5vws)e>m=0+2Y~#n1%kb=NXlT+^$#v9N z0Lt8wQ#?o)_j$PRavtm~z!aRPQ85^H^}u0bjlfDm(!3xG(oMQY?(DW6m1QdXq-PG; z7jW?rNj(vW&SZZ>B^q=2mU!8NLql4|nTI;pSkw9gbip(A^U<9DVj%Sjd-T0)ldwku z!O)$tFvVGRJnSI!t*v+U;QlSXfMu%J>v5B@Rq<`V$DQ>YTCkc=so?hUx&dda4;A1r z>~5vZ0E0M|B&lv|71*mTuRX`GB3G>9RzF7}+2HIgGrV-?p|bN%&4si|xxb+z1S}F2 zOBQ37uO?>1n_T3UF8nYp?uWnU&+53X|N94hR8WunjZ{}VH({S=x7sRbdLq7vyftJ? z2@;dF{)x|0nI%sYQ|%pe)%r zxP>}6S+ylPH{St~1KGov%?}z^A&&&(B(s+ngv{wKZ_L(*D^+nzoie`$NZ_*#zQ@&T zeLY@LZ5;akVZ}L=Qc=fIphsO^5%YJ0FQWW3*3|ahxk16yr=ZgTqunNMFFko^CZVSh zlk<_(ZLf{~ks&04%zz`tNla=O_`5r6W>d-%mdkEryHLIgIZyrq88$=4=Im4xR_}|) zZ!?V3+6QZ7$+wYJ=>nqKQ2L_gKw%=9`ds2Mdo6`avM-uO$tdP}7Jandkx0}XQhkn# zzq9uFBxvJ^#%sW$s)6J+j5 zXmAN{4mTo60nJnc2C6XtOBsVbJYc5&a0nZ|e?0yj+kThaCezk^Cm!F<|A=cu`uO@u zMai;5H6<@WD$n?-1{?Pzr2mF?F||EI+58#(N9dB2U*+$o$gl7(T>0jTu!?94mCA7^eb%}7cOyZN?nfVx+L$x~x>^tyJj$vmKZOXBKkU?mdopygE`0+rPi zx3F#q)PBC|6M{n@2|m%_24@G{?ql$@S=PPaEh1sG9v zxo35;K!!nAr&^P|c$6z+&vUa@eX|Uw&nednN1SCQSFNx={#kvzFb``4ixf3m zIY=2lKDmS2WGQx#gfP0BOAD4i?UoNdWtRz&Q=#>Y75@;X*z^@rxbLVa`YnIz{oaTE zNGmThd0`N_?*0!a>=f<^TOdF{&|-km!E9iB4IUs0KsvY|y6}%EN>L%XAjjOs+WGAJ z=wAmEmK)JGoI&Uq$`1%&(sh$n^lmT{o9pDd>t(CQ;o9Sr;gFtdZ>-qZg7jbc*P~uh_&U$wOO;{P3h!F3|a}dH-WoGGsXGBvB2c7p<>_CnJAYP}_#gD0t)$ z$Is_In%83bCJkJDij^-Lbnh)JKexs8f3E|dDy=BUEES;}7{*+oxV&iNODhNv#y<$} z=-mY})V@*#j#N6^A*B940E$3$zfmk;3ReX3DO;=d*_(!|f4FL$#0mL1ToWidl)O|S z_mi9mELAQ#S-D7+a2+=an87R;9t|U~1&sgF{`AZ#ZsOL+=sb67R?kPP;SQrDJP#F^ zsr<9}0#5FYl#3;3$mekh_XV=g`LVN$408Oz1ZU^F@kv7gMcyAWTE+yQfcY<&di4?0 z09J)>xHkZoQg!{E*RBSy?JCKOX7n%2$6 z-dzz8T10-8&ZG00yi<2%x`4@L8oj$ZXP|WgZ7E%-(h>@kqIJqt!{ou4J@Anf#HcEw zPSv)TmeUHAmeK2Am3|mkp+~W?)6eVg;c7e2H48x zBw;iPnvFX(a}Y+nn8^W#;6K4qA&N3hg$HYE=n|Dy)1^$6Gxud`0!yZ0d*p;(03ud^ zy^hvb&{_%?^-|c8>2fAn_!5YCX`?Ov6`*x_BAqZdP7`m!E4|c0ttvHBo2}NJT1HQs ze_rYk1e$5HO|)A}>0a7uufbmK{SDV?ndJ&?hXXVWWefy|nb5Neb%C#pK9tl%P-U{v z%DOV=mf@tF5qHo|q4_JBR-PLXOPn6TUrQ#9e83Sw*iIv zU^kn1C|EKWK_mS%Ah;Pks|+@@OxM8{T4o@Zf(mvI z55b=nM5d)6kW5m_Lx%`#@%0J~At8s1=`iJf)}P0CE6_pa-@`H5WIHbP7t4>QJLNX9vAkd8^)UWbAP6$@LZXWxAVbOYkgCYh!Pi4lzTy1%B>Pf9ZYnAH}3- z*{;*nGg_ZWZvV-oB*dF(WQ0^x71UW+hk8Cp_g2sc=tD&+CHpenk8FnaqFX;|TH%e* z9ifj@(1+=xs1s>xxwM`XyvIu)rw0VwCz$GAQ(yL@$J9)4{viA{r49G#c+Z$S3LaiI z8H1fq(Zeb|M4x7oLLr4te=>z$^SG9N2w2ERGL4D=I9HuNqS6>W3ax}f`>ts|P^Zvm z@RHI@6xXbm9v9ry(J7RMY_2a`aPR71XW4B1S$a}He-4?~NS8>v_Z&;WYl>KnqBJ7-hpw*<(4p-DB;Erm4B)LPDS{#kCnL(dCt zzl#E4aVwa$czprcYdPwIDCcme_C!|1U))PSuuI$zk*W(Ap#uWp$Ho58;-{sE*^$YJ zfcvRRKNF?1B4(sbe>9@m?fS5nel8lSJLrFy&YLbuYc7$Di~9RZ6dwe@uT*+bv?gxR zf2UDHLuJLEg$yM9E&WcA_+R7?)37(a^as(%yhwk9vCtzREf&@5r9ab0gl1l{v<@{6 zC3O?M!(VOl{tcWYFh zcWyW`&qG3pOe@HR0(&Pf@bG-DEH=)i05VspTrF}nH!FPJEICoc3S)q%V+;_aFop)l zP;Po#SxD2ff0q4{T+T}wqs1MJ(W0uHR%OPB;l?2?$s`KN)CwvpIWi|N=M^e1V@wxw zhcbE=o-@%8PA~qV;Cea8wH_!IqWp_Sb&NfdNz}9rhH)r2Br^t) zMeQA%TY4kA4{q7j(jMtJ*xS>w>)_TMT^(L-L2JjGxOJj&ZV-)ggVi{5yFFtT>@y74 zJf{=@f2D8cEh09yg6#A&72XCLgRGuD?B$3Jh}mU9;ruBh4ewxD7AzgZW*I&BN(>mh ziz!$}F_R7^NNhzIC6VZOw|xa*NB`8Izi`@_wbT62%UAIpm3#SWG=pW%ix>j~;()!P z=|~#* zs~lrgJ~te{KY{96l8>ex)n>uuGMb%`c#snwpktC*Tn4EfgILng;xZ@8J7YPjGNU7z ziy8fhkvX(Gk4lucz zopwj%<+s`80do~2D`Ae3vs%C2n@KP&f1Tw*W`gvc{0^aDj8k(=qot>B`xmPR?nWM%F_Tp@8f$^zMC-x zxq5eR4y{vI3_c*+I&2E>TUd_fzE&@Pkna^rKrwaahT_Qipb*^GDr(jJ{9!?Jf23IL z(A^If6~w*; z?}1Z(f$4(T18(_hnK5l-&KgXmo>nd-3e?K(mCc5>6~3tQ)BGjdE37LV)Q^&pwQ#S) z&+u1NlKHDJYC|%1Na3%+nyEu^jPYK6&d&RoKPnRF@-yfpj11b3Z`tb@e>%>eq_``W zHjyW%v=QIIjMQf2l5wjwh-GwmTwut$YYW7S)B^oRCLq)v5C#Y+jB#TgxNhmo8p)ig z+m?O7x>V%vtNgs^JCwARHbhpo8tiRe{t^FJ)aIYKNc@@Cy2(NO%_oXe2h_a_mDEVt zmb7j{8H0tCIim0{RsMyjf5xg%)u5J6>nIZ!1*crg#_ZLsWwQbZRQGHCjX?b^(~`4- z%8a=}HZ#K!NGa0IY^23L=>CEKsPgamPfQ#BAATw`rjrHMokCmE$m&;$>$>FdWOl&m z)`l3}takOU{5O^V!Y`N18@mT#Hk8i4BUNORx;`YLf13b*mCvaBe-8<>i!%lf^-2;U z9Xu^Lie6DxK3T%#A{V~ncqJJ#j^vgU*fE*tQzR9Izl^818it9apbd#{E7lZ_VRf}E zc~xnS$S$5Fa)vkpeqLJ|acM0jlw*p5vTxcoxin9j54VyQ6lcuBR|hLNBB)YOqvR9U z!GXe8h=^BOD85uIf0M*0GA*2n7=9$tiDqrej<}AS5rg&?cv&o6pi1XUOT5%!|GH4f zvaj?*$t>7b&`TGoQk8_MWDe?v2r}Dt(=V&+RUEinS|JRG@uWH{KKj7Hj+!Oxo*$h3 zJSiyE3UmxBOJT8wLQ9;~a_QJ0+H$+Y7xq%5dSM}87BbO_f7fWu3%N;ZkQ#*^Fy;8l z+=R>08U>@C^*y3XHwO(!x~UB1eKROeJu9R4i#yRqn*t8KOlnf8LRwpLV^InvOY4y& z6Y0aoAta#nWk$@|ua--OGHHW!xhjPv3`wq-h()h-g$Rf$X%kb&Wa>o&%jl;Juf;h@YL`0DJV={S3<~|Q zxVKlNt>PnLnaimuw=2>%bOF+Krp5q#4}8Z1N3?_qAS?S%)arm{Ww3y0Sj8X=>X^3N zqTq|)7_lk>iEJQee_T8ouuaPZ z`ZGo<5HsR>A7m?9YOlD%ISXt11#1V2EoPx>=owC%+R@3XD;+F;=(T8c8;0RJ zTsm&wf4E6n@v_B&nSvZcHW#06QG>Wc4M@NZjXq_R6tyGE%uPgmQ2BjdC;x_^K7e<&Sro+Qon7}Z6ij>=e%vr_NLQ=+o& zBpJok>#>>@t9yzoIjkHJE78hf09L;KB)w^jj*Zi;(XexzZjXje(A)F$&QZE+l#Y+n z`=Vi2$nPAb_di1SF@@cJ_apQ%rsI6t?-IX1$@BzBhvht-IL`O`<;uJelNOBA7;pvZ zfB49mXR!WQo}M^PexS)v&gcE|!8|>kr>}-xBWE7K{@1Mi2C+ZCIZxkg5`fhJ{k9ES z?Q&jg{rY^Kz9*250O|V{Qa~U%CqezPdlGEt!}O!OX%T>bVgb8HsA8Oc79FMkJ{1BQ zAj1lz_A7b%#c`?Pf$=T5(=0B&}8~QNxNwRw*HCGxKs7 zAbuqb0wZTm!A@E!voDKNVzcs90B98$d1mpu$?pVH>>OjYdz|h7=c8OvnalIse-rG> z^TJ7MQ)h{-eY_~oi=$1-J+wg3^YM~AU$kfB%yWKA6u<1KR)jRN^V))`t?f_yozaju za%E*q=!xg(Q{=;$gM(CgBtI%caf_(Rsq{@aD+#S}=pC z86ka~*GGN4VU#aFW&hkLem=}?e|vn~F~*%Z>oir1(1J)V;P~B;pF%#~KE~a%?9Q`R zT%aOCGZYoCbw1uX$~|Kog$!cB?q~!dDf0Qo*L&^G+IB- z%c7$kALW4)e5h-jQveUupWrMkF~&y@j`9uT{Dx>3B5#~;1W8xjD8D&0f6BK2KH7bP zZxi%s6BzdKTl4((Xp?-8aO}B$ceSl^VLKn+QQT7@lRQFm{BB3JY*{801(`8^XP)m0 zD?Wbj7{5On_W1Gh19`qL&mS4*kHL?eO-i0WS*?JlPt9MR=TBSiCFAu3oJ*WezdvZZ zSy&eKQ%>+G2tl=09#H+Rf3Rl+Zi1CZ#ESIpy09nYSNtA9DI^G;;Ll9Z5|JT@L8pS6 z=LDaMhSef9kKYv$QmRE_E9?E9x+#R7EG1O<>7Jl@f=`e0)6s|@lKP$XQ0bTR{H&FQ zqg^6St}cX+CEqrS#MdXVu^sKs^EdCN)gfU|nuEu;t&|cN=jWpWf4BaikH05EkAG0a z`{60><}kwSr&av3l#hRYOk3;XuMV}FV=&DU*-9CmLvT+ z+WizQMWlnqEBL#Bo<24v@d&Bg{c`sRFGPy!hJDXGw0(p%#G{63F=LblwcdY3eAs2Vm zpQhd8QdM++1Q6AEX;GK+F4-R9ZGBt;ETo9?DCrv0D+1IDFD2JwEAD ztgpk0jFnYAjJJ(@@>0vEgx;*>?T$KtwXGVHwg{EYV4k~Ae-(8Mq(-WYZ0p$a#PooH1&29;1t$_t9$S2(58GNS8RjOP4xdqRX7GP!mS( zwXWr~Th0}t^{$I4?CPWqt{rr_D@Dz&!?e*gOjo$xOPgE|Qj5EaTHR}@&3zZOyYHqB z_w%$_-a=dCx6@YnYt$*fK-=U$L01^rp)ZLX{|8V@2MEVi07E4e007D}b)$q0%WLwQzAecs$;-Nd zASxmv2qLK4kS~#nq5^hlp^Wh%1BQZAKtXf}4pBfw6cmwp&P}qWT{hR>FFo(vkMniU z{hxF9eEi_U02Ygt0^2UTZ1s{$s=JNge?~JFs`gh0d#dZJgLbsfiWrV%$9z#cWYT!t zjF?8kq{&_*;S2Vf!HtPzG*RvEF(L`GzPc~$iyD1Ci)C~-H!lhd7@Lg7h!G1np548{3_1!t0yE`k(y=0q zK|2;q#^YwpX>6fwMt8(ipwh-oMr2;Z4jPg3t-iFjiEVP5Wj8W^l0Y%930Vneg%uYl z%W`q6JIRq+8;=~^6f>R1wX0ice^UuBBdtAFI2o4_6~UJ^kg?F#!|# zYr2j}n9N@@1>7~fuMD#_D5w%BpwLtNrqnEG8-Ir6ou2E2f_VZH!ltvzf8c{mpVs8; z#;m70j=`}S=A%Yn>Zr&LhjZ?R7!(;@XXOpGy-LRkte_4{1m@;F!7*B7==^LD=cSdP zjHE!>@hvj2=j%8b%Xsz_e=^rfuoNB3(?h2TOd@BOcPH#f(lJ*VPOpv?Y41)Ks62d1 zDEI_jNFx|D6O@q)DJR1``t~a28pcUU-Hb zr2w4G3E7TSV_>3VOTsau3RY9(%sAca@`GltA}bxT)ik1H!5XYBe?kY&r90kZSdnDh zJd5IBgehf8^CirA2(Y&E2`TajRIr|su8#*Igb3yNQi%@vQ|Qug0WPFt3=sf32k5POw*CcHVT&e?km<5rfT#*GFEMn@M&;M?CEXnO;5$&MkH%LTOA|6AF?7MP{_m z+0sTkD8^Y27Oe4f``K{+ti76n(*d037~VYDfUe=5dU+nO0CJFdc)it$BU zO%5G8uizR=3aYQ|=4MC7SFo%Y*Wx+?$Cw=WD(3RQ4HU_UDH>}?$Qz?#n3%XpD7%RuqWbW)B70MGJctpNfASD{o7H++vZu$4o1xXFA?ww{ zbWYj1)>vOM11H((N3yjpV{pzA1&`%9C|O8;qTz8oAyBw>%}U=A6;BG(jxNlRaoAGy zw1!8qhjHlOwzNr^`JZaog`d$CAt|9Y>il#($06H=pOe~P#7@x2FSr@lgz zs*2f8e^n2IOcmXU-YNne%Gnnv>GNc2HZc_ZisGIydd#(P!m?R4 zivLigs3CR?D@I^FJ=eFEUL)RNUX(Or!8C~c7a#Nf0~EDxE0#HPRnWs=+UPC{6t^VV zf1XabIi-5(-Jyy?!mSgUnpB~XV_Ytcm>sjoUU_Xrk!*W}#(=%bsJCjxKxz05sY_ z@G}Yk3Dc=EH=Dtv!#Ajku0+&I@M|%_fIyc`EM&DL*fHD9e%b4a#j?E+)M{6be`;Ty zj5$`+JbiP}?32xoXwpP8m%f=<^e{tJxy7oghoq4Pa<`(&N{~HO^qjLoRa7tJT!Sk7 zSsgN9G|@;e$Q&I@$3Q{O#Il^uu=VVmiBk!-Mt8Jk<70+$)=(E;&_XY3YUUYE+mq35 zGroo+M7UH)O&>)Tg_BG8Jq8ffe>0TcVv^EJOj3He0dUd!GEAWt_X^@_X}^c)tlGf( z_1=OVsHoe4Y4tl$>Dz%B-ohQ2HH10$f&WTSjk)Q4h1*FdNq1jYJA(Ovw%S2VOJTtX z>H@W0L#UVR!W51#ZKi)IoH&G~gQ!g5)U9Z$OQB^e8fZ@i{VD?~tQIWX*I2w);@?C{sP+OFC4_IfZtP}LT~3FqJG8Qta_S@ zd{Vkvu5N`^@ADRYnG%9GerFINTpiWH}CfKwRa=su8@xYMtWNUdJgtNAiV;Y+Vvf0(n9&Vd3lf?a|2 zyyMZp2p%U3hp@Z!sUbWwglALO>sM2F-mChR0km_#io86qt3HtRNa-qlkvtm4D=F+N z{ry3=vh!+J>Fd(tHxEt;zf#bwmKV7$3^W(rBK+m*wvRirDL}s&QrJB?i6Atd4)_cB zfJ^^8jKAEEf28nXf9Xdl4z_0iFG!aQePzN$eu?%GQ4sL##QTAOx3DYVE)$-Pf-<3Y z6gGQOqPX1C)iER{rbH=aO-fALiUh}@oulAayfieU^rNVS(J z)mTl^2~@tAe^!b)l2(foB|TZJmNY8*#H->Iagn%6(yPU_l3p*iOM0^ymh>U9SJJ)W zd9fc5FN&8WzhAt?)OC&PM)w4HMnSamqf#jJo|Dn53@=S?$ zm$)mKmy~z{%+m=xH=vS$SKv$n;7+))4h8h&FQj*-2UijZ-vAYN5vYCyO)N(-fvhgV zm>{B<=vszJt~HqKx&S4vAWB_fl({a&6!&VByDvb6JBX?7UQBaugx76LJ#Go~?*9Q$ zO9u!}1dt)a<&)icU4Pq312GVW|5&xPuGV_G@op77bzQ0`Ma3II6cj;0@G{*_x6$l@ zWLq!9K8SDOg$Q2w06vsBTNM!*$jtot=1)l8KVIJeY+_#EvERRF+`CN~+)~_fcio`v z*4!Y8Ql(|4lGuxq7O`$fleEN}9cjIwL&2@>M%LYJOKqvn8>I&WVJ`e@>#4mHnuhzUW>Zd%6?zt$4SI~lcxhl zC4TO|$3j~w-G4Q7M%K!ZiRsf{m&+`_EmNcWDpuKnz~ahZga7dAl|W%-^~!;R$uf$l zI4EIk3?ryIC}TXYW(0;0`IS)TrpP}tglbN4Rm~aBg2TZCuXEfjpuhoC)~>H#Ftz@S z>Dn`9pMU{c7+4fO0Z>Z^2t=Mc0&4*P0OtV!08mQ<1d~V*7L&|-M}HA1L$(|qvP}`9 z6jDcE$(EPEf?NsMWp)>mXxB>G$Z3wYX%eT2l*V%1)^uAZjamt$qeSWzyLHo~Y15=< z+Qx3$rdOKYhok&&0FWRF%4wrdA7*Ff&CHwk{`bE(eC0czzD`8jMNZJgbLWP4J>EL1 zrBCT*rZv%;&bG!{(|=Ze!pLc^VVUu~mC-S7>p5L>bWDzGPCPxXr%ySBywjS7eiGK;*?i?^3SIg!6H8!T(g4QQ%tWV0x-GTxc>x`MRw2YvQwFLXi(-2*! zpH1fqj&WM*)ss%^jQh*xx>$V^%w2Z&j!JV31wR!8-t%AmCUa;)Y-AU<8!|LS2%021Y5tmW3yZsi6 zH<#N!hAI1YOn3Won&Sv+4!2kBB?os0>2|tcxyat=z9bOEGV>NELSSm<+>3@EO`so2dTfRpG`DsAVrtljgQiju@ zLi;Ew$mLtxrwweRuSZebVg~sWWptaT7 z4VV)J7hC9B-cNaEhxy8v@MbAw(nN(FFn>3184{8gUtj=V_*gGP(WQby4xL6c6(%y8 z3!VL#8W`a1&e9}n@)*R^Im^+5^aGq99C`xc8L2Ne1WWY>>Fx9mmi@ts)>Sv|Ef~2B zXN7kvbe@6II43cH)FLy+yI?xkdQd-GTC)hTvjO{VdXGXsOz-7Xj=I4e57Lj&0e_C+ zAH@(u#l-zKg!>k+E-Qjf-cLWyx_m%Td}$9YvGPN_@+qVd*Q)5cI$TrLpP-Mh>_<6k zysd!BC`cEXVf*Q0Y(UgdE^PYo5;;FDXeF@IGwN8mf~#|e4$?Ec!zTJEQCEM2VQr*k z8Kzplz+)oH5+-jyAK;GP8!A zSKV>V#gDFTsa`xXt|1Uc3i&PSgl%D=JEwjW^F5vD0l6G!z|~>y03#T)?a;@!*(vAwmBFr?|-8vt&)jK z!?QG5DNz%WTH4H>vbUDpIEl_O19mVOmP_8bVz-kCsYEtX_1Ovb zj+KS444hDHKJfNHwq&hQ29#QGU>;3P1P+D_kVfmXiA~y=y{YGCGep{s6iwTA*ge*SZSH9K;{Gc1^NWT z@{>XOdHMwf#oVVr5e4%x1I%+r&CEE*Qu8V$tmu5mm?%|OR}{L++~wCzm$RIp(7a-4 zuUW|Jw)8G^n5G$)e{tS^RU&@6hKR!RWWQzWdvkgoyCMKT%caX_=zlus#?;Tc<%xwM zJewbXg?^RAe+_wMk=A>m=A@r~0~#Z6hmh`q^b!Z`=jde+%aR2&hxQ>`<7bXmDk+!% ze+$*7qh)2_^In4P`ktr>O8z!|UZGd$clcz~c=h>Hr~z=--z_oAmq3RVC-fGwS&sJu z1-B|M{Jx;us@*hy_J0o)`U?9cH0RlBfikrIP@yl=AE9!T32=5+P-i$<+jN!7%+FG| z&!5nrvTOegUa57UpZ*+hJA>p2ga0MxsK21E^Uo8!3b{#gdjViLw zDj?{%qL2b=fc}>G8S&udSPszN3la#if5csvd~EsYTU;zzV}C*VHpkOH)4w1W41*h( zbOQ8mmEBsPEo@ObLg z93$OR0O5mpOQ~kA@~zx=sm%~6;&yQdTLO>ECg3w&$V;K3Rxm$Mx#E3$#)AP`Y5ET>GF+K7Ons=3AJy$clM99)e@XPVK;DaXeI#{!nwqZB>eS#gwM4Gc z+UQjZ#jeu&%Mv~fw1GC37KsP2q#o_EXrxGY9xc+Ai=@m@d~k~Hixz2HYVc*MpSt<2 z$TixLN>0<8uJ7@5d0V_2pQVkF7Vq{{!dIm33#3Ft_}G2)yjM)!d^I{4d6C{M=mM$U zf6tOXHRy?rH1$Si=)u8jv@ewuk!jjLMIV6_5a7L3EjF@9Y$D=$k&f1(*4c#dO{r8e z(v+H}hoI~Q3P)vOmA?n#aMPBi8^%0|sj#w@`5rIzh zQ!tSbr|=trz3XA)gH(s7qlZqzSnr3Gf1k$a6s-R${PJy>^CsjPC{3BNQR^|!p8G=V zW%6Eb%Fa-3=o*=+gf}`(Z);pdp9v&gz7C z*}oPKd5d(eNI!)2=dpg8p7eD2T72>A&r(Oc#kZr8Zl0T=_oWh8{A0N9vXFPxf7T*> z@F=#&(1(wn_rW1wit#=dQbR@h$qP^^nkv#IIQ!Y8pN*0_p744iBi`tUFE&yiA8GoT zkhf%^=TflG&)tw(+<*mIXdUgu%{CxCbK8#JowN2@0SO=M^#R!H6?`{v`CUe5FJ?Sw zyCTwGaWuckZrbd*cS97n*}$HSe?&KIhht~x@pz>vsk20GwyCM?#|=m*99Q+xzrHv4AaMp^qVvE1qqxlUZ9nHsoy&~b@Pi; zbSxIXMqg&hucX*B)AZGlZ<_wNNMB2M8@&ts^)Xsm@z<+UH@_KAm7Vk&fBsM1e8*q} zC%twfR;0hW%s)2}p$g))S6XPbY}b-1+g56mZJ4@bdpGTo?Oxg^+aw*3?Jyme?QuE* z>k?^{mF+lLvMtd2WXr!S_d)uoY)gJo;16IEvvuH(Z&YlEF~4MtgVERw{mtdnP$YGQ zLX5QNiKcH()87Fhz);gaf8Zxp{{AQY07^yr*Rp8*MAN@Z(f^s9xq-6?{;3ChGh2NJ z5h72l13;O%#FbbiB|~{IS`?nriNJPIz>*(s7WJjAq^m9+Eguv+(JTTuX-2FlipGi# z>xbCfU@qZdcZ!5pBz#h2ErNo*n((t*0g$h4ur7sb6@-iGc#L$?z0#Uu)Xh){P%^cBVZ7wOS8%9=n+@X6!d z0j(RK8a`Hw2l5S1eVl@8los!kPhF(7@ijcCcL%PBB!<=~MKK)m$2=`T0Eu_#R=NXI zH=h{{`4iqLa>{Mue;U1>Y8Hp4#o-&#kU!*$UlB)|#anUx3hcmxfhe0Q0&^ZadKv7! zbC8#@-C);d@h~h3LJ*D3;sie9@`|I)B2%(-WLk{fsNVS{3NYNyg}nR)ue=tyK_MEW zlVVgDvV8=;&C^-g=a&0t>2a|ceQr0P|8{y#_POQ$^YjVXUgwtkpQOvO&n@>kdb!Un z_g|vV%RaZ<|2lm`_POQ$>nH%Z&n^1GBO19cTkgk1x9oGv{j_*W>RF15CZPW_^!Tj4^T{T!k9N#2;RO7iBy{i;&QUo$Tz+ znfE#GOwP=ozrTJ1Sc55We021t`blp}YoGj;%5y1uf!uNG{2U zc(N@c!)lX%wI3y3q;Kp>H=-52V;i3A7>>%(TwkwPYfo4kR?qm|#C16kwWU$vA^EoB z6NQd%bM%nHh`l&oU46V-HClA2e;$PpNH>BcwCIK7lE8cr+NK@KmP_V`PLn)Sf8 zDbz3|Fu5lWrRhrFHeWUO$ci zK|;QNMYU4B-{xxq=2gh0MJ_>CzIO%I2C`dQ0}U%zLwzhCD9eXj_~Pck%ya+e`Xnf; z1j}62O+JMJ**YJ(mx~=JE+{p9z;saHl6M^@O>uaJ(zL_pbbfg95AEkMI{P zQrP_-wu~WeK)#DjC~RTz1jWl>>J%&u_A8uVH0UJwtHj+O|MgSsVS$&sSO#aG3~yMr6^X${<>0 zQle|Lj@}|34Nrzqkl>m>`@k4<9*UKfc&#)tI4W!!rdA{x!$&L15^Z=Vs_fD^%wvtV z4GjkS3$YfV7A6gE;|0p94J`((b7fR@!QilW^Ak`-SZ_W1@A@+aUavpvf)AYzv|)!q z4VaP^lJwjZ|A#8&wqkPDwLy5?V^3lqxn2iXkLKsKp3v z)lw?h02Q#9dcl*)Nir~*8P80hEVZkB@JF-{`qDZ}%ic=6I zm%FuV~79YG9K?LnO!Z^jy-SC}sEQ=yjZJve> zhLEVZ{w5(ZoQbyviJ%i_b(}#LLsvu9$Wy~P3VYSGP5*j5?A-{?qgO|N4=ynDG-o(t zyH$VDmx5O`yrrVG6j*nCTSp%*G6XD#7Z}brjGFxGwwDl7VfqSEf=l#B~g+q=IW=b5Z!M<&ucX9YRuprWo1}sWhaiRi-Z__Z`V_?vU@yo}2(i zFdD}DxXjRbRIlL*gGOwBofG%{2tGu67-Ps#wKfT;#rvpD6d}xUOenjnl!5P12Z*7q zw!2cYy^fD{X!wL7>>Y4wID{LA*tcu0;U>}9^SSiBWz#PcPvS>06_ak^GaXZyW_ZJ^ z=DocXy5lp)=I}XgE9)%v+M=maz{HH12<9-a6nE%cQa3OVKU(g8u^m{zqPmtPawHNk zWR7wCpHO$PtcdUx!|AF`o4_oZJa38m07T<0{69Jm_wcovhi@1zG{6_Cwr^I%)O|y^ zYO*wZw@?12&fKV)RzYoo?-}~1q;zC-qb%&GVmhg#?!i<=i!>0|LdgHijnpTlpo4>E zJ*c*hO|z2vk8U1+%7RKMp{yWG^+$Y3922QYvQ(DNhU(N_cuU6$Dzv>0=5xNOeup?c zNo$t6oTaTgSFPlQTvG0VOE^gcRX<`ALi8~FK&RITk_PxKQN!sc(4M3F**1D|x$G9+ z+(ut+b|{%kY$001J2kwwjltaQEs*i>3w*#Zn|y(f7#?GPoIb8Gtu3 z6l++mVQpv&_A5%Vi@5j`T=XJZe@D@ehm?9h2I}XB_@(}4kR&~YHrm3(cAUT?`X&;S z^aR@e0Z>Z|2MApz`fv6F008!r5R-0yTcB1zlqZ!0#k7KfkdSS=y&hcen!76`8u=i8 z2484mW8w=xfFH^@+q=`!9=6HN?9Tr;yF0V{>-UeJ0FZ%A0-r7~^SKXVk(SPwS{9eZ zQbn8-OIociE7X)VHCfZj4Ci&GFlsOiR;iIJRaxoGXw(dGxk43#&53m>S)=uTq|9>^ zv)ObhvxHhb=kS$=qTqy4rO7l7nJURDW4f$LID5`?1J}a&-2B3PE?H*h;zu740{(*5 z&`a#OtS|ymO_x%VPRj~QUFfu4XL{-O9v0OB=uyFEst^tz2VT!z4g<2#lRmMJ`j5ZM7xZ*AM>%2rvSpe(=Ig+{%mm`qu9D$$nuwfAVtg)wU1D1@Oa-0qBDX0)tL}srdd3AKVr| zu!4652w2`d0fsD36d(v8?%fw448z=eKw!vV=GK+cg<@B0$2aAJ0j^IF7?!T;tpbe1 z;%>zpHr&Lcv2JbrpgXly(as#!?0ARvZ(9Tyw9dPLBI6nnUO(iIoc8&R_JI|#ma!w& zAcT?E9qq-QVS__Pcf=Ea+u?_rKX*`?w+8~YR^5P4}7sOkF z9^v<)Wd+*~+BRU@A=_f}TNYc7Hi#bHH2iMhXaTblw9&-j;qmcz7z^KOLL_{r36tEL z;@)&98f?OhrwP%oz<(i#LEKIdh93L_^e1MUFzdwUAZf=#X!!zWeTi=n`C^CXA?1cg z9Q>gxKI!0TcYM;pGp_iegD<(`iw>T3#itznkvl%+;5k=(+QA>Y9v3?#|5p?&G^NcjljeZ~g^f18y^%J9)Cd^>|=NijQzL5oim< zlYvkmuB9`wBAK$LhSPsqg44Xt6)qW^7KbGx93STK5hI&60&Pi2F?cADNrlr=CM*jZ zLoF@q;~O@SuHKr*C$ow|6UMLxJIZx~e9?Ss^Ty`ZaDtBpPPoAs zJW(yH$N4T<;S2#yPeoF?lu&qNOqVhlu1EGea_2aYXH89ap^|@L(Gh7>iYStriu4X0 z;c?T2YBH74HPSR?ZZItAvUReitVH^z=C?2`C}=rO7dV=-77=68sE%uDQcf{6cFi77 zhpm&o07Yne+0~cxtd5_*)sP&)@HC}ize=e%9 z#0xj(imzo}crbrYe63*c7RTYjDhiU1%Z6##t_Qui5BGbp8h+wH(WFEnJTC%R=pic) zGR)Vxl-NNqUE8ZG40R2ST?P81rl{~1FV5^e_8Pg(x$FW_6(mpMLKFJ(*W5>({#DW*Q zoCKbj>CJyx?{us_MShE|Mu(*hn_8mTv>ROv%chy0TJ@sGvER$E`JN~loQ0D;f|Gu7 zWz6bozzKCPos?s8CQ8kPJJs7yy@Vnhlrv7zVopqhG;I`3KjYvJ7U3Q84o~47P9z6E zG=+Dj6AqqAR72W5+#J*NkpVf)wXA6$(M~T?7#4pzGDBrUrkr3p#=R| z)ud>4j>mb%X;#lOggUgWlJKjV=@*U0pX+Y^LM!$sbuI0$Ut`oayK%Cl!#hQF;YI3S zNlkxGOJ@1oTeu+m*V=%8d-n8%+f;C_H)8o;-_FbP`qm5+m$!#sUS3~az?6UCnEncp zrIoW1GYikZ3^9(J+*73a_E2=I+@yTZzO&nHEt<<$te&=8HKwBfgjml-JG}$lI=92@ z4z$bd>F@tEaq6laA2^*uV=f+<_SYxIZ2lu1)15Avq4jrv%t_4M85a1jrdBbg?&OBO z?w|X;yr%s=o>F|n{!ss|&@a-Ga?>Xp`Tt1WnzOgFxn}QvF`pdqH+A0O6M<{R?*8aI zm|Fe9w=3;hq}hV*9V%VFm_Nouyj`+eMRi@5yyP88PxBQT&vbZ!!)Ky@-W>G*(aL2R zRrh*#Vd#O=-{*82{_t)2Q0>X_c9z?Dty^;DE4*(gK1oaCZ038&qGr3{1N+o{&GW)S zR_RrFeoeXT93w9WTJ=k2WmwRsyZJjz~raN31L?*7OZAKosxIC_$obw$Vto-F(G};KG84}n`sf{TwU%2wY3la+hh1Mo zOk8XAThu>BWiTy&7qj>ZQ^xVsJ)L}CZf)Xc&#mN8-WF1DX4>(>Q`45ejQ0=-ZM4zk z5L6XanSS@s%!u+}4U5KdXED2N1@ELz7MFYE%Vl0?GTZp&z)8j5fxVV0(M{Jk-YLI# zD7^e3@2_*4y-s~w)iFmb?A6PWbS|JU~kQ>A{z z<#_KpR{ZVn&J%Zz?8+_T3iQ3CX&uXK`8Ms6*u@`B+O_xJ&pYz;K_cUp%GV7lwA_XQ7h?=EiYO%jA1g4LkyE%H;C7 zPBKh~SnewUyI}=DY{&pStppCf@lAGIC^PvppTgt~O9f-}d3G+pn zHcEm8XU#X20bkb$bjx(06{tEH6~T)57MRE&F1=%5uthQcpfXUA=H!#g@?du$?pR}B zus~7Bs}5H9dx4fr4CvY|pq0)*@1y!kP7|oePX>Iq6EG0Z0Tmgcm@-Wp?51-IwPcVl z;ju?iv_==K$b6Bx4B|cu^pKur092#|ys(EK0ARQEYY^^{l%|QCuAjeEkp14?q>9h4@!6nkbbJ&fg5yu+?X8=+3#!VJj5-STn zB^PM!VxULuP~>AB87AvHdVm8Jad0aGgFcF?DbAA>SBOrobXEl`gda@_j7wDOI$XgD zA?Lm7ffXYk=VyXqs+K2Iu@*=nEBNf4$p*_rnW}xj5^+A_U=u*+w%i1|eiP93x+o@C zhJh7Ihbe;@`y&KjUXYgX_u)8xbzqD+z9U^n!xP?doXqyT+|nlWGZ zf)zbpp(6wDM6oe2=%E;$(+^UFIrO3?4Q`17gDC*02i4ujCr@1I$qFe_?ym&yj++j) RhRK)Bhkwq`;Yh)md4RrtR%sNbw?F7+wVN@9oT5^KvyxHCChVwDz29-_(~6`YI}kOI zb^sOR2x~T#ZdIJ>Rf@`fWMMck8Z~Fk7!ymA-q=^Hp5eZ$X)}%69EWv#a)HMQBo+#f z36F86&q=PH!h1hfL>Ol{cXt`zy7GFq%Eq79O{IA-u!cH*(wj1wN}D2M4WT6o(qxrW zEB}r}@-+r4&wIr;xO0(AI@=cYWb?m21~K;0A^-T{gEQnxfCN&@N(#Zq#RXZY87O0m z;t0Wp7M~;I&<5qU1T+?pjfUye_TixR_f>$?rT1}+*6u;9Gn0cXM{`4grB6(W zyBDpHwv$&%UIzt(jZMh^e3jZ{I@kE301olpI{yj0+;ZWogmFjno1+v zMW;sMFf7sR(_fhVjl~QhEC!kN?S1GnQ8&fuPw9z{5eDbyAAsT&CyjpUf=RK)X*YhW zwf>HLeXJxlm0mFjo>lB@ni;CUkg)*JRligsG*5>@wN*UJvbS&X^}x zn@^UJmJ90QY)d4OLkji-vg;l*>VWz+eRS?0G0Bg!HhZc?2Wz}S3kMg^_@+65nA?uo zkBwh=aDQVGH8XVK>zh0u{gJbev&iTnS1h3p(pF$?`aC^rhJj2lK`5&HHV#_?kJb zGMSi_SJ(*5xg|k>>Dvgt0#5hN#b8)>x5&pj4Wy_c7=p-XQ=>p*vRykohWoq+vj1uk znu?X~2=n2?uaB_*+Lr;+&434q#3lhbD9@_k1Te#nwy}MM^TTHt=B7p23Hvw*C##@< z$6AnfJ+Ri~X^`J(;3$v;d?J5C5U~zQwBA9#k|t1Y#>7ZrY#I@2J`|kfQ=Sxhc*rH| z{varkusu6HJ$Ca6x^v$ZA6sX;#AVi73(ebp61*3)LCF6yToc0LMMm{D%k+S_eJ<3CTZgjVEpgE=i5mX z0o|kFlPT7$0gM?NfN_Wk=T=zCXFhtz_fJrXuKFQ#uaUzUCWj%}$pz$g05t#ar{-1o z#ZYh6o&A&s>>NA5>#m&gf?X>M)bj>Q7YY}AR8nPC<0CJ`QolY!M*@PhNF4%4$5nFf z4{VxA-;8{~$A&>%Yo@~y4|O}IqYemSgP7Sy?d}}+e`ng%{?_hDUhCm`I`hP=rda|n zVWx~(i&}Q|fj^k+l$Y30zv6ME&AX7HTjy~frLaX)QgCMmQq3_qKEcRyY7nk_fa}Z$ ztrwMjNeJ|A@3=y7o^6LMBj@LkTyHm7pK(Vxq%M=uXr;M7{wWsrG~I1ki5OQ6#92Ih%Quj|8Z|qUzyy6 zUf%s*-I*73e%AX}cTI5r+ZsgVR1jr6I*hnu%*rSWqzs(T0KD7A4U}76 z)lH{eBF=pRy0q*o<*iM4@ojv65`y{#TKm=!5+7PwC>z)to^he4BI9`z60IYcFC8XC zZ<65C;OV<=0*{u4*i@nn?J4m6_p_jauY-;RSof^%yxer|uPQvyzOCP1x_-}6H;)~6 zkQH$^6A(lu&B^q)5vwSypjGu5P`Y#UdzM%Uhuh>vlisoS7c?a}|1hah-vo_i`e5;! z93hb``au;ow+t;(wB3-=ww(pgb`ZrEODvFvfEiQvXaSX6+A0ooWdEx3u-oBf9V((3iwRO z7r|AqsNjl$(oTUVvOf^E%G%WX=xJnm>@^c!%RBGy7j<>%w26$G5`?s89=$6leu-z; zm&YocPl2@2EDw6AVuSU&r>cR{&34@7`cLYzqnX)TU_5wibwZ+NC5dMyxz3f!>0(Y zJDdZUg*VS5udu>$bd~P>Zq^r)bO{ndzlaMiO5{7vEWb3Jf#FOpb7ZDmmnP?5x?`TX z@_zlHn)+{T;BtNeJ1Kdp2+u!?dDx4`{9omcB_-%HYs2n5W-t74WV76()dbBN+P)HN zEpCJy82#5rQM+vTjIbX*7<~F)AB_%L*_LL*fW-7b@ATWT1AoUpajnr9aJ19 zmY}jSdf+bZ;V~9%$rJ-wJ3!DTQ3``rU@M~E-kH$kdWfBiS8QL&(56OM&g*O73qNi( zRjq8{%`~n?-iv!fKL>JDO7S4!aujA}t+u6;A0sxCv_hy~Y2Pbe53I*A1qHMYgSCj0z6O zJ!z}o>nI#-@4ZvRP|M!GqkTNYb7Y)$DPWBF3NCjNU-395FoDOuM6T+OSEwNQn3C`D z-I}Tw$^1)2!XX+o@sZp^B4*!UJ=|lZi63u~M4Q%rQE`2}*SW$b)?||O1ay`#&Xjc! z0RB3AaS%X&szV$SLIsGT@24^$5Z8p%ECKsnE92`h{xp^i(i3o%;W{mjAQmWf(6O8A zf7uXY$J^4o{w}0hV)1am8s1awoz0g%hOx4-7 zx8o@8k%dNJ(lA#*fC+}@0ENA#RLfdZB|fY9dXBb;(hk%{m~8J)QQ7CO5zQ4|)Jo4g z67cMld~VvYe6F!2OjfYz?+gy}S~<7gU@;?FfiET@6~z&q*ec+5vd;KI!tU4``&reW zL3}KkDT;2%n{ph5*uxMj0bNmy2YRohzP+3!P=Z6JA*Crjvb+#p4RTQ=sJAbk@>dP^ zV+h!#Ct4IB`es)P;U!P5lzZCHBH#Q(kD*pgWrlx&qj1p`4KY(+c*Kf7$j5nW^lOB#@PafVap`&1;j9^+4;EDO%G9G4gK zBzrL7D#M1;*$YefD2I-+LH{qgzvY8#|K=-X`LN578mTYqDhU}$>9W&VOs z*wW$@o?Vfqr4R0v4Yo_zlb?HKOFS zU@WY7^A8Y{P)qU9gAz52zB8JHL`Ef!)aK7P)8dct2GxC*y2eQV4gSRoLzW*ovb>hR zb0w+7w?v6Q5x1@S@t%$TP0Wiu2czDS*s8^HFl3HOkm{zwCL7#4wWP6AyUGp_WB8t8 zon>`pPm(j}2I7<SUzI=fltEbSR`iSoE1*F3pH4`ax^yEo<-pi;Os;iXcNrWfCGP^Jmp935cN;!T8bve@Qljm z>3ySDAULgN1!F~X7`sAjokd_;kBL99gBC2yjO+ zEqO##8mjsq`|9xpkae&q&F=J#A}#1%b%i3jK-lptc_O$uVki1KJ?Y=ulf*D$sa)HC z=vNki?1aP~%#31<#s+6US0>wX5}nI zhec(KhqxFhhq%8hS?5p|OZ02EJsNPTf!r5KKQB>C#3||j4cr3JZ%iiKUXDCHr!!{g z=xPxc@U28V8&DpX-UCYz*k~2e)q?lRg<{o%1r;+U)q^{v&abJ9&nc6a32ft(Yk}`j ztiQP@yEKf@Nu3F;yo9O})Roh9P08j7@%ftn7U1y;`mard4+5 zB62wpg$Py_YvQ!PE2HpuC}3el-F3g{*&a z3q{eLy6Xz|F+aMrn8R8IW2NZu{tgsyc(>*TdV79@?V$jG(O+Iz2rnDBc|1cK8gR$Y zthvVTI;(eYhOdjapHe=9KI`|2i;{VIfvnR6`qof=4a=(BTZkev78+6GJW**Z!|yvS zes)T%U573C~Hm`&XJzE=2t7tFIZM`!^r^&z;W?dOj-N+a10^>wV(l~2naa?s; zTxU{z;Go|Ve!vUjUrZ$B#mWH)NSdxi;dWa-@w)-$wBOpo`DEG<;C#W||W}&@z>C`*j9V|`ai)z*2PG`TZt6T{a zj!#m3`Vz5R9wJkNMsJ1`fSCS2mHnizWDT!G0Ukp$%*_^X1=k=%mmO$^_0_d|kc8ek4_DZwomL(>GGtfEB)Wy&cfZ@9-T|hAq&fx;XR$$_yl6iogcR{u zm9g)axS6=_IL4=wQXf|EkzO68$Ms4*JXAt8gFxLCibt^C#C|I|v|U{%A;+NaBX-Yn z`HAmP*x5Ux@@Wkpxest$F~K8v0wlb9$3gHoPU(RMt+!BfjH?`8>KMK|!{28+fAk%6 zWdfyaD;Dr~`aJHn0}HIf^Y9*keGvm6!t?o%;je)wm`Dm$fN?YtdPI7S=Y23+15L{J zr;n3MYg`<50nW^`BM$&M(+PQ7@p7Lvn(kE`cmoNS7UkQmfvXQBs_unhdfM){k`Ho! zHL0#a6}Uzs=(bu;jnBAu>}%LzU3+{sDa6~)q_|pW1~*Is5J(~!lWvX(NpK_$=3Rbn zej|)%uR0imC;D5qF7p}kdg(-e{8#o!D_}?Fa<&{!5#8^b(dQl40ES%O_S(k8Z$?Hs z;~ee=^2*5S#A*gzEJgBkXyn*|;BBH97OOmvaZ>&U&RfU0P(?jgLPyFzybR2)7wG`d zkkwi) zJ^sn7D-;I;%VS+>JLjS6a2bmmL^z^IZTokqBEWpG=9{ zZ@<^lIYqt3hPZgAFLVv6uGt}XhW&^JN!ZUQ|IO5fq;G|b|H@nr{(q!`hDI8ss7%C$ zL2}q02v(8fb2+LAD>BvnEL8L(UXN0um^QCuG@s}4!hCn@Pqn>MNXS;$oza~}dDz>J zx3WkVLJ22a;m4TGOz)iZO;Era%n#Tl)2s7~3%B<{6mR!X`g^oa>z#8i)szD%MBe?uxDud2It3SKV>?7XSimsnk#5p|TaeZ7of*wH>E{djABdP7#qXq- z7iLK+F>>2{EYrg>)K^JAP;>L@gIShuGpaElqp)%cGY2UGfX1E;7jaP6|2dI@cYG%4 zr`K1dRDGg3CuY~h+s&b2*C>xNR_n>ftWSwQDO(V&fXn=Iz`58^tosmz)h73w%~rVOFitWa9sSsrnbp|iY8z20EdnnHIxEX6||k-KWaxqmyo?2Yd?Cu$q4)Qn8~hf0=Lw#TAuOs(*CwL085Qn9qZxg=)ntN*hVHrYCF3cuI2CJk7zS2a%yTNifAL{2M>vhQxo?2 zfu8%hd1$q{Sf0+SPq8pOTIzC&9%Ju9Rc1U9&yjGazlHEDaxY|nnS7rATYCW_NA&U? zN!7-zF#DXu0}k4pjN05yu#>x8o#Jx7|Fk=%OR((ti%UVKWQNH>+JhH#ziW1hD=rk* zD#1j?WuGxd-8VqG@n_Lqj^i=VBOg@GLePo0oHX9P*e7qBzIs1lzyp;}L3tP1 zl5;OiHG&-flQ;rYznH%~hz>fuJ!n*H#O)3NM3`3Z9H|VFfS-_xHRCuLjoIS9wT!F0 zJ-kV3w>7EguDzoBPxW>Rra0#+Y?;Woi7qJ1kpxTad?O?^=1cG@GeNtRZRi8_l-1CS z`(#oF<;VYR(l(gHIYH$y2=rj5m3QL{HQgbW9O!TU*jGj!bFazIL?MYnJEvELf}=I5 zTA6EhkHVTa0U#laMQ6!wT;4Tm4_gN$lp?l~w37UJeMInp}P>2%3b^Pv_E1wcwh zI$`G-I~h!*k^k!)POFjjRQMq+MiE@Woq$h3Dt8A%*8xj1q#x?x%D+o3`s*)JOj2oD7-R4Z*QKknE3S9x z8yA8NsVl&>T`a;qPP9b7l{gF&2x9t5iVUdV-yOC12zJnqe5#5wx0so2I)@8xb$uPG zNmv=X)TjpHG(H!$6Xp>)*S}r538R99Y{Pofv}pAFlUK;xi{E43^->z1srWR=J$8N! z4jRu;EAiLG9R$5#{gR){5?o^W^!t140^f=vCVSs@vK7#`-fv`P*WV|>nX610pK08< z>r#{r)fR?2pNG}8o)?uvX#UJI)YM5CG@0E8s1lEV`rom|kBmf={%h!o|26a=lNJbX z6gkBS7e{-p$-Vubn$(l_IbwS02j;+6h2Q5F7P?Du2N!r;Ql$M>S7Frf*r3M`!bvWU zbTgl2p}E<*fv?`N8=B71Dk03J=K@EEQ^|GY*NoHaB~(}_ zx`Su{onY@5(Owc#f`!=H`+_#I<0#PTT9kxp4Ig;Y4*Zi>!ehJ3AiGpwSGd<{Q7Ddh z8jZ(NQ*Nsz5Mu_F_~rtIK$YnxRsOcP-XzNZ)r|)zZYfkLFE8jK)LV-oH{?#)EM%gW zV^O7T z0Kmc1`!7m_~ zJl!{Cb80G#fuJa1K3>!bT@5&ww_VSVYIh_R#~;If$43z`T4-@R=a1Px7r@*tdBOTw zj-VzI{klG5NP!tNEo#~KLk(n`6CMgiinc1-i79z$SlM+eaorY!WDll+m6%i+5_6Mc zf#5j#MYBbY)Z#rd21gtgo3y@c(zQVYaIYKI%y2oVzbPWm;IE#Cw$8O$fV}v}S%QDA zkwxW{fa#Goh1O|+=CF3h3DWNw+L^ly?BNQ7DY~Eca}5nt^>p#3cc9s3iDub0nh`Wy z?oH|dW8-HG@d5E@U>NWPjnhTjr7C${Iwj#;F2G@++N=Y2tjV;z57RNgE|kXQC)1h- zx8ODU>kk};J8KiSUx5jSsA_XPou1OH8=R~q9{`r>VnHkU6A=!zNOH8IGJoO!+bQys zDS2-H(7+Jfe+&zf#;OSV=83I|^M;0`Kv*#4%%O7x>@BgGMU*@ajUvY>cYw^`*jm@+ z{LZ2lr{OTMoQXn2XUsK-l72oysi9vgV4Sux^1GsW6zTV;?p#J06EvSVyUq5$f4kq< z{Chq5Z?I%ZW}6&uL+f&0uCW#^LyL!Ac2*QRII5TDGfZ43YpXyS^9%6HBqqog$Sal3 zJjI$J+@}ja9Xp)Bnbk+pi=*ZAHN}8q@g$$g<6_4?ej&Rw)I%w(%jgGlS5dTHN`9(^<}Hg zD$PbZX+X>;$v4NjGJxMDvVBiIam$cP-;h0YqQ{YgxYn-g&!}lHgaG3^B=>Z!D*7tp zu19e;r`u*+@4h41Da&NZv$qy-i6#DdI)EVvmKO*PvIKz-9E5R*k#|`$zJza8QJ)Q{ zf~Vl+I=8oaq)K!lL7Et5ycH;m&LKIvC|z4FH5bo|>#Kg5z+Jy*8Ifai}5A#%@)TgPRaC4f>Qk&} z4WciN&V(T~u^xBgH=iP(#nd;_@L&`7FUF>Qm-;hOljv(!74f&if;fz2Mg=b%^8$^C zna!2I&iCz&9I5ckX-5mVoAwz~)_&b#&k$e+pp=U2q-OjkS@yZ8ly1$2Vh?}yF0={P zPd3O@g{0L=eT-Dm9?imeUP(!As&DJ_D=5lwQ=3)XWXg)12CoB=-g-HX9RSXgL;yo0 z?$7z8Sy9w?DvA^u`Fnl7r_J&_jJ7claq*2l9E~#iJIWAPXuAHfmF3-4YjFYhOXkNJ zVz8BS_4KCUe68n{cPOTTuD<#H&?*|ayPR2-eJ2U0j$#P!>fhd(LXM>b_0^Gm27$;s ze#JTrkdpb*ws{iJ1jprw#ta&Lz6OjSJhJgmwIaVo!K}znCdX>y!=@@V_=VLZlF&@t z!{_emFt$Xar#gSZi_S5Sn#7tBp`eSwPf73&Dsh52J3bXLqWA`QLoVjU35Q3S4%|Zl zR2x4wGu^K--%q2y=+yDfT*Ktnh#24Sm86n`1p@vJRT|!$B3zs6OWxGN9<}T-XX>1; zxAt4#T(-D3XwskNhJZ6Gvd?3raBu$`W+c(+$2E{_E_;yghgs~U1&XO6$%47BLJF4O zXKZLVTr6kc$Ee0WUBU0cw+uAe!djN=dvD*scic%t)0Jp*1& zhjKqEK+U~w93c<~m_Oh;HX{|zgz=>@(45=Ynh{k#3xlfg!k z>hsq90wPe(!NljYbnuL6s`Z!wQSL8|(A*@M8K>`nPJ<9Hb^ zB6o?#^9zP>3hp0>JAite*3N?Rm>nJ1Lpq4)eqSe8KM_f(0DB?k8DNN6(3 zU#>-{0}3~vYJ7iIwC?Zbh@aJ8kfIvY%RveZltThMN73#Ew}jOwVw+|vU5u-wMoo9C zO(tv#&5`DOhlzunPV?M~qlM|K74x4cBC_AC?2GNw_-Uv&QtPOj(7L4NtVh$`J%xci zioGVvj5s|GY886)(}g`4WS3_%%PrF(O|s-n&-SdfbssL`!Gi7Hrz_r$IO@*$1fYbQ zgdp6?(IUaNPaH7}0%U|9X8HFonsJRrVwfmf*o1;k0+PwV^i%f7U{LAayu`!x*FmhN za(#a^@Idw9)jN)K!=sFC(G)ZNaYY169*IJ_ouY9>W8tC>S&MEp$+7 zy)NFumpuE>=7T@`j}8pa)MGpJaZoG(Ex3AzzH>gUU^eyWp*N2Fx+9*4k~BU;lQ1PG zj4)_JlelzJ==t*7=n2(}B4^^bqqcKFcJ7yVzbH_CWK?{eXdpKm);4|o{aM=M&`E$=_~PVi2>>L zKTN_x&qA)@ak=v=0Hl5H6~?LOfO@1+fu5(sB|VWID)w?%{m+n#7bLaszEJ#;$HMdt z9qP0gk)hIYvE1!jseA^FGTyK=i4eTPjTL$R;6FywMBZBPlh2ar9!8wlj1sinLF-1g zR5}hLq>pb1|AC-WcF!38e*kFv|9n<$etuB=xE%B=PUs}iVFl>m;BiWUqRIxYh7}L&2w@{SS-t(zUp`wLWAyO=PEE=Ekvn@YS*K@($=i zBkTMaH<&cAk${idNy0KZ8xh}u;eAl*tstdM8DYnM5N;bDa`AB+(8>DqX+mj17R2xBp45UES|H*#GHb_%Nc{xWs7l{0pqmiBIPe@r=X%Y-h<-Ceo;4I>isrw1Hd zZd*VjT`H9gxbf{b3krEKNAaV$k>SzK(gzv}>;byq##WEhzTN^@B4+VJvW>y|U}}AQ z4^Bdz9%QKBWCy+h$I?L@ffl{fLLL41Tx|M+NjjRf(`KjHG4^y=x3l z!!-{*v7_^6MiJOC@C$WV=hz9J^Y^lK9#tzs6}-

Gn4F+B~IivciU9^t0j-Mgao3 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 DDw4oP diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e2847c8..002b867 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradlew b/gradlew index f5feea6..23d15a9 100755 --- a/gradlew +++ b/gradlew @@ -86,8 +86,7 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s -' "$PWD" ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum @@ -115,7 +114,7 @@ case "$( uname )" in #( NONSTOP* ) nonstop=true ;; esac -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar +CLASSPATH="\\\"\\\"" # Determine the Java command to use to start the JVM. @@ -206,7 +205,7 @@ fi DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, # and any embedded shellness will be escaped. # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be # treated as '${Hostname}' itself on the command line. @@ -214,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' set -- \ "-Dorg.gradle.appname=$APP_BASE_NAME" \ -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ "$@" # Stop when "xargs" is not available. diff --git a/gradlew.bat b/gradlew.bat index 9d21a21..db3a6ac 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -70,11 +70,11 @@ goto fail :execute @rem Setup the command line -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar +set CLASSPATH= @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* :end @rem End local scope for the variables with windows NT shell diff --git a/hypo-asm/build.gradle.kts b/hypo-asm/build.gradle.kts index c34b624..0f887e8 100644 --- a/hypo-asm/build.gradle.kts +++ b/hypo-asm/build.gradle.kts @@ -16,29 +16,32 @@ repositories { } dependencies { - api(projects.hypoCore) + compileOnlyApi(libs.annotations) api(libs.bundles.asm) + api(libs.slf4j.api) - testImplementation(projects.hypoTest) -} - -tasks.compileTestJava { - options.release = 21 -} + api(projects.hypoCore) + api(projects.hypoModel) + api(projects.hypoTypes) -tasks.jar { - manifest { - attributes( - "Automatic-Module-Name" to "dev.denwav.hypo.asm" - ) - } + testImplementation(projects.hypoTest) } hypoJava { javadocLibs.add(libs.annotations) - javadocLibs.add(libs.errorprone.annotations) javadocLibs.addAll(libs.bundles.asm) - javadocProjects.addAll(projects.hypoCore, projects.hypoModel) + javadocLibs.add(libs.slf4j.api) + javadocProjects.addAll(projects.hypoCore, projects.hypoModel, projects.hypoTypes) + + patchJavadocList.register("org.objectweb.asm") { + library.set(libs.asm.core) + } + patchJavadocList.register("org.objectweb.asm.tree") { + library.set(libs.asm.tree) + } + patchJavadocList.register("org.slf4j") { + library.set(libs.slf4j.api) + } } hypoPublish { diff --git a/hypo-asm/hypo-asm-hydrate/build.gradle.kts b/hypo-asm/hypo-asm-hydrate/build.gradle.kts index e5c1285..3e50acc 100644 --- a/hypo-asm/hypo-asm-hydrate/build.gradle.kts +++ b/hypo-asm/hypo-asm-hydrate/build.gradle.kts @@ -6,6 +6,10 @@ plugins { } dependencies { + compileOnlyApi(libs.annotations) + + api(projects.hypoTypes) + api(projects.hypoModel) api(projects.hypoCore) api(projects.hypoAsm) api(projects.hypoHydrate) @@ -21,10 +25,7 @@ tasks.jar { hypoJava { javadocLibs.add(libs.annotations) - javadocLibs.add(libs.errorprone.annotations) - javadocLibs.add(libs.jgrapht) - javadocLibs.addAll(libs.bundles.asm) - javadocProjects.addAll(projects.hypoAsm, projects.hypoHydrate, projects.hypoCore, projects.hypoModel) + javadocProjects.addAll(projects.hypoAsm, projects.hypoHydrate, projects.hypoCore, projects.hypoModel, projects.hypoTypes) } hypoPublish { diff --git a/hypo-asm/hypo-asm-hydrate/src/main/java/dev/denwav/hypo/asm/hydrate/BridgeMethodHydrator.java b/hypo-asm/hypo-asm-hydrate/src/main/java/dev/denwav/hypo/asm/hydrate/BridgeMethodHydrator.java index f3089b5..33bc927 100644 --- a/hypo-asm/hypo-asm-hydrate/src/main/java/dev/denwav/hypo/asm/hydrate/BridgeMethodHydrator.java +++ b/hypo-asm/hypo-asm-hydrate/src/main/java/dev/denwav/hypo/asm/hydrate/BridgeMethodHydrator.java @@ -26,7 +26,7 @@ import dev.denwav.hypo.model.data.ClassData; import dev.denwav.hypo.model.data.HypoKey; import dev.denwav.hypo.model.data.MethodData; -import dev.denwav.hypo.model.data.MethodDescriptor; +import dev.denwav.hypo.types.desc.MethodDescriptor; import java.io.IOException; import java.util.HashSet; import java.util.List; @@ -152,8 +152,8 @@ public void hydrate(@NotNull AsmMethodData data, @NotNull HypoContext context) t } // The descriptors need to be the same size - final MethodDescriptor invokeDesc = MethodDescriptor.parseDescriptor(invoke.desc); - if (data.params().size() != invokeDesc.getParams().size()) { + final MethodDescriptor invokeDesc = MethodDescriptor.parse(invoke.desc); + if (data.params().size() != invokeDesc.getParameters().size()) { return; } diff --git a/hypo-asm/hypo-asm-hydrate/src/main/java/dev/denwav/hypo/asm/hydrate/LambdaCallHydrator.java b/hypo-asm/hypo-asm-hydrate/src/main/java/dev/denwav/hypo/asm/hydrate/LambdaCallHydrator.java index 2a61ea7..67b46c7 100644 --- a/hypo-asm/hypo-asm-hydrate/src/main/java/dev/denwav/hypo/asm/hydrate/LambdaCallHydrator.java +++ b/hypo-asm/hypo-asm-hydrate/src/main/java/dev/denwav/hypo/asm/hydrate/LambdaCallHydrator.java @@ -27,8 +27,8 @@ import dev.denwav.hypo.model.data.ClassData; import dev.denwav.hypo.model.data.HypoKey; import dev.denwav.hypo.model.data.MethodData; -import dev.denwav.hypo.model.data.MethodDescriptor; -import dev.denwav.hypo.model.data.types.JvmType; +import dev.denwav.hypo.types.desc.MethodDescriptor; +import dev.denwav.hypo.types.desc.TypeDescriptor; import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -44,7 +44,6 @@ import org.objectweb.asm.tree.VarInsnNode; import static dev.denwav.hypo.asm.HypoAsmUtil.toDescriptor; -import static dev.denwav.hypo.model.data.MethodDescriptor.parseDescriptor; /** * This is a {@link HydrationProvider} for determining lambda expressions present in methods, and the @@ -135,8 +134,8 @@ public void hydrate(final @NotNull AsmMethodData data, final @NotNull HypoContex continue; } - final MethodDescriptor desc = parseDescriptor(dyn.desc); - final List<@NotNull JvmType> params = desc.getParams(); + final MethodDescriptor desc = MethodDescriptor.parse(dyn.desc); + final List params = desc.getParameters(); final int paramsSize = params.size(); final int[] closureIndices = new int[paramsSize]; @@ -157,7 +156,7 @@ public void hydrate(final @NotNull AsmMethodData data, final @NotNull HypoContex } } - final MethodData targetMethod = owner.method(handle.getName(), parseDescriptor(handle.getDesc())); + final MethodData targetMethod = owner.method(handle.getName(), MethodDescriptor.parse(handle.getDesc())); if (targetMethod == null) { continue; } diff --git a/hypo-asm/hypo-asm-hydrate/src/main/java/dev/denwav/hypo/asm/hydrate/LocalClassHydrator.java b/hypo-asm/hypo-asm-hydrate/src/main/java/dev/denwav/hypo/asm/hydrate/LocalClassHydrator.java index 5f3188e..af2ad43 100644 --- a/hypo-asm/hypo-asm-hydrate/src/main/java/dev/denwav/hypo/asm/hydrate/LocalClassHydrator.java +++ b/hypo-asm/hypo-asm-hydrate/src/main/java/dev/denwav/hypo/asm/hydrate/LocalClassHydrator.java @@ -30,7 +30,7 @@ import dev.denwav.hypo.model.data.FieldData; import dev.denwav.hypo.model.data.HypoKey; import dev.denwav.hypo.model.data.MethodData; -import dev.denwav.hypo.model.data.types.JvmType; +import dev.denwav.hypo.types.desc.TypeDescriptor; import java.io.IOException; import java.util.ArrayList; import java.util.Iterator; @@ -183,7 +183,7 @@ private void findNewCalls(final MethodData method, final ArrayList } private int @Nullable [] handleNestedConst(final MethodInsnNode insn, final AsmClassData nestedClass) { - final ArrayList capturedVariables = new ArrayList<>(); + final ArrayList capturedVariables = new ArrayList<>(); for (final FieldData field : nestedClass.fields()) { if (!field.isSynthetic()) { continue; @@ -192,7 +192,7 @@ private void findNewCalls(final MethodData method, final ArrayList continue; } if (field.name().startsWith("val$")) { - capturedVariables.add(field.fieldType()); + capturedVariables.add(field.descriptor()); } } diff --git a/hypo-asm/hypo-asm-hydrate/src/main/java/dev/denwav/hypo/asm/hydrate/SuperConstructorHydrator.java b/hypo-asm/hypo-asm-hydrate/src/main/java/dev/denwav/hypo/asm/hydrate/SuperConstructorHydrator.java index 06c833f..e849173 100644 --- a/hypo-asm/hypo-asm-hydrate/src/main/java/dev/denwav/hypo/asm/hydrate/SuperConstructorHydrator.java +++ b/hypo-asm/hypo-asm-hydrate/src/main/java/dev/denwav/hypo/asm/hydrate/SuperConstructorHydrator.java @@ -28,9 +28,9 @@ import dev.denwav.hypo.model.data.ConstructorData; import dev.denwav.hypo.model.data.HypoKey; import dev.denwav.hypo.model.data.MethodData; -import dev.denwav.hypo.model.data.MethodDescriptor; -import dev.denwav.hypo.model.data.types.JvmType; -import dev.denwav.hypo.model.data.types.PrimitiveType; +import dev.denwav.hypo.types.PrimitiveType; +import dev.denwav.hypo.types.desc.MethodDescriptor; +import dev.denwav.hypo.types.desc.TypeDescriptor; import java.io.IOException; import java.util.ArrayDeque; import java.util.ArrayList; @@ -125,11 +125,10 @@ private void hydrate0(final @NotNull AsmConstructorData data) throws IOException throw new IllegalStateException("Could not determine owner of super method"); } - final MethodData targetMethod = targetClass.method("", MethodDescriptor.parseDescriptor(desc)); - if (!(targetMethod instanceof ConstructorData)) { + final MethodData targetMethod = targetClass.method("", MethodDescriptor.parse(desc)); + if (!(targetMethod instanceof final ConstructorData targetConstructor)) { throw new IllegalStateException("Target constructor is not an instance of " + ConstructorData.class.getName()); } - final ConstructorData targetConstructor = (ConstructorData) targetMethod; final ArrayList superCallParams = new ArrayList<>(); final SuperCall superCallData = new SuperCall(data, targetConstructor, superCallParams); @@ -156,7 +155,7 @@ private void hydrate0(final @NotNull AsmConstructorData data) throws IOException if (arg instanceof Variable) { varArgument = (Variable) arg; simpleArg = true; - } else if (arg instanceof MethodCall) { + } else if (arg instanceof final MethodCall subCall) { // We will still match the name if a constructor parameter is the only argument passed to a method // This could be for example where the subclass calls a method to transform the input, but it's // still the same input. For example maybe something like: @@ -170,7 +169,6 @@ private void hydrate0(final @NotNull AsmConstructorData data) throws IOException // public SomeClass(String s) { // super(s.toUppercase()); // } - final MethodCall subCall = (MethodCall) arg; if (subCall.args.size() != 1 && !(subCall.receiver instanceof Variable)) { continue; } @@ -476,7 +474,7 @@ private void hydrate0(final @NotNull AsmConstructorData data) throws IOException } private static int @NotNull [] buildParamIndexMapping(final @NotNull MethodData data) throws IOException { - final List<@NotNull JvmType> targetParams = data.params(); + final List targetParams = data.params(); final int[] outputParamIndices = new int[targetParams.size()]; int currentIndex = 0; int currentTargetIndex = 1; // `this` is 0 @@ -485,7 +483,7 @@ private void hydrate0(final @NotNull AsmConstructorData data) throws IOException currentTargetIndex++; // index 1 is the outer class } - for (final JvmType paramType : targetParams) { + for (final TypeDescriptor paramType : targetParams) { outputParamIndices[currentIndex] = currentTargetIndex; currentIndex++; currentTargetIndex++; @@ -514,6 +512,7 @@ interface MethodCallArgument {} *

This is mainly what we're looking to keep track of in {@link MethodCall}. This will tell us the LVT index * corresponding to the method call index. */ +@SuppressWarnings("ClassCanBeRecord") final class Variable implements MethodCallArgument { /** * Model for {@link SuperConstructorHydrator}. @@ -533,6 +532,7 @@ final class Variable implements MethodCallArgument { /** * Model for {@link SuperConstructorHydrator}. */ +@SuppressWarnings("ClassCanBeRecord") final class FieldAccess implements MethodCallArgument { /** * Model for {@link SuperConstructorHydrator}. @@ -571,6 +571,7 @@ final class Constant implements MethodCallArgument { * *

{@code new} expressions. */ +@SuppressWarnings("ClassCanBeRecord") final class NewCall implements MethodCallArgument { /** * Model for {@link SuperConstructorHydrator}. diff --git a/hypo-asm/hypo-asm-hydrate/src/main/java/module-info.java b/hypo-asm/hypo-asm-hydrate/src/main/java/module-info.java new file mode 100644 index 0000000..1dfd528 --- /dev/null +++ b/hypo-asm/hypo-asm-hydrate/src/main/java/module-info.java @@ -0,0 +1,31 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +/** + * {@link dev.denwav.hypo.asm ASM} implementation of the {@link dev.denwav.hypo.hydrate hydrate} module. + */ +module dev.denwav.hypo.asm.hydrator { + requires transitive dev.denwav.hypo.asm; + requires transitive dev.denwav.hypo.hydrate; + + requires static transitive org.jetbrains.annotations; + + requires org.slf4j; + + exports dev.denwav.hypo.asm.hydrate; +} diff --git a/hypo-asm/hypo-asm-test-data/build.gradle.kts b/hypo-asm/hypo-asm-test-data/build.gradle.kts index adb7219..1554846 100644 --- a/hypo-asm/hypo-asm-test-data/build.gradle.kts +++ b/hypo-asm/hypo-asm-test-data/build.gradle.kts @@ -4,11 +4,18 @@ plugins { `hypo-test-scenario-data` } +// 21 by default tasks.withType().configureEach { + javaCompiler = javaToolchains.compilerFor { + languageVersion = JavaLanguageVersion.of(21) + } options.release = 21 } tasks.compileScenario01Java { + javaCompiler = javaToolchains.compilerFor { + languageVersion = JavaLanguageVersion.of(11) + } options.release = 11 } @@ -18,11 +25,3 @@ tasks.compileScenario03Java { } options.release = 17 } - -tasks.compileScenario12Java { - javaCompiler = javaToolchains.compilerFor { - languageVersion = JavaLanguageVersion.of(21) - } - options.release = 21 -} - diff --git a/hypo-asm/hypo-asm-test-data/src/scenario-03/java/scenario03/TestClass.java b/hypo-asm/hypo-asm-test-data/src/scenario-03/java/scenario03/TestClass.java index 925f547..171e727 100644 --- a/hypo-asm/hypo-asm-test-data/src/scenario-03/java/scenario03/TestClass.java +++ b/hypo-asm/hypo-asm-test-data/src/scenario-03/java/scenario03/TestClass.java @@ -2,7 +2,7 @@ import java.util.function.IntSupplier; -// Compiled with JDK 21 +// Compiled with JDK 17 public class TestClass { public void doSwitch(SomeEnum someEnum) { diff --git a/hypo-asm/hypo-asm-test-data/src/scenario-04/java/scenario04/TestClass.java b/hypo-asm/hypo-asm-test-data/src/scenario-04/java/scenario04/TestClass.java index d3591ff..38d7517 100644 --- a/hypo-asm/hypo-asm-test-data/src/scenario-04/java/scenario04/TestClass.java +++ b/hypo-asm/hypo-asm-test-data/src/scenario-04/java/scenario04/TestClass.java @@ -1,5 +1,6 @@ package scenario04; +// Compiled with JDK 21 public sealed interface TestClass {} final class TestSubClass implements TestClass {} diff --git a/hypo-asm/src/main/java/dev/denwav/hypo/asm/AsmClassData.java b/hypo-asm/src/main/java/dev/denwav/hypo/asm/AsmClassData.java index 2527cd2..5a82e2c 100644 --- a/hypo-asm/src/main/java/dev/denwav/hypo/asm/AsmClassData.java +++ b/hypo-asm/src/main/java/dev/denwav/hypo/asm/AsmClassData.java @@ -27,6 +27,8 @@ import dev.denwav.hypo.model.data.LazyClassData; import dev.denwav.hypo.model.data.MethodData; import dev.denwav.hypo.model.data.Visibility; +import dev.denwav.hypo.types.desc.MethodDescriptor; +import dev.denwav.hypo.types.sig.ClassSignature; import java.io.IOException; import java.util.ArrayList; import java.util.EnumSet; @@ -44,7 +46,6 @@ import org.objectweb.asm.tree.RecordComponentNode; import static dev.denwav.hypo.asm.HypoAsmUtil.toJvmType; -import static dev.denwav.hypo.model.data.MethodDescriptor.parseDescriptor; import static org.objectweb.asm.Type.getType; /** @@ -82,6 +83,16 @@ public AsmClassData(final @NotNull ClassNode node) { return this.node.name; } + @Override + public @Nullable ClassSignature computeSignature() { + final String sig = this.node.signature; + if (sig != null) { + return ClassSignature.parse(sig); + } else { + return null; + } + } + @Override public @Nullable ClassData computeOuterClass() throws IOException { // Simple case (anonymous classes & local classes) @@ -114,7 +125,7 @@ public boolean computeStaticInnerClass() { throw HypoModelUtil.rethrow(e); } if (outerClass != null) { - final MethodData outerMethod = outerClass.method(this.node.outerMethod, parseDescriptor(this.node.outerMethodDesc)); + final MethodData outerMethod = outerClass.method(this.node.outerMethod, MethodDescriptor.parse(this.node.outerMethodDesc)); if (outerMethod != null) { return outerMethod.isStatic(); } diff --git a/hypo-asm/src/main/java/dev/denwav/hypo/asm/AsmFieldData.java b/hypo-asm/src/main/java/dev/denwav/hypo/asm/AsmFieldData.java index 71fd106..adf9b6c 100644 --- a/hypo-asm/src/main/java/dev/denwav/hypo/asm/AsmFieldData.java +++ b/hypo-asm/src/main/java/dev/denwav/hypo/asm/AsmFieldData.java @@ -18,20 +18,21 @@ package dev.denwav.hypo.asm; -import dev.denwav.hypo.model.data.AbstractFieldData; import dev.denwav.hypo.model.data.ClassData; import dev.denwav.hypo.model.data.FieldData; +import dev.denwav.hypo.model.data.LazyFieldData; import dev.denwav.hypo.model.data.Visibility; -import dev.denwav.hypo.model.data.types.JvmType; +import dev.denwav.hypo.types.desc.TypeDescriptor; +import dev.denwav.hypo.types.sig.TypeSignature; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.objectweb.asm.Opcodes; -import org.objectweb.asm.Type; import org.objectweb.asm.tree.FieldNode; /** * Implementation of {@link FieldData} based on {@code asm}'s {@link FieldNode}. */ -public class AsmFieldData extends AbstractFieldData implements FieldData { +public class AsmFieldData extends LazyFieldData implements FieldData { private final @NotNull ClassData parentClass; private final @NotNull FieldNode node; @@ -55,11 +56,6 @@ public AsmFieldData(final @NotNull ClassData parentClass, final @NotNull FieldNo return this.node; } - @Override - public @NotNull JvmType fieldType() { - return HypoAsmUtil.toJvmType(Type.getType(this.node.desc)); - } - @Override public @NotNull Visibility visibility() { return HypoAsmUtil.accessToVisibility(this.node.access); @@ -89,4 +85,19 @@ public boolean isSynthetic() { public @NotNull ClassData parentClass() { return this.parentClass; } + + @Override + public @NotNull TypeDescriptor computeDescriptor() { + return TypeDescriptor.parse(this.node.desc); + } + + @Override + public @Nullable TypeSignature computeSignature() { + final String sig = this.node.signature; + if (sig != null) { + return TypeSignature.parse(sig); + } else { + return null; + } + } } diff --git a/hypo-asm/src/main/java/dev/denwav/hypo/asm/AsmMethodData.java b/hypo-asm/src/main/java/dev/denwav/hypo/asm/AsmMethodData.java index a5d9c6a..e99dd7f 100644 --- a/hypo-asm/src/main/java/dev/denwav/hypo/asm/AsmMethodData.java +++ b/hypo-asm/src/main/java/dev/denwav/hypo/asm/AsmMethodData.java @@ -21,9 +21,11 @@ import dev.denwav.hypo.model.data.ClassData; import dev.denwav.hypo.model.data.LazyMethodData; import dev.denwav.hypo.model.data.MethodData; -import dev.denwav.hypo.model.data.MethodDescriptor; import dev.denwav.hypo.model.data.Visibility; +import dev.denwav.hypo.types.desc.MethodDescriptor; +import dev.denwav.hypo.types.sig.MethodSignature; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import org.objectweb.asm.Opcodes; import org.objectweb.asm.tree.MethodNode; @@ -105,11 +107,26 @@ public boolean isStatic() { @Override public @NotNull MethodDescriptor computeDescriptor() { - return MethodDescriptor.parseDescriptor(this.node.desc); + return MethodDescriptor.parse(this.node.desc); + } + + @Override + public @Nullable MethodSignature computeSignature() { + final String sig = this.node.signature; + if (sig != null) { + return MethodSignature.parse(sig); + } else { + return null; + } } @Override public @NotNull String descriptorText() { return this.node.desc; } + + @Override + public @Nullable String signatureText() { + return this.node.signature; + } } diff --git a/hypo-asm/src/main/java/dev/denwav/hypo/asm/AsmOutputWriter.java b/hypo-asm/src/main/java/dev/denwav/hypo/asm/AsmOutputWriter.java index bd1bc14..d7c2ed1 100644 --- a/hypo-asm/src/main/java/dev/denwav/hypo/asm/AsmOutputWriter.java +++ b/hypo-asm/src/main/java/dev/denwav/hypo/asm/AsmOutputWriter.java @@ -41,6 +41,7 @@ /** * Implementation of {@link HypoOutputWriter} for {@link AsmClassDataProvider ASM providers}. */ +@SuppressWarnings("ClassCanBeRecord") public final class AsmOutputWriter implements HypoOutputWriter { private final @NotNull Path outputFile; @@ -75,8 +76,7 @@ public void write(final @NotNull HypoContext context) throws IOException { final ZipOutputStream zos = new ZipOutputStream(bos) ) { for (final ClassProviderRoot root : context.getProvider().roots()) { - if (root instanceof JarClassProviderRoot) { - final JarClassProviderRoot jarRoot = (JarClassProviderRoot) root; + if (root instanceof final JarClassProviderRoot jarRoot) { final Path jarFile = jarRoot.getJarFile(); try ( final InputStream in = Files.newInputStream(jarFile); diff --git a/hypo-asm/src/main/java/dev/denwav/hypo/asm/HypoAsmUtil.java b/hypo-asm/src/main/java/dev/denwav/hypo/asm/HypoAsmUtil.java index 69c42b0..a5cc349 100644 --- a/hypo-asm/src/main/java/dev/denwav/hypo/asm/HypoAsmUtil.java +++ b/hypo-asm/src/main/java/dev/denwav/hypo/asm/HypoAsmUtil.java @@ -18,12 +18,9 @@ package dev.denwav.hypo.asm; -import dev.denwav.hypo.model.data.MethodDescriptor; import dev.denwav.hypo.model.data.Visibility; -import dev.denwav.hypo.model.data.types.ArrayType; -import dev.denwav.hypo.model.data.types.ClassType; -import dev.denwav.hypo.model.data.types.JvmType; -import dev.denwav.hypo.model.data.types.PrimitiveType; +import dev.denwav.hypo.types.desc.MethodDescriptor; +import dev.denwav.hypo.types.desc.TypeDescriptor; import org.jetbrains.annotations.NotNull; import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; @@ -59,43 +56,18 @@ private HypoAsmUtil() {} } /** - * Map an {@code asm} {@link Type} object into a Hypo {@link JvmType}. + * Map an {@code asm} {@link Type} object into a Hypo {@link TypeDescriptor}. * - * @param type The {@code asm} {@link Type} to convert to a Hypo {@link JvmType}. - * @return A {@link JvmType} which matches the given {@link Type}. + * @param type The {@code asm} {@link Type} to convert to a Hypo {@link TypeDescriptor}. + * @return A {@link TypeDescriptor} which matches the given {@link Type}. */ @SuppressWarnings("ReferenceEquality") - public static @NotNull JvmType toJvmType(final @NotNull Type type) { + public static @NotNull TypeDescriptor toJvmType(final @NotNull Type type) { if (type.getSort() == Type.METHOD) { throw new IllegalArgumentException("Given type is a method descriptor: " + type); } - if (type == Type.CHAR_TYPE) { - return PrimitiveType.CHAR; - } else if (type == Type.BYTE_TYPE) { - return PrimitiveType.BYTE; - } else if (type == Type.SHORT_TYPE) { - return PrimitiveType.SHORT; - } else if (type == Type.INT_TYPE) { - return PrimitiveType.INT; - } else if (type == Type.LONG_TYPE) { - return PrimitiveType.LONG; - } else if (type == Type.FLOAT_TYPE) { - return PrimitiveType.FLOAT; - } else if (type == Type.DOUBLE_TYPE) { - return PrimitiveType.DOUBLE; - } else if (type == Type.BOOLEAN_TYPE) { - return PrimitiveType.BOOLEAN; - } else if (type == Type.VOID_TYPE) { - return PrimitiveType.VOID; - } - - final String desc = type.getDescriptor(); - if (desc.startsWith("[")) { - return new ArrayType(toJvmType(type.getElementType()), type.getDimensions()); - } else { - return new ClassType(desc); - } + return TypeDescriptor.parse(type.getDescriptor()); } /** @@ -108,6 +80,6 @@ private HypoAsmUtil() {} throw new IllegalArgumentException("Given type is not a method descriptor: " + type); } - return MethodDescriptor.parseDescriptor(type.getDescriptor()); + return MethodDescriptor.parse(type.getDescriptor()); } } diff --git a/hypo-asm/src/main/java/module-info.java b/hypo-asm/src/main/java/module-info.java new file mode 100644 index 0000000..4f6e5d8 --- /dev/null +++ b/hypo-asm/src/main/java/module-info.java @@ -0,0 +1,33 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +/** + * ASM-based implementation of {@code dev.denwav.hypo.model}. + */ +module dev.denwav.hypo.asm { + requires static transitive dev.denwav.hypo.core; + + requires static transitive org.jetbrains.annotations; + + requires org.objectweb.asm; + requires transitive org.objectweb.asm.tree; + + requires org.slf4j; + + exports dev.denwav.hypo.asm; +} diff --git a/hypo-asm/src/test/java/dev/denwav/hypo/asm/scenarios/Scenario03Test.java b/hypo-asm/src/test/java/dev/denwav/hypo/asm/scenarios/Scenario03Test.java index e63d986..c7592b6 100644 --- a/hypo-asm/src/test/java/dev/denwav/hypo/asm/scenarios/Scenario03Test.java +++ b/hypo-asm/src/test/java/dev/denwav/hypo/asm/scenarios/Scenario03Test.java @@ -20,10 +20,10 @@ import dev.denwav.hypo.model.data.FieldData; import dev.denwav.hypo.model.data.MethodData; -import dev.denwav.hypo.model.data.MethodDescriptor; -import dev.denwav.hypo.model.data.types.ClassType; -import dev.denwav.hypo.model.data.types.PrimitiveType; import dev.denwav.hypo.test.framework.TestScenarioBase; +import dev.denwav.hypo.types.PrimitiveType; +import dev.denwav.hypo.types.desc.ClassTypeDescriptor; +import dev.denwav.hypo.types.desc.MethodDescriptor; import java.io.IOException; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.DisplayName; @@ -33,7 +33,7 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; -@DisplayName("[asm] Scenario 03 - Synthetic classes and members (Java 17)") +@DisplayName("[asm] Scenario 03 - Synthetic classes and members (Java 21)") public class Scenario03Test extends TestScenarioBase { @Override @@ -49,11 +49,11 @@ public void testSyntheticMembers() { final var inner = this.findClass("scenario03/TestClass$Inner"); assertNotNull(inner); - final MethodData intSupplierSynth = testClass.method("lambda$intSupplier$0", MethodDescriptor.parseDescriptor("(Lscenario03/TestClass$Inner;)I")); + final MethodData intSupplierSynth = testClass.method("lambda$intSupplier$0", MethodDescriptor.parse("(Lscenario03/TestClass$Inner;)I")); assertNotNull(intSupplierSynth, "Did not find expected lambda$intSupplier$0 synthetic member in TestClass"); assertTrue(intSupplierSynth.isSynthetic()); - final FieldData innerSyntheticOuterThisField = inner.field("this$0", new ClassType("scenario03.TestClass")); + final FieldData innerSyntheticOuterThisField = inner.field("this$0", ClassTypeDescriptor.of("scenario03.TestClass")); assertNotNull(innerSyntheticOuterThisField, "Did not find expected this$0 synthetic member in TestClass$Inner"); assertTrue(innerSyntheticOuterThisField.isSynthetic()); } @@ -66,7 +66,7 @@ public void testNonSyntheticMembers() { final var inner = this.findClass("scenario03/TestClass$Inner"); assertNotNull(inner); - final MethodData intSupplier = testClass.method("intSupplier", MethodDescriptor.parseDescriptor("(Lscenario03/TestClass$Inner;)Ljava/util/function/IntSupplier;")); + final MethodData intSupplier = testClass.method("intSupplier", MethodDescriptor.parse("(Lscenario03/TestClass$Inner;)Ljava/util/function/IntSupplier;")); assertNotNull(intSupplier, "Did not find expected method intSupplier in TestClass"); assertFalse(intSupplier.isSynthetic()); diff --git a/hypo-asm/src/test/java/dev/denwav/hypo/asm/scenarios/Scenario05Test.java b/hypo-asm/src/test/java/dev/denwav/hypo/asm/scenarios/Scenario05Test.java index cb8928f..57b0a1d 100644 --- a/hypo-asm/src/test/java/dev/denwav/hypo/asm/scenarios/Scenario05Test.java +++ b/hypo-asm/src/test/java/dev/denwav/hypo/asm/scenarios/Scenario05Test.java @@ -24,13 +24,13 @@ import dev.denwav.hypo.hydrate.generic.LambdaClosure; import dev.denwav.hypo.model.data.MethodData; import dev.denwav.hypo.test.framework.TestScenarioBase; +import dev.denwav.hypo.types.desc.MethodDescriptor; import java.util.List; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import static dev.denwav.hypo.model.data.MethodDescriptor.parseDescriptor; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; @@ -66,9 +66,9 @@ public boolean includeJdk() { @BeforeEach public void setupRunnable() { this.runnableRun = this.findClass("java/lang/Runnable") - .method("run", parseDescriptor("()V")); + .method("run", MethodDescriptor.parse("()V")); this.functionApply = this.findClass("java/util/function/Function") - .method("apply", parseDescriptor("(Ljava/lang/Object;)Ljava/lang/Object;")); + .method("apply", MethodDescriptor.parse("(Ljava/lang/Object;)Ljava/lang/Object;")); } @Test @@ -77,14 +77,14 @@ public void testLambdaCalls() { final var testClass = this.findClass("scenario05/TestClass"); assertNotNull(testClass); - final MethodData testMethod = testClass.method("test", parseDescriptor("()V")); + final MethodData testMethod = testClass.method("test", MethodDescriptor.parse("()V")); assertNotNull(testMethod); final List methodClosures = testMethod.get(HypoHydration.LAMBDA_CALLS); assertNotNull(methodClosures); assertEquals(1, methodClosures.size()); - final LambdaClosure methodClosure = methodClosures.get(0); + final LambdaClosure methodClosure = methodClosures.getFirst(); assertNotNull(methodClosure); assertEquals(this.runnableRun, methodClosure.getInterfaceMethod()); final MethodData call = methodClosure.getLambda(); @@ -136,14 +136,14 @@ public void testStaticLambdaCalls() { final var testClass = this.findClass("scenario05/TestClass"); assertNotNull(testClass); - final MethodData testMethod = testClass.method("testStatic", parseDescriptor("()V")); + final MethodData testMethod = testClass.method("testStatic", MethodDescriptor.parse("()V")); assertNotNull(testMethod); final List methodClosures = testMethod.get(HypoHydration.LAMBDA_CALLS); assertNotNull(methodClosures); assertEquals(1, methodClosures.size()); - final LambdaClosure methodClosure = methodClosures.get(0); + final LambdaClosure methodClosure = methodClosures.getFirst(); assertNotNull(methodClosure); final MethodData call = methodClosure.getLambda(); assertNotNull(call); @@ -160,13 +160,13 @@ public void testFunction() { final var testClass = this.findClass("scenario05/TestClass"); assertNotNull(testClass); - final MethodData testMethod = testClass.method("testFunction", parseDescriptor("()V")); + final MethodData testMethod = testClass.method("testFunction", MethodDescriptor.parse("()V")); assertNotNull(testMethod); final List lambdas = testMethod.get(HypoHydration.LAMBDA_CALLS); assertNotNull(lambdas); - final LambdaClosure lambda = lambdas.get(0); + final LambdaClosure lambda = lambdas.getFirst(); assertNotNull(lambda); assertEquals(testMethod, lambda.getContainingMethod()); diff --git a/hypo-asm/src/test/java/dev/denwav/hypo/asm/scenarios/Scenario06Test.java b/hypo-asm/src/test/java/dev/denwav/hypo/asm/scenarios/Scenario06Test.java index 1ec1f76..5f19b83 100644 --- a/hypo-asm/src/test/java/dev/denwav/hypo/asm/scenarios/Scenario06Test.java +++ b/hypo-asm/src/test/java/dev/denwav/hypo/asm/scenarios/Scenario06Test.java @@ -24,12 +24,12 @@ import dev.denwav.hypo.hydrate.generic.LocalClassClosure; import dev.denwav.hypo.model.data.MethodData; import dev.denwav.hypo.test.framework.TestScenarioBase; +import dev.denwav.hypo.types.desc.MethodDescriptor; import java.util.List; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import static dev.denwav.hypo.model.data.MethodDescriptor.parseDescriptor; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -58,14 +58,14 @@ public void testLocalClasses() { final var testClass = this.findClass("scenario06/TestClass"); assertNotNull(testClass); - final MethodData testMethod = testClass.method("test", parseDescriptor("()V")); + final MethodData testMethod = testClass.method("test", MethodDescriptor.parse("()V")); assertNotNull(testMethod); final List localClasses = testMethod.get(HypoHydration.LOCAL_CLASSES); assertNotNull(localClasses); assertEquals(3, localClasses.size()); - final LocalClassClosure firstAnon = localClasses.get(0); + final LocalClassClosure firstAnon = localClasses.getFirst(); assertNotNull(firstAnon); assertEquals(testMethod, firstAnon.getContainingMethod()); assertEquals("scenario06/TestClass$1", firstAnon.getLocalClass().name()); diff --git a/hypo-asm/src/test/java/dev/denwav/hypo/asm/scenarios/Scenario07Test.java b/hypo-asm/src/test/java/dev/denwav/hypo/asm/scenarios/Scenario07Test.java index 8895cee..02cff55 100644 --- a/hypo-asm/src/test/java/dev/denwav/hypo/asm/scenarios/Scenario07Test.java +++ b/hypo-asm/src/test/java/dev/denwav/hypo/asm/scenarios/Scenario07Test.java @@ -26,12 +26,12 @@ import dev.denwav.hypo.hydrate.generic.LocalClassClosure; import dev.denwav.hypo.model.data.MethodData; import dev.denwav.hypo.test.framework.TestScenarioBase; +import dev.denwav.hypo.types.desc.MethodDescriptor; import java.util.List; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import static dev.denwav.hypo.model.data.MethodDescriptor.parseDescriptor; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; @@ -60,13 +60,13 @@ public void testLocalClasses() { final var testClass = this.findClass("scenario07/TestClass"); assertNotNull(testClass); - final MethodData testMethod = testClass.method("test", parseDescriptor("()V")); + final MethodData testMethod = testClass.method("test", MethodDescriptor.parse("()V")); assertNotNull(testMethod); final List firstLambdas = testMethod.get(HypoHydration.LAMBDA_CALLS); assertNotNull(firstLambdas); assertEquals(1, firstLambdas.size()); - final LambdaClosure firstLambda = firstLambdas.get(0); + final LambdaClosure firstLambda = firstLambdas.getFirst(); final List secondLambdas = firstLambda.getLambda().get(HypoHydration.LAMBDA_CALLS); assertNotNull(secondLambdas); diff --git a/hypo-asm/src/test/java/dev/denwav/hypo/asm/scenarios/Scenario08Test.java b/hypo-asm/src/test/java/dev/denwav/hypo/asm/scenarios/Scenario08Test.java index 5655dbf..894921c 100644 --- a/hypo-asm/src/test/java/dev/denwav/hypo/asm/scenarios/Scenario08Test.java +++ b/hypo-asm/src/test/java/dev/denwav/hypo/asm/scenarios/Scenario08Test.java @@ -20,9 +20,9 @@ import dev.denwav.hypo.model.data.ClassKind; import dev.denwav.hypo.model.data.FieldData; -import dev.denwav.hypo.model.data.types.ClassType; -import dev.denwav.hypo.model.data.types.PrimitiveType; import dev.denwav.hypo.test.framework.TestScenarioBase; +import dev.denwav.hypo.types.PrimitiveType; +import dev.denwav.hypo.types.desc.TypeDescriptor; import java.util.List; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.DisplayName; @@ -53,9 +53,9 @@ public void testRecords() { assertEquals(4, components.size()); - assertEquals(testClass.field("first", new ClassType("Ljava/lang/String;")), components.get(0)); + assertEquals(testClass.field("first", TypeDescriptor.parse("Ljava/lang/String;")), components.get(0)); assertEquals(testClass.field("second", PrimitiveType.INT), components.get(1)); assertEquals(testClass.field("third", PrimitiveType.LONG), components.get(2)); - assertEquals(testClass.field("fourth", new ClassType("Ljava/lang/Object;")), components.get(3)); + assertEquals(testClass.field("fourth", TypeDescriptor.parse("Ljava/lang/Object;")), components.get(3)); } } diff --git a/hypo-asm/src/test/java/dev/denwav/hypo/asm/scenarios/Scenario12Test.java b/hypo-asm/src/test/java/dev/denwav/hypo/asm/scenarios/Scenario12Test.java index 015b7f3..fa30ae9 100644 --- a/hypo-asm/src/test/java/dev/denwav/hypo/asm/scenarios/Scenario12Test.java +++ b/hypo-asm/src/test/java/dev/denwav/hypo/asm/scenarios/Scenario12Test.java @@ -20,10 +20,10 @@ import dev.denwav.hypo.model.data.FieldData; import dev.denwav.hypo.model.data.MethodData; -import dev.denwav.hypo.model.data.MethodDescriptor; -import dev.denwav.hypo.model.data.types.ClassType; -import dev.denwav.hypo.model.data.types.PrimitiveType; import dev.denwav.hypo.test.framework.TestScenarioBase; +import dev.denwav.hypo.types.PrimitiveType; +import dev.denwav.hypo.types.desc.ClassTypeDescriptor; +import dev.denwav.hypo.types.desc.MethodDescriptor; import java.io.IOException; import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.DisplayName; @@ -49,11 +49,11 @@ public void testSyntheticMembers() { final var inner = this.findClass("scenario03/TestClass$Inner"); assertNotNull(inner); - final MethodData intSupplierSynth = testClass.method("lambda$intSupplier$0", MethodDescriptor.parseDescriptor("(Lscenario03/TestClass$Inner;)I")); + final MethodData intSupplierSynth = testClass.method("lambda$intSupplier$0", MethodDescriptor.parse("(Lscenario03/TestClass$Inner;)I")); assertNotNull(intSupplierSynth, "Did not find expected lambda$intSupplier$0 synthetic member in TestClass"); assertTrue(intSupplierSynth.isSynthetic()); - final FieldData innerSyntheticOuterThisField = inner.field("this$0", new ClassType("scenario03.TestClass")); + final FieldData innerSyntheticOuterThisField = inner.field("this$0", ClassTypeDescriptor.of("scenario03.TestClass")); assertNotNull(innerSyntheticOuterThisField, "Did not find expected this$0 synthetic member in TestClass$Inner"); assertTrue(innerSyntheticOuterThisField.isSynthetic()); } @@ -66,7 +66,7 @@ public void testNonSyntheticMembers() { final var inner = this.findClass("scenario03/TestClass$Inner"); assertNotNull(inner); - final MethodData intSupplier = testClass.method("intSupplier", MethodDescriptor.parseDescriptor("(Lscenario03/TestClass$Inner;)Ljava/util/function/IntSupplier;")); + final MethodData intSupplier = testClass.method("intSupplier", MethodDescriptor.parse("(Lscenario03/TestClass$Inner;)Ljava/util/function/IntSupplier;")); assertNotNull(intSupplier, "Did not find expected method intSupplier in TestClass"); assertFalse(intSupplier.isSynthetic()); diff --git a/hypo-core/build.gradle.kts b/hypo-core/build.gradle.kts index 1416d07..0049801 100644 --- a/hypo-core/build.gradle.kts +++ b/hypo-core/build.gradle.kts @@ -6,20 +6,13 @@ plugins { } dependencies { - api(projects.hypoModel) -} + compileOnlyApi(libs.annotations) -tasks.jar { - manifest { - attributes( - "Automatic-Module-Name" to "dev.denwav.hypo.core" - ) - } + api(projects.hypoModel) } hypoJava { javadocLibs.add(libs.annotations) - javadocLibs.add(libs.errorprone.annotations) javadocProjects.add(projects.hypoModel) } diff --git a/hypo-core/src/main/java/module-info.java b/hypo-core/src/main/java/module-info.java new file mode 100644 index 0000000..83a5650 --- /dev/null +++ b/hypo-core/src/main/java/module-info.java @@ -0,0 +1,28 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +/** + * Core module for Hypo, which primarily provides {@link dev.denwav.hypo.core.HypoContext HypoContext}. + */ +module dev.denwav.hypo.core { + requires transitive dev.denwav.hypo.model; + + requires static transitive org.jetbrains.annotations; + + exports dev.denwav.hypo.core; +} diff --git a/hypo-hydrate/build.gradle.kts b/hypo-hydrate/build.gradle.kts index 34447ba..b10b27b 100644 --- a/hypo-hydrate/build.gradle.kts +++ b/hypo-hydrate/build.gradle.kts @@ -3,26 +3,36 @@ plugins { `hypo-java` `hypo-module` `hypo-publish` + `hypo-test-scenario` +} + +hypoTest { + testDataProject = projects.hypoHydrate.hypoHydrateTestData +} + +repositories { + // for tests + maven("https://maven.quiltmc.org/repository/release/") } dependencies { - implementation(projects.hypoCore) + compileOnlyApi(libs.annotations) + compileOnlyApi(libs.errorprone.annotations) + implementation(libs.jgrapht) -} -tasks.jar { - manifest { - attributes( - "Automatic-Module-Name" to "dev.denwav.hypo.hydrate" - ) - } + implementation(projects.hypoCore) + implementation(projects.hypoModel) + implementation(projects.hypoTypes) + + testImplementation(projects.hypoTest) } hypoJava { javadocLibs.add(libs.annotations) javadocLibs.add(libs.errorprone.annotations) javadocLibs.add(libs.jgrapht) - javadocProjects.addAll(projects.hypoCore, projects.hypoModel) + javadocProjects.addAll(projects.hypoCore, projects.hypoModel, projects.hypoTypes) } hypoPublish { diff --git a/hypo-hydrate/hypo-hydrate-test-data/build.gradle.kts b/hypo-hydrate/hypo-hydrate-test-data/build.gradle.kts new file mode 100644 index 0000000..100ff69 --- /dev/null +++ b/hypo-hydrate/hypo-hydrate-test-data/build.gradle.kts @@ -0,0 +1,12 @@ +plugins { + java + `hypo-java` + `hypo-test-scenario-data` +} + +tasks.withType().configureEach { + javaCompiler = javaToolchains.compilerFor { + languageVersion = JavaLanguageVersion.of(21) + } + options.release = 21 +} diff --git a/hypo-hydrate/hypo-hydrate-test-data/src/scenario-01/java/scenario01/TestClass.java b/hypo-hydrate/hypo-hydrate-test-data/src/scenario-01/java/scenario01/TestClass.java new file mode 100644 index 0000000..d070228 --- /dev/null +++ b/hypo-hydrate/hypo-hydrate-test-data/src/scenario-01/java/scenario01/TestClass.java @@ -0,0 +1,8 @@ +package scenario01; + +// Compiled with JDK 21 +public class TestClass extends TestSuperClass { + + @Override + void test() {} +} diff --git a/hypo-hydrate/hypo-hydrate-test-data/src/scenario-01/java/scenario01/TestSuperClass.java b/hypo-hydrate/hypo-hydrate-test-data/src/scenario-01/java/scenario01/TestSuperClass.java new file mode 100644 index 0000000..6e78b24 --- /dev/null +++ b/hypo-hydrate/hypo-hydrate-test-data/src/scenario-01/java/scenario01/TestSuperClass.java @@ -0,0 +1,7 @@ +package scenario01; + +// Compiled with JDK 21 +public class TestSuperClass { + + void test() {} +} diff --git a/hypo-hydrate/hypo-hydrate-test-data/src/scenario-02/java/scenario02/TestClass.java b/hypo-hydrate/hypo-hydrate-test-data/src/scenario-02/java/scenario02/TestClass.java new file mode 100644 index 0000000..e2fa6a0 --- /dev/null +++ b/hypo-hydrate/hypo-hydrate-test-data/src/scenario-02/java/scenario02/TestClass.java @@ -0,0 +1,11 @@ +package scenario02; + +public class TestClass extends TestSuperClass { + + @Override + void test() {} + @Override + void test(int i) {} + @Override + void test(int i1, int i2) {} +} diff --git a/hypo-hydrate/hypo-hydrate-test-data/src/scenario-02/java/scenario02/TestSuperClass.java b/hypo-hydrate/hypo-hydrate-test-data/src/scenario-02/java/scenario02/TestSuperClass.java new file mode 100644 index 0000000..ba8ba13 --- /dev/null +++ b/hypo-hydrate/hypo-hydrate-test-data/src/scenario-02/java/scenario02/TestSuperClass.java @@ -0,0 +1,10 @@ +package scenario02; + +public class TestSuperClass { + + void test() {} + void test(int i) {} + void test(char c) {} + void test(int i1, int i2) {} + void test(long l) {} +} diff --git a/hypo-hydrate/src/main/java/module-info.java b/hypo-hydrate/src/main/java/module-info.java new file mode 100644 index 0000000..5e987cc --- /dev/null +++ b/hypo-hydrate/src/main/java/module-info.java @@ -0,0 +1,32 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +/** + * Hydration fills in additional information to the class data model, the primary entrypoint being + * {@link dev.denwav.hypo.hydrate.HydrationManager HydrationManager}. + */ +module dev.denwav.hypo.hydrate { + requires transitive dev.denwav.hypo.core; + + requires static transitive org.jetbrains.annotations; + + requires org.jgrapht.core; + + exports dev.denwav.hypo.hydrate; + exports dev.denwav.hypo.hydrate.generic; +} diff --git a/hypo-hydrate/src/test/java/dev/denwav/hypo/hydrate/Scenario01Test.java b/hypo-hydrate/src/test/java/dev/denwav/hypo/hydrate/Scenario01Test.java new file mode 100644 index 0000000..074f49a --- /dev/null +++ b/hypo-hydrate/src/test/java/dev/denwav/hypo/hydrate/Scenario01Test.java @@ -0,0 +1,53 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.hydrate; + +import dev.denwav.hypo.model.data.ClassData; +import dev.denwav.hypo.model.data.MethodData; +import dev.denwav.hypo.test.framework.TestScenarioBase; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@DisplayName("[hydrate] Scenario 01 - Base Override (Java 21)") +class Scenario01Test extends TestScenarioBase { + + @Override + public @NotNull Env env() { + return () -> "scenario-01"; + } + + @Test + @DisplayName("Test simple override") + void hydrationTest() { + final ClassData testClass = this.findClass("scenario01.TestClass"); + final ClassData testSuperClass = this.findClass("scenario01.TestSuperClass"); + + final MethodData testMethod = findMethod(testClass, "test"); + final MethodData expectedTestSuperMethod = findMethod(testSuperClass, "test"); + + final MethodData actualSuperTestMethod = testMethod.superMethod(); + assertNotNull(actualSuperTestMethod); + + assertEquals(expectedTestSuperMethod, actualSuperTestMethod); + } +} diff --git a/hypo-hydrate/src/test/java/dev/denwav/hypo/hydrate/Scenario02Test.java b/hypo-hydrate/src/test/java/dev/denwav/hypo/hydrate/Scenario02Test.java new file mode 100644 index 0000000..c4607ff --- /dev/null +++ b/hypo-hydrate/src/test/java/dev/denwav/hypo/hydrate/Scenario02Test.java @@ -0,0 +1,98 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.hydrate; + +import dev.denwav.hypo.model.data.ClassData; +import dev.denwav.hypo.model.data.MethodData; +import dev.denwav.hypo.test.framework.TestScenarioBase; +import dev.denwav.hypo.types.desc.MethodDescriptor; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.DisplayName; +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.assertNull; + +@DisplayName("[hydrate] Scenario 02 - Overrides with overloads (Java 21)") +class Scenario02Test extends TestScenarioBase { + + @Override + public @NotNull Env env() { + return () -> "scenario-02"; + } + + @Test + @DisplayName("Test overload 1/4") + void testNoArgs() { + final ClassData testClass = this.findClass("scenario02.TestClass"); + final ClassData testSuperClass = this.findClass("scenario02.TestSuperClass"); + + final MethodData testMethod = findMethod(testClass, "test", "()V"); + final MethodData expectedTestSuperMethod = findMethod(testSuperClass, "test", "()V"); + + final MethodData actualSuperTestMethod = testMethod.superMethod(); + assertNotNull(actualSuperTestMethod); + + assertEquals(expectedTestSuperMethod, actualSuperTestMethod); + } + + @Test + @DisplayName("Test overload 2/4") + void testInt() { + final ClassData testClass = this.findClass("scenario02.TestClass"); + final ClassData testSuperClass = this.findClass("scenario02.TestSuperClass"); + + final MethodData testMethod = findMethod(testClass, "test", "(I)V"); + final MethodData expectedTestSuperMethod = findMethod(testSuperClass, "test", "(I)V"); + + final MethodData actualSuperTestMethod = testMethod.superMethod(); + assertNotNull(actualSuperTestMethod); + + assertEquals(expectedTestSuperMethod, actualSuperTestMethod); + } + + @Test + @DisplayName("Test overload 3/4") + void testTwoInts() { + final ClassData testClass = this.findClass("scenario02.TestClass"); + final ClassData testSuperClass = this.findClass("scenario02.TestSuperClass"); + + final MethodData testMethod = findMethod(testClass, "test", "(II)V"); + final MethodData expectedTestSuperMethod = findMethod(testSuperClass, "test", "(II)V"); + + final MethodData actualSuperTestMethod = testMethod.superMethod(); + assertNotNull(actualSuperTestMethod); + + assertEquals(expectedTestSuperMethod, actualSuperTestMethod); + } + + @Test + @DisplayName("Test overload 4/4") + void testNoOverride() { + final ClassData testClass = this.findClass("scenario02.TestClass"); + final ClassData testSuperClass = this.findClass("scenario02.TestSuperClass"); + + final MethodData testMethod = findMethod(testSuperClass, "test", "(J)V"); + assertEquals(0, testMethod.childMethods().size()); + + final MethodData actualSuperTestMethod = testClass.method("test", MethodDescriptor.parse("(J)V")); + assertNull(actualSuperTestMethod); + } +} diff --git a/hypo-mappings/build.gradle.kts b/hypo-mappings/build.gradle.kts index 2dafa53..11ad9df 100644 --- a/hypo-mappings/build.gradle.kts +++ b/hypo-mappings/build.gradle.kts @@ -6,6 +6,11 @@ plugins { } dependencies { + compileOnlyApi(libs.annotations) + compileOnlyApi(libs.errorprone.annotations) + + api(projects.hypoTypes) + api(projects.hypoModel) api(projects.hypoCore) api(projects.hypoHydrate) @@ -24,9 +29,11 @@ tasks.jar { hypoJava { javadocLibs.add(libs.annotations) javadocLibs.add(libs.errorprone.annotations) + javadocLibs.add(libs.lorenz) javadocLibs.add(libs.bombe) - javadocProjects.addAll(projects.hypoHydrate, projects.hypoCore, projects.hypoModel) + + javadocProjects.addAll(projects.hypoHydrate, projects.hypoCore, projects.hypoModel, projects.hypoTypes) } hypoPublish { diff --git a/hypo-mappings/src/main/java/dev/denwav/hypo/mappings/LorenzUtil.java b/hypo-mappings/src/main/java/dev/denwav/hypo/mappings/LorenzUtil.java index d52a420..63aaeb8 100644 --- a/hypo-mappings/src/main/java/dev/denwav/hypo/mappings/LorenzUtil.java +++ b/hypo-mappings/src/main/java/dev/denwav/hypo/mappings/LorenzUtil.java @@ -21,11 +21,12 @@ import dev.denwav.hypo.model.data.ClassData; import dev.denwav.hypo.model.data.FieldData; import dev.denwav.hypo.model.data.MethodData; -import dev.denwav.hypo.model.data.MethodDescriptor; -import dev.denwav.hypo.model.data.types.ArrayType; -import dev.denwav.hypo.model.data.types.ClassType; -import dev.denwav.hypo.model.data.types.JvmType; -import dev.denwav.hypo.model.data.types.PrimitiveType; +import dev.denwav.hypo.types.PrimitiveType; +import dev.denwav.hypo.types.VoidType; +import dev.denwav.hypo.types.desc.ArrayTypeDescriptor; +import dev.denwav.hypo.types.desc.ClassTypeDescriptor; +import dev.denwav.hypo.types.desc.MethodDescriptor; +import dev.denwav.hypo.types.desc.TypeDescriptor; import java.lang.invoke.MethodHandles; import java.lang.invoke.VarHandle; import java.util.ArrayList; @@ -160,34 +161,40 @@ private LorenzUtil() {} * @return The same descriptor, but in the Hypo model. */ public static @NotNull MethodDescriptor convertDesc(final @NotNull org.cadixdev.bombe.type.MethodDescriptor desc) { - final ArrayList params = new ArrayList<>(desc.getParamTypes().size()); + final ArrayList params = new ArrayList<>(desc.getParamTypes().size()); for (final org.cadixdev.bombe.type.FieldType paramType : desc.getParamTypes()) { params.add(convertType(paramType)); } - return new MethodDescriptor(params, convertType(desc.getReturnType())); + return MethodDescriptor.of(params, convertType(desc.getReturnType())); } /** - * Convert a Bombe {@link org.cadixdev.bombe.type.Type Type} into a Hypo {@link JvmType}. + * Convert a Bombe {@link org.cadixdev.bombe.type.Type Type} into a Hypo {@link TypeDescriptor}. * * @param type The Bombe type to convert. * @return The same type, but in the Hypo model. */ - public static @NotNull JvmType convertType(final @NotNull org.cadixdev.bombe.type.Type type) { - // ArrayType - if (type instanceof org.cadixdev.bombe.type.ArrayType) { - final org.cadixdev.bombe.type.ArrayType array = (org.cadixdev.bombe.type.ArrayType) type; - return new ArrayType(convertType(array.getComponent()), array.getDimCount()); + public static @NotNull TypeDescriptor convertType(final @NotNull org.cadixdev.bombe.type.Type type) { + return switch (type) { + // ArrayType + case final org.cadixdev.bombe.type.ArrayType array -> + ArrayTypeDescriptor.of(array.getDimCount(), convertType(array.getComponent())); // Primitive (BaseType and VoidType) - } else if (type instanceof org.cadixdev.bombe.type.PrimitiveType) { - return PrimitiveType.fromChar(((org.cadixdev.bombe.type.PrimitiveType) type).getKey()); + case org.cadixdev.bombe.type.VoidType ignored -> VoidType.INSTANCE; + case org.cadixdev.bombe.type.BaseType baseType -> switch (baseType) { + case CHAR -> PrimitiveType.CHAR; + case BYTE -> PrimitiveType.BYTE; + case SHORT -> PrimitiveType.SHORT; + case INT -> PrimitiveType.INT; + case LONG -> PrimitiveType.LONG; + case FLOAT -> PrimitiveType.FLOAT; + case DOUBLE -> PrimitiveType.DOUBLE; + case BOOLEAN -> PrimitiveType.BOOLEAN; + }; // ObjectType is the only possibility left - } else if (type instanceof org.cadixdev.bombe.type.ObjectType) { - final org.cadixdev.bombe.type.ObjectType obj = (org.cadixdev.bombe.type.ObjectType) type; - return new ClassType(obj.getClassName()); - } else { - throw new IllegalStateException("Unknown type: " + type); - } + case final org.cadixdev.bombe.type.ObjectType obj -> ClassTypeDescriptor.of(obj.getClassName()); + default -> throw new IllegalStateException("Unknown type: " + type); + }; } /** diff --git a/hypo-mappings/src/main/java/dev/denwav/hypo/mappings/changes/CopyConstructorMappingChange.java b/hypo-mappings/src/main/java/dev/denwav/hypo/mappings/changes/CopyConstructorMappingChange.java index 9dd02f8..5e95f0e 100644 --- a/hypo-mappings/src/main/java/dev/denwav/hypo/mappings/changes/CopyConstructorMappingChange.java +++ b/hypo-mappings/src/main/java/dev/denwav/hypo/mappings/changes/CopyConstructorMappingChange.java @@ -25,6 +25,7 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.function.BiConsumer; import org.cadixdev.lorenz.MappingSet; import org.cadixdev.lorenz.model.ClassMapping; import org.cadixdev.lorenz.model.MethodMapping; @@ -140,17 +141,61 @@ public void applyChange(final @NotNull MappingSet input, final @NotNull MemberRe for (final MethodParameterMapping thatParamMapping : that.superMapping.getParameterMappings()) { final int index = thatParamMapping.getIndex(); for (final SuperCall.SuperCallParameter thatParam : that.params) { - if (index == thatParam.getSuperIndex()) { - final String existingMapping = mergedParams.get(thatParam.getThisIndex()); - if (existingMapping == null) { + if (index != thatParam.getSuperIndex()) { + continue; + } + final String existingMapping = mergedParams.get(thatParam.getThisIndex()); + if (existingMapping == null) { + mergedParams.put(thatParam.getThisIndex(), thatParamMapping.getDeobfuscatedName()); + } else { + if (existingMapping.equals(thatParamMapping.getDeobfuscatedName())) { + // nothing to do, they match + break; + } + // We don't have a good solution here. They don't match and we don't really have a way of + // determining which one may take precedence. Don't fail here, just take one of the names. + // + // The following logic to determine which name to take is effectively random, and shouldn't be + // considered meaningful in any way. It's simply here as an attempt at keeping the result of this + // method deterministic. + + // Keep the one with more param names present + if (thisCount > thatCount) { + break; + } else if (thisCount < thatCount) { + mergedParams.put(thatParam.getThisIndex(), thatParamMapping.getDeobfuscatedName()); + break; + } + + // Keep the one with the shorter class name + final int thisClassNameLen = this.superMapping.getParent().getDeobfuscatedName().length(); + final int thatClassNameLen = that.superMapping.getParent().getDeobfuscatedName().length(); + if (thisClassNameLen < thatClassNameLen) { + break; + } else if (thisClassNameLen > thatClassNameLen) { + mergedParams.put(thatParam.getThisIndex(), thatParamMapping.getDeobfuscatedName()); + break; + } + + // Keep the shorter parameter name + final int existingLen = existingMapping.length(); + final int otherLen = thatParamMapping.getDeobfuscatedName().length(); + if (existingLen < otherLen) { + break; + } else if (existingLen > otherLen) { + mergedParams.put(thatParam.getThisIndex(), thatParamMapping.getDeobfuscatedName()); + break; + } + + // Keep the parameter name with the smaller hashcode (remember, this is arbitrary and random) + // This is our last attempt, if the hashcodes are equal, even though the strings are not, we will + // just take the other one. + final int existingHash = existingMapping.hashCode(); + final int otherHash = thatParamMapping.getDeobfuscatedName().hashCode(); + if (existingHash < otherHash) { + break; + } else if (existingHash > otherHash) { mergedParams.put(thatParam.getThisIndex(), thatParamMapping.getDeobfuscatedName()); - } else { - if (existingMapping.equals(thatParamMapping.getDeobfuscatedName())) { - // nothing to do, they match - break; - } - return MergeResult.failure("Cannot merge super calls from two constructors with " + - "different parameter mapping results"); } } } diff --git a/hypo-mappings/src/main/java/dev/denwav/hypo/mappings/changes/MemberReference.java b/hypo-mappings/src/main/java/dev/denwav/hypo/mappings/changes/MemberReference.java index b2bc29e..393e499 100644 --- a/hypo-mappings/src/main/java/dev/denwav/hypo/mappings/changes/MemberReference.java +++ b/hypo-mappings/src/main/java/dev/denwav/hypo/mappings/changes/MemberReference.java @@ -111,7 +111,7 @@ public MemberReference( * @return The new {@link MemberReference}. */ public static @NotNull MemberReference of(final @NotNull FieldData field) { - return new MemberReference(field.parentClass().name(), field.name(), field.fieldType().asInternalName()); + return new MemberReference(field.parentClass().name(), field.name(), field.descriptorText()); } /** diff --git a/hypo-mappings/src/main/java/dev/denwav/hypo/mappings/contributors/CopyRecordParameters.java b/hypo-mappings/src/main/java/dev/denwav/hypo/mappings/contributors/CopyRecordParameters.java index f6a1ee2..1cdbbc6 100644 --- a/hypo-mappings/src/main/java/dev/denwav/hypo/mappings/contributors/CopyRecordParameters.java +++ b/hypo-mappings/src/main/java/dev/denwav/hypo/mappings/contributors/CopyRecordParameters.java @@ -26,8 +26,8 @@ import dev.denwav.hypo.model.data.ClassKind; import dev.denwav.hypo.model.data.FieldData; import dev.denwav.hypo.model.data.MethodData; -import dev.denwav.hypo.model.data.types.JvmType; -import dev.denwav.hypo.model.data.types.PrimitiveType; +import dev.denwav.hypo.types.PrimitiveType; +import dev.denwav.hypo.types.desc.TypeDescriptor; import java.util.ArrayList; import java.util.List; import org.cadixdev.lorenz.model.ClassMapping; @@ -99,7 +99,7 @@ public void contribute( // must have the same types for (int i = 0; i < len; i++) { - if (!method.param(i).equals(components.get(i).fieldType())) { + if (!method.param(i).equals(components.get(i).descriptor())) { continue outer; } } @@ -122,7 +122,7 @@ public void contribute( registry.submitChange(AddNewParameterMappingsChange.of(ref, newName)); lvtIndex++; - final JvmType paramType = method.param(i); + final TypeDescriptor paramType = method.param(i); if (paramType == PrimitiveType.LONG || paramType == PrimitiveType.DOUBLE) { lvtIndex++; } diff --git a/hypo-meta/hypo-catalog/build.gradle.kts b/hypo-meta/hypo-catalog/build.gradle.kts index 3e88e03..fe2a4ba 100644 --- a/hypo-meta/hypo-catalog/build.gradle.kts +++ b/hypo-meta/hypo-catalog/build.gradle.kts @@ -16,8 +16,9 @@ catalog { library("hypo-hydrate", projects.hypoHydrate) library("hypo-mappings", projects.hypoMappings) library("hypo-model", projects.hypoModel) + library("hypo-types", projects.hypoTypes) - bundle("hypo-base", listOf("hypo-model", "hypo-core", "hypo-hydrate")) + bundle("hypo-base", listOf("hypo-types", "hypo-model", "hypo-core", "hypo-hydrate")) bundle("hypo-asm", listOf("hypo-asm-base", "hypo-asm-hydrate")) } } diff --git a/hypo-meta/hypo-platform/build.gradle.kts b/hypo-meta/hypo-platform/build.gradle.kts index fd7a5b4..2be72f6 100644 --- a/hypo-meta/hypo-platform/build.gradle.kts +++ b/hypo-meta/hypo-platform/build.gradle.kts @@ -15,5 +15,6 @@ dependencies { api(projects.hypoHydrate) api(projects.hypoMappings) api(projects.hypoModel) + api(projects.hypoTypes) } } diff --git a/hypo-model/build.gradle.kts b/hypo-model/build.gradle.kts index a221730..cdac137 100644 --- a/hypo-model/build.gradle.kts +++ b/hypo-model/build.gradle.kts @@ -8,20 +8,14 @@ plugins { dependencies { compileOnlyApi(libs.annotations) - api(libs.slf4j.api) -} -tasks.jar { - manifest { - attributes( - "Automatic-Module-Name" to "dev.denwav.hypo.model" - ) - } + api(projects.hypoTypes) } hypoJava { javadocLibs.add(libs.annotations) - javadocLibs.add(libs.errorprone.annotations) + + javadocProjects.add(projects.hypoTypes) } hypoPublish { diff --git a/hypo-model/src/main/java/dev/denwav/hypo/model/AbstractClassDataProvider.java b/hypo-model/src/main/java/dev/denwav/hypo/model/AbstractClassDataProvider.java index ccc8ea4..d1dfcd0 100644 --- a/hypo-model/src/main/java/dev/denwav/hypo/model/AbstractClassDataProvider.java +++ b/hypo-model/src/main/java/dev/denwav/hypo/model/AbstractClassDataProvider.java @@ -25,6 +25,7 @@ import dev.denwav.hypo.model.data.HypoKey; import dev.denwav.hypo.model.data.MethodData; import dev.denwav.hypo.model.data.Visibility; +import dev.denwav.hypo.types.sig.ClassSignature; import java.io.IOException; import java.util.Collection; import java.util.EnumSet; @@ -268,6 +269,11 @@ public void setRequireFullClasspath(boolean requireFullClasspath) { throw new IllegalStateException(); } + @Override + public @Nullable ClassSignature signature() { + throw new IllegalStateException(); + } + @Override public @Nullable ClassData outerClass() { throw new IllegalStateException(); diff --git a/hypo-model/src/main/java/dev/denwav/hypo/model/ClassDataProvider.java b/hypo-model/src/main/java/dev/denwav/hypo/model/ClassDataProvider.java index d1ad5fc..910446c 100644 --- a/hypo-model/src/main/java/dev/denwav/hypo/model/ClassDataProvider.java +++ b/hypo-model/src/main/java/dev/denwav/hypo/model/ClassDataProvider.java @@ -19,8 +19,8 @@ package dev.denwav.hypo.model; import dev.denwav.hypo.model.data.ClassData; -import dev.denwav.hypo.model.data.types.ClassType; -import dev.denwav.hypo.model.data.types.JvmType; +import dev.denwav.hypo.types.desc.ClassTypeDescriptor; +import dev.denwav.hypo.types.desc.TypeDescriptor; import java.io.IOException; import java.util.Collection; import java.util.stream.Stream; @@ -118,15 +118,15 @@ public interface ClassDataProvider extends AutoCloseable { * @return The parsed {@link ClassData} object corresponding with the given name, or {@code null} if the class name * cannot be found. * @throws IOException If an IO error occurs while attempting to read the class file. - * @see #findClass(JvmType) + * @see #findClass(TypeDescriptor) */ @Contract("null -> null") @Nullable ClassData findClass(final @Nullable String className) throws IOException; /** - * This is a convenience method for resolving the {@link ClassData} object corresponding to a give {@link JvmType}. - * The {@link JvmType} passed to this method must be a {@link ClassType}, or this method will always return - * {@code null}. + * This is a convenience method for resolving the {@link ClassData} object corresponding to a give + * {@link TypeDescriptor}. The {@link TypeDescriptor} passed to this method must be a {@link ClassTypeDescriptor}, + * or this method will always return {@code null}. * *

This method is implemented by default as: * @@ -137,19 +137,19 @@ public interface ClassDataProvider extends AutoCloseable { *

and as such has identical semantics to {@link #findClass(String)}. Any implementations which override this * method must match these semantics to fully satisfy this method's contact. * - * @param type The {@link JvmType} of the class to find. Must be an instance of {@link ClassType} or this method - * will always return {@code null}. - * @return The {@link ClassData} object corresponding with the given {@link JvmType}, or {@code null} if the type is - * not a {@link ClassType} or the class name cannot be found. + * @param type The {@link TypeDescriptor} of the class to find. Must be an instance of {@link ClassTypeDescriptor} + * or this method will always return {@code null}. + * @return The {@link ClassData} object corresponding with the given {@link TypeDescriptor}, or {@code null} if the + * type is not a {@link ClassTypeDescriptor} or the class name cannot be found. * @throws IOException If an IO error occurs while attempting to read the class file. * @see #findClass(String) */ @Contract("null -> null") - default @Nullable ClassData findClass(final @Nullable JvmType type) throws IOException { - if (!(type instanceof ClassType)) { + default @Nullable ClassData findClass(final @Nullable TypeDescriptor type) throws IOException { + if (!(type instanceof ClassTypeDescriptor)) { return null; } - return this.findClass(type.asInternalName()); + return this.findClass(type.asInternal()); } /** diff --git a/hypo-model/src/main/java/dev/denwav/hypo/model/HypoModelUtil.java b/hypo-model/src/main/java/dev/denwav/hypo/model/HypoModelUtil.java index 8ceb494..76da33e 100644 --- a/hypo-model/src/main/java/dev/denwav/hypo/model/HypoModelUtil.java +++ b/hypo-model/src/main/java/dev/denwav/hypo/model/HypoModelUtil.java @@ -18,6 +18,7 @@ package dev.denwav.hypo.model; +import dev.denwav.hypo.types.HypoTypesUtil; import java.util.function.Consumer; import java.util.function.Function; import org.jetbrains.annotations.Contract; @@ -45,8 +46,7 @@ private HypoModelUtil() {} * @return The normalized class name. */ public static @NotNull String normalizedClassName(final @NotNull String className) { - final int index = className.endsWith(";") ? 1 : 0; - return className.substring(index, className.length() - index).replace('.', '/'); + return HypoTypesUtil.normalizedClassName(className); } /** @@ -62,8 +62,7 @@ private HypoModelUtil() {} @SuppressWarnings("TypeParameterUnusedInFormals") @Contract(value = "_ -> param1", pure = true) public static @Nullable T cast(final @Nullable Object o) { - @SuppressWarnings("unchecked") final T t = (T) o; - return t; + return HypoTypesUtil.cast(o); } /** diff --git a/hypo-model/src/main/java/dev/denwav/hypo/model/SystemClassProviderRoot.java b/hypo-model/src/main/java/dev/denwav/hypo/model/SystemClassProviderRoot.java index d0780a8..9024afe 100644 --- a/hypo-model/src/main/java/dev/denwav/hypo/model/SystemClassProviderRoot.java +++ b/hypo-model/src/main/java/dev/denwav/hypo/model/SystemClassProviderRoot.java @@ -48,7 +48,8 @@ final ModuleReader[] readers = new ModuleReader[refs.size()]; int index = 0; for (final ModuleReference ref : refs) { - readers[index++] = ref.open(); + @SuppressWarnings("resource") final ModuleReader openedReader = ref.open(); + readers[index++] = openedReader; } this.readers = Arrays.asList(readers); } diff --git a/hypo-model/src/main/java/dev/denwav/hypo/model/data/AbstractClassData.java b/hypo-model/src/main/java/dev/denwav/hypo/model/data/AbstractClassData.java index b153ad1..336dab9 100644 --- a/hypo-model/src/main/java/dev/denwav/hypo/model/data/AbstractClassData.java +++ b/hypo-model/src/main/java/dev/denwav/hypo/model/data/AbstractClassData.java @@ -110,9 +110,12 @@ public boolean isRequireFullClasspath() { @Override public boolean equals(final Object o) { - if (this == o) return true; - if (!(o instanceof ClassData)) return false; - final ClassData that = (ClassData) o; + if (this == o) { + return true; + } + if (!(o instanceof final ClassData that)) { + return false; + } return this.name().equals(that.name()); } diff --git a/hypo-model/src/main/java/dev/denwav/hypo/model/data/AbstractFieldData.java b/hypo-model/src/main/java/dev/denwav/hypo/model/data/AbstractFieldData.java index c30fd79..8ad7614 100644 --- a/hypo-model/src/main/java/dev/denwav/hypo/model/data/AbstractFieldData.java +++ b/hypo-model/src/main/java/dev/denwav/hypo/model/data/AbstractFieldData.java @@ -34,21 +34,25 @@ public AbstractFieldData() {} @Override public boolean equals(final Object o) { - if (this == o) return true; - if (!(o instanceof FieldData)) return false; - final FieldData that = (FieldData) o; + if (this == o) { + return true; + } + if (!(o instanceof final FieldData that)) { + return false; + } return this.parentClass().equals(that.parentClass()) && this.name().equals(that.name()) && - this.fieldType().equals(that.fieldType()); + this.descriptor().equals(that.descriptor()) && + Objects.equals(this.signature(), that.signature()); } @Override public int hashCode() { - return Objects.hash(this.parentClass(), this.name(), this.fieldType()); + return Objects.hash(this.parentClass(), this.name(), this.descriptor(), this.signature()); } @Override public String toString() { - return this.parentClass().name() + "#" + this.name() + " " + this.fieldType(); + return this.parentClass().name() + "#" + this.name() + " " + this.descriptor(); } } diff --git a/hypo-model/src/main/java/dev/denwav/hypo/model/data/AbstractMethodData.java b/hypo-model/src/main/java/dev/denwav/hypo/model/data/AbstractMethodData.java index 3141403..01b1146 100644 --- a/hypo-model/src/main/java/dev/denwav/hypo/model/data/AbstractMethodData.java +++ b/hypo-model/src/main/java/dev/denwav/hypo/model/data/AbstractMethodData.java @@ -57,9 +57,12 @@ public void setSuperMethod(final @Nullable MethodData superMethod) { @Override public boolean equals(final Object o) { - if (this == o) return true; - if (!(o instanceof MethodData)) return false; - final MethodData that = (MethodData) o; + if (this == o) { + return true; + } + if (!(o instanceof final MethodData that)) { + return false; + } return this.parentClass().equals(that.parentClass()) && this.name().equals(that.name()) && this.descriptor().equals(that.descriptor()); diff --git a/hypo-model/src/main/java/dev/denwav/hypo/model/data/ClassData.java b/hypo-model/src/main/java/dev/denwav/hypo/model/data/ClassData.java index 5e678cc..84cf569 100644 --- a/hypo-model/src/main/java/dev/denwav/hypo/model/data/ClassData.java +++ b/hypo-model/src/main/java/dev/denwav/hypo/model/data/ClassData.java @@ -20,7 +20,9 @@ import dev.denwav.hypo.model.ClassDataProvider; import dev.denwav.hypo.model.HypoModelUtil; -import dev.denwav.hypo.model.data.types.JvmType; +import dev.denwav.hypo.types.desc.MethodDescriptor; +import dev.denwav.hypo.types.desc.TypeDescriptor; +import dev.denwav.hypo.types.sig.ClassSignature; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; @@ -98,6 +100,13 @@ public interface ClassData extends HypoData { */ @NotNull String name(); + /** + * The generic signature of this class. + * + * @return The generic signature of this class. + */ + @Nullable ClassSignature signature(); + /** * This class data's outer class data, or {@code null} if this class data does not have an outer class. * @@ -463,9 +472,9 @@ default boolean doesExtendOrImplement(final @NotNull ClassData that) { * @param type The type of the field to find. * @return The field data this class declares with the given name and type, or {@code null} if it can't be found. */ - default @Nullable FieldData field(final @NotNull String name, final @NotNull JvmType type) { + default @Nullable FieldData field(final @NotNull String name, final @NotNull TypeDescriptor type) { for (final FieldData field : this.fields()) { - if (field.name().equals(name) && field.fieldType().equals(type)) { + if (field.name().equals(name) && field.descriptor().equals(type)) { return field; } } diff --git a/hypo-model/src/main/java/dev/denwav/hypo/model/data/FieldData.java b/hypo-model/src/main/java/dev/denwav/hypo/model/data/FieldData.java index 28f4812..6a2a9ff 100644 --- a/hypo-model/src/main/java/dev/denwav/hypo/model/data/FieldData.java +++ b/hypo-model/src/main/java/dev/denwav/hypo/model/data/FieldData.java @@ -18,8 +18,10 @@ package dev.denwav.hypo.model.data; -import dev.denwav.hypo.model.data.types.JvmType; +import dev.denwav.hypo.types.desc.TypeDescriptor; +import dev.denwav.hypo.types.sig.TypeSignature; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; /** * Java field model. @@ -35,6 +37,49 @@ public interface FieldData extends MemberData { * The type of this field data. * * @return The type of this field data. + * @deprecated Use {@link #descriptor()}. */ - @NotNull JvmType fieldType(); + @Deprecated(since = "3.0.0") + default @NotNull TypeDescriptor fieldType() { + return this.descriptor(); + } + + /** + * The type of this field data. + * + * @return The type of this field data. + */ + @NotNull TypeDescriptor descriptor(); + + /** + * The descriptor text of this field data. + * + * @return The descriptor text of this field data. + * @see #descriptor() + */ + default @NotNull String descriptorText() { + return this.descriptor().asInternal(); + } + + /** + * The generic type signature of this field data. + * + * @return The type signature of this field data. + */ + @Nullable TypeSignature signature(); + + /** + * The internal JVM text representation of this field's generic type signature. + * + * @return The internal JVM text representation of this field's generic type signature. + * @see #signature() + */ + default @Nullable String signatureText() { + final TypeSignature sig = this.signature(); + if (sig != null) { + return sig.asInternal(); + } else { + return null; + } + } } diff --git a/hypo-model/src/main/java/dev/denwav/hypo/model/data/LazyClassData.java b/hypo-model/src/main/java/dev/denwav/hypo/model/data/LazyClassData.java index be0c988..44bb8eb 100644 --- a/hypo-model/src/main/java/dev/denwav/hypo/model/data/LazyClassData.java +++ b/hypo-model/src/main/java/dev/denwav/hypo/model/data/LazyClassData.java @@ -18,6 +18,7 @@ package dev.denwav.hypo.model.data; +import dev.denwav.hypo.types.sig.ClassSignature; import java.io.IOException; import java.util.EnumSet; import java.util.List; @@ -53,6 +54,13 @@ public LazyClassData() {} */ public abstract @NotNull String computeName(); + /** + * {@code compute} variant of {@link #signature()}. + * + * @return The generic signature of the class. + */ + public abstract @Nullable ClassSignature computeSignature(); + /** * {@code compute} variant of {@link #outerClass()}. * @@ -166,6 +174,13 @@ public LazyClassData() {} return this.name.getNotNull(); } + @SuppressWarnings("this-escape") + private final @NotNull LazyValue signature = LazyValue.of(this::computeSignature); + @Override + public @Nullable ClassSignature signature() { + return this.signature.get(); + } + @SuppressWarnings("this-escape") private final @NotNull LazyValue outerClass = LazyValue.of(this::computeOuterClass); @Override diff --git a/hypo-model/src/main/java/dev/denwav/hypo/model/data/LazyFieldData.java b/hypo-model/src/main/java/dev/denwav/hypo/model/data/LazyFieldData.java new file mode 100644 index 0000000..42874ca --- /dev/null +++ b/hypo-model/src/main/java/dev/denwav/hypo/model/data/LazyFieldData.java @@ -0,0 +1,63 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.model.data; + +import dev.denwav.hypo.types.desc.TypeDescriptor; +import dev.denwav.hypo.types.sig.TypeSignature; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Base implementation of {@link FieldData} which lazily retrieves and caches the field type. + */ +public abstract class LazyFieldData extends AbstractFieldData { + + /** + * Default constructor. + */ + public LazyFieldData() {} + + /** + * {@code compute} variant of {@link #descriptor()}. + * + * @return This field's descriptor. + */ + public abstract @NotNull TypeDescriptor computeDescriptor(); + + /** + * {@code compute} variant of {@link #signature()}. + * + * @return This field's generic type signature. + */ + public abstract @Nullable TypeSignature computeSignature(); + + @SuppressWarnings("this-escape") + private final @NotNull LazyValue descriptor = LazyValue.of(this::computeDescriptor); + @Override + public @NotNull TypeDescriptor descriptor() { + return this.descriptor.getNotNull(); + } + + @SuppressWarnings("this-escape") + private final @NotNull LazyValue signature = LazyValue.of(this::computeSignature); + @Override + public @Nullable TypeSignature signature() { + return this.signature.get(); + } +} diff --git a/hypo-model/src/main/java/dev/denwav/hypo/model/data/LazyMethodData.java b/hypo-model/src/main/java/dev/denwav/hypo/model/data/LazyMethodData.java index 443ced3..56e8f72 100644 --- a/hypo-model/src/main/java/dev/denwav/hypo/model/data/LazyMethodData.java +++ b/hypo-model/src/main/java/dev/denwav/hypo/model/data/LazyMethodData.java @@ -18,7 +18,10 @@ package dev.denwav.hypo.model.data; +import dev.denwav.hypo.types.desc.MethodDescriptor; +import dev.denwav.hypo.types.sig.MethodSignature; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; /** * Base implementation of {@link MethodData} which lazily retrieves and caches the method descriptor. @@ -37,6 +40,13 @@ public LazyMethodData() {} */ public abstract @NotNull MethodDescriptor computeDescriptor(); + /** + * {@code compute} variant of {@link #signature()}. + * + * @return This method's generic type signature. + */ + public abstract @Nullable MethodSignature computeSignature(); + @SuppressWarnings("this-escape") private final @NotNull LazyValue descriptor = LazyValue.of(this::computeDescriptor); @@ -44,4 +54,12 @@ public LazyMethodData() {} public @NotNull MethodDescriptor descriptor() { return this.descriptor.getNotNull(); } + + @SuppressWarnings("this-escape") + private final @NotNull LazyValue signature = LazyValue.of(this::computeSignature); + + @Override + public @Nullable MethodSignature signature() { + return this.signature.get(); + } } diff --git a/hypo-model/src/main/java/dev/denwav/hypo/model/data/MethodData.java b/hypo-model/src/main/java/dev/denwav/hypo/model/data/MethodData.java index 6adc5e7..05010aa 100644 --- a/hypo-model/src/main/java/dev/denwav/hypo/model/data/MethodData.java +++ b/hypo-model/src/main/java/dev/denwav/hypo/model/data/MethodData.java @@ -18,8 +18,10 @@ package dev.denwav.hypo.model.data; -import dev.denwav.hypo.model.data.types.JvmType; -import dev.denwav.hypo.model.data.types.PrimitiveType; +import dev.denwav.hypo.types.PrimitiveType; +import dev.denwav.hypo.types.desc.MethodDescriptor; +import dev.denwav.hypo.types.desc.TypeDescriptor; +import dev.denwav.hypo.types.sig.MethodSignature; import java.util.List; import java.util.Set; import org.jetbrains.annotations.NotNull; @@ -50,7 +52,29 @@ public interface MethodData extends MemberData { * @see #descriptor() */ default @NotNull String descriptorText() { - return this.descriptor().toInternalString(); + return this.descriptor().asInternal(); + } + + /** + * The generic signature of this method. + * + * @return This method's generic signature. + */ + @Nullable MethodSignature signature(); + + /** + * The internal JVM text representation of this method's generic signature. + * + * @return The internal JVM text representation of this method's generic signature. + * @see #signature() + */ + default @Nullable String signatureText() { + final MethodSignature sig = this.signature(); + if (sig != null) { + return sig.asInternal(); + } else { + return null; + } } /** @@ -85,8 +109,8 @@ default boolean isConstructor() { * @return This method's list of parameters. * @see #descriptor() */ - default @NotNull List<@NotNull JvmType> params() { - return this.descriptor().getParams(); + default @NotNull List params() { + return this.descriptor().getParameters(); } /** @@ -99,8 +123,8 @@ default boolean isConstructor() { * @throws IndexOutOfBoundsException If the given index is out of bounds. * @see #paramLvt(int) */ - default @NotNull JvmType param(final int i) { - final List<@NotNull JvmType> params = this.descriptor().getParams(); + default @NotNull TypeDescriptor param(final int i) { + final List params = this.descriptor().getParameters(); if (i < 0 || i >= params.size()) { throw new IndexOutOfBoundsException( "Index out of range: " + i + ", list has " + params.size() + " items" @@ -119,12 +143,12 @@ default boolean isConstructor() { * @see #param(int) */ @SuppressWarnings("EmptyCatch") - default @Nullable JvmType paramLvt(final int i) { - final List<@NotNull JvmType> params = this.descriptor().getParams(); + default @Nullable TypeDescriptor paramLvt(final int i) { + final List params = this.descriptor().getParameters(); // `this` int index = this.isStatic() ? 0 : 1; - for (final JvmType param : params) { + for (final TypeDescriptor param : params) { if (index == i) { return param; } @@ -144,7 +168,7 @@ default boolean isConstructor() { * @return This method's return type. * @see #descriptor() */ - default @NotNull JvmType returnType() { + default @NotNull TypeDescriptor returnType() { return this.descriptor().getReturnType(); } diff --git a/hypo-model/src/main/java/dev/denwav/hypo/model/data/MethodDescriptor.java b/hypo-model/src/main/java/dev/denwav/hypo/model/data/MethodDescriptor.java deleted file mode 100644 index a62f319..0000000 --- a/hypo-model/src/main/java/dev/denwav/hypo/model/data/MethodDescriptor.java +++ /dev/null @@ -1,254 +0,0 @@ -/* - * Hypo, an extensible and pluggable Java bytecode analytical model. - * - * Copyright (C) 2023 Kyle Wood (DenWav) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the Lesser GNU General Public License as published by - * the Free Software Foundation, version 3 of the License only. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - */ - -package dev.denwav.hypo.model.data; - -import com.google.errorprone.annotations.Immutable; -import dev.denwav.hypo.model.data.types.ArrayType; -import dev.denwav.hypo.model.data.types.ClassType; -import dev.denwav.hypo.model.data.types.JvmType; -import dev.denwav.hypo.model.data.types.PrimitiveType; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; -import java.util.Objects; -import org.jetbrains.annotations.Contract; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -/** - * The type descriptor for a method. This refers to a method's basic descriptor, not its generic type signature. - * - *

A method's descriptor consists of 2 components: - * - *

    - *
  1. The method parameter list
  2. - *
  3. The method return type
  4. - *
- */ -@Immutable -public final class MethodDescriptor { - - @SuppressWarnings("Immutable") // errorprone doesn't know that this is immutable - private final @NotNull List<@NotNull JvmType> params; - private final @NotNull JvmType returnType; - - /** - * Create a new descriptor for the given method parameter types and return type. - * - * @param params The method parameter types. - * @param returnType The method return type. - */ - public MethodDescriptor(final @NotNull List<@NotNull JvmType> params, final @NotNull JvmType returnType) { - this.params = List.copyOf(params); - this.returnType = returnType; - } - - /** - * Parses the given method descriptor string into a new {@link MethodDescriptor}. The descriptor string must be a - * valid descriptor in the internal JVM format. Invalid descriptor strings will result in a - * {@link IllegalArgumentException}. - * - * @param desc The method descriptor string to parse. - * @return The new {@link MethodDescriptor}. - * @throws IllegalArgumentException If the given descriptor is not valid. - */ - public static @NotNull MethodDescriptor parseDescriptor(final @NotNull String desc) { - if (!desc.startsWith("(")) { - throw new IllegalArgumentException("desc is invalid: Does not start with '(': " + desc); - } - - final ArrayList params = new ArrayList<>(); - JvmType returnType = null; - - final JvmType[] ref = new JvmType[1]; - - final int len = desc.length(); - // starting at 1, skipping first '(' - for (int i = 1; i < len; i++) { - i = parseType(ref, desc, i); - final JvmType t = ref[0]; - if (t == null) { - parseType(ref, desc, i + 1); - returnType = ref[0]; - break; - } - params.add(t); - } - - if (returnType == null) { - throw new IllegalArgumentException("desc is invalid: Does not have a return type: " + desc); - } - - return new MethodDescriptor(params, returnType); - } - - private static int parseType(final @Nullable JvmType @NotNull [] ref, final @NotNull String desc, final int index) { - /* - * The `ref` parameter is our ugly way of doing an `out` parameter, since we need to return the type parsed as - * well as the new index. So we return `int` for the new index and simply set the type result into `ref`. `ref` - * is an array of length 1 - there aren't any validation checks here as this is a private method. - * - * This method also returns the index _before_ the next index. So each of the single-character types simply - * return `index` instead of `index + 1`. This is intentional for 2 reasons: - * - * 1. The calling method, `parseString`, is doing range checks on the `desc` string. If the character just - * parsed is the final character in the string we don't want to overrun that on accident. - * 2. The calling method is doing a standard `for` loop, so the index variable will be incremented - * automatically anyways if it satisfies the bounds-check. If we increment here then that will result in a - * double-increment. - */ - - final char c = desc.charAt(index); - switch (c) { - case 'B': - ref[0] = PrimitiveType.BYTE; - return index; - case 'S': - ref[0] = PrimitiveType.SHORT; - return index; - case 'I': - ref[0] = PrimitiveType.INT; - return index; - case 'J': - ref[0] = PrimitiveType.LONG; - return index; - case 'F': - ref[0] = PrimitiveType.FLOAT; - return index; - case 'D': - ref[0] = PrimitiveType.DOUBLE; - return index; - case 'C': - ref[0] = PrimitiveType.CHAR; - return index; - case 'Z': - ref[0] = PrimitiveType.BOOLEAN; - return index; - case 'V': - ref[0] = PrimitiveType.VOID; - return index; - case 'L': - final int end = desc.indexOf(';', index); - if (end == -1) { - throw new IllegalArgumentException("desc is invalid: Class type at index " + index + - " is not terminated: " + desc); - } - ref[0] = new ClassType(desc.substring(index, end + 1)); - return end; - case '[': - final int len = desc.length(); - int dim = 1; - int i = index; - i++; - for (; i < len; i++) { - if (desc.charAt(i) == '[') { - dim++; - } else { - break; - } - } - - final int newIndex = parseType(ref, desc, i); - final JvmType parsed = ref[0]; - if (parsed == null) { - throw new IllegalArgumentException("desc is invalid: Array type at index " + index + - " is not terminated: " + desc); - } - ref[0] = new ArrayType(parsed, dim); - return newIndex; - case ')': - ref[0] = null; - return index; - default: - throw new IllegalArgumentException("desc is invalid: Unknown type char at index " + index + " '" + c + - "': " + desc); - } - } - - /** - * Returns the method parameter component of the descriptor. - * @return The method parameter component of the descriptor. - */ - public @NotNull List<@NotNull JvmType> getParams() { - return this.params; - } - - /** - * Returns the method return type of the descriptor. - * @return The method return type of the descriptor. - */ - public @NotNull JvmType getReturnType() { - return this.returnType; - } - - /** - * Returns the string representation of this descriptor in the internal JVM format. - * - *

Use {@link #toString()} for a more human-readable string representation. - * - * @return The string representation of this descriptor in the internal JVM format. - */ - @Contract(pure = true) - public @NotNull String toInternalString() { - final StringBuilder sb = new StringBuilder(); - sb.append('('); - for (final JvmType param : this.params) { - param.asInternalName(sb); - } - sb.append(')'); - this.returnType.asInternalName(sb); - return sb.toString(); - } - - @Override - public boolean equals(final Object o) { - if (this == o) return true; - if (o == null || this.getClass() != o.getClass()) return false; - final MethodDescriptor that = (MethodDescriptor) o; - return this.params.equals(that.params) && this.returnType.equals(that.returnType); - } - - @Override - public int hashCode() { - return Objects.hash(this.params, this.returnType); - } - - /** - * Returns the string representation of this descriptor in a human-readable format. - * - *

Use {@link #toInternalString()} for a string representation in the internal JVM format. - * - * @return The string representation of this descriptor in a human-readable format. - */ - @Override - public String toString() { - final StringBuilder sb = new StringBuilder(); - sb.append('('); - final Iterator<@NotNull JvmType> iter = this.params.iterator(); - while (iter.hasNext()) { - iter.next().asReadableName(sb); - if (iter.hasNext()) { - sb.append(", "); - } - } - sb.append(") "); - this.returnType.asReadableName(sb); - return sb.toString(); - } -} diff --git a/hypo-model/src/main/java/dev/denwav/hypo/model/data/package-info.java b/hypo-model/src/main/java/dev/denwav/hypo/model/data/package-info.java index 1a65e60..afdbf33 100644 --- a/hypo-model/src/main/java/dev/denwav/hypo/model/data/package-info.java +++ b/hypo-model/src/main/java/dev/denwav/hypo/model/data/package-info.java @@ -27,6 +27,6 @@ * up the first part. * *

The Hypo class data model is centered around {@link dev.denwav.hypo.model.data.ClassData ClassData}. Java types - * are modeled in the {@link dev.denwav.hypo.model.data.types} package. + * are modeled in the {@link dev.denwav.hypo.types} package found in the {@code hypo-types} module. */ package dev.denwav.hypo.model.data; diff --git a/hypo-model/src/main/java/dev/denwav/hypo/model/data/types/ArrayType.java b/hypo-model/src/main/java/dev/denwav/hypo/model/data/types/ArrayType.java deleted file mode 100644 index aa80c9a..0000000 --- a/hypo-model/src/main/java/dev/denwav/hypo/model/data/types/ArrayType.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Hypo, an extensible and pluggable Java bytecode analytical model. - * - * Copyright (C) 2023 Kyle Wood (DenWav) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the Lesser GNU General Public License as published by - * the Free Software Foundation, version 3 of the License only. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - */ - -package dev.denwav.hypo.model.data.types; - -import com.google.errorprone.annotations.Immutable; -import java.util.Objects; -import org.jetbrains.annotations.NotNull; - -/** - * An array type. Array types in Java have 2 components: - *

    - *
  1. - * The base type, which is one of either: - *
      - *
    • {@link PrimitiveType Primitive type}
    • - *
    • {@link ClassType Class type}
    • - *
    - *
  2. - *
  3. The dimension of the array
  4. - *
- */ -@Immutable -public final class ArrayType implements JvmType { - - private final @NotNull JvmType baseType; - - private final int dimension; - - /** - * Create a new array type with the given base type and dimension. - * - * @param baseType The base type of this array type. - * @param dimension The dimension of this array type. - * @throws IllegalArgumentException If the given {@code dimension <= 0} or the given {@code baseType} is an - * {@link ArrayType} - */ - public ArrayType(final @NotNull JvmType baseType, final int dimension) { - if (dimension <= 0) { - throw new IllegalArgumentException("Invalid array dimension: " + dimension); - } - if (baseType instanceof ArrayType) { - throw new IllegalArgumentException("Invalid array base type: " + baseType.getClass().getName()); - } - - this.baseType = baseType; - this.dimension = dimension; - } - - /** - * Get the base type of this array type. - * - * @return The base type of this array type. - */ - public @NotNull JvmType baseType() { - return this.baseType; - } - - /** - * Get the dimension of this array type. - * - * @return The dimension of this array type. - */ - public int dimension() { - return this.dimension; - } - - private void internalPrefix(final @NotNull StringBuilder sb) { - for (int i = 0; i < this.dimension; i++) { - sb.append('['); - } - } - - private void readableSuffix(final @NotNull StringBuilder sb) { - for (int i = 0; i < this.dimension; i++) { - sb.append("[]"); - } - } - - @Override - public void asReadableName(final @NotNull StringBuilder sb) { - this.baseType.asReadableName(sb); - this.readableSuffix(sb); - } - - @Override - public void asInternalName(final @NotNull StringBuilder sb) { - this.internalPrefix(sb); - this.baseType.asInternalName(sb); - } - - @Override - public boolean equals(final Object o) { - if (this == o) return true; - if (!(o instanceof ArrayType)) return false; - final ArrayType arrayType = (ArrayType) o; - return this.dimension == arrayType.dimension && this.baseType.equals(arrayType.baseType); - } - - @Override - public int hashCode() { - return Objects.hash(this.baseType, this.dimension); - } - - @Override - public String toString() { - return this.asReadableName(); - } -} diff --git a/hypo-model/src/main/java/dev/denwav/hypo/model/data/types/ClassType.java b/hypo-model/src/main/java/dev/denwav/hypo/model/data/types/ClassType.java deleted file mode 100644 index 3ee9ed0..0000000 --- a/hypo-model/src/main/java/dev/denwav/hypo/model/data/types/ClassType.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Hypo, an extensible and pluggable Java bytecode analytical model. - * - * Copyright (C) 2023 Kyle Wood (DenWav) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the Lesser GNU General Public License as published by - * the Free Software Foundation, version 3 of the License only. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - */ - -package dev.denwav.hypo.model.data.types; - -import com.google.errorprone.annotations.Immutable; -import dev.denwav.hypo.model.HypoModelUtil; -import java.util.Objects; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; - -/** - * A standard class or reference type. - */ -@Immutable -public final class ClassType implements JvmType { - - private final @NotNull String className; - - /** - * Create a new class type from the given class name. - * - * @param className The name of the class this type represents. - */ - public ClassType(final @NotNull String className) { - this.className = HypoModelUtil.normalizedClassName(className); - } - - @Override - public void asReadableName(final @NotNull StringBuilder sb) { - sb.append(this.className.replace('/', '.')); - } - - @Override - public void asInternalName(final @NotNull StringBuilder sb) { - sb.append('L').append(this.className).append(';'); - } - - /** - * Returns the primitive type associated with this class type if this is a primitive wrapper class type. - * - * @return The primitive type associated with this class type, or {@code null} if this is a primitive wrapper class - * type. - */ - public @Nullable PrimitiveType toPrimitiveType() { - for (final PrimitiveType value : PrimitiveType.values()) { - if (value.toWrapperType().equals(this)) { - return value; - } - } - return null; - } - - @Override - public boolean equals(final Object o) { - if (this == o) return true; - if (!(o instanceof ClassType)) return false; - final ClassType classType = (ClassType) o; - return this.className.equals(classType.className); - } - - @Override - public int hashCode() { - return Objects.hash(this.className); - } - - @Override - public String toString() { - return this.asReadableName(); - } -} diff --git a/hypo-model/src/main/java/dev/denwav/hypo/model/data/types/JvmType.java b/hypo-model/src/main/java/dev/denwav/hypo/model/data/types/JvmType.java deleted file mode 100644 index 879cbbc..0000000 --- a/hypo-model/src/main/java/dev/denwav/hypo/model/data/types/JvmType.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Hypo, an extensible and pluggable Java bytecode analytical model. - * - * Copyright (C) 2023 Kyle Wood (DenWav) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the Lesser GNU General Public License as published by - * the Free Software Foundation, version 3 of the License only. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - */ - -package dev.denwav.hypo.model.data.types; - -import com.google.errorprone.annotations.Immutable; -import org.jetbrains.annotations.NotNull; - -/** - * A type in the Java language. There are 3 general categories of types in the JVM: - *
    - *
  • {@link PrimitiveType Primitive types}
  • - *
  • {@link ClassType Class types}
  • - *
  • {@link ArrayType Array types}
  • - *
- */ -@Immutable -public interface JvmType { - - /** - * Print this type name as a human-readable name to the given {@link StringBuilder}. - * - * @param sb The {@link StringBuilder} to print this type's human-readable name to. - */ - void asReadableName(final @NotNull StringBuilder sb); - - /** - * Print this type name as an internal JVM name to the given {@link StringBuilder}. - * - * @param sb The {@link StringBuilder} to print this type's internal JVM name to. - */ - void asInternalName(final @NotNull StringBuilder sb); - - /** - * Returns this type's human-readable name. - * @return This type's human-readable name. - */ - default @NotNull String asReadableName() { - final StringBuilder sb = new StringBuilder(); - this.asReadableName(sb); - return sb.toString(); - } - - /** - * Returns this type's internal JVM name. - * @return This type's internal JVM name. - */ - default @NotNull String asInternalName() { - final StringBuilder sb = new StringBuilder(); - this.asInternalName(sb); - return sb.toString(); - } -} diff --git a/hypo-model/src/main/java/dev/denwav/hypo/model/data/types/PrimitiveType.java b/hypo-model/src/main/java/dev/denwav/hypo/model/data/types/PrimitiveType.java deleted file mode 100644 index 12f4b6f..0000000 --- a/hypo-model/src/main/java/dev/denwav/hypo/model/data/types/PrimitiveType.java +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Hypo, an extensible and pluggable Java bytecode analytical model. - * - * Copyright (C) 2023 Kyle Wood (DenWav) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the Lesser GNU General Public License as published by - * the Free Software Foundation, version 3 of the License only. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - */ - -package dev.denwav.hypo.model.data.types; - -import com.google.errorprone.annotations.Immutable; -import org.jetbrains.annotations.NotNull; - -/** - * Primitive JVM types. - */ -@Immutable -public enum PrimitiveType implements JvmType { - /** - * {@link Character#TYPE} - */ - CHAR("char", "C", "java/lang/Character"), - /** - * {@link Byte#TYPE} - */ - BYTE("byte", "B", "java/lang/Byte"), - /** - * {@link Short#TYPE} - */ - SHORT("short", "S", "java/lang/Short"), - /** - * {@link Integer#TYPE} - */ - INT("int", "I", "java/lang/Integer"), - /** - * {@link Long#TYPE} - */ - LONG("long", "J", "java/lang/Long"), - /** - * {@link Float#TYPE} - */ - FLOAT("float", "F", "java/lang/Float"), - /** - * {@link Double#TYPE} - */ - DOUBLE("double", "D", "java/lang/Double"), - /** - * {@link Boolean#TYPE} - */ - BOOLEAN("boolean", "Z", "java/lang/Boolean"), - /** - * {@link Void#TYPE} - */ - VOID("void", "V", "java/lang/Void"); - - private final @NotNull String readableName; - private final @NotNull String internalName; - private final @NotNull ClassType wrapperType; - - PrimitiveType( - final @NotNull String readableName, - final @NotNull String internalName, - final @NotNull String wrapperType - ) { - this.readableName = readableName; - this.internalName = internalName; - this.wrapperType = new ClassType(wrapperType); - } - - /** - * Returns the associated wrapper {@link ClassType class type} for this primitive type. - * @return The associated wrapper {@link ClassType class type} for this primitive type. - */ - public @NotNull ClassType toWrapperType() { - return this.wrapperType; - } - - @Override - public void asReadableName(final @NotNull StringBuilder sb) { - sb.append(this.readableName); - } - - @Override - public void asInternalName(final @NotNull StringBuilder sb) { - sb.append(this.internalName); - } - - /** - * Get the primitive type associated with the given character. The character must be a valid internal JVM primitive - * type character. - * - * @param c The character to match to a primitive type. - * @return The primitive type associated with the given character. - * @throws IllegalStateException If the given character does not match an internal JVM primitive type character. - * @see #asInternalName() - */ - public static @NotNull PrimitiveType fromChar(final char c) { - for (final PrimitiveType v : PrimitiveType.values()) { - if (v.internalName.charAt(0) == c) { - return v; - } - } - throw new IllegalStateException("Unknown type: " + c); - } - - @Override - public String toString() { - return this.asReadableName(); - } -} diff --git a/hypo-model/src/main/java/module-info.java b/hypo-model/src/main/java/module-info.java new file mode 100644 index 0000000..0e4a173 --- /dev/null +++ b/hypo-model/src/main/java/module-info.java @@ -0,0 +1,29 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +/** + * Primary data model module for Hypo. + */ +module dev.denwav.hypo.model { + requires transitive dev.denwav.hypo.types; + + requires static transitive org.jetbrains.annotations; + + exports dev.denwav.hypo.model; + exports dev.denwav.hypo.model.data; +} diff --git a/hypo-model/src/test/java/dev/denwav/hypo/model/data/MethodDescriptorTest.java b/hypo-model/src/test/java/dev/denwav/hypo/model/data/MethodDescriptorTest.java deleted file mode 100644 index 376bec2..0000000 --- a/hypo-model/src/test/java/dev/denwav/hypo/model/data/MethodDescriptorTest.java +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Hypo, an extensible and pluggable Java bytecode analytical model. - * - * Copyright (C) 2023 Kyle Wood (DenWav) - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the Lesser GNU General Public License as published by - * the Free Software Foundation, version 3 of the License only. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public License - * along with this program. If not, see . - */ - -package dev.denwav.hypo.model.data; - -import dev.denwav.hypo.model.data.types.ArrayType; -import dev.denwav.hypo.model.data.types.ClassType; -import dev.denwav.hypo.model.data.types.JvmType; -import dev.denwav.hypo.model.data.types.PrimitiveType; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; - -@DisplayName("MethodDescriptor Tests") -public class MethodDescriptorTest { - - @Test - @DisplayName("Test MethodDescriptor.parseDescriptor() and MethodDescriptor.toInternalString()") - public void testParse() { - // Test a whole bunch of permutations, no need to make each permutation its own test though - final JvmType[] testTypes = new JvmType[]{ - PrimitiveType.CHAR, - PrimitiveType.BYTE, - PrimitiveType.SHORT, - PrimitiveType.INT, - PrimitiveType.LONG, - PrimitiveType.FLOAT, - PrimitiveType.DOUBLE, - PrimitiveType.BOOLEAN, - new ClassType("Ljava/lang/Object;"), - new ClassType("Ljava/lang/String;"), - new ArrayType(new ClassType("Ljava/lang/Object;"), 1), - new ArrayType(new ClassType("Ljava/lang/String;"), 2) - }; - - final ArrayList returnTypeList = new ArrayList<>(Arrays.asList(testTypes)); - returnTypeList.add(PrimitiveType.VOID); - final JvmType[] returnTypes = returnTypeList.toArray(new JvmType[0]); - - final JvmType[] params = new JvmType[4]; - final List paramList = Arrays.asList(params); - - for (final JvmType param0 : testTypes) { - for (final JvmType param1 : testTypes) { - for (final JvmType param2 : testTypes) { - for (final JvmType param3 : testTypes) { - params[0] = param0; - params[1] = param1; - params[2] = param2; - params[3] = param3; - - for (final JvmType returnType : returnTypes) { - final MethodDescriptor dec = new MethodDescriptor(paramList, returnType); - Assertions.assertEquals(dec, MethodDescriptor.parseDescriptor(dec.toInternalString())); - } - } - } - } - } - } -} diff --git a/hypo-test/build.gradle.kts b/hypo-test/build.gradle.kts index 3c4b3e6..90ec0fd 100644 --- a/hypo-test/build.gradle.kts +++ b/hypo-test/build.gradle.kts @@ -26,6 +26,8 @@ dependencies { api(libs.log4j.core) api(libs.slf4j.log4j2.impl) + compileOnly(libs.log4j.requirement) + api(libs.junit.api) runtimeOnly(libs.junit.runtime) } diff --git a/hypo-test/hypo-test-data/build.gradle.kts b/hypo-test/hypo-test-data/build.gradle.kts index 7fc72aa..43d1dae 100644 --- a/hypo-test/hypo-test-data/build.gradle.kts +++ b/hypo-test/hypo-test-data/build.gradle.kts @@ -3,3 +3,11 @@ plugins { `hypo-java` `hypo-test-scenario-data` } + +// 21 by default +tasks.withType().configureEach { + javaCompiler = javaToolchains.compilerFor { + languageVersion = JavaLanguageVersion.of(21) + } + options.release = 21 +} diff --git a/hypo-test/hypo-test-data/src/scenario-01/java/scenario01/ChildClass01.java b/hypo-test/hypo-test-data/src/scenario-01/java/scenario01/ChildClass01.java index ce3e406..90ee1f8 100644 --- a/hypo-test/hypo-test-data/src/scenario-01/java/scenario01/ChildClass01.java +++ b/hypo-test/hypo-test-data/src/scenario-01/java/scenario01/ChildClass01.java @@ -1,5 +1,6 @@ package scenario01; +// Compiled with JDK 21 public class ChildClass01 extends ParentClass { @Override diff --git a/hypo-test/hypo-test-data/src/scenario-01/java/scenario01/ChildClass02.java b/hypo-test/hypo-test-data/src/scenario-01/java/scenario01/ChildClass02.java index eaa7b91..75780b1 100644 --- a/hypo-test/hypo-test-data/src/scenario-01/java/scenario01/ChildClass02.java +++ b/hypo-test/hypo-test-data/src/scenario-01/java/scenario01/ChildClass02.java @@ -1,5 +1,6 @@ package scenario01; +// Compiled with JDK 21 public class ChildClass02 extends ParentClass { @Override diff --git a/hypo-test/hypo-test-data/src/scenario-01/java/scenario01/ParentClass.java b/hypo-test/hypo-test-data/src/scenario-01/java/scenario01/ParentClass.java index 0d664c7..442f38c 100644 --- a/hypo-test/hypo-test-data/src/scenario-01/java/scenario01/ParentClass.java +++ b/hypo-test/hypo-test-data/src/scenario-01/java/scenario01/ParentClass.java @@ -1,5 +1,6 @@ package scenario01; +// Compiled with JDK 21 public class ParentClass { public void method() {} diff --git a/hypo-test/hypo-test-data/src/scenario-02/java/scenario02/ChildClass.java b/hypo-test/hypo-test-data/src/scenario-02/java/scenario02/ChildClass.java index f989a3b..5529f36 100644 --- a/hypo-test/hypo-test-data/src/scenario-02/java/scenario02/ChildClass.java +++ b/hypo-test/hypo-test-data/src/scenario-02/java/scenario02/ChildClass.java @@ -1,5 +1,6 @@ package scenario02; +// Compiled with JDK 21 public class ChildClass extends ParentClass { @Override diff --git a/hypo-test/hypo-test-data/src/scenario-02/java/scenario02/GrandChildClass.java b/hypo-test/hypo-test-data/src/scenario-02/java/scenario02/GrandChildClass.java index 7fa6c4f..e78fd1e 100644 --- a/hypo-test/hypo-test-data/src/scenario-02/java/scenario02/GrandChildClass.java +++ b/hypo-test/hypo-test-data/src/scenario-02/java/scenario02/GrandChildClass.java @@ -1,5 +1,6 @@ package scenario02; +// Compiled with JDK 21 public class GrandChildClass extends ChildClass { @Override diff --git a/hypo-test/hypo-test-data/src/scenario-02/java/scenario02/ParentClass.java b/hypo-test/hypo-test-data/src/scenario-02/java/scenario02/ParentClass.java index eb55184..39979ad 100644 --- a/hypo-test/hypo-test-data/src/scenario-02/java/scenario02/ParentClass.java +++ b/hypo-test/hypo-test-data/src/scenario-02/java/scenario02/ParentClass.java @@ -1,5 +1,6 @@ package scenario02; +// Compiled with JDK 21 public class ParentClass { public void method() {} diff --git a/hypo-test/hypo-test-data/src/scenario-03/java/scenario03/ChildClassByte.java b/hypo-test/hypo-test-data/src/scenario-03/java/scenario03/ChildClassByte.java index 7a4703b..f3be28b 100644 --- a/hypo-test/hypo-test-data/src/scenario-03/java/scenario03/ChildClassByte.java +++ b/hypo-test/hypo-test-data/src/scenario-03/java/scenario03/ChildClassByte.java @@ -1,5 +1,6 @@ package scenario03; +// Compiled with JDK 21 public class ChildClassByte extends ParentClass { @Override diff --git a/hypo-test/hypo-test-data/src/scenario-03/java/scenario03/ChildClassString.java b/hypo-test/hypo-test-data/src/scenario-03/java/scenario03/ChildClassString.java index be40fab..5c4c120 100644 --- a/hypo-test/hypo-test-data/src/scenario-03/java/scenario03/ChildClassString.java +++ b/hypo-test/hypo-test-data/src/scenario-03/java/scenario03/ChildClassString.java @@ -1,5 +1,6 @@ package scenario03; +// Compiled with JDK 21 public class ChildClassString extends ParentClass { @Override diff --git a/hypo-test/hypo-test-data/src/scenario-03/java/scenario03/GrandChildClassString.java b/hypo-test/hypo-test-data/src/scenario-03/java/scenario03/GrandChildClassString.java index f137b5c..0c83016 100644 --- a/hypo-test/hypo-test-data/src/scenario-03/java/scenario03/GrandChildClassString.java +++ b/hypo-test/hypo-test-data/src/scenario-03/java/scenario03/GrandChildClassString.java @@ -1,5 +1,6 @@ package scenario03; +// Compiled with JDK 21 public class GrandChildClassString extends ChildClassString { @Override diff --git a/hypo-test/hypo-test-data/src/scenario-03/java/scenario03/ParentClass.java b/hypo-test/hypo-test-data/src/scenario-03/java/scenario03/ParentClass.java index 9215669..60a0740 100644 --- a/hypo-test/hypo-test-data/src/scenario-03/java/scenario03/ParentClass.java +++ b/hypo-test/hypo-test-data/src/scenario-03/java/scenario03/ParentClass.java @@ -1,5 +1,6 @@ package scenario03; +// Compiled with JDK 21 public class ParentClass { public Object method() { diff --git a/hypo-test/hypo-test-data/src/scenario-04/java/scenario04/ChildClassByte.java b/hypo-test/hypo-test-data/src/scenario-04/java/scenario04/ChildClassByte.java index 1b6101f..8e48178 100644 --- a/hypo-test/hypo-test-data/src/scenario-04/java/scenario04/ChildClassByte.java +++ b/hypo-test/hypo-test-data/src/scenario-04/java/scenario04/ChildClassByte.java @@ -1,5 +1,6 @@ package scenario04; +// Compiled with JDK 21 public class ChildClassByte extends ParentClass { @Override diff --git a/hypo-test/hypo-test-data/src/scenario-04/java/scenario04/ChildClassString.java b/hypo-test/hypo-test-data/src/scenario-04/java/scenario04/ChildClassString.java index 5660747..be1d3c9 100644 --- a/hypo-test/hypo-test-data/src/scenario-04/java/scenario04/ChildClassString.java +++ b/hypo-test/hypo-test-data/src/scenario-04/java/scenario04/ChildClassString.java @@ -1,5 +1,6 @@ package scenario04; +// Compiled with JDK 21 public class ChildClassString extends ParentClass { @Override diff --git a/hypo-test/hypo-test-data/src/scenario-04/java/scenario04/ParentClass.java b/hypo-test/hypo-test-data/src/scenario-04/java/scenario04/ParentClass.java index 4741b6c..c765740 100644 --- a/hypo-test/hypo-test-data/src/scenario-04/java/scenario04/ParentClass.java +++ b/hypo-test/hypo-test-data/src/scenario-04/java/scenario04/ParentClass.java @@ -1,5 +1,6 @@ package scenario04; +// Compiled with JDK 21 public class ParentClass { public Object a() { diff --git a/hypo-test/hypo-test-data/src/scenario-05/java/scenario05/ChildClassByte.java b/hypo-test/hypo-test-data/src/scenario-05/java/scenario05/ChildClassByte.java index 987c15d..d4929f7 100644 --- a/hypo-test/hypo-test-data/src/scenario-05/java/scenario05/ChildClassByte.java +++ b/hypo-test/hypo-test-data/src/scenario-05/java/scenario05/ChildClassByte.java @@ -1,5 +1,6 @@ package scenario05; +// Compiled with JDK 21 public class ChildClassByte extends ParentClass { // implements Str diff --git a/hypo-test/hypo-test-data/src/scenario-05/java/scenario05/ChildClassString.java b/hypo-test/hypo-test-data/src/scenario-05/java/scenario05/ChildClassString.java index fa42c08..8444e97 100644 --- a/hypo-test/hypo-test-data/src/scenario-05/java/scenario05/ChildClassString.java +++ b/hypo-test/hypo-test-data/src/scenario-05/java/scenario05/ChildClassString.java @@ -1,5 +1,6 @@ package scenario05; +// Compiled with JDK 21 public class ChildClassString extends ParentClass { // implements str diff --git a/hypo-test/hypo-test-data/src/scenario-05/java/scenario05/ParentClass.java b/hypo-test/hypo-test-data/src/scenario-05/java/scenario05/ParentClass.java index 14ea752..67bfa6c 100644 --- a/hypo-test/hypo-test-data/src/scenario-05/java/scenario05/ParentClass.java +++ b/hypo-test/hypo-test-data/src/scenario-05/java/scenario05/ParentClass.java @@ -1,5 +1,6 @@ package scenario05; +// Compiled with JDK 21 public abstract class ParentClass implements Str { public Object b() { diff --git a/hypo-test/hypo-test-data/src/scenario-05/java/scenario05/Str.java b/hypo-test/hypo-test-data/src/scenario-05/java/scenario05/Str.java index 79236b7..eacfa7a 100644 --- a/hypo-test/hypo-test-data/src/scenario-05/java/scenario05/Str.java +++ b/hypo-test/hypo-test-data/src/scenario-05/java/scenario05/Str.java @@ -1,5 +1,6 @@ package scenario05; +// Compiled with JDK 21 public interface Str { String a(); diff --git a/hypo-test/hypo-test-data/src/scenario-06/java/scenario06/ChildClass.java b/hypo-test/hypo-test-data/src/scenario-06/java/scenario06/ChildClass.java index 4507e73..19b7a00 100644 --- a/hypo-test/hypo-test-data/src/scenario-06/java/scenario06/ChildClass.java +++ b/hypo-test/hypo-test-data/src/scenario-06/java/scenario06/ChildClass.java @@ -2,6 +2,7 @@ import java.util.List; +// Compiled with JDK 21 public class ChildClass extends ParentClass { public ChildClass(int left, int right, long up, String down, List forward, Object back) { diff --git a/hypo-test/hypo-test-data/src/scenario-06/java/scenario06/GrandChildClass.java b/hypo-test/hypo-test-data/src/scenario-06/java/scenario06/GrandChildClass.java index 57e6ffd..eee34b2 100644 --- a/hypo-test/hypo-test-data/src/scenario-06/java/scenario06/GrandChildClass.java +++ b/hypo-test/hypo-test-data/src/scenario-06/java/scenario06/GrandChildClass.java @@ -2,6 +2,7 @@ import java.util.List; +// Compiled with JDK 21 public class GrandChildClass extends ChildClass { public GrandChildClass(int left, int right, long up, String down, List forward, Object back) { diff --git a/hypo-test/hypo-test-data/src/scenario-06/java/scenario06/GreatGrandChildClass.java b/hypo-test/hypo-test-data/src/scenario-06/java/scenario06/GreatGrandChildClass.java index b533fa7..9f57d7a 100644 --- a/hypo-test/hypo-test-data/src/scenario-06/java/scenario06/GreatGrandChildClass.java +++ b/hypo-test/hypo-test-data/src/scenario-06/java/scenario06/GreatGrandChildClass.java @@ -2,6 +2,7 @@ import java.util.List; +// Compiled with JDK 21 public class GreatGrandChildClass extends GrandChildClass { public GreatGrandChildClass(int left, int right, long up, String down, List forward, Object back) { diff --git a/hypo-test/hypo-test-data/src/scenario-06/java/scenario06/ParentClass.java b/hypo-test/hypo-test-data/src/scenario-06/java/scenario06/ParentClass.java index e544baf..e1b5a99 100644 --- a/hypo-test/hypo-test-data/src/scenario-06/java/scenario06/ParentClass.java +++ b/hypo-test/hypo-test-data/src/scenario-06/java/scenario06/ParentClass.java @@ -2,6 +2,7 @@ import java.util.List; +// Compiled with JDK 21 public class ParentClass { public ParentClass(int left, int right, long up, String down, List forward, Object back) {} diff --git a/hypo-test/hypo-test-data/src/scenario-07/java/scenario07/ChildClass.java b/hypo-test/hypo-test-data/src/scenario-07/java/scenario07/ChildClass.java index 31c68e3..8a4492a 100644 --- a/hypo-test/hypo-test-data/src/scenario-07/java/scenario07/ChildClass.java +++ b/hypo-test/hypo-test-data/src/scenario-07/java/scenario07/ChildClass.java @@ -2,6 +2,7 @@ import java.util.List; +// Compiled with JDK 21 public class ChildClass extends ParentClass { public ChildClass(int right, int left, String down, long up, Object back, List forward) { diff --git a/hypo-test/hypo-test-data/src/scenario-07/java/scenario07/GrandChildClass.java b/hypo-test/hypo-test-data/src/scenario-07/java/scenario07/GrandChildClass.java index 56119ea..4fa9ad5 100644 --- a/hypo-test/hypo-test-data/src/scenario-07/java/scenario07/GrandChildClass.java +++ b/hypo-test/hypo-test-data/src/scenario-07/java/scenario07/GrandChildClass.java @@ -2,6 +2,7 @@ import java.util.List; +// Compiled with JDK 21 public class GrandChildClass extends ChildClass { public GrandChildClass(Object back, List forward, String down, long up, int right, int left) { diff --git a/hypo-test/hypo-test-data/src/scenario-07/java/scenario07/GreatGrandChildClass.java b/hypo-test/hypo-test-data/src/scenario-07/java/scenario07/GreatGrandChildClass.java index 3b471ed..e467c38 100644 --- a/hypo-test/hypo-test-data/src/scenario-07/java/scenario07/GreatGrandChildClass.java +++ b/hypo-test/hypo-test-data/src/scenario-07/java/scenario07/GreatGrandChildClass.java @@ -3,6 +3,7 @@ import java.util.List; import java.util.Locale; +// Compiled with JDK 21 public class GreatGrandChildClass extends GrandChildClass { public GreatGrandChildClass(int left, int right, long up, String down, List forward, Object back) { diff --git a/hypo-test/hypo-test-data/src/scenario-07/java/scenario07/ParentClass.java b/hypo-test/hypo-test-data/src/scenario-07/java/scenario07/ParentClass.java index 5a3fea1..98db99f 100644 --- a/hypo-test/hypo-test-data/src/scenario-07/java/scenario07/ParentClass.java +++ b/hypo-test/hypo-test-data/src/scenario-07/java/scenario07/ParentClass.java @@ -2,6 +2,7 @@ import java.util.List; +// Compiled with JDK 21 public class ParentClass { public ParentClass(int left, int right, long up, String down, List forward, Object back) {} diff --git a/hypo-test/hypo-test-data/src/scenario-08/java/scenario08/TestClass.java b/hypo-test/hypo-test-data/src/scenario-08/java/scenario08/TestClass.java index 3c8f4f7..025591a 100644 --- a/hypo-test/hypo-test-data/src/scenario-08/java/scenario08/TestClass.java +++ b/hypo-test/hypo-test-data/src/scenario-08/java/scenario08/TestClass.java @@ -1,5 +1,6 @@ package scenario08; +// Compiled with JDK 21 public class TestClass { public TestClass(int i, int j) {} diff --git a/hypo-test/hypo-test-data/src/scenario-09/java/scenario09/ChildClass.java b/hypo-test/hypo-test-data/src/scenario-09/java/scenario09/ChildClass.java index f4c9231..ece5718 100644 --- a/hypo-test/hypo-test-data/src/scenario-09/java/scenario09/ChildClass.java +++ b/hypo-test/hypo-test-data/src/scenario-09/java/scenario09/ChildClass.java @@ -1,5 +1,6 @@ package scenario09; +// Compiled with JDK 21 public class ChildClass extends ParentClass { public ChildClass(long i, long j) { diff --git a/hypo-test/hypo-test-data/src/scenario-09/java/scenario09/ParentClass.java b/hypo-test/hypo-test-data/src/scenario-09/java/scenario09/ParentClass.java index 42f3304..fd4177f 100644 --- a/hypo-test/hypo-test-data/src/scenario-09/java/scenario09/ParentClass.java +++ b/hypo-test/hypo-test-data/src/scenario-09/java/scenario09/ParentClass.java @@ -1,5 +1,6 @@ package scenario09; +// Compiled with JDK 21 public class ParentClass { public ParentClass(int i, int j) {} diff --git a/hypo-test/hypo-test-data/src/scenario-10/java/scenario10/ChildClass.java b/hypo-test/hypo-test-data/src/scenario-10/java/scenario10/ChildClass.java index 9c10e83..c381f1b 100644 --- a/hypo-test/hypo-test-data/src/scenario-10/java/scenario10/ChildClass.java +++ b/hypo-test/hypo-test-data/src/scenario-10/java/scenario10/ChildClass.java @@ -1,5 +1,6 @@ package scenario10; +// Compiled with JDK 21 public class ChildClass extends ParentClass { @Override diff --git a/hypo-test/hypo-test-data/src/scenario-10/java/scenario10/ParentClass.java b/hypo-test/hypo-test-data/src/scenario-10/java/scenario10/ParentClass.java index 4a40970..1350a68 100644 --- a/hypo-test/hypo-test-data/src/scenario-10/java/scenario10/ParentClass.java +++ b/hypo-test/hypo-test-data/src/scenario-10/java/scenario10/ParentClass.java @@ -1,5 +1,6 @@ package scenario10; +// Compiled with JDK 21 public abstract class ParentClass { public abstract T get(); diff --git a/hypo-test/hypo-test-data/src/scenario-11/java/scenario11/ChildClass.java b/hypo-test/hypo-test-data/src/scenario-11/java/scenario11/ChildClass.java index e204dee..15cf1ef 100644 --- a/hypo-test/hypo-test-data/src/scenario-11/java/scenario11/ChildClass.java +++ b/hypo-test/hypo-test-data/src/scenario-11/java/scenario11/ChildClass.java @@ -1,5 +1,6 @@ package scenario11; +// Compiled with JDK 21 public class ChildClass extends ParentClass { @Override diff --git a/hypo-test/hypo-test-data/src/scenario-11/java/scenario11/GrandChildClass.java b/hypo-test/hypo-test-data/src/scenario-11/java/scenario11/GrandChildClass.java index 801dccd..e930661 100644 --- a/hypo-test/hypo-test-data/src/scenario-11/java/scenario11/GrandChildClass.java +++ b/hypo-test/hypo-test-data/src/scenario-11/java/scenario11/GrandChildClass.java @@ -1,5 +1,6 @@ package scenario11; +// Compiled with JDK 21 public class GrandChildClass extends ChildClass { @Override diff --git a/hypo-test/hypo-test-data/src/scenario-11/java/scenario11/ParentClass.java b/hypo-test/hypo-test-data/src/scenario-11/java/scenario11/ParentClass.java index cfa59a9..6d8f953 100644 --- a/hypo-test/hypo-test-data/src/scenario-11/java/scenario11/ParentClass.java +++ b/hypo-test/hypo-test-data/src/scenario-11/java/scenario11/ParentClass.java @@ -1,5 +1,6 @@ package scenario11; +// Compiled with JDK 21 public abstract class ParentClass { public abstract T getWith(K k, V v); diff --git a/hypo-test/hypo-test-data/src/scenario-12/java/scenario12/BaseClass.java b/hypo-test/hypo-test-data/src/scenario-12/java/scenario12/BaseClass.java index cb95dfd..8198ec1 100644 --- a/hypo-test/hypo-test-data/src/scenario-12/java/scenario12/BaseClass.java +++ b/hypo-test/hypo-test-data/src/scenario-12/java/scenario12/BaseClass.java @@ -1,5 +1,6 @@ package scenario12; +// Compiled with JDK 21 public class BaseClass { public BaseClass(int one, int two, int three) {} diff --git a/hypo-test/hypo-test-data/src/scenario-12/java/scenario12/ChildClass.java b/hypo-test/hypo-test-data/src/scenario-12/java/scenario12/ChildClass.java index c88c3a3..8b62265 100644 --- a/hypo-test/hypo-test-data/src/scenario-12/java/scenario12/ChildClass.java +++ b/hypo-test/hypo-test-data/src/scenario-12/java/scenario12/ChildClass.java @@ -1,5 +1,6 @@ package scenario12; +// Compiled with JDK 21 public class ChildClass extends TestClass.InnerClass { public ChildClass(int one, int two, int three) { diff --git a/hypo-test/hypo-test-data/src/scenario-12/java/scenario12/TestClass.java b/hypo-test/hypo-test-data/src/scenario-12/java/scenario12/TestClass.java index b76a4b4..9b6924d 100644 --- a/hypo-test/hypo-test-data/src/scenario-12/java/scenario12/TestClass.java +++ b/hypo-test/hypo-test-data/src/scenario-12/java/scenario12/TestClass.java @@ -1,5 +1,6 @@ package scenario12; +// Compiled with JDK 21 public class TestClass { public static class InnerClass extends BaseClass { diff --git a/hypo-test/hypo-test-data/src/scenario-13/java/scenario13/TestClass.java b/hypo-test/hypo-test-data/src/scenario-13/java/scenario13/TestClass.java index aca3063..1b921a5 100644 --- a/hypo-test/hypo-test-data/src/scenario-13/java/scenario13/TestClass.java +++ b/hypo-test/hypo-test-data/src/scenario-13/java/scenario13/TestClass.java @@ -3,6 +3,7 @@ import java.util.Random; import java.util.concurrent.ThreadLocalRandom; +// Compiled with JDK 21 public class TestClass { private String thisValue = Integer.toString(ThreadLocalRandom.current().nextInt()); diff --git a/hypo-test/hypo-test-data/src/scenario-13/java/scenario13/TestInterface.java b/hypo-test/hypo-test-data/src/scenario-13/java/scenario13/TestInterface.java index 095c30b..eef1e5f 100644 --- a/hypo-test/hypo-test-data/src/scenario-13/java/scenario13/TestInterface.java +++ b/hypo-test/hypo-test-data/src/scenario-13/java/scenario13/TestInterface.java @@ -1,5 +1,6 @@ package scenario13; +// Compiled with JDK 21 public interface TestInterface { void apply(T1 t1, T2 t2); diff --git a/hypo-test/src/main/java/dev/denwav/hypo/test/framework/TestScenarioBase.java b/hypo-test/src/main/java/dev/denwav/hypo/test/framework/TestScenarioBase.java index 057637d..1e528f5 100644 --- a/hypo-test/src/main/java/dev/denwav/hypo/test/framework/TestScenarioBase.java +++ b/hypo-test/src/main/java/dev/denwav/hypo/test/framework/TestScenarioBase.java @@ -31,7 +31,7 @@ import dev.denwav.hypo.model.HypoModelUtil; import dev.denwav.hypo.model.data.ClassData; import dev.denwav.hypo.model.data.MethodData; -import dev.denwav.hypo.model.data.MethodDescriptor; +import dev.denwav.hypo.types.desc.MethodDescriptor; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; @@ -45,6 +45,7 @@ import java.util.Map; import net.fabricmc.lorenztiny.TinyMappingFormat; import org.apache.logging.log4j.Level; +import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.core.config.Configurator; import org.cadixdev.lorenz.MappingSet; import org.cadixdev.lorenz.io.MappingFormats; @@ -154,11 +155,11 @@ public void teardown() throws Exception { public static @NotNull MethodData findMethod(final ClassData data, final String name) { final List<@NotNull MethodData> methods = data.methods(name); assertEquals(1, methods.size()); - return methods.get(0); + return methods.getFirst(); } public static @NotNull MethodData findMethod(final ClassData data, final String name, final String desc) { - final MethodData method = data.method(name, MethodDescriptor.parseDescriptor(desc)); + final MethodData method = data.method(name, MethodDescriptor.parse(desc)); assertNotNull(method); return method; } @@ -420,6 +421,7 @@ public interface TestCase { * @return An array of mappings expected to be the result of each step of the change contributors process. * @see #finishMappings() */ + @SuppressWarnings("MultipleNullnessAnnotations") default @NotNull MappingSet @Nullable [] stageMappings() { return null; } diff --git a/hypo-test/src/test/java/dev/denwav/hypo/test/scenarios/Scenario01Test.java b/hypo-test/src/test/java/dev/denwav/hypo/test/scenarios/Scenario01Test.java index df52700..d5958f0 100644 --- a/hypo-test/src/test/java/dev/denwav/hypo/test/scenarios/Scenario01Test.java +++ b/hypo-test/src/test/java/dev/denwav/hypo/test/scenarios/Scenario01Test.java @@ -122,6 +122,7 @@ void treePropagation() throws Exception { } @Override + @SuppressWarnings("MultipleNullnessAnnotations") public @NotNull MappingSet @Nullable [] stageMappings() { return new MappingSet[] { // First stage, propagate up diff --git a/hypo-test/src/test/java/dev/denwav/hypo/test/scenarios/Scenario03Test.java b/hypo-test/src/test/java/dev/denwav/hypo/test/scenarios/Scenario03Test.java index 45407c6..3e775ae 100644 --- a/hypo-test/src/test/java/dev/denwav/hypo/test/scenarios/Scenario03Test.java +++ b/hypo-test/src/test/java/dev/denwav/hypo/test/scenarios/Scenario03Test.java @@ -70,6 +70,7 @@ void testOverloads() throws Exception { } @Override + @SuppressWarnings("MultipleNullnessAnnotations") public @NotNull MappingSet @Nullable [] stageMappings() { return new MappingSet[] { // First stage, propagate up diff --git a/hypo-types/build.gradle.kts b/hypo-types/build.gradle.kts new file mode 100644 index 0000000..44b7d5b --- /dev/null +++ b/hypo-types/build.gradle.kts @@ -0,0 +1,31 @@ +plugins { + `java-library` + `hypo-java` + `hypo-test` + `hypo-module` + `hypo-publish` +} + +dependencies { + compileOnlyApi(libs.annotations) + compileOnlyApi(libs.errorprone.annotations) + + testImplementation(libs.guava) +} + +hypoJava { + javadocLibs.add(libs.annotations) + javadocLibs.add(libs.errorprone.annotations) +} + +hypoPublish { + component = components.named("java") +} + +val typesExport = project(":types-export") +tasks.test { + dependsOn(typesExport.tasks.named("buildTypesExport")) + + val zipFile = typesExport.layout.buildDirectory.file("types-export/types-export.zip").get().asFile.absolutePath + systemProperty("hypo.types.zip", zipFile) +} diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/HypoTypesUtil.java b/hypo-types/src/main/java/dev/denwav/hypo/types/HypoTypesUtil.java new file mode 100644 index 0000000..e027435 --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/HypoTypesUtil.java @@ -0,0 +1,65 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types; + +import org.jetbrains.annotations.Contract; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * General utility class used by {@code hypo-types}. + */ +public final class HypoTypesUtil { + + private HypoTypesUtil() {} + + /** + * Normalize the given class name into a standard format matching the JVM internal class name format. Class type + * descriptors may also be passed in (that is, class names that looks like {@code Lcom/example/Main;}) and this + * method will strip the initial {@code L} and the final {@code ;}. + * + *

This method does not validate the format of the class name provided, it simply runs a couple standard String + * functions on it to help increase cache-hit rate and reduce the likelihood of a class name in a slightly different + * format causing issues. + * + * @param className The class name to normalize. + * @return The normalized class name. + */ + public static @NotNull String normalizedClassName(final @NotNull String className) { + final int index = className.endsWith(";") ? 1 : 0; + return className.substring(index, className.length() - index).replace('.', '/'); + } + + /** + * Perform an unsafe, unchecked cast to the type specified by {@link T}. This method does not in any way verify or + * validate that the cast will succeed, nor does it protect from failed casts from occurring. This method's intended + * purpose is for handing cases where Java's type system falls short, such as when handling capture types. Any code + * which uses this method should independently verify that the cast will never fail on their own. + * + * @param o The object to cast to {@link T}. Can be {@code null}, if so then {@code null} will be returned. + * @param The type to cast {@code o} to. + * @return The given parameter {@code o}, cast to {@link T}. + */ + @SuppressWarnings("TypeParameterUnusedInFormals") + @Contract(value = "_ -> param1", pure = true) + public static @Nullable T cast(final @Nullable Object o) { + @SuppressWarnings("unchecked") final T t = (T) o; + return t; + } +} diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/PrimitiveType.java b/hypo-types/src/main/java/dev/denwav/hypo/types/PrimitiveType.java new file mode 100644 index 0000000..5a03b4e --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/PrimitiveType.java @@ -0,0 +1,256 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types; + +import dev.denwav.hypo.types.desc.ClassTypeDescriptor; +import dev.denwav.hypo.types.desc.TypeDescriptor; +import dev.denwav.hypo.types.sig.ClassTypeSignature; +import dev.denwav.hypo.types.sig.TypeSignature; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Primitive types in Java, implementation for both {@link TypeDescriptor} and + * {@link TypeSignature}. + * + *

There are 8 total primitive types in the JVM: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
+ * Java Primitive Types + *
NameWrapper TypeInternal Name
{@code char}{@link java.lang.Character java/lang/Character}{@code C}
{@code byte}{@link java.lang.Byte java/lang/Byte}{@code B}
{@code short}{@link java.lang.Short java/lang/Short}{@code S}
{@code int}{@link java.lang.Integer java/lang/Integer}{@code I}
{@code long}{@link java.lang.Long java/lang/Long}{@code J}
{@code float}{@link java.lang.Float java/lang/Float}{@code F}
{@code double}{@link java.lang.Double java/lang/Double}{@code D}
{@code boolean}{@link java.lang.Boolean java/lang/Boolean}{@code Z}
+ */ +public enum PrimitiveType implements TypeDescriptor, TypeSignature { + /** + * {@code char} + */ + CHAR("char", 'C', "java/lang/Character"), + /** + * {@code byte} + */ + BYTE("byte", 'B', "java/lang/Byte"), + /** + * {@code short} + */ + SHORT("short", 'S', "java/lang/Short"), + /** + * {@code int} + */ + INT("int", 'I', "java/lang/Integer"), + /** + * {@code long} + */ + LONG("long", 'J', "java/lang/Long"), + /** + * {@code float} + */ + FLOAT("float", 'F', "java/lang/Float"), + /** + * {@code double} + */ + DOUBLE("double", 'D', "java/lang/Double"), + /** + * {@code boolean} + */ + BOOLEAN("boolean", 'Z', "java/lang/Boolean"), + ; + + private final @NotNull String readableName; + private final char internalName; + private final @NotNull String wrapperType; + + PrimitiveType( + final @NotNull String readableName, + final char internalName, + final @NotNull String wrapperType + ) { + this.readableName = readableName; + this.internalName = internalName; + this.wrapperType = wrapperType; + } + + @Override + public void asReadable(final @NotNull StringBuilder sb) { + sb.append(this.readableName); + } + + @Override + public @NotNull String asReadable() { + return this.readableName; + } + + @Override + public void asInternal(final @NotNull StringBuilder sb) { + this.asInternal(sb, false); + } + + @Override + public @NotNull String asInternal() { + return String.valueOf(this.internalName); + } + + @Override + public void asInternal(final @NotNull StringBuilder sb, final boolean withBindKey) { + sb.append(this.internalName); + } + + @Override + public @NotNull PrimitiveType asSignature() { + return this; + } + + @Override + public @NotNull PrimitiveType bind(final @NotNull TypeVariableBinder binder) { + return this; + } + + @Override + public boolean isUnbound() { + return false; + } + + @Override + public @NotNull PrimitiveType asDescriptor() { + return this; + } + + /** + * Returns the wrapper type name for this primitive type. + * @return The wrapper type name for this primitive type. + */ + public @NotNull String getWrapperType() { + return this.wrapperType; + } + + /** + * Returns the primitive type for the given internal name character if the + * character matches a primitive type. Returns {@code null} if the given + * character does not match a primitive type. + * + * @param c The character to match to a primitive type name. + * @return The {@link PrimitiveType} for the given internal name character, + * or {@code null}. + */ + public static @Nullable PrimitiveType fromChar(final char c) { + return switch (c) { + case 'C' -> PrimitiveType.CHAR; + case 'B' -> PrimitiveType.BYTE; + case 'S' -> PrimitiveType.SHORT; + case 'I' -> PrimitiveType.INT; + case 'J' -> PrimitiveType.LONG; + case 'F' -> PrimitiveType.FLOAT; + case 'D' -> PrimitiveType.DOUBLE; + case 'Z' -> PrimitiveType.BOOLEAN; + default -> null; + }; + } + + /** + * Convenience alternative to {@link #fromChar(char)} that accepts a + * single-character {@link String} instead. If the given string is + * {@code null} or contains more than a single character, {@code null} is + * returned. + * + * @param c The character to match to a primitive type name. + * @return The {@link PrimitiveType} for the given internal name character, + * or {@code null}. + */ + public static @Nullable PrimitiveType fromChar(final @Nullable String c) { + if (c == null) { + return null; + } + if (c.length() != 1) { + return null; + } + return PrimitiveType.fromChar(c.charAt(0)); + } + + /** + * Returns the {@link ClassTypeDescriptor} for the wrapper type for this + * primitive type. + * + * @return The {@link ClassTypeDescriptor} for the wrapper type for this + * primitive type. + */ + public @NotNull ClassTypeDescriptor getWrapperTypeDescriptor() { + return ClassTypeDescriptor.of(this.getWrapperType()); + } + + /** + * Returns the {@link ClassTypeSignature} for the wrapper type for this + * primitive type. + * + * @return The {@link ClassTypeSignature} for the wrapper type for this + * primitive type. + */ + + public @NotNull ClassTypeSignature getWrapperTypeSignature() { + return ClassTypeSignature.of(this.getWrapperType()); + } + + @Override + public String toString() { + return this.asReadable(); + } +} diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/TypeBindable.java b/hypo-types/src/main/java/dev/denwav/hypo/types/TypeBindable.java new file mode 100644 index 0000000..b70a2dd --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/TypeBindable.java @@ -0,0 +1,79 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types; + +import dev.denwav.hypo.types.sig.ClassSignature; +import dev.denwav.hypo.types.sig.MethodSignature; +import dev.denwav.hypo.types.sig.ReferenceTypeSignature; +import dev.denwav.hypo.types.sig.ThrowsSignature; +import dev.denwav.hypo.types.sig.TypeSignature; +import dev.denwav.hypo.types.sig.param.TypeArgument; +import dev.denwav.hypo.types.sig.param.TypeParameter; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** + * A type which can be bound to a {@link dev.denwav.hypo.types.sig.param.TypeParameter TypeParameter}, or may contain + * nested objects which themselves may satisfy that scenario. The intended pattern is for objects which fall in the + * latter category to simply recursively call this method on all matching nested values. + * + *

Type variable binding only applies to {@link dev.denwav.hypo.types.sig.TypeSignature TypeSignature}-based types. + */ +public sealed interface TypeBindable + extends TypeRepresentable + permits TypeSignature, ClassSignature, MethodSignature, ReferenceTypeSignature, ThrowsSignature, TypeArgument, TypeParameter { + + /** + * Return a new instance of {@code this} (possibly as a different type) where all unbound type variables have been + * bound to their associated type parameters using the provided {@code binder}. All valid type definitions must be + * able to be bound, so failure to find a valid type parameter definition for an unbound type variable will result + * in an {@link IllegalStateException}. + * + * @param binder The {@link TypeVariableBinder} to bind with. + * @return A new instance of {@code this} (possibly as a different type) where all unbound type variables have been + * bound to their associated type parameters + * @throws IllegalStateException If a type variable could not be bound to an appropriate type parameter. + */ + @NotNull TypeBindable bind(final @NotNull TypeVariableBinder binder); + + /** + * Returns true if {@code this} contains any instances of + * {@link dev.denwav.hypo.types.sig.param.TypeVariable.Unbound TypeVariable.Unbound}. If this method returns + * {@code true} then certain resolution operations such as + * {@link dev.denwav.hypo.types.sig.TypeSignature#asDescriptor() TypeSignatureasDescriptor()} will fail. + * + * @return {@code true} if {@code this} contains an unbound type variable. + */ + boolean isUnbound(); + + /** + * Internal method, use {@link #asInternal(StringBuilder)} or {@link #asInternal()} instead. + * @param sb The string builder. + * @param withBindKey Bind key flag. + */ + @ApiStatus.Internal + void asInternal(final @NotNull StringBuilder sb, final boolean withBindKey); + + @Override + default @NotNull String internKey() { + final StringBuilder sb = new StringBuilder(); + this.asInternal(sb, true); + return sb.toString(); + } +} diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/TypeRepresentable.java b/hypo-types/src/main/java/dev/denwav/hypo/types/TypeRepresentable.java new file mode 100644 index 0000000..ec6f10a --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/TypeRepresentable.java @@ -0,0 +1,140 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types; + +import com.google.errorprone.annotations.Immutable; +import dev.denwav.hypo.types.desc.Descriptor; +import dev.denwav.hypo.types.desc.MethodDescriptor; +import dev.denwav.hypo.types.desc.TypeDescriptor; +import dev.denwav.hypo.types.intern.InternKey; +import dev.denwav.hypo.types.kind.ArrayType; +import dev.denwav.hypo.types.kind.ClassType; +import dev.denwav.hypo.types.kind.MethodType; +import dev.denwav.hypo.types.kind.ValueType; +import dev.denwav.hypo.types.sig.ArrayTypeSignature; +import dev.denwav.hypo.types.sig.ClassSignature; +import dev.denwav.hypo.types.sig.ClassTypeSignature; +import dev.denwav.hypo.types.sig.MethodSignature; +import dev.denwav.hypo.types.sig.ReferenceTypeSignature; +import dev.denwav.hypo.types.sig.Signature; +import dev.denwav.hypo.types.sig.ThrowsSignature; +import dev.denwav.hypo.types.sig.TypeSignature; +import dev.denwav.hypo.types.sig.param.BoundedTypeArgument; +import dev.denwav.hypo.types.sig.param.TypeArgument; +import dev.denwav.hypo.types.sig.param.TypeParameter; +import dev.denwav.hypo.types.sig.param.TypeVariable; +import dev.denwav.hypo.types.sig.param.WildcardBound; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.WildcardType; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * A value which represents a type in Java. "types" include 5 major categories: + *

    + *
  • {@link dev.denwav.hypo.types.desc.TypeDescriptor TypeDescriptor}
  • + *
  • {@link dev.denwav.hypo.types.desc.MethodDescriptor MethodDescriptor}
  • + *
  • {@link dev.denwav.hypo.types.sig.TypeSignature TypeSignature}
  • + *
  • {@link dev.denwav.hypo.types.sig.MethodSignature MethodSignature}
  • + *
  • {@link dev.denwav.hypo.types.sig.ClassSignature ClassSignature}
  • + *
+ * + *

All types can be represented in two different ways, {@link #asReadable()}, and {@link #asInternal()}. Generally + * "readable" will be in source code format, as would appear in a Java source file. Internal format matches exactly what + * is present in compiled Java bytecode for the given type. + * + *

Implementations of this interface should have their {@link Object#toString() toString()} method defer to + * {@link #asReadable()} to assist with debugging. {@link #asInternal()} should be used for serialization, as it matches + * 1:1 with corresponding {@code parse()} methods for each type. + */ +@Immutable +public sealed interface TypeRepresentable extends InternKey + permits ValueType, MethodType, ArrayType, ClassType, TypeBindable, + Descriptor, TypeDescriptor, MethodDescriptor, + Signature, TypeSignature, MethodSignature, ClassSignature, ReferenceTypeSignature, + TypeParameter, TypeVariable, TypeVariable.Unbound, TypeArgument, BoundedTypeArgument, WildcardBound, ThrowsSignature { + + /** + * Create a new {@link TypeRepresentable} using the best match implementation for the given {@link Type}. This + * method will return {@link TypeSignature} or {@link TypeArgument} as these types contain the most information. + * {@link TypeDescriptor type descriptors} cannot contain generic type information, so this method prefers types + * that won't lose that information. + * + * @param type The {@link Type} to create a new {@link TypeRepresentable} from. + * @return The new {@link TypeRepresentable}. + */ + static @Nullable TypeRepresentable of(final Type type) { + if (type instanceof final Class clazz) { + return TypeSignature.ofOrNull(clazz); + } else if (type instanceof final ParameterizedType parameterizedType) { + return ClassTypeSignature.of(parameterizedType); + } else if (type instanceof final GenericArrayType arrayType) { + return ArrayTypeSignature.of(arrayType); + } else if (type instanceof final java.lang.reflect.TypeVariable typeVar) { + return TypeVariable.of(typeVar); + } else if (type instanceof final WildcardType wildcardType) { + return TypeArgument.of(wildcardType); + } else { + return null; + } + } + + /** + * Print this type name as a human-readable name to the given {@link StringBuilder}. + * + * @param sb The {@link StringBuilder} to print this type's human-readable name to. + */ + void asReadable(final @NotNull StringBuilder sb); + + /** + * Print this type name as an internal JVM name to the given {@link StringBuilder}. + * + * @param sb The {@link StringBuilder} to print this type's internal JVM name to. + */ + void asInternal(final @NotNull StringBuilder sb); + + @Override + default @NotNull String internKey() { + return this.asInternal(); + } + + /** + * Returns this type's human-readable name. + * + * @return This type's human-readable name. + */ + default @NotNull String asReadable() { + final StringBuilder sb = new StringBuilder(); + this.asReadable(sb); + return sb.toString(); + } + + /** + * Returns this type's internal JVM name. + * + * @return This type's internal JVM name. + */ + default @NotNull String asInternal() { + final StringBuilder sb = new StringBuilder(); + this.asInternal(sb); + return sb.toString(); + } +} diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/TypeVariableBinder.java b/hypo-types/src/main/java/dev/denwav/hypo/types/TypeVariableBinder.java new file mode 100644 index 0000000..027526b --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/TypeVariableBinder.java @@ -0,0 +1,69 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types; + +import dev.denwav.hypo.types.sig.param.TypeParameter; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * An object which provides bindings for {@link dev.denwav.hypo.types.sig.param.TypeVariable TypeVariables}. This is + * generally as simple as finding the instance of {@link TypeParameter} which correspond with the given type variable + * name. + * + *

Typical usage of this class is by passing it into the {@link TypeBindable#bind(TypeVariableBinder)} method. A + * convenience method is also provided, {@link #bind(TypeBindable)}, which inverts that mechanism. This can make it + * simpler when dealing with possibly {@code null} values. + */ +public interface TypeVariableBinder { + + /** + * A convenience method which inverts the typical order of the {@link TypeBindable#bind(TypeVariableBinder)} method + * call. This can be used to safely call the bind method on possibly {@code null} objects. + * + * @param bindable The bindable object to pass this object into. + * @return The result of calling {@link TypeBindable#bind(TypeVariableBinder)} if {@code bindable} is not null, + * @param The type of the bindable. + */ + default @Nullable T bind(final @Nullable T bindable) { + if (bindable == null) { + return null; + } + return HypoTypesUtil.cast(bindable.bind(this)); + } + + /** + * Return the associated {@link TypeParameter} for the given variable name. + * + * @param name The variable name. + * @return The associated {@link TypeParameter}, or {@code null} if not found. + */ + @Nullable TypeParameter bindingFor(final @NotNull String name); + + /** + * Essentially a no-op operation which binds all unbound type variables to a fictional {@link TypeParameter}. This + * can be used to guarantee a {@link dev.denwav.hypo.types.sig.TypeSignature TypeSignature} will always resolve, + * even if the result is not accurate. + * + * @return A basic {@link TypeVariableBinder} which produces fictional bindings. + */ + static TypeVariableBinder object() { + return TypeParameter::of; + } +} diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/VoidType.java b/hypo-types/src/main/java/dev/denwav/hypo/types/VoidType.java new file mode 100644 index 0000000..8bf109c --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/VoidType.java @@ -0,0 +1,79 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types; + +import dev.denwav.hypo.types.desc.TypeDescriptor; +import dev.denwav.hypo.types.sig.TypeSignature; +import org.jetbrains.annotations.NotNull; + +/** + * The {@code void} type in Java. This class is an implementation for both {@link TypeDescriptor} and + * {@link TypeSignature}. {@code void} is only void when used as the return type in a method descriptor or signature, it + * cannot be used as the type for any field or variable. + * + *

The wrapper type for {@code void} is {@link java.lang.Void java/lang/Void}. The internal name for {@code void} is + * {@code V}. + */ +public enum VoidType implements TypeDescriptor, TypeSignature { + /** + * The singleton instance of this type object. + */ + INSTANCE, + ; + + @Override + public void asReadable(final @NotNull StringBuilder sb) { + sb.append("void"); + } + + @Override + public void asInternal(final @NotNull StringBuilder sb) { + this.asInternal(sb, false); + } + + @Override + public void asInternal(final @NotNull StringBuilder sb, final boolean withBindKey) { + sb.append('V'); + } + + @Override + public @NotNull VoidType asSignature() { + return this; + } + + @Override + public @NotNull VoidType asDescriptor() { + return this; + } + + @Override + public @NotNull VoidType bind(final @NotNull TypeVariableBinder binder) { + return this; + } + + @Override + public boolean isUnbound() { + return false; + } + + @Override + public String toString() { + return this.asReadable(); + } +} diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/desc/ArrayTypeDescriptor.java b/hypo-types/src/main/java/dev/denwav/hypo/types/desc/ArrayTypeDescriptor.java new file mode 100644 index 0000000..ec17513 --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/desc/ArrayTypeDescriptor.java @@ -0,0 +1,124 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types.desc; + +import com.google.errorprone.annotations.Immutable; +import dev.denwav.hypo.types.intern.Intern; +import dev.denwav.hypo.types.kind.ArrayType; +import dev.denwav.hypo.types.sig.ArrayTypeSignature; +import java.util.Objects; +import org.jetbrains.annotations.NotNull; + +/** + * A {@link TypeDescriptor} representing an array type. Array types consist of a {@link #getDimension() dimension} and a + * {@link #getBaseType() base type}, which can be either a {@link dev.denwav.hypo.types.PrimitiveType PrimitiveType} or + * a {@link ClassTypeDescriptor}. + * + *

Array type descriptors have the internal format of {@code [}. The leading {@code [} is repeated to + * denote the dimensionality of the array, so a 3-dimension array type would start with {@code [[[}. + * + * @see ArrayType + */ +@Immutable +public final class ArrayTypeDescriptor extends Intern implements TypeDescriptor, ArrayType { + + private final int dimension; + private final @NotNull TypeDescriptor baseType; + + /** + * Create a {@link ArrayTypeDescriptor} instance. + * + * @param dimension The dimension for the new type. + * @param baseType The base type for the new type. + * @return The new {@link ArrayTypeDescriptor}. + */ + public static @NotNull ArrayTypeDescriptor of(final int dimension, final @NotNull TypeDescriptor baseType) { + return new ArrayTypeDescriptor(dimension, baseType).intern(); + } + + private ArrayTypeDescriptor( + final int dimension, + final @NotNull TypeDescriptor baseType + ) { + this.dimension = dimension; + this.baseType = baseType; + } + + @Override + public void asReadable(final @NotNull StringBuilder sb) { + this.baseType.asReadable(sb); + sb.ensureCapacity(sb.length() + 2 * this.dimension); + //noinspection StringRepeatCanBeUsed + for (int i = 0; i < this.dimension; i++) { + sb.append("[]"); + } + } + + @Override + public void asInternal(final @NotNull StringBuilder sb) { + sb.ensureCapacity(sb.length() + this.dimension); + //noinspection StringRepeatCanBeUsed + for (int i = 0; i < this.dimension; i++) { + sb.append('['); + } + this.baseType.asInternal(sb); + } + + @Override + public @NotNull ArrayTypeSignature asSignature() { + return ArrayTypeSignature.of(this.dimension, this.baseType.asSignature()); + } + + /** + * Get the dimension of this array type. + * @return The dimension of this array type. + */ + @Override + public int getDimension() { + return this.dimension; + } + + /** + * Get the base type of this array type. + * @return The base type of this array type. + */ + @Override + public @NotNull TypeDescriptor getBaseType() { + return this.baseType; + } + + @Override + public boolean equals(final Object o) { + if (!(o instanceof final ArrayTypeDescriptor that)) { + return false; + } + return this.dimension == that.dimension + && Objects.equals(this.baseType, that.baseType); + } + + @Override + public int hashCode() { + return Objects.hash(this.dimension, this.baseType); + } + + @Override + public String toString() { + return this.asReadable(); + } +} diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/desc/ClassTypeDescriptor.java b/hypo-types/src/main/java/dev/denwav/hypo/types/desc/ClassTypeDescriptor.java new file mode 100644 index 0000000..7754773 --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/desc/ClassTypeDescriptor.java @@ -0,0 +1,93 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types.desc; + +import com.google.errorprone.annotations.Immutable; +import dev.denwav.hypo.types.HypoTypesUtil; +import dev.denwav.hypo.types.intern.Intern; +import dev.denwav.hypo.types.kind.ClassType; +import dev.denwav.hypo.types.sig.ClassTypeSignature; +import java.util.Objects; +import org.jetbrains.annotations.NotNull; + +/** + * A {@link TypeDescriptor} representing a class type. Class (or reference) types follow the internal format of + * {@code L;}. + * + * @see ObjectType + */ +@Immutable +public final class ClassTypeDescriptor extends Intern implements TypeDescriptor, ClassType { + + private final @NotNull String name; + + /** + * Create a {@link ClassTypeDescriptor} instance. + * + * @param name The class name for the new type. + * @return The new {@link ClassTypeDescriptor}. + */ + public static @NotNull ClassTypeDescriptor of(final @NotNull String name) { + return new ClassTypeDescriptor(HypoTypesUtil.normalizedClassName(name)).intern(); + } + + private ClassTypeDescriptor(final @NotNull String name) { + this.name = name; + } + + @Override + public void asReadable(final @NotNull StringBuilder sb) { + sb.append(this.name.replace('/', '.')); + } + + @Override + public void asInternal(final @NotNull StringBuilder sb) { + sb.append('L'); + sb.append(this.name); + sb.append(';'); + } + + @Override + public @NotNull ClassTypeSignature asSignature() { + return ClassTypeSignature.of(null, this.name, null); + } + + @Override + public @NotNull String getName() { + return this.name; + } + + @Override + public boolean equals(final Object o) { + if (!(o instanceof final ClassTypeDescriptor that)) { + return false; + } + return Objects.equals(this.name, that.name); + } + + @Override + public int hashCode() { + return Objects.hashCode(this.name); + } + + @Override + public String toString() { + return this.asReadable(); + } +} diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/desc/Descriptor.java b/hypo-types/src/main/java/dev/denwav/hypo/types/desc/Descriptor.java new file mode 100644 index 0000000..b887388 --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/desc/Descriptor.java @@ -0,0 +1,33 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types.desc; + +import dev.denwav.hypo.types.TypeRepresentable; + +/** + * Descriptor is a marker interface for {@link TypeDescriptor} and {@link MethodDescriptor}. It can be used to + * differentiate a descriptor type from a signature type of a {@link TypeRepresentable}. + * + * @see TypeDescriptor + * @see MethodDescriptor + * @see dev.denwav.hypo.types.sig.Signature Signature + */ +public sealed interface Descriptor extends TypeRepresentable + permits TypeDescriptor, MethodDescriptor { +} diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/desc/MethodDescriptor.java b/hypo-types/src/main/java/dev/denwav/hypo/types/desc/MethodDescriptor.java new file mode 100644 index 0000000..d03b9eb --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/desc/MethodDescriptor.java @@ -0,0 +1,229 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types.desc; + +import com.google.errorprone.annotations.Immutable; +import dev.denwav.hypo.types.kind.MethodType; +import dev.denwav.hypo.types.intern.Intern; +import dev.denwav.hypo.types.TypeRepresentable; +import dev.denwav.hypo.types.parsing.JvmTypeParseFailureException; +import dev.denwav.hypo.types.parsing.JvmTypeParser; +import dev.denwav.hypo.types.sig.MethodSignature; +import dev.denwav.hypo.types.sig.TypeSignature; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import org.jetbrains.annotations.NotNull; + +/** + * A JVM method descriptor. + * + *

Method descriptors consist of 2 parts, a {@link #getParameters() parameter list} and a + * {@link #getReturnType() return type}. + * + *

Method descriptors are used by the JVM to wire together method calls and do method call lookups. Method + * descriptors do not contain any generic type information. + * + *

This class is immutable. + * + * @see Descriptor + * @see TypeDescriptor + * @see MethodSignature + */ +@Immutable +public final class MethodDescriptor + extends Intern + implements MethodType, Descriptor, TypeRepresentable { + + @SuppressWarnings("Immutable") + private final @NotNull List parameters; + private final @NotNull TypeDescriptor returnType; + + /** + * Create a {@link MethodDescriptor} instance. + * + * @param parameters The parameter types for the new method descriptor. + * @param returnType The return type for the new method descriptor. + * @return The new {@link MethodDescriptor}. + */ + public static @NotNull MethodDescriptor of( + final @NotNull List parameters, + final @NotNull TypeDescriptor returnType + ) { + return new MethodDescriptor(parameters, returnType).intern(); + } + + /** + * Parse the given internal JVM method descriptor text into a new {@link MethodDescriptor}. + * + *

This method throws {@link JvmTypeParseFailureException} if the given text is not a valid method descriptor. + * Use {@link JvmTypeParser#parseMethodDescriptor(String, int)} if you prefer to have {@code null} be returned + * instead. + * + * @param text The text to parse. + * @return The {@link MethodDescriptor}. + * @throws JvmTypeParseFailureException If the given text does not represent a valid JVM method descriptor. + */ + public static @NotNull MethodDescriptor parse(final @NotNull String text) throws JvmTypeParseFailureException { + return parse(text, 0); + } + + /** + * Parse the given internal JVM method descriptor text into a new {@link MethodDescriptor}. + * + *

This method throws {@link JvmTypeParseFailureException} if the given text is not a valid method descriptor. + * Use {@link JvmTypeParser#parseMethodDescriptor(String, int)} if you prefer to have {@code null} be returned + * instead. + * + * @param text The text to parse. + * @param from The index to start parsing from. + * @return The {@link MethodDescriptor}. + * @throws JvmTypeParseFailureException If the given text does not represent a valid JVM method descriptor. + */ + public static @NotNull MethodDescriptor parse( + final @NotNull String text, + final int from + ) throws JvmTypeParseFailureException { + if (text.length() > 1 && from == 0) { + final MethodDescriptor r = Intern.tryFind(MethodDescriptor.class, text); + if (r != null) { + return r; + } + } + final MethodDescriptor result = JvmTypeParser.parseMethodDescriptor(text, from); + if (result == null) { + throw new JvmTypeParseFailureException("text is not a valid method descriptor: " + text.substring(from)); + } + return result; + } + + /** + * Create a new {@link MethodDescriptor} matching the given {@link Method}. + * + * @param method The {@link Method} to create a descriptor from. + * @return A new {@link MethodDescriptor} matching the given {@link Method}. + * + * @see TypeDescriptor#of(Class) + */ + public static @NotNull MethodDescriptor of(final @NotNull Method method) { + final int len = method.getParameterCount(); + final Class[] methodParams = method.getParameterTypes(); + final ArrayList params = new ArrayList<>(len); + for (int i = 0; i < len; i++) { + params.add(TypeDescriptor.of(methodParams[i])); + } + return MethodDescriptor.of(params, TypeDescriptor.of(method.getReturnType())); + } + + private MethodDescriptor( + final @NotNull List parameters, + final @NotNull TypeDescriptor returnType + ) { + this.parameters = List.copyOf(parameters); + this.returnType = returnType; + } + + @Override + public void asReadable(final @NotNull StringBuilder sb) { + this.returnType.asReadable(sb); + sb.append(" ("); + for (int i = 0; i < this.parameters.size(); i++) { + this.parameters.get(i).asReadable(sb); + if (i < this.parameters.size() - 1) { + sb.append(", "); + } + } + sb.append(')'); + } + + @Override + public void asInternal(final @NotNull StringBuilder sb) { + sb.append('('); + for (final TypeDescriptor param : this.parameters) { + param.asInternal(sb); + } + sb.append(')'); + this.returnType.asInternal(sb); + } + + /** + * Return this descriptor as a valid {@link MethodSignature}. Signatures are a super set of descriptors, so the + * result of this method is guaranteed to maintain all type information. That is to say, the following code will + * evaluate to {@code true}: + *


+     *     MethodDescriptor desc = MethodDescriptor.parse(text);
+     *     desc.equals(desc.asSignature().asDescriptor());
+     * 
+ * + * @return A {@link MethodSignature} which represents the same type as this descriptor. + */ + public @NotNull MethodSignature asSignature() { + final List sigParams = this.parameters.stream() + .map(TypeDescriptor::asSignature) + .collect(Collectors.toList()); + return MethodSignature.of( + Collections.emptyList(), + sigParams, + this.returnType.asSignature(), + Collections.emptyList() + ); + } + + /** + * Get the list of parameter types for this method descriptor. The returned list is immutable. + * + * @return The list of parameter types for this method descriptor. + */ + @Override + public @NotNull List getParameters() { + return this.parameters; + } + + /** + * Get the return type for this method descriptor. + * + * @return The return type for this method descriptor. + */ + @Override + public @NotNull TypeDescriptor getReturnType() { + return this.returnType; + } + + @Override + public boolean equals(final Object o) { + if (!(o instanceof final MethodDescriptor that)) { + return false; + } + return Objects.equals(this.parameters, that.parameters) + && Objects.equals(this.returnType, that.returnType); + } + + @Override + public int hashCode() { + return Objects.hash(this.parameters, this.returnType); + } + + @Override + public String toString() { + return this.asReadable(); + } +} diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/desc/TypeDescriptor.java b/hypo-types/src/main/java/dev/denwav/hypo/types/desc/TypeDescriptor.java new file mode 100644 index 0000000..d8b54dd --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/desc/TypeDescriptor.java @@ -0,0 +1,153 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types.desc; + +import com.google.errorprone.annotations.Immutable; +import dev.denwav.hypo.types.kind.ValueType; +import dev.denwav.hypo.types.intern.Intern; +import dev.denwav.hypo.types.PrimitiveType; +import dev.denwav.hypo.types.TypeRepresentable; +import dev.denwav.hypo.types.VoidType; +import dev.denwav.hypo.types.parsing.JvmTypeParseFailureException; +import dev.denwav.hypo.types.parsing.JvmTypeParser; +import dev.denwav.hypo.types.sig.TypeSignature; +import java.lang.reflect.Method; +import org.jetbrains.annotations.NotNull; + +/** + * A JVM type descriptor. + * + *

A type descriptor can be one of four different kinds: + *

    + *
  • {@link dev.denwav.hypo.types.PrimitiveType PrimitiveType}
  • + *
  • {@link ClassTypeDescriptor}
  • + *
  • {@link ArrayTypeDescriptor}
  • + *
  • {@link dev.denwav.hypo.types.VoidType VoidType}
  • + *
+ * + *

Type descriptors are used by the JVM to do all type checking and method table lookups. They contain no generic + * type information. + * + *

All implementations of this interface must be immutable. + * + * @see Descriptor + * @see ValueType + * @see MethodDescriptor + * @see TypeSignature + */ +@Immutable +public sealed interface TypeDescriptor + extends ValueType, Descriptor, TypeRepresentable + permits PrimitiveType, VoidType, ArrayTypeDescriptor, ClassTypeDescriptor { + + /** + * Return the equivalent {@link TypeSignature} which matches this {@link TypeDescriptor}. Since signatures are a + * super set of descriptors the returned signature is guaranteed to match exactly with this descriptor. That is to + * say, the following code will evaluate to {@code true}: + *


+     *     TypeDescriptor desc = TypeDescriptor.parse(text);
+     *     desc.equals(desc.asSignature().asDescriptor());
+     * 
+ * + * @return A {@link TypeSignature} which represents the same type as this descriptor. + */ + @NotNull TypeSignature asSignature(); + + /** + * Parse the given internal JVM type descriptor text into a new {@link TypeDescriptor}. + * + *

This method throws {@link JvmTypeParseFailureException} if the given text is not a valid type descriptor. Use + * {@link JvmTypeParser#parseTypeDescriptor(String, int)} if you prefer to have {@code null} be returned instead. + * + * @param text The text to parse. + * @return The {@link TypeDescriptor}. + * @throws JvmTypeParseFailureException If the given text does not represent a valid JVM type descriptor. + */ + static @NotNull TypeDescriptor parse(final @NotNull String text) throws JvmTypeParseFailureException { + return parse(text, 0); + } + + /** + * Parse the given internal JVM type descriptor text from the given index into a new {@link TypeDescriptor}. + * + *

This method throws {@link JvmTypeParseFailureException} if the given text is not a valid type descriptor. Use + * {@link JvmTypeParser#parseTypeDescriptor(String, int)} if you prefer to have {@code null} be returned instead. + * + * @param text The text to parse. + * @param from The index to start parsing from. + * @return The {@link TypeDescriptor}. + * @throws JvmTypeParseFailureException If the given text does not represent a valid JVM type descriptor. + */ + static @NotNull TypeDescriptor parse(final @NotNull String text, final int from) throws JvmTypeParseFailureException { + if (text.length() > 1 && from == 0) { + final TypeDescriptor r = Intern.tryFind(ClassTypeDescriptor.class, text); + if (r != null) { + return r; + } + } + final TypeDescriptor result = JvmTypeParser.parseTypeDescriptor(text, from); + if (result == null) { + throw new JvmTypeParseFailureException("text is not a valid type descriptor: " + text.substring(from)); + } + return result; + } + + /** + * Create a new {@link TypeDescriptor} from the given {@link Class}. + * + * @param clazz The {@link Class} to create a {@link TypeDescriptor} from. + * @return A new {@link TypeDescriptor} from the given {@link Class}. + * @see MethodDescriptor#of(Method) + */ + static @NotNull TypeDescriptor of(final @NotNull Class clazz) { + if (clazz == void.class) { + return VoidType.INSTANCE; + } else if (clazz == boolean.class) { + return PrimitiveType.BOOLEAN; + } else if (clazz == char.class) { + return PrimitiveType.CHAR; + } else if (clazz == byte.class) { + return PrimitiveType.BYTE; + } else if (clazz == short.class) { + return PrimitiveType.SHORT; + } else if (clazz == int.class) { + return PrimitiveType.INT; + } else if (clazz == long.class) { + return PrimitiveType.LONG; + } else if (clazz == float.class) { + return PrimitiveType.FLOAT; + } else if (clazz == double.class) { + return PrimitiveType.DOUBLE; + } else if (clazz.isArray()) { + Class baseType = clazz; + int dim = 0; + while (true) { + final Class next = baseType.getComponentType(); + if (next == null) { + break; + } + dim++; + baseType = next; + } + return ArrayTypeDescriptor.of(dim, TypeDescriptor.of(baseType)); + } else { + return ClassTypeDescriptor.of(clazz.getName()); + } + } +} diff --git a/hypo-model/src/main/java/dev/denwav/hypo/model/data/types/package-info.java b/hypo-types/src/main/java/dev/denwav/hypo/types/desc/package-info.java similarity index 59% rename from hypo-model/src/main/java/dev/denwav/hypo/model/data/types/package-info.java rename to hypo-types/src/main/java/dev/denwav/hypo/types/desc/package-info.java index 1213cc0..960c1b1 100644 --- a/hypo-model/src/main/java/dev/denwav/hypo/model/data/types/package-info.java +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/desc/package-info.java @@ -17,15 +17,16 @@ */ /** - * Java types model for the Hypo class data model defined in {@link dev.denwav.hypo.model.data}. + *

Type Descriptors

* - *

The base type model class is {@link dev.denwav.hypo.model.data.types.JvmType JvmType}, which is the base - * interface for the 3 categories of types: + *

Type descriptors are the internal types used by the JVM to verify classes and fields, and link method overrides. + * This package breaks type descriptors up into two categories, types and methods. * *

    - *
  • {@link dev.denwav.hypo.model.data.types.PrimitiveType primitive types}
  • - *
  • {@link dev.denwav.hypo.model.data.types.ClassType class types}
  • - *
  • {@link dev.denwav.hypo.model.data.types.ArrayType array types}
  • + *
  • {@link dev.denwav.hypo.types.desc.TypeDescriptor}
  • + *
  • {@link dev.denwav.hypo.types.desc.MethodDescriptor}
  • *
+ * + * @see dev.denwav.hypo.types.sig Type Signatures */ -package dev.denwav.hypo.model.data.types; +package dev.denwav.hypo.types.desc; diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/intern/Intern.java b/hypo-types/src/main/java/dev/denwav/hypo/types/intern/Intern.java new file mode 100644 index 0000000..0682976 --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/intern/Intern.java @@ -0,0 +1,187 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types.intern; + +import dev.denwav.hypo.types.HypoTypesUtil; +import dev.denwav.hypo.types.TypeRepresentable; +import java.lang.ref.Reference; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Interning base class which enables types classes to be interned, that is to say, prevent multiple copies of equal + * values to exist on the heap. This is useful for two primary reasons: first, all type classes are pure data classes, + * their identity is meaningless. Second, and more importantly, when analyzing large jar files with many classes many of + * the same type descriptors will be encountered over and over again, and it would be an inefficient use of memory to + * store separate copies for each type when they are identical. Common examples include core Java types just as + * {@code Ljava/lang/Object;} and {@code Ljava/lang/String;}, but this also applies to extremely common method + * descriptors and signatures, such as {@code ()V}, etc. + * + *

This is thread-safe, and utilizes a concurrent hashmap to enable lock-less handling of value instances. + * + *

The internal state of the internment can be checked via two static helper methods on this class, + * {@link #tryFind(Class, String)} and {@link #internmentSize(Class)}. + * + * @param The type of {@code this}. Any other type will result in a {@link ClassCastException} at runtime. + */ +public abstract class Intern> implements InternKey { + + private static final IdentityHashMap>, AtomicLong> interns = new IdentityHashMap<>(); + private static final List>> newInterns = new ArrayList<>(); + static { + final Thread t = new Thread(() -> { + while (true) { + try { + //noinspection BusyWait + Thread.sleep(500); + } catch (final InterruptedException e) { + break; + } + for (final var entry : interns.entrySet()) { + final var map = entry.getKey(); + final AtomicLong lastSize = entry.getValue(); + if (Math.abs(map.mappingCount() - lastSize.get()) < 10_000) { + continue; + } + map.values().removeIf(r -> r.get() == null); + lastSize.set(map.mappingCount()); + } + + // Prevent CME + if (!newInterns.isEmpty()) { + synchronized (newInterns) { + for (final var newIntern : newInterns) { + interns.put(newIntern, new AtomicLong(0)); + } + newInterns.clear(); + } + } + } + }, "Interning Cleanup"); + t.setDaemon(true); + t.start(); + } + + private static final class InternClassValue extends ClassValue>> { + @Override + protected ConcurrentHashMap> computeValue(final @NotNull Class type) { + final ConcurrentHashMap> map = new ConcurrentHashMap<>(); + synchronized (newInterns) { + newInterns.add(map); + } + return map; + } + } + + private static final InternClassValue internment = new InternClassValue(); + + private static final boolean interningDisabled = Boolean.getBoolean("hypo.interning.disabled"); + + /** + * {@code protected} constructor as this class must only be used by extending. + */ + protected Intern() { + } + + /** + * Return a class which is guaranteed to have the same value as {@code this}, but may be a different instance. An + * interned value satisfies the following rule: + *

    + *
  • {@code obj.intern().equals(obj)}
  • + *
  • {@code obj.equals(obj.intern())}
  • + *
+ * + * If two objects exist, {@code o1} and {@code o2}, where {@code o1.equals(o2)}, then the following will also be true: + *
    + *
  • {@code o1.intern() == o2.intern()}
  • + *
+ * + * @return Return the interned version of this object. + */ + public final T intern() { + if (interningDisabled) { + return HypoTypesUtil.cast(this); + } + + final T t = HypoTypesUtil.cast(this); + try { + final ConcurrentHashMap> map = internment.get(this.getClass()); + final String key = t.internKey(); + final WeakReference ref = new WeakReference<>(t); + final T res = HypoTypesUtil.cast(map.computeIfAbsent(key, k -> ref).get()); + if (res != null) { + return res; + } + + // Edge case, handle scenarios where the reference returned from `map` was garbage collected but was still + // present in the map. When that occurs, we replace the entry with our new valid entry and return it, + // guaranteeing this method will never return `null`. + // + // Note that this will not result in "leaks" of this value (even though such leaks would be harmless) as + // the above case can only happen when the stored value was already considered weakly reachable. When that + // happens, it was already being GCed and no longer exists on the heap. + map.put(key, ref); + return t; + } finally { + Reference.reachabilityFence(t); + } + } + + /** + * Attempt to find the given object of the type {@code c} in the internment map by its string value, {@code key}. + * The key for interned objects is always {@link TypeRepresentable#asInternal()}. Returns {@code null} if the given + * key does not exist in the map. + * + * @param c The {@link Class} object of the type to check. + * @param key The {@link TypeRepresentable#asInternal()} key of the value to check. + * @return The currently interned value for the given key, if it exists, otherwise {@code null}. + * @param The type of the interned value. + */ + public static @Nullable T tryFind(final @NotNull Class c, final @NotNull String key) { + final WeakReference ref = internment.get(c).get(key); + if (ref == null) { + return null; + } + final Object r = ref.get(); + try { + if (r != null) { + return HypoTypesUtil.cast(r); + } + return null; + } finally { + Reference.reachabilityFence(r); + } + } + + /** + * Get the current estimated size of the internment map for the given class type. + * + * @param c The class to check. + * @return The current estimated size of the internment map. + */ + public static long internmentSize(final @NotNull Class c) { + return internment.get(c).mappingCount(); + } +} diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/intern/InternKey.java b/hypo-types/src/main/java/dev/denwav/hypo/types/intern/InternKey.java new file mode 100644 index 0000000..ff7e123 --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/intern/InternKey.java @@ -0,0 +1,41 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types.intern; + +import org.jetbrains.annotations.NotNull; + +/** + * Classes which can be interned using {@link Intern}. + */ +public interface InternKey { + /** + * A string key to uniquely identify this object for interning. Interned + * objects are {@code value} types, which is to mean they have no + * (useful1) identity. This rule means implementations of this + * method must guarantee whenever a string returned from this method is equal + * to another instance, they are always completely interchangeable. + * + *

1 All Java objects contain an identity simply due to the + * specification of classes in the JVM. The concept here of a "useful" + * identity simply means that identity has no actual use.

+ * + * @return A string key used to uniquely identify this object for interning. + */ + @NotNull String internKey(); +} diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/intern/package-info.java b/hypo-types/src/main/java/dev/denwav/hypo/types/intern/package-info.java new file mode 100644 index 0000000..c53a3d7 --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/intern/package-info.java @@ -0,0 +1,29 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +/** + * Utility package for interning value objects, used by type descriptors and + * signatures in this module. + * + *

Interning is useful for reducing the amount of memory used by value + * objects in scenarios where we expect to see many copies of identical values + * objects existing on the heap. + * + * @see dev.denwav.hypo.types.intern.Intern + */ +package dev.denwav.hypo.types.intern; diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/kind/ArrayType.java b/hypo-types/src/main/java/dev/denwav/hypo/types/kind/ArrayType.java new file mode 100644 index 0000000..fbcc60d --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/kind/ArrayType.java @@ -0,0 +1,44 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types.kind; + +import dev.denwav.hypo.types.TypeRepresentable; +import dev.denwav.hypo.types.desc.ArrayTypeDescriptor; +import dev.denwav.hypo.types.sig.ArrayTypeSignature; +import org.jetbrains.annotations.NotNull; + +/** + * ArrayKind is a marker interface for {@link ArrayTypeDescriptor} and {@link ArrayTypeSignature}. + */ +public sealed interface ArrayType + extends TypeRepresentable + permits ArrayTypeDescriptor, ArrayTypeSignature { + + /** + * Get the dimension of this array type. + * @return The dimension of this array type. + */ + int getDimension(); + + /** + * Get the base type of this array type. + * @return The base type of this array type. + */ + @NotNull ValueType getBaseType(); +} diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/kind/ClassType.java b/hypo-types/src/main/java/dev/denwav/hypo/types/kind/ClassType.java new file mode 100644 index 0000000..e25fdd7 --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/kind/ClassType.java @@ -0,0 +1,40 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types.kind; + +import dev.denwav.hypo.types.TypeRepresentable; +import dev.denwav.hypo.types.desc.ClassTypeDescriptor; +import dev.denwav.hypo.types.sig.ClassTypeSignature; +import org.jetbrains.annotations.NotNull; + +/** + * ClassType is a marker interface for {@link ClassTypeDescriptor} and {@link ClassTypeSignature}. + */ +public sealed interface ClassType + extends TypeRepresentable + permits ClassTypeDescriptor, ClassTypeSignature { + + /** + * Get the class name for this type. The name does not include the {@code L} and {@code ;} format characters as seen + * in the internal name format. + * + * @return The class name for this type. + */ + @NotNull String getName(); +} diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/kind/MethodType.java b/hypo-types/src/main/java/dev/denwav/hypo/types/kind/MethodType.java new file mode 100644 index 0000000..98f1c59 --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/kind/MethodType.java @@ -0,0 +1,47 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types.kind; + +import dev.denwav.hypo.types.TypeRepresentable; +import dev.denwav.hypo.types.desc.MethodDescriptor; +import dev.denwav.hypo.types.sig.MethodSignature; +import java.util.List; +import org.jetbrains.annotations.NotNull; + +/** + * MethodType is a marker interface for {@link MethodDescriptor} and {@link MethodSignature}. + */ +public sealed interface MethodType + extends TypeRepresentable + permits MethodDescriptor, MethodSignature { + + /** + * Get the list of parameter types for this method type. The returned list is immutable. + * + * @return The list of parameter types for this method type. + */ + @NotNull List getParameters(); + + /** + * Get the return type for this method type. + * + * @return The return type for this method type. + */ + @NotNull ValueType getReturnType(); +} diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/kind/ValueType.java b/hypo-types/src/main/java/dev/denwav/hypo/types/kind/ValueType.java new file mode 100644 index 0000000..ddcd85a --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/kind/ValueType.java @@ -0,0 +1,31 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types.kind; + +import dev.denwav.hypo.types.TypeRepresentable; +import dev.denwav.hypo.types.desc.TypeDescriptor; +import dev.denwav.hypo.types.sig.TypeSignature; + +/** + * ValueType is a marker interface for {@link TypeDescriptor} and {@link TypeSignature}. + */ +public sealed interface ValueType + extends TypeRepresentable + permits TypeDescriptor, TypeSignature { +} diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/kind/package-info.java b/hypo-types/src/main/java/dev/denwav/hypo/types/kind/package-info.java new file mode 100644 index 0000000..6580db4 --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/kind/package-info.java @@ -0,0 +1,24 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +/** + * Type kinds, which are categories for different groupings of type objects. + * Each interface in this package is a marker interface for a different kind of + * type, each with a different scope. + */ +package dev.denwav.hypo.types.kind; diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/package-info.java b/hypo-types/src/main/java/dev/denwav/hypo/types/package-info.java new file mode 100644 index 0000000..087ae15 --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/package-info.java @@ -0,0 +1,30 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +/** + * The Java type system, consisting of type descriptors and type signatures. + * + *

    + *
  • Type Descriptors -> {@link dev.denwav.hypo.types.desc Hypo Type Descriptors}
  • + *
  • Type Signatures -> {@link dev.denwav.hypo.types.sig Hypo Type Signatures}
  • + *
+ * + *

In addition to implementations for type models, this module provides an API for comparing, matching, and extracting + * values from types using {@link dev.denwav.hypo.types.pattern patterns}. + */ +package dev.denwav.hypo.types; diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/parsing/JvmTypeParseFailureException.java b/hypo-types/src/main/java/dev/denwav/hypo/types/parsing/JvmTypeParseFailureException.java new file mode 100644 index 0000000..5142b2d --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/parsing/JvmTypeParseFailureException.java @@ -0,0 +1,33 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types.parsing; + +/** + * General exception type used to indicate a parse failed. + */ +public final class JvmTypeParseFailureException extends RuntimeException { + /** + * Constructor for {@link JvmTypeParseFailureException}. + * + * @param message The exception message. + */ + public JvmTypeParseFailureException(final String message) { + super(message); + } +} diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/parsing/JvmTypeParser.java b/hypo-types/src/main/java/dev/denwav/hypo/types/parsing/JvmTypeParser.java new file mode 100644 index 0000000..1992439 --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/parsing/JvmTypeParser.java @@ -0,0 +1,569 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types.parsing; + +import dev.denwav.hypo.types.PrimitiveType; +import dev.denwav.hypo.types.VoidType; +import dev.denwav.hypo.types.desc.ArrayTypeDescriptor; +import dev.denwav.hypo.types.desc.ClassTypeDescriptor; +import dev.denwav.hypo.types.desc.MethodDescriptor; +import dev.denwav.hypo.types.desc.TypeDescriptor; +import dev.denwav.hypo.types.sig.ArrayTypeSignature; +import dev.denwav.hypo.types.sig.ClassSignature; +import dev.denwav.hypo.types.sig.ClassTypeSignature; +import dev.denwav.hypo.types.sig.MethodSignature; +import dev.denwav.hypo.types.sig.ReferenceTypeSignature; +import dev.denwav.hypo.types.sig.ThrowsSignature; +import dev.denwav.hypo.types.sig.TypeSignature; +import dev.denwav.hypo.types.sig.param.BoundedTypeArgument; +import dev.denwav.hypo.types.sig.param.TypeArgument; +import dev.denwav.hypo.types.sig.param.TypeParameter; +import dev.denwav.hypo.types.sig.param.TypeVariable; +import dev.denwav.hypo.types.sig.param.WildcardArgument; +import dev.denwav.hypo.types.sig.param.WildcardBound; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Simple parser for internal JVM type names, descriptors, and signatures. + */ +public final class JvmTypeParser { + + private JvmTypeParser() { + } + + // Public API + + /** + * Parse the given text, starting at the given index, into a new {@link TypeDescriptor}. + * + * @param text The text to parse. + * @param from The index to start parsing from. + * @return The {@link TypeDescriptor}, or {@code null} if the text is not a valid type descriptor. + */ + public static @Nullable TypeDescriptor parseTypeDescriptor( + final @NotNull String text, + final int from + ) throws JvmTypeParseFailureException { + final ParserState source = new ParserState(text, from); + if (consumeTypeDesc(source)) { + return source.getLastResult(); + } + return null; + } + + /** + * Parse the given text, starting at the given index, into a new {@link MethodDescriptor}. + * + * @param text The text to parse. + * @param from The index to start parsing from. + * @return The {@link MethodDescriptor}, or {@code null} if the text is not a valid method descriptor. + */ + public static @Nullable MethodDescriptor parseMethodDescriptor( + final @NotNull String text, + final int from + ) { + final ParserState source = new ParserState(text, from); + if (consumeMethodDesc(source)) { + return source.getLastResult(); + } + return null; + } + + /** + * Parse the given text, starting at the given index, into a new {@link TypeSignature}. + * + * @param text The text to parse. + * @param from The index to start parsing from. + * @return The {@link TypeSignature}, or {@code null} if the text is not a valid type signature. + */ + public static @Nullable TypeSignature parseTypeSignature( + final @NotNull String text, + final int from + ) throws JvmTypeParseFailureException { + final ParserState source = new ParserState(text, from); + if (consumeTypeSig(source)) { + return source.getLastResult(); + } + return null; + } + + /** + * Parse the given text, starting at the given index, into a new {@link MethodSignature}. + * + * @param text The text to parse. + * @param from The index to start parsing from. + * @return The {@link MethodSignature}. + * @throws JvmTypeParseFailureException If the given text does not represent a valid JVM method signature. + */ + public static @Nullable MethodSignature parseMethodSignature( + final @NotNull String text, + final int from + ) throws JvmTypeParseFailureException { + final ParserState source = new ParserState(text, from); + if (consumeMethodSignature(source)) { + return source.getLastResult(); + } + return null; + } + + /** + * Parse the given text, starting at the given index, into a new {@link ClassSignature}. + * + * @param text The text to parse. + * @param from The index to start parsing from. + * @return The {@link ClassSignature}. + * @throws JvmTypeParseFailureException If the given text does not represent a valid JVM class signature. + */ + public static @Nullable ClassSignature parseClassSignature( + final @NotNull String text, + final int from + ) throws JvmTypeParseFailureException { + final ParserState source = new ParserState(text, from); + if (consumeClassSig(source)) { + return source.getLastResult(); + } + return null; + } + + // Type Descriptor + + private static boolean consumeTypeDesc(final @NotNull ParserState source) { + if (source.isAtEnd()) { + return false; + } + + return consumePrimitive(source) + || consumeVoid(source) + || consumeArrayDesc(source) + || consumeClassTypeDesc(source); + } + + private static boolean consumePrimitive(final @NotNull ParserState source) { + final PrimitiveType type = PrimitiveType.fromChar(source.current()); + if (type != null) { + source.advance(); + return source.setLastResult(type); + } + return false; + } + + private static boolean consumeVoid(final @NotNull ParserState source) { + if (source.current() == 'V') { + source.advance(); + return source.setLastResult(VoidType.INSTANCE); + } + return false; + } + + private static boolean consumeArrayDesc(final @NotNull ParserState source) { + if (source.current() != '[') { + return false; + } + + int dim = 1; + while (source.advance() == '[') { + dim++; + } + + if (source.isAtEnd()) { + return false; + } + if (!consumeTypeDesc(source)) { + return false; + } + + final TypeDescriptor baseType = source.getLastResult(); + return source.setLastResult(ArrayTypeDescriptor.of(dim, baseType)); + } + + private static boolean consumeClassTypeDesc(final @NotNull ParserState source) { + if (source.current() != 'L') { + return false; + } + + source.advanceAndMark(); + + while (!source.isAtEnd()) { + if (source.advance() == ';') { + break; + } + } + if (source.isAtEnd()) { + return false; + } + + final String className = source.substringFromMark(); + return source.advanceIfSet(ClassTypeDescriptor.of(className)); + } + + private static boolean consumeMethodDesc(final @NotNull ParserState source) { + if (source.current() != '(') { + return false; + } + + source.advance(); + + final ArrayList paramTypes = new ArrayList<>(); + while (consumeTypeDesc(source)) { + paramTypes.add(source.getLastResult()); + } + + if (source.current() != ')') { + return false; + } + + source.advance(); + + if (!consumeTypeDesc(source)) { + return false; + } + + final TypeDescriptor returnType = source.getLastResult(); + return source.setLastResult(MethodDescriptor.of(paramTypes, returnType)); + } + + // Type Signature + + private static boolean consumeTypeSig(final @NotNull ParserState source) { + if (source.isAtEnd()) { + return false; + } + + return consumePrimitive(source) + || consumeVoid(source) + || consumeArraySig(source) + || consumeClassTypeSig(source) + || consumeTypeVariable(source); + } + + private static boolean consumeArraySig(final @NotNull ParserState source) { + if (source.current() != '[') { + return false; + } + + int dim = 1; + while (source.advance() == '[') { + dim++; + } + + if (source.isAtEnd()) { + return false; + } + if (!consumeTypeSig(source)) { + return false; + } + + final TypeSignature baseType = source.getLastResult(); + return source.setLastResult(ArrayTypeSignature.of(dim, baseType)); + } + + @SuppressWarnings("DuplicatedCode") + private static boolean consumeClassTypeSig(final @NotNull ParserState source) { + if (source.current() != 'L') { + return false; + } + + source.advanceAndMark(); + + while (!source.isAtEnd()) { + final char c = source.advance(); + if (c == ';' || c == '<' || c == '.') { + break; + } + } + if (source.isAtEnd()) { + return false; + } + + final String className = source.substringFromMark(); + + final List typeArgs; + if (consumeTypeArguments(source)) { + typeArgs = source.getLastResult(); + } else { + typeArgs = Collections.emptyList(); + } + ClassTypeSignature currentClassType = ClassTypeSignature.of(className, typeArgs); + + while (consumeSubclass(source, currentClassType)) { + currentClassType = source.getLastResult(); + } + + if (source.current() == ';') { + source.advance(); + return source.setLastResult(currentClassType); + } + + return false; + } + + @SuppressWarnings("DuplicatedCode") + private static boolean consumeSubclass( + final @NotNull ParserState source, + final @NotNull ClassTypeSignature parentClass + ) { + if (source.current() != '.') { + return false; + } + + source.advanceAndMark(); + + while (!source.isAtEnd()) { + final char c = source.advance(); + if (c == ';' || c == '<' || c == '.') { + break; + } + } + if (source.isAtEnd()) { + return false; + } + + final String className = source.substringFromMark(); + final List typeArgs; + if (consumeTypeArguments(source)) { + typeArgs = source.getLastResult(); + } else { + typeArgs = Collections.emptyList(); + } + + final char c = source.current(); + if (c == ';' || c == '.') { + return source.setLastResult(ClassTypeSignature.of(parentClass, className, typeArgs)); + } + + return false; + } + + private static boolean consumeTypeArguments(final @NotNull ParserState source) { + if (source.current() != '<') { + return false; + } + + source.advanceAndMark(); + + final ArrayList typeArgs = new ArrayList<>(); + while (consumeTypeArgument(source)) { + typeArgs.add(source.getLastResult()); + } + + if (source.current() != '>') { + return false; + } + + source.advance(); + return source.setLastResult(typeArgs); + } + + private static boolean consumeTypeArgument(final @NotNull ParserState source) { + return consumeWildcardTypeArgument(source) || consumeBoundedTypeArgument(source); + } + + private static boolean consumeWildcardTypeArgument(final @NotNull ParserState source) { + if (source.current() == '*') { + source.advance(); + return source.setLastResult(WildcardArgument.INSTANCE); + } + return false; + } + + private static boolean consumeBoundedTypeArgument(final @NotNull ParserState source) { + final char c = source.current(); + final WildcardBound bound; + if (c == '+') { + bound = WildcardBound.UPPER; + source.advance(); + } else if (c == '-') { + bound = WildcardBound.LOWER; + source.advance(); + } else { + bound = null; + } + + if (consumeTypeSig(source)) { + final ReferenceTypeSignature boundedType = source.getLastResult(); + if (bound == null) { + return source.setLastResult(boundedType); + } else { + return source.setLastResult(BoundedTypeArgument.of(bound, boundedType)); + } + } + return false; + } + + private static boolean consumeTypeVariable(final @NotNull ParserState source) { + if (source.current() != 'T') { + return false; + } + + source.advanceAndMark(); + + while (!source.isAtEnd()) { + if (source.advance() == ';') { + break; + } + } + if (source.isAtEnd()) { + return false; + } + + final String typeName = source.substringFromMark(); + source.advance(); + return source.setLastResult(TypeVariable.unbound(typeName)); + } + + private static boolean consumeMethodSignature(final @NotNull ParserState source) { + List typeParams; + if (consumeTypeParameters(source)) { + typeParams = source.getLastResult(); + } else { + typeParams = Collections.emptyList(); + } + + if (source.current() != '(') { + return false; + } + + source.advance(); + + final ArrayList methodParameters = new ArrayList<>(); + while (consumeTypeSig(source)) { + methodParameters.add(source.getLastResult()); + } + if (source.isAtEnd() || source.current() != ')') { + return false; + } + + source.advance(); + + if (!consumeTypeSig(source)) { + return false; + } + final TypeSignature returnType = source.getLastResult(); + + final ArrayList throwsSignatures = new ArrayList<>(); + while (consumeThrowsSignature(source)) { + throwsSignatures.add(source.getLastResult()); + } + + return source.setLastResult(MethodSignature.of(typeParams, methodParameters, returnType, throwsSignatures)); + } + + private static boolean consumeTypeParameters(final @NotNull ParserState source) { + if (source.current() != '<') { + return false; + } + + source.advance(); + + final ArrayList typeParams = new ArrayList<>(); + while (consumeTypeParameter(source)) { + final TypeParameter typeParam = source.getLastResult(); + typeParams.add(typeParam); + } + if (source.isAtEnd() || source.current() != '>') { + return false; + } + + source.advance(); + return source.setLastResult(typeParams); + } + + private static boolean consumeTypeParameter(final @NotNull ParserState source) { + if (source.current() == '>') { + return false; + } + + source.mark(); + + while (!source.isAtEnd()) { + if (source.advance() == ':') { + break; + } + } + if (source.isAtEnd()) { + return false; + } + + final String paramName = source.substringFromMark(); + + if (!consumeTypeParamBounds(source)) { + return false; + } + final ReferenceTypeSignature classBound = source.getLastResultOrNull(); + + final ArrayList interfaceBounds = new ArrayList<>(); + while (consumeTypeParamBounds(source)) { + interfaceBounds.add(source.getLastResult()); + } + if (source.isAtEnd()) { + return false; + } + + return source.setLastResult(TypeParameter.of(paramName, classBound, interfaceBounds)); + } + + private static boolean consumeTypeParamBounds(final @NotNull ParserState source) { + if (source.current() != ':') { + return false; + } + + source.advance(); + + char next = source.current(); + if (next == ':' || next == '>') { + return true; + } + + return consumeTypeSig(source); + } + + private static boolean consumeThrowsSignature(final @NotNull ParserState source) { + if (source.current() != '^') { + return false; + } + + source.advance(); + return consumeTypeSig(source); + } + + private static boolean consumeClassSig(final @NotNull ParserState source) { + consumeTypeParameters(source); + List typeParams = source.getLastResultOrNull(); + if (typeParams == null) { + typeParams = Collections.emptyList(); + } + + if (!consumeClassTypeSig(source)) { + return false; + } + + final ClassTypeSignature superClass = source.getLastResult(); + + final List superInterfaces = new ArrayList<>(); + while (consumeClassTypeSig(source)) { + superInterfaces.add(source.getLastResult()); + } + + return source.setLastResult(ClassSignature.of(typeParams, superClass, superInterfaces)); + } +} diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/parsing/ParserState.java b/hypo-types/src/main/java/dev/denwav/hypo/types/parsing/ParserState.java new file mode 100644 index 0000000..ed8043a --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/parsing/ParserState.java @@ -0,0 +1,194 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types.parsing; + +import dev.denwav.hypo.types.HypoTypesUtil; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Internal state used by {@link JvmTypeParser}. + */ +@ApiStatus.Internal +/* package */ final class ParserState { + + private final @NotNull String text; + + private int mark = -1; + private int index; + + private @Nullable Object lastResult = null; + + /** + * Constructor for {@link ParserState}. + * @param text Text to parse. + * @param from Index to start parsing from. + */ + /* package */ ParserState(final @NotNull String text, final int from) { + this.text = text; + + if (from < 0 || from > text.length()) { + throw new IllegalArgumentException("Invalid start index for string: " + from); + } + this.index = from; + } + + /** + * Get the last result, if set. + * @param The type of the last result. + * @return The last result, or {@code null} if no result has been set. + */ + @SuppressWarnings("TypeParameterUnusedInFormals") + /* package */ @Nullable T getLastResultOrNull() { + final Object res = this.lastResult; + this.lastResult = null; + return HypoTypesUtil.cast(res); + } + + /** + * Get the last result, fail if no result is set. + * @param The type of the last result. + * @return The last result. + * @throws NullPointerException If no result has been set. + */ + @SuppressWarnings("TypeParameterUnusedInFormals") + /* package */ @NotNull T getLastResult() { + final T res = this.getLastResultOrNull(); + if (res == null) { + throw new NullPointerException("No last result set"); + } + return res; + } + + /** + * Set the result for {@link #getLastResult()}. + * @param lastResult The result. + * @return {@code true} if the given result was not {@code null}. + */ + /* package */ boolean setLastResult(final @Nullable Object lastResult) { + this.lastResult = lastResult; + return lastResult != null; + } + + /** + * {@link #advance() Advance} + * @param lastResult The result. + * @return if the given resultw as not {@code null}. + */ + /* package */ boolean advanceIfSet(final @Nullable Object lastResult) { + if (lastResult != null) { + this.setLastResult(lastResult); + this.advance(); + return true; + } + return false; + } + + /** + * Returns the current parser index. + * @return The current parser index. + */ + /* package */ int currentIndex() { + return this.index; + } + + /** + * Returns {@code true} if the parser is at the start of the input. + * @return {@code true} if the parser is at the start of the input. + */ + /* package */ boolean isAtStart() { + return this.index == 0; + } + + /** + * Returns {@code true} if the parser is at the end of the input. + * @return {@code true} if the parser is at the end of the input. + */ + /* package */ boolean isAtEnd() { + return this.index >= this.text.length(); + } + + /** + * Returns the character at the current parser index of the input. If the parser index is at the end of the input + * then {@code NULL} ({@code \0}) is returned. + * @return The character at the current parser index of the input. + */ + /* package */ char current() { + final int currentIndex = this.index; + if (currentIndex == this.text.length()) { + return '\0'; + } else { + return this.text.charAt(this.index); + } + } + + /** + * Call {@link #advance()} followed by {@link #mark()} + */ + /* package */ void advanceAndMark() { + this.advance(); + this.mark(); + } + + /** + * Advance the parser to the next character and return it. {@code NULL} ({@code \0}) is returned when the parser has + * reached the end of the input. + * @return The next character, or {@code \0}. + */ + /* package */ char advance() { + if (this.isAtEnd()) { + return '\0'; + } + final int next = ++this.index; + if (this.isAtEnd()) { + return '\0'; + } else { + return this.text.charAt(next); + } + } + + /** + * Mark the current index of the parser, to be used later by {@link #substringFromMark()}. + */ + /* package */ void mark() { + this.mark = this.currentIndex(); + } + + /** + * Returns the current marked index from {@link #mark()}. + * @return The current marked index. + */ + /* package */ int getMark() { + return this.mark; + } + + /** + * Return the substring made from the current marked index (from {@link #mark()}) to the current parser index. + * @return The substring from the marked index to the current parser index. + */ + /* package */ @NotNull String substringFromMark() { + return this.text.substring(this.mark, this.index); + } + + @Override + public String toString() { + return this.text; + } +} diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/parsing/package-info.java b/hypo-types/src/main/java/dev/denwav/hypo/types/parsing/package-info.java new file mode 100644 index 0000000..2e7fa38 --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/parsing/package-info.java @@ -0,0 +1,25 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +/** + * This package provides utilities for parsing all kinds of internal JVM types, + * including descriptors and signatures for both value types and method types. + * + * @see dev.denwav.hypo.types.parsing.JvmTypeParser + */ +package dev.denwav.hypo.types.parsing; diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/pattern/ClassSignaturePatterns.java b/hypo-types/src/main/java/dev/denwav/hypo/types/pattern/ClassSignaturePatterns.java new file mode 100644 index 0000000..29660e5 --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/pattern/ClassSignaturePatterns.java @@ -0,0 +1,141 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types.pattern; + +import dev.denwav.hypo.types.sig.ClassSignature; +import dev.denwav.hypo.types.sig.ClassTypeSignature; +import dev.denwav.hypo.types.sig.TypeParameterHolder; +import java.util.List; +import java.util.function.Predicate; +import org.jetbrains.annotations.NotNull; + +/** + * Factory for {@link TypePattern} for {@link ClassSignature} type objects. + */ +public final class ClassSignaturePatterns { + + private ClassSignaturePatterns() {} + + /** + * A type pattern where the type object is a {@link ClassSignature}. + * @return The type pattern. + */ + public static @NotNull TypePattern isClassSignature() { + return (ctx, t) -> t instanceof ClassSignature; + } + + /** + * A type pattern where the type object is a {@link ClassSignature} that matches the given predicate. + * + * @param predicate The predicate to test against the {@link ClassSignature}. + * @return The type pattern. + */ + public static @NotNull TypePattern isClassSignature(final @NotNull Predicate predicate) { + return (ctx, t) -> t instanceof final ClassSignature c && predicate.test(c); + } + + /** + * A type pattern where the type object is a {@link TypeParameterHolder} and it has at least one type parameter. + * @return The type pattern. + */ + public static @NotNull TypePattern hasTypeParameters() { + // same impl, but it might make more sense to use this class vs the other due to context + return MethodPatterns.hasTypeParameters(); + } + + /** + * A type pattern where the type object is a {@link TypeParameterHolder} and it has no type parameters. + * @return The type pattern. + */ + public static @NotNull TypePattern hasNoTypeParameters() { + // same impl, but it might make more sense to use this class vs the other due to context + return MethodPatterns.hasNoTypeParameters(); + } + + /** + * A type pattern where the type object is a {@link TypeParameterHolder} that also has exactly as many type + * parameters as the number of patterns given, and each pattern matches the corresponding type parameter. + * + * @implNote This re-uses {@link MethodPatterns#hasTypeParameters(TypePattern...)}, either this method or that + * method behave the same, these factories are just placed on the methods for the objects they are found + * on for convenience and clarity. + * + * @param typeParameters Array of type patterns which must match 1:1 with the type parameters of the + * {@link TypeParameterHolder}. + * @return The type pattern. + */ + public static @NotNull TypePattern hasTypeParameters(final TypePattern... typeParameters) { + // same impl, but it might make more sense to use this class vs the other due to context + return MethodPatterns.hasTypeParameters(typeParameters); + } + + /** + * A type pattern where the type object is a {@link ClassSignature} where the super class matches the given type + * pattern. + * + * @param superClass A pattern to match the super class of the {@link ClassSignature}. + * @return The type pattern. + */ + public static @NotNull TypePattern hasSuperClass(final TypePattern superClass) { + return (ctx, t) -> t instanceof final ClassSignature c && superClass.test(ctx, c.getSuperClass()); + } + + /** + * A type pattern where the type object is a {@link ClassSignature} and it has at least one super interface. + * @return The type pattern. + */ + public static @NotNull TypePattern hasSuperInterfaces() { + return (ctx, t) -> t instanceof final ClassSignature c && !c.getSuperInterfaces().isEmpty(); + } + + /** + * A type pattern where the type object is a {@link ClassSignature} and it has no type super interfaces. + * @return The type pattern. + */ + public static @NotNull TypePattern hasNoSuperInterfaces() { + return (ctx, t) -> t instanceof final ClassSignature c && c.getSuperInterfaces().isEmpty(); + } + + /** + * A type pattern where the type object is a {@link ClassSignature} that also has exactly as many super interfaces + * as the number of patterns given, and each pattern matches the corresponding super interface. + * + * @param superInterfaces Array of type patterns which must match 1:1 with the super interfaces of the + * {@link ClassSignature}. + * @return The type pattern. + */ + public static @NotNull TypePattern hasSuperInterfaces(final TypePattern... superInterfaces) { + return (ctx, t) -> { + if (!(t instanceof final ClassSignature c)) { + return false; + } + final List supers = c.getSuperInterfaces(); + if (supers.size() != superInterfaces.length) { + return false; + } + for (int i = 0; i < supers.size(); i++) { + final ClassTypeSignature superSig = supers.get(i); + if (!superInterfaces[i].test(ctx, superSig)) { + return false; + } + } + return true; + }; + } +} diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/pattern/MethodPatterns.java b/hypo-types/src/main/java/dev/denwav/hypo/types/pattern/MethodPatterns.java new file mode 100644 index 0000000..4b67794 --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/pattern/MethodPatterns.java @@ -0,0 +1,263 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types.pattern; + +import dev.denwav.hypo.types.desc.MethodDescriptor; +import dev.denwav.hypo.types.kind.MethodType; +import dev.denwav.hypo.types.kind.ValueType; +import dev.denwav.hypo.types.sig.MethodSignature; +import dev.denwav.hypo.types.sig.ThrowsSignature; +import dev.denwav.hypo.types.sig.TypeParameterHolder; +import dev.denwav.hypo.types.sig.param.TypeParameter; +import java.util.function.Predicate; +import org.jetbrains.annotations.NotNull; + +/** + * Factory for {@link TypePattern} for {@link dev.denwav.hypo.types.desc.MethodDescriptor MethodDescriptor} and + * {@link MethodSignature} type objects. + */ +public final class MethodPatterns { + + private MethodPatterns() {} + + /* + * MethodDescriptor & MethodSignature + */ + + /** + * A type pattern where the type object is a {@link MethodType}. + * @return The type pattern. + */ + public @NotNull TypePattern isMethod() { + return (ctx, t) -> t instanceof MethodType; + } + + /** + * A type pattern where the type object is a {@link MethodType} that matches the given predicate. + * + * @param predicate The predicate to test against the {@link MethodType}. + * @return The type pattern. + */ + public @NotNull TypePattern isMethod(final Predicate predicate) { + return (ctx, t) -> t instanceof final MethodType m && predicate.test(m); + } + + /** + * A type pattern where the type object is a {@link MethodType}, and the method has at least one parameter. + * @return The type pattern. + */ + public @NotNull TypePattern hasParams() { + return (ctx, t) -> t instanceof final MethodType m && !m.getParameters().isEmpty(); + } + + /** + * A type pattern where the type object is a {@link MethodType}, and the method has no parameters. + * @return The type pattern. + */ + public @NotNull TypePattern hasNoParams() { + return (ctx, t) -> t instanceof final MethodType m && m.getParameters().isEmpty(); + } + + /** + * A type pattern where the type object is a {@link MethodType} that also has exactly as many parameters as the + * number of patterns given, and each pattern matches the corresponding parameter. + * + * @param params Array of type patterns which must match 1:1 with the parameters of the {@link MethodType}. + * @return The type pattern. + */ + public @NotNull TypePattern hasParams(final @NotNull TypePattern... params) { + return (ctx, t) -> { + if (!(t instanceof final MethodType m)) { + return false; + } + if (m.getParameters().size() != params.length) { + return false; + } + for (int i = 0; i < m.getParameters().size(); i++) { + final ValueType paramType = m.getParameters().get(i); + if (!params[i].test(ctx, paramType)) { + return false; + } + } + return true; + }; + } + + /** + * A type pattern where the type object is a {@link MethodType} and the return type matches the given pattern. + * + * @param returnType The pattern to match the return type of the {@link MethodType}. + * @return The type pattern. + */ + public @NotNull TypePattern hasReturnType(final @NotNull TypePattern returnType) { + return (ctx, t) -> t instanceof final MethodType m && returnType.test(ctx, m.getReturnType()); + } + + /* + * MethodDescriptor + */ + + /** + * A type pattern where the type object is a {@link MethodDescriptor}. + * @return The type pattern. + */ + public @NotNull TypePattern isMethodDescriptor() { + return (ctx, t) -> t instanceof MethodDescriptor; + } + + /** + * A type pattern where the type object is a {@link MethodDescriptor} that matches the given predicate. + * + * @param predicate The predicate to test against the {@link MethodDescriptor}. + * @return The type pattern. + */ + public @NotNull TypePattern isMethodDescriptor(final @NotNull Predicate predicate) { + return (ctx, t) -> t instanceof final MethodDescriptor d && predicate.test(d); + } + + /* + * MethodSignature + */ + + /** + * A type pattern where the type object is a {@link MethodSignature}. + * @return The type pattern. + */ + public @NotNull TypePattern isMethodSignature() { + return (ctx, t) -> t instanceof MethodSignature; + } + + /** + * A type pattern where the type object is a {@link MethodSignature} that matches the given predicate. + * + * @param predicate The predicate to test against the {@link MethodSignature}. + * @return The type pattern. + */ + public @NotNull TypePattern isMethodSignature(final @NotNull Predicate predicate) { + return (ctx, t) -> t instanceof final MethodSignature s && predicate.test(s); + } + + /** + * A type pattern where the type object is a {@link MethodSignature} that has at least one throws signature. + * @return The type pattern. + */ + public @NotNull TypePattern hasThrows() { + return (ctx, t) -> { + if (!(t instanceof final MethodSignature sig)) { + return false; + } + return !sig.getThrowsSignatures().isEmpty(); + }; + } + + /** + * A type pattern where the type object is a {@link MethodSignature} that has no throws signature. + * @return The type pattern. + */ + public @NotNull TypePattern hasNoThrows() { + return (ctx, t) -> { + if (!(t instanceof final MethodSignature sig)) { + return false; + } + return sig.getThrowsSignatures().isEmpty(); + }; + } + + /** + * A type pattern where the type object is a {@link MethodSignature} that also has exactly as many throws signatures + * as the number of patterns given, and each pattern matches the corresponding throws signature. + * + * @param throwsClauses Array of type patterns which must match 1:1 with the throws signatures of the + * {@link MethodSignature}. + * @return The type pattern. + */ + public @NotNull TypePattern hasThrows(final @NotNull TypePattern... throwsClauses) { + return (ctx, t) -> { + if (!(t instanceof final MethodSignature sig)) { + return false; + } + if (sig.getThrowsSignatures().size() != throwsClauses.length) { + return false; + } + for (int i = 0; i < sig.getThrowsSignatures().size(); i++) { + final ThrowsSignature throwsSig = sig.getThrowsSignatures().get(i); + if (!throwsClauses[i].test(ctx, throwsSig)) { + return false; + } + } + return true; + }; + } + + /** + * A type pattern where the type object is a {@link TypeParameterHolder} and it has at least one type parameter. + * @return The type pattern. + */ + public static @NotNull TypePattern hasTypeParameters() { + return (ctx, t) -> { + if (t instanceof final TypeParameterHolder paramHolder) { + return !paramHolder.getTypeParameters().isEmpty(); + } + return false; + }; + } + + /** + * A type pattern where the type object is a {@link TypeParameterHolder} and it has no type parameters. + * @return The type pattern. + */ + public static @NotNull TypePattern hasNoTypeParameters() { + return (ctx, t) -> { + if (t instanceof final TypeParameterHolder paramHolder) { + return paramHolder.getTypeParameters().isEmpty(); + } + return false; + }; + } + + /** + * A type pattern where the type object is a {@link TypeParameterHolder} that also has exactly as many type + * parameters as the number of patterns given, and each pattern matches the corresponding type parameter. + * + * @implNote This is re-used for {@link ClassSignaturePatterns#hasTypeParameters(TypePattern...)}, either this + * method or that method behave the same, these factories are just placed on the methods for the objects + * they are found on for convenience and clarity. + * + * @param typeParameters Array of type patterns which must match 1:1 with the type parameteres of the + * {@link TypeParameterHolder}. + * @return The type pattern. + */ + public static @NotNull TypePattern hasTypeParameters(final TypePattern... typeParameters) { + return (ctx, t) -> { + if (t instanceof final TypeParameterHolder paramHolder) { + if (paramHolder.getTypeParameters().size() != typeParameters.length) { + return false; + } + for (int i = 0; i < paramHolder.getTypeParameters().size(); i++) { + final TypeParameter param = paramHolder.getTypeParameters().get(i); + if (!typeParameters[i].test(ctx, param)) { + return false; + } + } + return true; + } + return false; + }; + } +} diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/pattern/TypeArgumentPatterns.java b/hypo-types/src/main/java/dev/denwav/hypo/types/pattern/TypeArgumentPatterns.java new file mode 100644 index 0000000..db4034f --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/pattern/TypeArgumentPatterns.java @@ -0,0 +1,98 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types.pattern; + +import dev.denwav.hypo.types.sig.param.BoundedTypeArgument; +import dev.denwav.hypo.types.sig.param.TypeArgument; +import dev.denwav.hypo.types.sig.param.WildcardArgument; +import dev.denwav.hypo.types.sig.param.WildcardBound; +import java.util.function.Predicate; +import org.jetbrains.annotations.NotNull; + +/** + * Factory for {@link TypePattern} for {@link TypeArgument}, {@link WildcardArgument}, and {@link BoundedTypeArgument} + * type objects. + */ +public final class TypeArgumentPatterns { + + private TypeArgumentPatterns() {} + + /** + * A type pattern where the type object is a {@link TypeArgument}. + * @return The type pattern. + */ + public static @NotNull TypePattern isTypeArgument() { + return (ctx, t) -> t instanceof TypeArgument; + } + + /** + * A type pattern where the type object is a {@link TypeArgument} that matches the given predicate. + * + * @param predicate The predicate to test against the {@link TypeArgument}. + * @return The type pattern. + */ + public static @NotNull TypePattern isTypeArgument(final @NotNull Predicate predicate) { + return (ctx, t) -> t instanceof final TypeArgument a && predicate.test(a); + } + + /** + * A type pattern where the type object is a {@link WildcardArgument}. + * @return The type pattern. + */ + public static @NotNull TypePattern isWildcard() { + return (ctx, t) -> t == WildcardArgument.INSTANCE; + } + + /** + * A type pattern where the type object is a {@link BoundedTypeArgument}. + * @return The type pattern. + */ + public static @NotNull TypePattern isBoundedArgument() { + return (ctx, t) -> t instanceof BoundedTypeArgument; + } + + /** + * A type pattern where the type object is a {@link BoundedTypeArgument} and {@link BoundedTypeArgument#getBounds()} + * is {@link WildcardBound#UPPER}. + * @return The type pattern. + */ + public static @NotNull TypePattern hasUpperBound() { + return (ctx, t) -> t instanceof final BoundedTypeArgument b && b.getBounds() == WildcardBound.UPPER; + } + + /** + * A type pattern where the type object is a {@link BoundedTypeArgument} and {@link BoundedTypeArgument#getBounds()} + * is {@link WildcardBound#LOWER}. + * @return The type pattern. + */ + public static @NotNull TypePattern hasLowerBound() { + return (ctx, t) -> t instanceof final BoundedTypeArgument b && b.getBounds() == WildcardBound.LOWER; + } + + /** + * A type pattern where the type object is a {@link BoundedTypeArgument} and + * {@link BoundedTypeArgument#getSignature() bound type} of the argument matches the given type pattern. + * + * @param bounds The pattern to test against the bound type of the {@link BoundedTypeArgument}. + * @return The type pattern. + */ + public static @NotNull TypePattern hasBounds(final TypePattern bounds) { + return (ctx, t) -> t instanceof final BoundedTypeArgument b && bounds.test(ctx, b.getSignature()); + } +} diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/pattern/TypeCapture.java b/hypo-types/src/main/java/dev/denwav/hypo/types/pattern/TypeCapture.java new file mode 100644 index 0000000..4a90d40 --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/pattern/TypeCapture.java @@ -0,0 +1,101 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types.pattern; + +import dev.denwav.hypo.types.TypeRepresentable; +import java.util.concurrent.ThreadLocalRandom; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * An anonymous capture for a {@link TypePattern}. Anonymous captures are still named using {@link #getKey()}, but the + * name used is randomly generated (and non-printable) and there is no reason to interact with it directly. Instead, use + * {@link #get(TypeMatch)} and {@link #getOrNull(TypeMatch)} to retrieve the associated capture from a + * {@link TypeMatch}. + */ +@SuppressWarnings("ClassCanBeRecord") +public class TypeCapture implements TypePattern { + + /** + * The random generated capture key, generated by {@link TypeCapture#randomKey()}. + */ + private final @NotNull String key; + /** + * The type pattern to use as the delegate for the capture. + */ + private final @NotNull TypePattern delegate; + + /** + * Create a new anonymous type capture. This constructor is package-private as you should use + * {@link TypePattern#capture(TypePattern)} instead. + * + * @param key The random generated capture key, generated by {@link TypeCapture#randomKey()}. + * @param delegate The type pattern to use as the delegate for the capture. + */ + /* package */ TypeCapture(final @NotNull String key, final @NotNull TypePattern delegate) { + this.key = key; + this.delegate = delegate; + } + + /** + * Generate a new random key name for {@link #getKey()}. + * @return The new random key name. + */ + /* package */ static @NotNull String randomKey() { + // Key names generated here only use non-printable characters, guaranteeing collision with user-created keys should be impossible + final int[] keyChars = ThreadLocalRandom.current().ints(8, '\0', '\u001F').toArray(); + return new String(keyChars, 0, keyChars.length); + } + + /** + * The randomly generated key name for this anonymous capture. This string will not be printable, and you typically + * should not need to interact with it directly. + * @return The randomly generated key name for this anonymous capture. + */ + public @NotNull String getKey() { + return this.key; + } + + /** + * Retrieve the captured value from the given match. This method will throw a {@link NullPointerException} if this + * capture does not exist in the given match. + * + * @param match The match to retrieve the capture from. + * @return The type object associated with this capture. + */ + public @NotNull TypeRepresentable get(final @NotNull TypeMatch match) { + return match.get(this.key); + } + + /** + * Retrieve the captured value from the given match, or {@code null} if this capture does not exist in the given + * match. + * + * @param match The match to retrieve the capture from. + * @return The type object associated with this capture, or {@code null} if it does not exist. + */ + public @Nullable TypeRepresentable getOrNull(final @NotNull TypeMatch match) { + return match.getOrNull(this.key); + } + + @Override + public boolean test(final @NotNull TypeMatchContext ctx, final @NotNull TypeRepresentable type) { + return this.delegate.test(ctx, type); + } +} diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/pattern/TypeMatch.java b/hypo-types/src/main/java/dev/denwav/hypo/types/pattern/TypeMatch.java new file mode 100644 index 0000000..7948b69 --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/pattern/TypeMatch.java @@ -0,0 +1,95 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types.pattern; + +import dev.denwav.hypo.types.TypeRepresentable; +import java.util.Map; +import java.util.Objects; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * The result of a {@link TypePattern#match(TypeRepresentable) TypePattern match test}. Contains two distinct values: + * the {@link #matches() match state} and the set of {@link #captures() captures}. {@link #get(String)} and + * {@link #getOrNull(String)} can be used to directly retrieve named captures. + * + *

When using anonymous captures retrieve the value using the {@link TypeCapture#get(TypeMatch)} and + * {@link TypeCapture#getOrNull(TypeMatch)} methods on the {@link TypeCapture} class. + * + * @param matches {@code true} if the type pattern matched, {@code false} otherwise. + * @param captures The map of captures and their associated type objects. + */ +public record TypeMatch( + boolean matches, + @NotNull Map captures +) { + + /** + * Constructor for {@link TypeMatch}. {@code captures} may be {@code null}, which results in this {@link TypeMatch} + * containing an empty map of captures. + * + * @param matches Whether the match succeeded. + * @param captures The map of captures and their associated type objects. + */ + public TypeMatch(boolean matches, @Nullable Map captures) { + this.matches = matches; + this.captures = captures != null ? Map.copyOf(captures) : Map.of(); + } + + /** + * {@code true} if the type pattern matched successfully. + * @return {@code true} if the type pattern matched successfully. + */ + @Override + public boolean matches() { + return this.matches; + } + + /** + * The map of captures and their associated type objects. The key of the map is the name of the capture, which will + * be a random string when using anonymous captures. + * + * @return The map of captures and their associated type objects. + */ + @Override + public @NotNull Map captures() { + return this.captures; + } + + /** + * Retrieve the named capture from this match. This method will throw a {@link NullPointerException} if the named + * capture does not exist in this match. + * + * @param name The name of the capture. + * @return The type object associated with the named capture. + */ + public @NotNull TypeRepresentable get(final String name) { + return Objects.requireNonNull(this.getOrNull(name), "No capture registered for name: " + name); + } + + /** + * Retrieve the named capture from this match, or {@code null} if no capture by the given name exists. + * + * @param name The name of the capture. + * @return The type object associated with the named capture, or {@code null} if it does not exist. + */ + public @Nullable TypeRepresentable getOrNull(final String name) { + return this.captures.get(name); + } +} diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/pattern/TypeMatchContext.java b/hypo-types/src/main/java/dev/denwav/hypo/types/pattern/TypeMatchContext.java new file mode 100644 index 0000000..bb80171 --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/pattern/TypeMatchContext.java @@ -0,0 +1,76 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types.pattern; + +import dev.denwav.hypo.types.TypeRepresentable; +import java.util.HashMap; +import java.util.Map; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Context class which stores capture data for a type match while it is executing. This API is not intended to be used + * directly, it is used by {@link TypePattern}. + */ +@ApiStatus.Internal +public final class TypeMatchContext { + + private @Nullable Map capturedTypes; + + /** + * Create a new instance of {@link TypeMatchContext}. + */ + /* package */ TypeMatchContext() {} + + private @NotNull Map capturedTypes() { + Map c = this.capturedTypes; + if (c != null) { + return c; + } + + synchronized (this) { + c = this.capturedTypes; + if (c != null) { + return c; + } + + this.capturedTypes = c = new HashMap<>(); + return c; + } + } + + /** + * Register a new captured object, associating it with {@code name}. + * + * @param name The name of the captured type object. + * @param type The type object that is to be captured. + */ + public void capture(final @NotNull String name, final @NotNull TypeRepresentable type) { + this.capturedTypes().put(name, type); + } + + /** + * Get the map of captured type objects. + * @return The map of captured type objects. + */ + /* package */ @Nullable Map getCapturedTypes() { + return this.capturedTypes; + } +} diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/pattern/TypeParameterPatterns.java b/hypo-types/src/main/java/dev/denwav/hypo/types/pattern/TypeParameterPatterns.java new file mode 100644 index 0000000..f20b4fd --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/pattern/TypeParameterPatterns.java @@ -0,0 +1,141 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types.pattern; + +import dev.denwav.hypo.types.sig.ReferenceTypeSignature; +import dev.denwav.hypo.types.sig.param.TypeParameter; +import java.util.List; +import java.util.function.Predicate; +import org.jetbrains.annotations.NotNull; + +/** + * Factory for {@link TypePattern} for {@link TypeParameter} type objects. + */ +public final class TypeParameterPatterns { + + private TypeParameterPatterns() {} + + /** + * A type pattern where the type object is a {@link TypeParameter}. + * @return The type pattern. + */ + public static @NotNull TypePattern isTypeParameter() { + return (ctx, t) -> t instanceof TypeParameter; + } + + /** + * A type pattern where the type object is a {@link TypeParameter} that matches the given predicate. + * + * @param predicate The predicate to test against the {@link TypeParameter}. + * @return The type pattern. + */ + public static @NotNull TypePattern isTypeParameter(final @NotNull Predicate predicate) { + return (ctx, t) -> t instanceof final TypeParameter p && predicate.test(p); + } + + /** + * A type pattern where the type object is a {@link TypeParameter} and the parameter's name matches the given + * predicate. + * + * @param name The predicate the type parameter's name must match. + * @return The type pattern. + */ + public static @NotNull TypePattern hasName(final @NotNull Predicate name) { + return (ctx, t) -> t instanceof final TypeParameter p && name.test(p.getName()); + } + + /** + * A type pattern where the type object is a {@link TypeParameter} and the parameter's name matches the given name. + * + * @param name The text the type parameter's name must match. + * @return The type pattern. + */ + public static @NotNull TypePattern hasName(final @NotNull String name) { + return hasName(name::equals); + } + + /** + * A type pattern where the type object is a {@link TypeParameter} and the type parameter contains a class bound. + * Class bounds may not be specified on a type parameter if there are interface bounds defined instead. In any case, + * when no class bound is defined (when {@link TypeParameter#getClassBound()} is {@code null}) then it is implied to + * be {@code Ljava/lang/Object;}. + * + * @return The type pattern. + */ + public static @NotNull TypePattern hasClassBound() { + return (ctx, t) -> + t instanceof TypeParameter p && p.getClassBound() != null; + } + + + /** + * A type pattern where the type object is a {@link TypeParameter} and the type parameter's class bound matches the + * given pattern. + * + * @param classBound The pattern the type parameter's class bound must match. + * @return The type pattern. + */ + public static @NotNull TypePattern hasClassBound(final @NotNull TypePattern classBound) { + return (ctx, t) -> + t instanceof TypeParameter p && p.getClassBound() != null && classBound.test(ctx, p.getClassBound()); + } + + /** + * A type pattern where the type object is a {@link TypeParameter} that has at least one interface bound. + * @return The type pattern. + */ + public static @NotNull TypePattern hasInterfaceBounds() { + return (ctx, t) -> t instanceof final TypeParameter p && !p.getInterfaceBounds().isEmpty(); + } + + /** + * A type pattern where the type object is a {@link TypeParameter} that has no interface bounds. + * @return The type pattern. + */ + public static @NotNull TypePattern hasNoInterfaceBounds() { + return (ctx, t) -> t instanceof final TypeParameter p && p.getInterfaceBounds().isEmpty(); + } + + /** + * A type pattern where the type object is a {@link TypeParameter} that also has exactly as many interface bounds + * as the number of patterns given, and each pattern matches the corresponding interface bound. + * + * @param interfaceBounds Array of type patterns which must match 1:1 with the interface bounds of the + * {@link TypeParameter}. + * @return The type pattern. + */ + public static @NotNull TypePattern hasInterfaceBounds(final @NotNull TypePattern... interfaceBounds) { + return (ctx, t) -> { + if (!(t instanceof final TypeParameter p)) { + return false; + } + final List inters = p.getInterfaceBounds(); + if (inters.size() != interfaceBounds.length) { + return false; + } + for (int i = 0; i < inters.size(); i++) { + final ReferenceTypeSignature inter = inters.get(i); + if (!interfaceBounds[i].test(ctx, inter)) { + return false; + } + } + return true; + }; + } +} diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/pattern/TypePattern.java b/hypo-types/src/main/java/dev/denwav/hypo/types/pattern/TypePattern.java new file mode 100644 index 0000000..960ce43 --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/pattern/TypePattern.java @@ -0,0 +1,278 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types.pattern; + +import dev.denwav.hypo.types.TypeRepresentable; +import dev.denwav.hypo.types.kind.MethodType; +import dev.denwav.hypo.types.kind.ValueType; +import java.io.Serializable; +import java.util.function.Predicate; +import org.jetbrains.annotations.NotNull; + +/** + * A type pattern matches a {@link TypeRepresentable JVM type definition}, including + * {@link dev.denwav.hypo.types.desc.Descriptor Descriptors} and {@link dev.denwav.hypo.types.sig.Signature Signatures}, + * and both {@link ValueType field types} as well as + * {@link MethodType method types}. It can also match + * {@link dev.denwav.hypo.types.sig.ClassSignature ClassSignatures}. + * + *

For this API the term "match" means to return {@code true} from the + * {@link #test(TypeMatchContext, TypeRepresentable) test method} if a given {@link TypeRepresentable} satisfies the + * condition of that pattern. This is a similar concept to the {@link java.util.regex.Pattern Pattern} class, but for + * Java types rather than Strings. + * + *

Like Pattern, TypePatterns can also define {@link TypeCapture captures} to extract components of a type pattern. + * For example, a type pattern could be defined to match against a {@code List} type, and a capture could be defined on + * the generic parameter to extract the type of the elements the List contains. + * + *

TypePatterns can be created directly by simply implementing + * {@link #test(TypeMatchContext, TypeRepresentable) test}, but the intended method of using this interface is to use + * the already defined TypePattern factories provided with this API: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
+ * Mapping between {@link TypeRepresentable} objects and their corresponding {@link TypePattern} factories. + *
Target TypeType Pattern Factories
{@link dev.denwav.hypo.types.desc.TypeDescriptor TypeDescriptor}{@link TypePatterns}
{@link dev.denwav.hypo.types.sig.TypeSignature TypeSignature}
{@link dev.denwav.hypo.types.desc.MethodDescriptor TypeDescriptor}{@link MethodPatterns}
{@link dev.denwav.hypo.types.sig.MethodSignature TypeSignature}
{@link dev.denwav.hypo.types.sig.param.TypeParameter TypeParameter}{@link TypeParameterPatterns}
{@link dev.denwav.hypo.types.sig.ClassSignature ClassSignature}{@link ClassSignaturePatterns}
+ * + * @see TypeCapture + * @see TypePatterns + * @see MethodPatterns + * @see TypeParameterPatterns + * @see ClassSignaturePatterns + */ +public interface TypePattern extends Serializable { + + /** + * Test if the given {@code type} matches this pattern. This method will return {@code false} when the given type + * does not match, though that can also be due to the kind of {@code type} also being incompatible. For example, + * if this pattern is testing the parameters of a method type and a field type is passed in, it will always return + * {@code false}. + * + *

This method can be called directly, but it is primarily an internal API, due to the {@code ctx} parameter. + * Users should typically prefer to use {@link #match(TypeRepresentable)} instead. + * + * @param ctx The {@link TypeMatchContext context}. + * @param type The {@link TypeRepresentable type} to test. + * @return {@code true} if the given type satisfies this pattern. + */ + boolean test(final @NotNull TypeMatchContext ctx, final @NotNull TypeRepresentable type); + + /** + * Test if the given {@code type} matches this pattern. The returned match object will contain both the match state + * and any captured values if {@link TypeMatch#matches()} is {@code true}. + * + * @param type The {@link TypeRepresentable type} to test. + * @return {@code true} if the given type satisfies this pattern. + */ + default @NotNull TypeMatch match(final @NotNull TypeRepresentable type) { + final TypeMatchContext ctx = new TypeMatchContext(); + final boolean matches = this.test(ctx, type); + return new TypeMatch(matches, matches ? ctx.getCapturedTypes() : null); + } + + /** + * Transform this pattern into a {@link Predicate} for use in standard Java APIs. + * + * @return A new {@link Predicate} which wraps this pattern and checks for {@link TypeMatch#matches()}. + */ + default @NotNull Predicate asPredicate() { + return t -> this.match(t).matches(); + } + + /** + * A named capture of a delegate pattern. Captured values are only held if the whole containing type pattern + * matches, which includes the given delegate pattern. The {@code name} of the capture is used to store and retrieve + * the type object which matches the given delegate pattern. + * + *

When a {@link TypeMatch} both {@link TypeMatch#matches() matches} and was generated from a {@link TypePattern} + * containing a capture of name {@code N}, then {@link TypeMatch#get(String) TypeMatch.get(N)} is guaranteed to + * return the value for the corresponding captured object unless the capture was inside an optional + * component of the {@link TypePattern}, such as {@link TypePattern#or(TypePattern)}. + * + *

This API does not check for name conflicts of already defined captures, defining a type pattern with multiple + * captures of the same name is undefined behavior. + * + *

Named and anonymous captures can be used together in the same type pattern. + * + *

Example: + *


+     *     // TypePattern which matches Lists and captures the generic type of the list
+     *     final TypePattern pattern = TypePatterns.isClass("java/util/List")
+     *         .and(TypePatterns.hasTypeArguments(TypePattern.capture("typeArg", TypePattern.any())));
+     *     // Example type to tests
+     *     final TypeSignature testType = TypeSignature.parse("Ljava/util/List<Ljava/lang/String;>;");
+     *     // typeArg is "Ljava/lang/String;"
+     *     final TypeSignature typeArg = (TypeSignature) pattern.match(testType).get("typeArg");
+     * 
+ * + * @param name The name of the capture, to be used with {@link TypeMatch#get(String)} + * @param delegate The type pattern the captured type object must satisfy. + * @return A new pattern which wraps the given delegate and captures the matching value. + * @see #capture(TypePattern) + */ + static TypePattern capture(final String name, final @NotNull TypePattern delegate) { + return (ctx, type) -> { + if (delegate.test(ctx, type)) { + ctx.capture(name, type); + return true; + } + return false; + }; + } + + /** + * An anonymous capture of a delegate pattern. Captured values are only held if the whole containing type pattern + * matches, which includes the given delegate pattern. Anonymous captures are managed using the returned + * {@link TypeCapture} object, which itself is also a {@link TypePattern}. + * + *

When a {@link TypeMatch} {@code M} both {@link TypeMatch#matches() matches} and was generated from a {@link TypePattern} + * containing a capture {@code C}, then {@link TypeCapture#get(TypeMatch) C.get(M)} is guaranteed to return the + * value for the corresponding captured object unless the capture was inside an optional + * component of the {@link TypePattern}, such as {@link TypePattern#or(TypePattern)}. + * + *

Named and anonymous captures can be used together in the same type pattern. + * + *

Example: + *


+     *     // Define the TypeCapture as a separate reference so it can be used later
+     *     final TypeCapture capture = TypePattern.capture(TypePattern.any());
+     *     // TypePattern which matches Lists and captures the generic type of the list
+     *     final TypePattern pattern = TypePatterns.isClass("java/util/List")
+     *         .and(TypePatterns.hasTypeArguments(capture));
+     *     // Example type to tests
+     *     final TypeSignature testType = TypeSignature.parse("Ljava/util/List<java/lang/String;>;");
+     *     // typeArg is "Ljava/lang/String;"
+     *     final TypeSignature typeArg = (TypeSignature) capture.get(pattern.match(testType));
+     * 
+ * +

Depending on preference, you can also move the TypeCapture definition to the call site: + *


+     *     // Define the TypeCapture as a separate reference so it can be used later
+     *     final TypeCapture capture;
+     *     // TypePattern which matches Lists and captures the generic type of the list
+     *     final TypePattern pattern = TypePatterns.isClass("java/util/List")
+     *         // (ab)use the fact that in Java assignments are also expressions
+     *         .and(TypePatterns.hasTypeArguments(capture = TypePattern.capture(TypePattern.any())));
+     *     // Example type to tests
+     *     final TypeSignature testType = TypeSignature.parse("Ljava/util/List<Ljava/lang/String;>;");
+     *     // typeArg is "Ljava/lang/String;"
+     *     final TypeSignature typeArg = (TypeSignature) capture.get(pattern.match(testType));
+     * 
+ * + * @param delegate The type pattern the captured type object must satisfy. + * @return A new pattern which wraps the given delegate and captures the matching value. + * @see #capture(String, TypePattern) + */ + static TypeCapture capture(final @NotNull TypePattern delegate) { + final String key = TypeCapture.randomKey(); + return new TypeCapture(key, capture(key, delegate)); + } + + /** + * Create a new pattern which requires both {@code this} and the given {@code other} pattern to match the same + * type object in order for the whole pattern to match. + * + * @param other The other pattern which must also match. + * @return A new pattern that wraps both {@code this} and {@code other} and requires both to match. + */ + default @NotNull TypePattern and(final @NotNull TypePattern other) { + return (ctx, t) -> this.test(ctx, t) && other.test(ctx, t); + } + + /** + * Create a new pattern which requires only one of {@code this} and the given {@code other} pattern to match a type + * object in order for the whole pattern to match. + * + *

Note that any {@link #capture(TypePattern) captures} defined in either of these patterns are no longer + * guaranteed to be present in the returned {@link TypeMatch} object if the other side of the {@code OR} matched and + * the side of the pattern the capture was defined in did not match. + * + *

Also note that the pattern is executed lazily by first checking {@code this}, then {@code other}. If both + * {@code this} and {@code other} match the given type, but the capture is only defined in {@code other}, then that + * capture will also not be present, as {@code other} never executed after {@code this} matched. + * + * @param other The other pattern which may also match. + * @return A new pattern that wraps both {@code this} and {@code other} and requires only one to match. + */ + default @NotNull TypePattern or(final @NotNull TypePattern other) { + return (ctx, t) -> this.test(ctx, t) || other.test(ctx, t); + } + + /** + * Invert {@code this} pattern and return a new pattern which only matches when {@code this} does not match. + * + * @return A new pattern that negates {@code this} pattern. + */ + default @NotNull TypePattern not() { + return (ctx, t) -> !this.test(ctx, t); + } + + /** + * A basic pattern which wraps a given predicate. + * + * @param predicate The predicate to test against the type object. + * @return A type pattern wrapping the given predicate. + */ + static @NotNull TypePattern is(final @NotNull Predicate predicate) { + return (ctx, t) -> predicate.test(t); + } + + /** + * A pattern which always matches. + * @return A pattern which always matches. + */ + static @NotNull TypePattern any() { + return (ctx, t) -> true; + } + + /** + * A pattern which never matches. + * @return A pattern which never matches. + */ + static @NotNull TypePattern none() { + return (ctx, t) -> false; + } +} diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/pattern/TypePatterns.java b/hypo-types/src/main/java/dev/denwav/hypo/types/pattern/TypePatterns.java new file mode 100644 index 0000000..bc8e52f --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/pattern/TypePatterns.java @@ -0,0 +1,429 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types.pattern; + +import dev.denwav.hypo.types.PrimitiveType; +import dev.denwav.hypo.types.VoidType; +import dev.denwav.hypo.types.desc.ClassTypeDescriptor; +import dev.denwav.hypo.types.desc.TypeDescriptor; +import dev.denwav.hypo.types.kind.ArrayType; +import dev.denwav.hypo.types.kind.ClassType; +import dev.denwav.hypo.types.kind.ValueType; +import dev.denwav.hypo.types.sig.ClassTypeSignature; +import dev.denwav.hypo.types.sig.ReferenceTypeSignature; +import dev.denwav.hypo.types.sig.TypeSignature; +import dev.denwav.hypo.types.sig.param.TypeArgument; +import java.util.List; +import java.util.function.Predicate; +import org.jetbrains.annotations.NotNull; + +/** + * Factory for {@link TypePattern} for {@link TypeDescriptor} and {@link TypeSignature} type objects. + */ +public final class TypePatterns { + + private TypePatterns() {} + + /* + * TypeDescriptor & TypeSignature + */ + + /** + * A type pattern where the type object is a {@link ValueType}. + * @return The type pattern. + */ + public static @NotNull TypePattern isType() { + return (ctx, t) -> t instanceof ValueType; + } + + /** + * A type pattern where the type object is a {@link ValueType} that matches the given predicate. + * + * @param predicate The predicate to test against the {@link ValueType}. + * @return The type pattern. + */ + public static @NotNull TypePattern isType(final Predicate predicate) { + return (ctx, t) -> t instanceof final ValueType v && predicate.test(v); + } + + /** + * A type pattern where the type object is a {@link ClassType}. + * @return The type pattern. + */ + public static @NotNull TypePattern isClass() { + return (ctx, t) -> t instanceof ClassType; + } + + /** + * A type pattern where the type object is a {@link ClassType} that matches the given predicate. + * + * @param predicate The predicate to test against the {@link ClassType}. + * @return The type pattern. + */ + public static @NotNull TypePattern isClass(final @NotNull Predicate predicate) { + return (ctx, t) -> t instanceof final ClassType c && predicate.test(c); + } + + /** + * A type pattern where the type object is a {@link ClassType} and the {@link ClassType#getName()} matches the given + * {@link Predicate predicate}. + * + * @param name The predicate the class name must match. + * @return The type pattern. + */ + public static @NotNull TypePattern isClassNamed(final @NotNull Predicate name) { + return (ctx, t) -> t instanceof final ClassType c && name.test(c.getName()); + } + /** + * A type pattern where the type object is a {@link ClassType} and the {@link ClassType#getName()} matches the given + * class name. + * + * @param name The text the class name must match. + * @return The type pattern. + */ + public static @NotNull TypePattern isClassNamed(final @NotNull String name) { + return isClassNamed(name::equals); + } + + /** + * A type pattern where the type object is a reference type ({@link ClassType}, {@link ArrayType}, or + * {@link ReferenceTypeSignature}). + * @return The type pattern. + */ + public static @NotNull TypePattern isReferenceType() { + return (ctx, t) -> t instanceof ClassType || t instanceof ArrayType || t instanceof ReferenceTypeSignature; + } + + /** + * A type pattern where the type object is a primitive type (does not include {@code void}). + * @return The type pattern. + */ + public static @NotNull TypePattern isPrimitive() { + return (ctx, t) -> t instanceof PrimitiveType; + } + + /** + * A type pattern where the type object is {@code void}. + * @return The type pattern. + */ + public static @NotNull TypePattern isVoid() { + return (ctx, t) -> t == VoidType.INSTANCE; + } + + /** + * A type pattern where the type object is {@code char} (not the wrapper type). + * @return The type pattern. + */ + public static @NotNull TypePattern isChar() { + return (ctx, t) -> t == PrimitiveType.CHAR; + } + + /** + * A type pattern where the type object is {@code byte} (not the wrapper type). + * @return The type pattern. + */ + public static @NotNull TypePattern isByte() { + return (ctx, t) -> t == PrimitiveType.BYTE; + } + + /** + * A type pattern where the type object is {@code short} (not the wrapper type). + * @return The type pattern. + */ + public static @NotNull TypePattern isShort() { + return (ctx, t) -> t == PrimitiveType.SHORT; + } + + /** + * A type pattern where the type object is {@code int} (not the wrapper type). + * @return The type pattern. + */ + public static @NotNull TypePattern isInt() { + return (ctx, t) -> t == PrimitiveType.INT; + } + + /** + * A type pattern where the type object is {@code long} (not the wrapper type). + * @return The type pattern. + */ + public static @NotNull TypePattern isLong() { + return (ctx, t) -> t == PrimitiveType.LONG; + } + + /** + * A type pattern where the type object is {@code float} (not the wrapper type). + * @return The type pattern. + */ + public static @NotNull TypePattern isFloat() { + return (ctx, t) -> t == PrimitiveType.FLOAT; + } + + /** + * A type pattern where the type object is {@code double} (not the wrapper type). + * @return The type pattern. + */ + public static @NotNull TypePattern isDouble() { + return (ctx, t) -> t == PrimitiveType.DOUBLE; + } + + /** + * A type pattern where the type object is {@code boolean} (not the wrapper type). + * @return The type pattern. + */ + public static @NotNull TypePattern isBoolean() { + return (ctx, t) -> t == PrimitiveType.BOOLEAN; + } + + /** + * A type pattern where the type object is a primitive integer type: {@code byte}, {@code short}, {@code int}, or + * {@code long} (not the wrapper types). + * @return The type pattern. + */ + public static @NotNull TypePattern isIntegerType() { + return (ctx, t) -> { + if (t instanceof final PrimitiveType p) { + return switch (p) { + case BYTE, SHORT, INT, LONG -> true; + default -> false; + }; + } + return false; + }; + } + + /** + * A type pattern where the type object is a primitive floating point type: {@code float} or {@code double} + * (not the wrapper types). + * @return The type pattern. + */ + public static @NotNull TypePattern isFloatingPointType() { + return (ctx, t) -> { + if (t instanceof final PrimitiveType p) { + return switch (p) { + case FLOAT, DOUBLE -> true; + default -> false; + }; + } + return false; + }; + } + + /** + * A type pattern where the type object is a type that is 2 slots wide in the LVT: {@code long} or {@code double}. + * All other types, including reference types, are only 1 slot wide, which includes the wrapper types for the 2 + * matching primitive types. + * @return The type pattern. + */ + public static @NotNull TypePattern isWide() { + return (ctx, t) -> { + if (t instanceof final PrimitiveType p) { + return switch (p) { + case LONG, DOUBLE -> true; + default -> false; + }; + } + return false; + }; + } + + /** + * A type pattern where the type object is allowed to be assigned to a field or a variable. Type objects which are + * not assignable are, for example, {@code void}, method descriptors, and class signatures. + * @return The type pattern. + */ + public static @NotNull TypePattern isAssignable() { + return (ctx, t) -> t instanceof ValueType && t != VoidType.INSTANCE; + } + + /** + * A type pattern where the type object is allowed to be a method's return type. Type objects which are not + * returnable are, for example, method descriptors and class signatures. + * @return The type pattern. + */ + public static @NotNull TypePattern isReturnable() { + return (ctx, t) -> t instanceof ValueType; + } + + /** + * A type pattern where the type object is an {@link ArrayType}. + * @return The type pattern. + */ + public static @NotNull TypePattern isArray() { + return (ctx, t) -> t instanceof ArrayType; + } + + /** + * A type pattern where the type object is an {@link ArrayType}, and the component type for the array matches the + * given pattern. + * + * @param baseType A pattern to match against the component type of the array. + * @return The type pattern. + */ + public static @NotNull TypePattern isArray(final @NotNull TypePattern baseType) { + return (ctx, t) -> t instanceof final ArrayType a && baseType.test(ctx, a.getBaseType()); + } + + /** + * A type pattern where the type object is an {@link ArrayType}, and the array's dimension matches the given value. + * + * @param dimension The dimension the array type must match. + * @return The type pattern. + */ + public static @NotNull TypePattern isArray(final int dimension) { + return (ctx, t) -> t instanceof final ArrayType a && a.getDimension() == dimension; + } + + /** + * A type pattern where the type object is an {@link ArrayType}, the array's dimension matches the given value, and + * the component type for the array matches the given pattern. + * + * @param dimension The dimension the array type must match. + * @param baseType A pattern to match against the component type of the array. + * @return The type pattern. + */ + public static @NotNull TypePattern isArray(final int dimension, final @NotNull TypePattern baseType) { + return (ctx, t) -> + t instanceof final ArrayType a && a.getDimension() == dimension && baseType.test(ctx, a.getBaseType()); + } + + /** + * A type pattern where the type object is an {@link ArrayType} that matches the given predicate. + * + * @param predicate The predicate to test against the {@link ArrayType}. + * @return The type pattern. + */ + public static @NotNull TypePattern isArray(final @NotNull Predicate predicate) { + return (ctx, t) -> t instanceof final ArrayType a && predicate.test(a); + } + + /* + * TypeDescriptor + */ + + /** + * A type pattern where the type object is a {@link TypeDescriptor}. + * @return The type pattern. + */ + public static @NotNull TypePattern isTypeDescriptor() { + return (ctx, t) -> t instanceof TypeDescriptor; + } + + /** + * A type pattern where the type object is a {@link TypeDescriptor} that matches the given predicate. + * + * @param predicate The predicate to test against the {@link TypeDescriptor}. + * @return The type pattern. + */ + public static @NotNull TypePattern isTypeDescriptor(final @NotNull Predicate predicate) { + return (ctx, t) -> t instanceof final TypeDescriptor d && predicate.test(d); + } + + /* + * TypeSignature + */ + + /** + * A type pattern where the type object is a {@link TypeSignature}. + * @return The type pattern. + */ + public static @NotNull TypePattern isTypeSignature() { + return (ctx, t) -> t instanceof TypeSignature; + } + + /** + * A type pattern where the type object is a {@link TypeSignature} that matches the given predicate. + * + * @param predicate The predicate to test against the {@link TypeSignature}. + * @return The type pattern. + */ + public static @NotNull TypePattern isTypeSignature(final @NotNull Predicate predicate) { + return (ctx, t) -> t instanceof final TypeSignature d && predicate.test(d); + } + + /** + * A type pattern where the type object is a {@link ClassTypeSignature} that also has at least one type argument. + * @return The type pattern. + */ + public static @NotNull TypePattern hasTypeArguments() { + return (ctx, t) -> t instanceof final ClassTypeSignature c && !c.getTypeArguments().isEmpty(); + } + + /** + * A type pattern where the type object is a {@link ClassTypeSignature} that also has no type arguments. + * @return The type pattern. + */ + // TODO clarify documentation on what this covers + public static @NotNull TypePattern hasNoTypeArguments() { + return (ctx, t) -> { + if (t instanceof final ClassTypeSignature c) { + return c.getTypeArguments().isEmpty(); + } else { + return t instanceof ClassTypeDescriptor; + } + }; + } + + /** + * A type pattern where the type object is a {@link ClassTypeSignature} that also has exactly as many type arguments + * as the number of patterns given, and each pattern matches the corresponding type argument. + * + * @param typeParameters Array of type patterns which must match 1:1 with the type arguments of the + * {@link ClassTypeSignature}. + * @return The type pattern. + */ + public static @NotNull TypePattern hasTypeArguments(final @NotNull TypePattern... typeParameters) { + return (ctx, t) -> { + if (!(t instanceof final ClassTypeSignature sig)) { + return false; + } + final List args = sig.getTypeArguments(); + if (args.size() != typeParameters.length) { + return false; + } + for (int i = 0; i < args.size(); i++) { + final TypeArgument arg = args.get(i); + if (!typeParameters[i].test(ctx, arg)) { + return false; + } + } + return true; + }; + } + + /** + * A type pattern where the type object is a {@link ClassTypeSignature} that is owned by a + * {@link ClassTypeSignature} that satisfies the given type pattern. + * + * @param owner The pattern which must match the owner of the {@link ClassTypeSignature}. + * @return The type pattern. + * @see ClassTypeSignature#getOwnerClass() + */ + public static @NotNull TypePattern ownerIs(final @NotNull TypePattern owner) { + return (ctx, t) -> { + if (t instanceof final ClassTypeSignature c) { + final ClassTypeSignature parentSig = c.getOwnerClass(); + if (parentSig == null) { + return false; + } + return owner.test(ctx, parentSig); + } + return false; + }; + } +} diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/pattern/TypeVariablePatterns.java b/hypo-types/src/main/java/dev/denwav/hypo/types/pattern/TypeVariablePatterns.java new file mode 100644 index 0000000..e6e7b3e --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/pattern/TypeVariablePatterns.java @@ -0,0 +1,99 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types.pattern; + +import dev.denwav.hypo.types.sig.param.TypeParameter; +import dev.denwav.hypo.types.sig.param.TypeVariable; +import java.util.function.Predicate; +import org.jetbrains.annotations.NotNull; + +/** + * Factory for {@link TypePattern} for {@link TypeVariable} type objects. + */ +public final class TypeVariablePatterns { + + private TypeVariablePatterns() {} + + /** + * A type pattern where the type object is a {@link TypeVariable}. + * @return The type pattern. + */ + public static @NotNull TypePattern isTypeVariable() { + return (ctx, t) -> t instanceof TypeVariable; + } + + /** + * A type pattern where the type object is a {@link TypeVariable} that matches the given predicate. + * + * @param predicate The predicate to test against the {@link TypeVariable}. + * @return The type pattern. + */ + public static @NotNull TypePattern isTypeVariable(final @NotNull Predicate predicate) { + return (ctx, t) -> t instanceof final TypeVariable v && predicate.test(v); + } + + /** + * A type pattern where the type object is a {@link TypeVariable} that is {@link TypeVariable#isUnbound()} bound. + * @return The type pattern. + */ + public static @NotNull TypePattern isBound() { + return (ctx, t) -> t instanceof final TypeVariable v && !v.isUnbound(); + } + + /** + * A type pattern where the type object is a {@link TypeVariable} where the + * {@link TypeVariable#getDefinition() definition} matches the given predicate. + * + * @param predicate The predicate to test against the {@link TypeVariable}. + * @return The type pattern. + */ + public static @NotNull TypePattern isBound(final @NotNull Predicate predicate) { + return (ctx, t) -> t instanceof final TypeVariable v && predicate.test(v.getDefinition()); + } + + /** + * A type pattern where the type object is a {@link TypeVariable} where the + * {@link TypeVariable#getDefinition() definition} satisfies the given {@link TypePattern}. + * + * @param definition The predicate to test against the {@link TypeVariable}. + * @return The type pattern. + */ + public static @NotNull TypePattern isBound(final @NotNull TypePattern definition) { + return (ctx, t) -> t instanceof TypeVariable && definition.test(ctx, t); + } + + /** + * A type pattern where the type object is a {@link TypeVariable} that is {@link TypeVariable#isUnbound()} unbound. + * @return The type pattern. + */ + public static @NotNull TypePattern isUnbound() { + return (ctx, t) -> t instanceof final TypeVariable v && v.isUnbound(); + } + + /** + * A type pattern where the type object is a {@link TypeVariable.Unbound unbound TypeVariable} that matches the + * given predicate. + * + * @param predicate The predicate to test against the {@link TypeVariable.Unbound unbound TypeVariable}. + * @return The type pattern. + */ + public static @NotNull TypePattern isUnbound(final @NotNull Predicate predicate) { + return (ctx, t) -> t instanceof final TypeVariable.Unbound u && predicate.test(u); + } +} diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/sig/ArrayTypeSignature.java b/hypo-types/src/main/java/dev/denwav/hypo/types/sig/ArrayTypeSignature.java new file mode 100644 index 0000000..0f2324e --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/sig/ArrayTypeSignature.java @@ -0,0 +1,142 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types.sig; + +import com.google.errorprone.annotations.Immutable; +import dev.denwav.hypo.types.intern.Intern; +import dev.denwav.hypo.types.TypeVariableBinder; +import dev.denwav.hypo.types.desc.ArrayTypeDescriptor; +import dev.denwav.hypo.types.kind.ArrayType; +import java.lang.reflect.GenericArrayType; +import java.util.Objects; +import org.jetbrains.annotations.NotNull; + +/** + * An array type signature. Array type signatures consist of a {@link #getDimension() dimension} and a + * {@link #getBaseType() base type}. As the base type is a {@link TypeSignature} it may contain generic type + * information. + */ +@Immutable +public final class ArrayTypeSignature + extends Intern + implements ReferenceTypeSignature, ArrayType { + + private final int dimension; + private final @NotNull TypeSignature baseType; + + /** + * Create a new {@link ArrayTypeSignature}. + * @param dimension The dimension for the array. + * @param baseType The base type for the array. + * @return A new {@link ArrayTypeSignature}. + */ + public static @NotNull ArrayTypeSignature of(final int dimension, final @NotNull TypeSignature baseType) { + return new ArrayTypeSignature(dimension, baseType).intern(); + } + + /** + * Create an {@link ArrayTypeSignature} matching the given {@link GenericArrayType}. + * @param arrayType The array type. + * @return A new {@link ArrayTypeSignature} matching the given {@link GenericArrayType}. + */ + public static @NotNull ArrayTypeSignature of(final @NotNull GenericArrayType arrayType) { + GenericArrayType componentType = arrayType; + int dim = 1; + while (componentType.getGenericComponentType() instanceof final GenericArrayType nextArrayType) { + componentType = nextArrayType; + dim++; + } + + return ArrayTypeSignature.of(dim, TypeSignature.of(componentType.getGenericComponentType())); + } + + private ArrayTypeSignature(final int dimension, final @NotNull TypeSignature baseType) { + this.dimension = dimension; + this.baseType = baseType; + } + + @Override + public void asReadable(final @NotNull StringBuilder sb) { + this.baseType.asReadable(sb); + sb.ensureCapacity(sb.length() + 2 * this.dimension); + //noinspection StringRepeatCanBeUsed + for (int i = 0; i < this.dimension; i++) { + sb.append("[]"); + } + } + + @Override + public void asInternal(final @NotNull StringBuilder sb) { + this.asInternal(sb, false); + } + + @Override + public void asInternal(final @NotNull StringBuilder sb, final boolean withBindKey) { + sb.ensureCapacity(sb.length() + this.dimension); + //noinspection StringRepeatCanBeUsed + for (int i = 0; i < this.dimension; i++) { + sb.append("["); + } + this.baseType.asInternal(sb, withBindKey); + } + + @Override + public @NotNull ArrayTypeDescriptor asDescriptor() { + return ArrayTypeDescriptor.of(this.dimension, this.baseType.asDescriptor()); + } + + @Override + public @NotNull ArrayTypeSignature bind(final @NotNull TypeVariableBinder binder) { + return ArrayTypeSignature.of(this.dimension, this.baseType.bind(binder)); + } + + @Override + public boolean isUnbound() { + return this.baseType.isUnbound(); + } + + @Override + public int getDimension() { + return this.dimension; + } + + @Override + public @NotNull TypeSignature getBaseType() { + return this.baseType; + } + + @Override + public boolean equals(final Object o) { + if (!(o instanceof final ArrayTypeSignature that)) { + return false; + } + return this.dimension == that.dimension + && Objects.equals(this.baseType, that.baseType); + } + + @Override + public int hashCode() { + return Objects.hash(this.dimension, this.baseType); + } + + @Override + public String toString() { + return this.asReadable(); + } +} diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/sig/ClassSignature.java b/hypo-types/src/main/java/dev/denwav/hypo/types/sig/ClassSignature.java new file mode 100644 index 0000000..16b832f --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/sig/ClassSignature.java @@ -0,0 +1,244 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types.sig; + +import com.google.errorprone.annotations.Immutable; +import dev.denwav.hypo.types.intern.Intern; +import dev.denwav.hypo.types.TypeBindable; +import dev.denwav.hypo.types.TypeRepresentable; +import dev.denwav.hypo.types.TypeVariableBinder; +import dev.denwav.hypo.types.parsing.JvmTypeParseFailureException; +import dev.denwav.hypo.types.parsing.JvmTypeParser; +import dev.denwav.hypo.types.sig.param.TypeParameter; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import org.jetbrains.annotations.NotNull; + +/** + * A JVM class signature. + * + *

A class signature defines the type parameters of a class declaration, as well as the generic super class and super + * interface definitions. Every part after the class name and before the opening brace of a class declaration is part of + * the class signature. + * + *

For example, the following class signature defines a class named {@code Foo} with an {@code extends} and + * {@code implements} clauses: + * + *


+ *     class Foo<T extends Number> extends List<T> implements Serializable {
+ * 
+ * + *

The {@link #getTypeParameters() type parameters} of a class signature match up with the + * {@link ClassTypeSignature#getTypeArguments() type arguemnts} of corresponding {@link ClassTypeSignature class type + * signatures} for the same type. + */ +@Immutable +public final class ClassSignature extends Intern implements Signature, TypeBindable, TypeRepresentable, TypeParameterHolder { + + @SuppressWarnings("Immutable") + private final @NotNull List typeParameters; + private final @NotNull ClassTypeSignature superClass; + @SuppressWarnings("Immutable") + private final @NotNull List superInterfaces; + + /** + * Create a new {@link ClassSignature}. + * @param typeParameters The class's {@link TypeParameter type parameters}. + * @param superClass The class's generic super class. + * @param superInterfaces The class's generic super interfaces. + * @return A new {@link ClassSignature}. + */ + public static @NotNull ClassSignature of( + final @NotNull List typeParameters, + final @NotNull ClassTypeSignature superClass, + final @NotNull List superInterfaces + ) { + return new ClassSignature(typeParameters, superClass, superInterfaces).intern(); + } + + /** + * Parse the given internal JVM class signature text into a new {@link ClassSignature}. + * + *

This method throws {@link JvmTypeParseFailureException} if the given text is not a valid class signature. Use + * {@link JvmTypeParser#parseClassSignature(String, int)} if you prefer to have {@code null} be returned instead. + * + * @param text The text to parse. + * @return The {@link ClassSignature}. + * @throws JvmTypeParseFailureException If the given text does not represent a valid JVM class signature. + */ + public static @NotNull ClassSignature parse(final String text) throws JvmTypeParseFailureException { + return parse(text, 0); + } + + /** + * Parse the given internal JVM class signature text into a new {@link ClassSignature}. + * + *

This method throws {@link JvmTypeParseFailureException} if the given text is not a valid class signature. Use + * {@link JvmTypeParser#parseClassSignature(String, int)} if you prefer to have {@code null} be returned instead. + * + * @param text The text to parse. + * @param from The index to start parsing from. + * @return The {@link ClassSignature}. + * @throws JvmTypeParseFailureException If the given text does not represent a valid JVM class signature. + */ + + public static @NotNull ClassSignature parse(final String text, final int from) throws JvmTypeParseFailureException { + if (text.length() > 1 && from == 0) { + final ClassSignature r = Intern.tryFind(ClassSignature.class, text); + if (r != null) { + return r; + } + } + final ClassSignature result = JvmTypeParser.parseClassSignature(text, from); + if (result == null) { + throw new JvmTypeParseFailureException("text is not a valid class signature: " + text.substring(from)); + } + return result; + } + + private ClassSignature( + final @NotNull List typeParameters, + final @NotNull ClassTypeSignature superClass, + final @NotNull List superInterfaces + ) { + this.typeParameters = List.copyOf(typeParameters); + this.superClass = superClass; + this.superInterfaces = List.copyOf(superInterfaces); + } + + @Override + public void asReadable(final @NotNull StringBuilder sb) { + if (!this.typeParameters.isEmpty()) { + sb.append('<'); + for (int i = 0; i < this.typeParameters.size(); i++) { + if (i > 0) { + sb.append(", "); + } + this.typeParameters.get(i).asReadable(sb); + } + sb.append("> "); + } + + sb.append("extends "); + this.superClass.asReadable(sb); + + if (!this.superInterfaces.isEmpty()) { + sb.append(" implements "); + } + for (int i = 0; i < this.superInterfaces.size(); i++) { + if (i > 0) { + sb.append(", "); + } + this.superInterfaces.get(i).asReadable(sb); + } + } + + @Override + public void asInternal(final @NotNull StringBuilder sb) { + this.asInternal(sb, false); + } + + @Override + public void asInternal(final @NotNull StringBuilder sb, final boolean withBindKey) { + if (!this.typeParameters.isEmpty()) { + sb.append('<'); + for (final TypeParameter param : this.typeParameters) { + param.asInternal(sb, withBindKey); + } + sb.append('>'); + } + this.superClass.asInternal(sb, withBindKey); + for (final TypeSignature superInt : this.superInterfaces) { + superInt.asInternal(sb, withBindKey); + } + } + + @Override + public @NotNull ClassSignature bind(final @NotNull TypeVariableBinder binder) { + return ClassSignature.of( + this.typeParameters.stream() + .map(t -> t.bind(binder)) + .collect(Collectors.toList()), + this.superClass.bind(binder), + this.superInterfaces.stream() + .map(t -> t.bind(binder)) + .collect(Collectors.toList()) + ); + } + + @Override + public boolean isUnbound() { + for (final TypeParameter p : this.typeParameters) { + if (p.isUnbound()) { + return true; + } + } + if (this.superClass.isUnbound()) { + return true; + } + for (final ClassTypeSignature c : this.superInterfaces) { + if (c.isUnbound()) { + return true; + } + } + return false; + } + + @Override + public @NotNull List getTypeParameters() { + return this.typeParameters; + } + + /** + * Get the generic super class of this class. + * @return The generic super class of this class. + */ + public @NotNull ClassTypeSignature getSuperClass() { + return this.superClass; + } + + /** + * Get the generic super interfaces of this class. + * @return The generic super interfaces of this class. + */ + public @NotNull List getSuperInterfaces() { + return this.superInterfaces; + } + + @Override + public boolean equals(final Object o) { + if (!(o instanceof final ClassSignature that)) { + return false; + } + return Objects.equals(this.typeParameters, that.typeParameters) + && Objects.equals(this.superClass, that.superClass) + && Objects.equals(this.superInterfaces, that.superInterfaces); + } + + @Override + public int hashCode() { + return Objects.hash(this.typeParameters, this.superClass, this.superInterfaces); + } + + @Override + public String toString() { + return this.asReadable(); + } +} diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/sig/ClassTypeSignature.java b/hypo-types/src/main/java/dev/denwav/hypo/types/sig/ClassTypeSignature.java new file mode 100644 index 0000000..78cdaa1 --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/sig/ClassTypeSignature.java @@ -0,0 +1,282 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types.sig; + +import com.google.errorprone.annotations.Immutable; +import dev.denwav.hypo.types.HypoTypesUtil; +import dev.denwav.hypo.types.intern.Intern; +import dev.denwav.hypo.types.TypeVariableBinder; +import dev.denwav.hypo.types.desc.ClassTypeDescriptor; +import dev.denwav.hypo.types.desc.TypeDescriptor; +import dev.denwav.hypo.types.kind.ClassType; +import dev.denwav.hypo.types.sig.param.TypeArgument; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * A {@link ReferenceTypeSignature} representing a class type. Class type signatures can contain type arguments, + * which correspond to the {@link ClassSignature#getTypeParameters() type parameters} of the class's signature. + * + *

The class {@link #getName() name} will typically be the fully qualified class name, except when this class + * type signature also contains an {@link #getOwnerClass() owner}. In that case, the name of this class will only + * contain the simple name of this class, which is appended after the fully qualified name of the owner class. + * + *

Owner classes in a class type signature occur when a non-static class is nested within another generic class. For + * example, the {@code Bar} class here is a nested class of the {@code Foo} class: + * + *


+ *     package com.example;
+ *
+ *     class Foo<T> {
+ *         class Bar<U> { ... }
+ *     }
+ *
+ *     Foo<String>.Bar<Integer> baz;
+ * 
+ * + *

In this scenario, the fully qualified name of the {@code baz} type is {@code com.example.Foo.Bar}, and the fully + * qualified type signature is com/example/Foo<String>.Bar<Integer>. + * + *

Note that generic nested static classes do not have an owner, since they are static, so they do not inherit any + * generic type information from the nest-host class. + */ +@Immutable +public final class ClassTypeSignature + extends Intern + implements ReferenceTypeSignature, ClassType, ThrowsSignature { + + private final @Nullable ClassTypeSignature ownerClass; + private final @NotNull String name; + @SuppressWarnings("Immutable") + private final @NotNull List typeArguments; + + /** + * Create a new {@link ClassTypeSignature} from the given {@code name}. + * @param name The name of the class. + * @return A new {@link ClassTypeSignature}. + */ + public static @NotNull ClassTypeSignature of( + final @NotNull String name + ) { + return ClassTypeSignature.of(null, name, null); + } + + /** + * Create a new {@link ClassTypeSignature} from the given {@code name} with the given {@code typeArguments}. + * @param name The name of the class. + * @param typeArguments The type arguments. + * @return A new {@link ClassTypeSignature}. + */ + public static @NotNull ClassTypeSignature of( + final @NotNull String name, + final @Nullable List typeArguments + ) { + return ClassTypeSignature.of(null, name, typeArguments); + } + + /** + * Create a new {@link ClassTypeSignature} from the given {@code ownerClass}, {@code name}, and + * {@code typeArguments}. + * + * @param ownerClass The owner class. + * @param name The name of the class. + * @param typeArguments The type arguments. + * @return A new {@link ClassTypeSignature}. + */ + public static @NotNull ClassTypeSignature of( + final @Nullable ClassTypeSignature ownerClass, + final @NotNull String name, + final @Nullable List typeArguments + ) { + return new ClassTypeSignature(ownerClass, HypoTypesUtil.normalizedClassName(name), typeArguments).intern(); + } + + /** + * Create a {@link ClassTypeSignature} matching the given {@link ParameterizedType}. + * @param parameterizedType The type. + * @return A new {@link ClassTypeSignature} matching the given {@link ParameterizedType}. + */ + public static @NotNull ClassTypeSignature of(final @NotNull ParameterizedType parameterizedType) { + final Type owner = parameterizedType.getOwnerType(); + final ClassTypeSignature ownerType = owner != null ? (ClassTypeSignature) TypeSignature.of(owner) : null; + final ClassTypeSignature rawType = (ClassTypeSignature) TypeSignature.of(parameterizedType.getRawType()); + + final String className; + if (ownerType == null) { + className = rawType.getName(); + } else { + final String rawName = rawType.getName(); + className = rawName.substring(rawName.lastIndexOf('.') + 1); + } + + final Type[] actualTypeArgs = parameterizedType.getActualTypeArguments(); + final ArrayList typeArgs = new ArrayList<>(actualTypeArgs.length); + for (final Type actualTypeArg : actualTypeArgs) { + typeArgs.add((TypeArgument) TypeSignature.of(actualTypeArg)); + } + + return ClassTypeSignature.of(ownerType, className, typeArgs); + } + + private ClassTypeSignature( + final @Nullable ClassTypeSignature ownerClass, + final @NotNull String name, + final @Nullable List typeArguments + ) { + this.ownerClass = ownerClass; + this.name = name; + if (typeArguments == null) { + this.typeArguments = Collections.emptyList(); + } else { + this.typeArguments = List.copyOf(typeArguments); + } + } + + @Override + public void asReadable(final @NotNull StringBuilder sb) { + if (this.ownerClass != null) { + this.ownerClass.asReadable(sb); + sb.append('.'); + } + + sb.append(this.name.replace('/', '.')); + if (!this.typeArguments.isEmpty()) { + sb.append("<"); + for (int i = 0; i < this.typeArguments.size(); i++) { + if (i > 0) { + sb.append(", "); + } + this.typeArguments.get(i).asReadable(sb); + } + sb.append('>'); + } + } + + @Override + public void asInternal(final @NotNull StringBuilder sb) { + this.asInternal(sb, false); + } + + @Override + public void asInternal(final @NotNull StringBuilder sb, final boolean withBindKey) { + if (this.ownerClass != null) { + this.ownerClass.asInternal(sb, withBindKey); + // Owner class will include a `;` at the end + sb.setLength(sb.length() - 1); + sb.append('.'); + } else { + sb.append('L'); + } + + sb.append(this.name); + if (!this.typeArguments.isEmpty()) { + sb.append("<"); + for (final TypeArgument typeArg : this.typeArguments) { + typeArg.asInternal(sb, withBindKey); + } + sb.append('>'); + } + + sb.append(';'); + } + + @Override + public @NotNull TypeDescriptor asDescriptor() { + return ClassTypeDescriptor.of(this.name); + } + + @Override + public @NotNull ClassTypeSignature bind(final @NotNull TypeVariableBinder binder) { + ClassTypeSignature newOwner; + if (this.ownerClass == null) { + newOwner = null; + } else { + newOwner = this.ownerClass.bind(binder); + } + return ClassTypeSignature.of( + newOwner, + this.name, + this.typeArguments.stream() + .map(t -> t.bind(binder)) + .collect(Collectors.toList()) + ); + } + + @Override + public boolean isUnbound() { + if (this.ownerClass != null && this.ownerClass.isUnbound()) { + return true; + } + for (final TypeArgument t : this.typeArguments) { + if (t.isUnbound()) { + return true; + } + } + return false; + } + + /** + * Get the owner class of this type, if present. Returns {@code null} if this type does not have an owner. + * @return The owner of this type. + */ + public @Nullable ClassTypeSignature getOwnerClass() { + return this.ownerClass; + } + + @Override + public @NotNull String getName() { + return this.name; + } + + /** + * Get the type arguments of this type. If this type has no type arguments, an empty list is returned. The returned + * list is immutable. + * @return The type arguments of this type. + */ + public @NotNull List getTypeArguments() { + return this.typeArguments; + } + + @Override + public boolean equals(final Object o) { + if (!(o instanceof final ClassTypeSignature that)) { + return false; + } + return Objects.equals(this.ownerClass, that.ownerClass) + && Objects.equals(this.name, that.name) + && Objects.equals(this.typeArguments, that.typeArguments); + } + + @Override + public int hashCode() { + return Objects.hash(this.ownerClass, this.name, this.typeArguments); + } + + @Override + public String toString() { + return this.asReadable(); + } +} diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/sig/MethodSignature.java b/hypo-types/src/main/java/dev/denwav/hypo/types/sig/MethodSignature.java new file mode 100644 index 0000000..fa1ced1 --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/sig/MethodSignature.java @@ -0,0 +1,306 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types.sig; + +import com.google.errorprone.annotations.Immutable; +import dev.denwav.hypo.types.TypeBindable; +import dev.denwav.hypo.types.kind.MethodType; +import dev.denwav.hypo.types.intern.Intern; +import dev.denwav.hypo.types.TypeRepresentable; +import dev.denwav.hypo.types.TypeVariableBinder; +import dev.denwav.hypo.types.desc.MethodDescriptor; +import dev.denwav.hypo.types.desc.TypeDescriptor; +import dev.denwav.hypo.types.parsing.JvmTypeParseFailureException; +import dev.denwav.hypo.types.parsing.JvmTypeParser; +import dev.denwav.hypo.types.sig.param.TypeParameter; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import org.jetbrains.annotations.NotNull; + +/** + * A JVM method signature. + * + *

A method signature contains {@link TypeParameterHolder type paramters}, {@link TypeSignature potentially generic} + * parameters, a return type (also a {@link TypeSignature}), and a {@link ThrowsSignature throws signature}, also + * potentially generic. + */ +@Immutable +public final class MethodSignature + extends Intern + implements MethodType, Signature, TypeRepresentable, TypeBindable, TypeParameterHolder { + + @SuppressWarnings("Immutable") + private final @NotNull List typeParameters; + @SuppressWarnings("Immutable") + private final @NotNull List parameters; + private final @NotNull TypeSignature returnType; + @SuppressWarnings("Immutable") + private final @NotNull List throwsSignatures; + + /** + * Create a new {@link MethodSignature}. + * @param typeParameters The method's {@link TypeParameter type parameters}. + * @param parameters The method's parameters. + * @param returnType The method's return type. + * @param throwsSignatures The method's throws signature. + * @return A new {@link MethodSignature}. + */ + public static @NotNull MethodSignature of( + final @NotNull List typeParameters, + final @NotNull List parameters, + final @NotNull TypeSignature returnType, + final @NotNull List throwsSignatures + ) { + return new MethodSignature(typeParameters, parameters, returnType, throwsSignatures).intern(); + } + + private MethodSignature( + final @NotNull List typeParameters, + final @NotNull List parameters, + final @NotNull TypeSignature returnType, + final @NotNull List throwsSignatures + ) { + this.typeParameters = List.copyOf(typeParameters); + this.parameters = List.copyOf(parameters); + this.returnType = returnType; + this.throwsSignatures = List.copyOf(throwsSignatures); + } + + /** + * Parse the given internal JVM method signature text into a new {@link MethodSignature}. + * + *

This method throws {@link JvmTypeParseFailureException} if the given text is not a valid method signature. Use + * {@link JvmTypeParser#parseMethodSignature(String, int)} if you prefer to have {@code null} be returned instead. + * + * @param text The text to parse. + * @return The {@link MethodSignature}. + * @throws JvmTypeParseFailureException If the given text does not represent a valid JVM method signature. + */ + public static @NotNull MethodSignature parse(final @NotNull String text) throws JvmTypeParseFailureException { + return parse(text, 0); + } + + /** + * Parse the given internal JVM method signature text into a new {@link MethodSignature}. + * + *

This method throws {@link JvmTypeParseFailureException} if the given text is not a valid method signature. Use + * {@link JvmTypeParser#parseMethodSignature(String, int)} if you prefer to have {@code null} be returned instead. + * + * @param text The text to parse. + * @param from The index to start parsing from. + * @return The {@link MethodSignature}. + * @throws JvmTypeParseFailureException If the given text does not represent a valid JVM method signature. + */ + public static @NotNull MethodSignature parse(final @NotNull String text, final int from) throws JvmTypeParseFailureException { + if (text.length() > 1 && from == 0) { + final MethodSignature r = Intern.tryFind(MethodSignature.class, text); + if (r != null) { + return r; + } + } + final MethodSignature result = JvmTypeParser.parseMethodSignature(text, from); + if (result == null) { + throw new JvmTypeParseFailureException("text is not a valid method signature: " + text.substring(from)); + } + return result; + } + + @Override + public @NotNull MethodSignature bind(final @NotNull TypeVariableBinder binder) { + final List newTypeParams = this.typeParameters.stream() + .map(t -> t.bind(binder)) + .collect(Collectors.toList()); + + final List newParams = this.parameters.stream() + .map(t -> t.bind(binder)) + .collect(Collectors.toList()); + + final TypeSignature newReturnType = this.returnType.bind(binder); + + final List newThrows = this.throwsSignatures.stream() + .map(t -> t.bind(binder)) + .collect(Collectors.toList()); + + return MethodSignature.of(newTypeParams, newParams, newReturnType, newThrows); + } + + @Override + public boolean isUnbound() { + for (final TypeParameter t : this.typeParameters) { + if (t.isUnbound()) { + return true; + } + } + for (final TypeSignature p : this.parameters) { + if (p.isUnbound()) { + return true; + } + } + if (this.returnType.isUnbound()) { + return true; + } + for (final ThrowsSignature t : this.throwsSignatures) { + if (t.isUnbound()) { + return true; + } + } + return false; + } + + @Override + public void asReadable(final @NotNull StringBuilder sb) { + if (!this.typeParameters.isEmpty()) { + sb.append('<'); + for (int i = 0; i < this.typeParameters.size(); i++) { + if (i > 0) { + sb.append(", "); + } + this.typeParameters.get(i).asReadable(sb); + } + sb.append("> "); + } + + this.returnType.asReadable(sb); + sb.append(" ("); + + for (int i = 0; i < this.parameters.size(); i++) { + if (i > 0) { + sb.append(", "); + } + this.parameters.get(i).asReadable(sb); + } + + sb.append(')'); + + if (!this.throwsSignatures.isEmpty()) { + sb.append(" throws "); + + for (int i = 0; i < this.throwsSignatures.size(); i++) { + if (i > 0) { + sb.append(", "); + } + this.throwsSignatures.get(i).asReadable(sb); + } + } + } + + @Override + public void asInternal(final @NotNull StringBuilder sb) { + this.asInternal(sb, false); + } + + @Override + public void asInternal(final @NotNull StringBuilder sb, final boolean withBindKey) { + if (!this.typeParameters.isEmpty()) { + sb.append('<'); + for (final TypeParameter typeParam : this.typeParameters) { + typeParam.asInternal(sb, withBindKey); + } + sb.append('>'); + } + + sb.append('('); + for (final TypeSignature param : this.parameters) { + param.asInternal(sb, withBindKey); + } + sb.append(')'); + + this.returnType.asInternal(sb, withBindKey); + + for (final ThrowsSignature typeSig : this.throwsSignatures) { + sb.append('^'); + typeSig.asInternal(sb, withBindKey); + } + } + + /** + * Return a possibly erased version of this signature as a {@link MethodDescriptor}. This is a lossy process as + * method and type descriptors cannot represent all components of method or type signatures - all generic type + * information will be lost. + * + *

This method will throw {@link IllegalStateException} if any parameters contain an + * {@link dev.denwav.hypo.types.sig.param.TypeVariable.Unbound unbound type variable}. Use the + * {@link #bind(TypeVariableBinder)} method in that case to create a version of this signature which has type + * variables which are properly bound to their parameters. + * + * @return A possibly erased version of this signature as a {@link MethodDescriptor}. + */ + public @NotNull MethodDescriptor asDescriptor() { + final List descParams = this.parameters.stream() + .map(TypeSignature::asDescriptor) + .collect(Collectors.toList()); + return MethodDescriptor.of(descParams, this.returnType.asDescriptor()); + } + + @Override + public @NotNull List getTypeParameters() { + return this.typeParameters; + } + + /** + * Get the list of parameter types for this method signature. The returned list is immutable. + * + * @return The list of parameter types for this method signature. + */ + + @Override + public @NotNull List getParameters() { + return this.parameters; + } + + /** + * Get the return type for this method signature. + * + * @return The return type for this method signature. + */ + @Override + public @NotNull TypeSignature getReturnType() { + return this.returnType; + } + + /** + * Get the list of throws signatures for this method signature. The returned list is immutable. + * @return The list of throws signatures for this method signature. + */ + public @NotNull List getThrowsSignatures() { + return this.throwsSignatures; + } + + @Override + public boolean equals(final Object o) { + if (!(o instanceof final MethodSignature that)) { + return false; + } + return Objects.equals(this.typeParameters, that.typeParameters) + && Objects.equals(this.parameters, that.parameters) + && Objects.equals(this.returnType, that.returnType) + && Objects.equals(this.throwsSignatures, that.throwsSignatures); + } + + @Override + public int hashCode() { + return Objects.hash(this.typeParameters, this.parameters, this.returnType, this.throwsSignatures); + } + + @Override + public String toString() { + return this.asReadable(); + } +} diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/sig/ReferenceTypeSignature.java b/hypo-types/src/main/java/dev/denwav/hypo/types/sig/ReferenceTypeSignature.java new file mode 100644 index 0000000..d84f872 --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/sig/ReferenceTypeSignature.java @@ -0,0 +1,41 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types.sig; + +import com.google.errorprone.annotations.Immutable; +import dev.denwav.hypo.types.TypeBindable; +import dev.denwav.hypo.types.TypeRepresentable; +import dev.denwav.hypo.types.TypeVariableBinder; +import dev.denwav.hypo.types.sig.param.TypeArgument; +import dev.denwav.hypo.types.sig.param.TypeVariable; +import org.jetbrains.annotations.NotNull; + +/** + * A reference type signature is a subset of {@link TypeSignature} which does not include + * {@link dev.denwav.hypo.types.PrimitiveType primitive types} or {@link dev.denwav.hypo.types.VoidType void}. This is + * simply a marker interface to denote those types. + */ +@Immutable +public sealed interface ReferenceTypeSignature + extends TypeBindable, TypeSignature, TypeArgument, TypeRepresentable + permits ClassTypeSignature, ArrayTypeSignature, TypeVariable, TypeVariable.Unbound { + + @Override + @NotNull ReferenceTypeSignature bind(final @NotNull TypeVariableBinder binder); +} diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/sig/Signature.java b/hypo-types/src/main/java/dev/denwav/hypo/types/sig/Signature.java new file mode 100644 index 0000000..76786f5 --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/sig/Signature.java @@ -0,0 +1,34 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types.sig; + +import dev.denwav.hypo.types.TypeRepresentable; + +/** + * Signature is a marker interface for {@link TypeSignature}, {@link MethodSignature}, and {@link ClassSignature}. It + * can be used to differentiate a signature type from a descriptor type of a {@link TypeRepresentable}. + * + * @see TypeSignature + * @see MethodSignature + * @see ClassSignature + * @see dev.denwav.hypo.types.desc.Descriptor Descriptor + */ +public sealed interface Signature extends TypeRepresentable + permits TypeSignature, MethodSignature, ClassSignature { +} diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/sig/ThrowsSignature.java b/hypo-types/src/main/java/dev/denwav/hypo/types/sig/ThrowsSignature.java new file mode 100644 index 0000000..69d2bb2 --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/sig/ThrowsSignature.java @@ -0,0 +1,41 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types.sig; + +import com.google.errorprone.annotations.Immutable; +import dev.denwav.hypo.types.TypeBindable; +import dev.denwav.hypo.types.TypeRepresentable; +import dev.denwav.hypo.types.TypeVariableBinder; +import dev.denwav.hypo.types.sig.param.TypeVariable; +import org.jetbrains.annotations.NotNull; + +/** + * {@link ClassTypeSignature Class type signatures} can contain a throws signatures, which are simply a subset of + * {@link TypeSignature type signatures} - notably {@link ClassTypeSignature class type signatures} and + * {@link dev.denwav.hypo.types.sig.param.TypeVariable type varaibles}. This is simple a marker interface to denote + * those types. + */ +@Immutable +public sealed interface ThrowsSignature + extends TypeBindable, TypeRepresentable + permits ClassTypeSignature, TypeVariable, TypeVariable.Unbound { + + @Override + @NotNull ThrowsSignature bind(final @NotNull TypeVariableBinder binder); +} diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/sig/TypeParameterHolder.java b/hypo-types/src/main/java/dev/denwav/hypo/types/sig/TypeParameterHolder.java new file mode 100644 index 0000000..a081473 --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/sig/TypeParameterHolder.java @@ -0,0 +1,36 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types.sig; + +import dev.denwav.hypo.types.sig.param.TypeParameter; +import java.util.List; +import org.jetbrains.annotations.NotNull; + +/** + * Marker interface for types that can hold type parameters. This includes {@link ClassSignature} + * and {@link MethodSignature}. + */ +public interface TypeParameterHolder { + + /** + * The type parameters held by this type. The returned list is immutable. + * @return The type parameters held by this type. + */ + @NotNull List getTypeParameters(); +} diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/sig/TypeSignature.java b/hypo-types/src/main/java/dev/denwav/hypo/types/sig/TypeSignature.java new file mode 100644 index 0000000..86b6f56 --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/sig/TypeSignature.java @@ -0,0 +1,191 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types.sig; + +import com.google.errorprone.annotations.Immutable; +import dev.denwav.hypo.types.kind.ValueType; +import dev.denwav.hypo.types.intern.Intern; +import dev.denwav.hypo.types.PrimitiveType; +import dev.denwav.hypo.types.TypeBindable; +import dev.denwav.hypo.types.TypeRepresentable; +import dev.denwav.hypo.types.TypeVariableBinder; +import dev.denwav.hypo.types.VoidType; +import dev.denwav.hypo.types.desc.TypeDescriptor; +import dev.denwav.hypo.types.parsing.JvmTypeParseFailureException; +import dev.denwav.hypo.types.parsing.JvmTypeParser; +import dev.denwav.hypo.types.sig.param.BoundedTypeArgument; +import dev.denwav.hypo.types.sig.param.TypeVariable; +import dev.denwav.hypo.types.sig.param.WildcardArgument; +import java.lang.reflect.GenericArrayType; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.lang.reflect.WildcardType; +import java.util.Arrays; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * A JVM type signature. + * + *

A type signature can be one of four different kinds: + *

    + *
  • {@link dev.denwav.hypo.types.PrimitiveType PrimitiveType}
  • + *
  • {@link ClassTypeSignature}
  • + *
  • {@link ArrayTypeSignature}
  • + *
  • {@link dev.denwav.hypo.types.VoidType VoidType}
  • + *
+ * + *

Type signatures are used by the Java compiler to enforce generic type checks at compile time. They are not used + * during runtime on the JVM, though they are present to allow for compiling against and debugging generic code. + * + *

All implementations of this interface must be immutable. + * + * @see MethodSignature + * @see TypeDescriptor + */ +@Immutable +public sealed interface TypeSignature + extends ValueType, Signature, TypeBindable, TypeRepresentable + permits PrimitiveType, VoidType, ReferenceTypeSignature { + + /** + * Return a possibly erased version of this signature as a {@link TypeDescriptor}. This is a lossy process as type + * descriptors cannot represent all components of type signatures - all generic type information will be lost. + * + *

This method will throw {@link IllegalStateException} if it contains an + * {@link dev.denwav.hypo.types.sig.param.TypeVariable.Unbound unbound type variable}. Use the + * {@link #bind(TypeVariableBinder)} method in that case to create a version of this signature which has type + * variables which are properly bound to their parameters. + * + * @return A possibly erased version of this signature as a {@link TypeDescriptor}. + */ + @NotNull TypeDescriptor asDescriptor(); + + /** + * Parse the given internal JVM type signature text into a new {@link TypeSignature}. + * + *

This method throws {@link JvmTypeParseFailureException} if the given text is not a valid type signature. Use + * {@link JvmTypeParser#parseTypeSignature(String, int)} if you prefer to have {@code null} be returned instead. + * + * @param text The text to parse. + * @return The {@link TypeSignature}. + * @throws JvmTypeParseFailureException If the given text does not represent a valid JVM type signature. + */ + static @NotNull TypeSignature parse(final @NotNull String text) throws JvmTypeParseFailureException { + return parse(text, 0); + } + + /** + * Parse the given internal JVM type signature text into a new {@link TypeSignature}. + * + *

This method throws {@link JvmTypeParseFailureException} if the given text is not a valid type signature. Use + * {@link JvmTypeParser#parseTypeSignature(String, int)} if you prefer to have {@code null} be returned instead. + * + * @param text The text to parse. + * @param from The index to start parsing from. + * @return The {@link TypeSignature}. + * @throws JvmTypeParseFailureException If the given text does not represent a valid JVM type signature. + */ + static @NotNull TypeSignature parse(final @NotNull String text, final int from) throws JvmTypeParseFailureException { + if (text.length() > 1 && from == 0) { + final TypeSignature r = Intern.tryFind(ClassTypeSignature.class, text); + if (r != null) { + return r; + } + } + final TypeSignature result = JvmTypeParser.parseTypeSignature(text, from); + if (result == null) { + throw new JvmTypeParseFailureException("text is not a valid type signature: " + text.substring(from)); + } + return result; + } + + /** + * Create a {@link TypeSignature} matching the given {@link Class}. Note the {@link Class} does not include type + * parameter information so this will only + * + * @param clazz The {@link Class}. + * @return A new {@link TypeSignature} matching the given {@link Class}. + */ + static @NotNull TypeSignature of(final @NotNull Class clazz) { + final java.lang.reflect.TypeVariable>[] params = clazz.getTypeParameters(); + if (params.length == 0) { + return TypeDescriptor.of(clazz).asSignature(); + } else { + return ClassTypeSignature.of(clazz.getName(), Arrays.stream(params).map(TypeVariable::of).toList()); + } + } + + /** + * Attempt to create a new {@link TypeSignature} from the given {@link Type}. {@link TypeSignature} in our model + * does not cover every possible thing {@link Type} can represent, so this may fail. Notable in particular, since + * {@link WildcardArgument} and {@link BoundedTypeArgument} are type arguments (which are not type signatures), then + * if {@link Type} implements {@link WildcardType} this method will fail. + * + *

If you want this method to simply return {@code null} instead of throwing an exception, use + * {@link #ofOrNull(Type)}. + * + *

If you want full flexibility to support as many possible values of {@link Type} as possible, use + * {@link TypeRepresentable#of(Type)}. + * + * @param type The type. + * @return The new {@link TypeSignature}. + * @see TypeSignature#ofOrNull(Type) + * @see TypeRepresentable#of(Type) + */ + static @NotNull TypeSignature of(final @NotNull Type type) { + return switch (type) { + case final Class clazz -> of(clazz); + case final ParameterizedType parameterizedType -> ClassTypeSignature.of(parameterizedType); + case final GenericArrayType arrayType -> ArrayTypeSignature.of(arrayType); + case final java.lang.reflect.TypeVariable typeVar -> TypeVariable.of(typeVar); + case final WildcardType wildcardType -> + throw new IllegalArgumentException("Unsupported type: " + wildcardType.getClass().getName() + + " (WildcardType is a TypeArgument, not a TypeSignature)"); + default -> throw new IllegalArgumentException("Unsupported type: " + type.getClass().getName()); + }; + } + + /** + * Attempt to create a new {@link TypeSignature} from the given {@link Type}. {@link TypeSignature} in our model + * does not cover every possible thing {@link Type} can represent, so this may fail. Notable in particular, since + * {@link WildcardArgument} and {@link BoundedTypeArgument} are type arguments (which are not type signatures), then + * if {@link Type} implements {@link WildcardType} this method will fail. + * + *

If you want this method to throw an exception instead of simply returning {@code null}, use {@link #of(Type)}. + * + *

If you want full flexibility to support as many possible values of {@link Type} as possible, use + * {@link TypeRepresentable#of(Type)}. + * + * @param type The type. + * @return The new {@link TypeSignature}. + * @see TypeSignature#of(Type) + * @see TypeRepresentable#of(Type) + */ + static @Nullable TypeSignature ofOrNull(final @NotNull Type type) { + try { + return of(type); + } catch (final IllegalArgumentException ignored) { + return null; + } + } + + @Override + @NotNull TypeSignature bind(final @NotNull TypeVariableBinder binder); +} diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/sig/package-info.java b/hypo-types/src/main/java/dev/denwav/hypo/types/sig/package-info.java new file mode 100644 index 0000000..25513a5 --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/sig/package-info.java @@ -0,0 +1,92 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +/** + *

Type Signatures

+ * + *

Type signatures are the types used by the Java compiler. Unlike type descriptors, they can include generic type + * information, including type variables. This packages breaks type signatures up into three categories, types, methods, + * and class signatures. + * + *

    + *
  • {@link dev.denwav.hypo.types.sig.TypeSignature TypeSignature}
  • + *
  • {@link dev.denwav.hypo.types.sig.MethodSignature MethodSignature}
  • + *
  • {@link dev.denwav.hypo.types.sig.ClassSignature ClassSignature}
  • + *
+ * + *

Type Parameters, Variables, and Arguments

+ *

Often confused or used interchangeably, the terms "type parameter", "type variable", and "type argument" refer to + * three distinct concepts in the Java type system. It's important to understand each one if you want to be able to + * effectively use the APIs in this package. + * + *

{@link dev.denwav.hypo.types.sig.param.TypeParameter Type Parameter}

+ *

A type parameter is attached to either a method or class signature (note, as in a + * {@link dev.denwav.hypo.types.sig.ClassSignature class signature}, not a + * {@link dev.denwav.hypo.types.sig.ClassTypeSignature class type signature}), and represents types which are generic at + * the declaration of the type parameter holder. Type parameters can be upper or lower bounds defined to limit allowed + * types. Examples: + * + *


+ *     class SomeClass<T extends Number> // Class Signature
+ *     <T extends Number> void someMethod() // Method Signature
+*
+ *     <T> void unboundedMethod() // Method Signature
+ * 
+ * + *

In the above examples, both {@code SomeClass} and {@code someMethod} have a defined type parameter, named + * {@code T}. The bounds of {@code T} is that whatever type {@code T} represents must extend {@code Number}. + *

{@code unboundedMethod} shows an example of a type parameter without a defined bound. In practice, this means the + * actual bounds of {@code T} is {@code extends Object}. + * + *

{@link dev.denwav.hypo.types.sig.param.TypeArgument Type Argument}

+ *

A type argument is the call-site equivalent of a type parameter. When a class is extended which requires + * parameters, or a method is called which requires type parameters, the call-site types specified in the {@code <>} + * brackets are the type arguments. A type argument always corresponds to some defined type parameter. + * + *


+ *     class SomeClass<T extends Number> // Class Signature
+ *                     ^ type parameter
+ *     class ExtendingClass extends SomeClass<Integer> // Class Signature
+ *                                            ^ type argument
+ * 
+ * + *

In the above example {@code SomeClass} contains a type parameter {@code T}. {@code ExtendingClass} then extends + * {@code SomeClass}, and provides an argument for {@code T}, which is {@code Integer} in this instance. + * + *


+ *     <T extends Number> void someMethod() // Method Signature
+ *      ^ type parameter
+ *     this.<Integer>someMethod()
+ *           ^ type argument
+ * 
+ * + *

{@link dev.denwav.hypo.types.sig.param.TypeVariable Type Variable}

+ *

A type variable is the usage of a type parameter. Type variables can be used as the type for method + * signatures or field types. They can also be used as the argument for a type parameter. + * + *


+ *     class SomeClass<T extends Number> // Class Signature
+ *                     ^ type parameter
+ *     class ExtendingClass<R extends Number> extends SomeClass<R> // Class Signature
+ *                          ^ type parameter                    ^ type argument
+ *                                                              ^ type variable
+ * 
+ * + * @see dev.denwav.hypo.types.desc Type Descriptors + */ +package dev.denwav.hypo.types.sig; diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/sig/param/BoundedTypeArgument.java b/hypo-types/src/main/java/dev/denwav/hypo/types/sig/param/BoundedTypeArgument.java new file mode 100644 index 0000000..78c53f4 --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/sig/param/BoundedTypeArgument.java @@ -0,0 +1,145 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types.sig.param; + +import com.google.errorprone.annotations.Immutable; +import dev.denwav.hypo.types.intern.Intern; +import dev.denwav.hypo.types.TypeRepresentable; +import dev.denwav.hypo.types.TypeVariableBinder; +import dev.denwav.hypo.types.sig.ReferenceTypeSignature; +import dev.denwav.hypo.types.sig.TypeSignature; +import java.util.Objects; +import org.jetbrains.annotations.NotNull; + +/** + * A {@link TypeArgument} which is a wildcard type with either an upper or lower bound. For a wildcard type argument + * with no bound, use {@link WildcardArgument}. + * + *

Type arguments can have either an {@link WildcardBound#UPPER upper} or {@link WildcardBound#LOWER lower} bound. + * These correspond to {@code ? extends} and {@code ? super} respectively. To put another way, the type argument for + * the following {@code List} type is a bounded type argument with its upper bound set to be {@code Serializable}: + *


+ *     List<? extends Serializable>
+ * 
+ * + *

And the type argument for the follow {@code List} type is a bounded type argument with its lower bound set to be + * {@code String}: + *


+ *     List<? super String>
+ * 
+ * + *

The bound type does not need to be a concrete class type, it can be a type variable instead, such as: + *


+ *     List<? extends T>
+ * 
+ * + * @see WildcardArgument + */ +@Immutable +public final class BoundedTypeArgument extends Intern implements TypeArgument, TypeRepresentable { + + private final @NotNull WildcardBound bounds; + private final @NotNull ReferenceTypeSignature signature; + + /** + * Create a new instance of {@link BoundedTypeArgument}. + * + * @param bounds The bound for this type argument. + * @param signature The corresponding bound type. + * @return The new {@link BoundedTypeArgument}. + */ + public static @NotNull BoundedTypeArgument of( + final @NotNull WildcardBound bounds, + final @NotNull ReferenceTypeSignature signature + ) { + return new BoundedTypeArgument(bounds, signature).intern(); + } + + private BoundedTypeArgument( + final @NotNull WildcardBound bounds, + final @NotNull ReferenceTypeSignature signature + ) { + this.bounds = bounds; + this.signature = signature; + } + + @Override + public void asReadable(final @NotNull StringBuilder sb) { + this.bounds.asReadable(sb); + sb.append(' '); + + this.signature.asReadable(sb); + } + + @Override + public void asInternal(final @NotNull StringBuilder sb) { + this.asInternal(sb, false); + } + + @Override + public void asInternal(final @NotNull StringBuilder sb, final boolean withBindKey) { + this.bounds.asInternal(sb); + this.signature.asInternal(sb, withBindKey); + } + + @Override + public @NotNull BoundedTypeArgument bind(final @NotNull TypeVariableBinder binder) { + return BoundedTypeArgument.of(this.bounds, this.signature.bind(binder)); + } + + @Override + public boolean isUnbound() { + return this.signature.isUnbound(); + } + + /** + * Return the {@link WildcardBound bounds} for this type argument. + * @return The {@link WildcardBound bounds} for this type argument. + */ + public @NotNull WildcardBound getBounds() { + return this.bounds; + } + + /** + * Return the {@link TypeSignature bound type} for this type argument. + * @return The {@link TypeSignature bound type} for this type argument. + */ + public @NotNull ReferenceTypeSignature getSignature() { + return this.signature; + } + + @Override + public boolean equals(final Object o) { + if (!(o instanceof final BoundedTypeArgument that)) { + return false; + } + return this.bounds == that.bounds + && Objects.equals(this.signature, that.signature); + } + + @Override + public int hashCode() { + return Objects.hash(this.bounds, this.signature); + } + + @Override + public String toString() { + return this.asReadable(); + } +} diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/sig/param/TypeArgument.java b/hypo-types/src/main/java/dev/denwav/hypo/types/sig/param/TypeArgument.java new file mode 100644 index 0000000..73d914a --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/sig/param/TypeArgument.java @@ -0,0 +1,68 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types.sig.param; + +import com.google.errorprone.annotations.Immutable; +import dev.denwav.hypo.types.TypeBindable; +import dev.denwav.hypo.types.TypeRepresentable; +import dev.denwav.hypo.types.TypeVariableBinder; +import dev.denwav.hypo.types.sig.ReferenceTypeSignature; +import dev.denwav.hypo.types.sig.TypeSignature; +import java.lang.reflect.Type; +import java.lang.reflect.WildcardType; +import java.util.Objects; +import org.jetbrains.annotations.NotNull; + +/** + * A type argument is the type value passed into the type parameter list of a generic type signature. Type arguments are + * optional for type signatures to maintain backwards compatibility with pre-generic types. When a type does include + * type arguments, however, they must match 1:1 with the declared type parameters. + */ +@Immutable +public sealed interface TypeArgument + extends TypeBindable, TypeRepresentable + permits ReferenceTypeSignature, BoundedTypeArgument, WildcardArgument { + + @Override + @NotNull TypeArgument bind(final @NotNull TypeVariableBinder binder); + + /** + * Create a new {@link TypeArgument} from the given {@link WildcardType}. + * @param wildcardType The wildcard type. + * @return A new {@link TypeArgument} from the given {@link WildcardType}. + */ + static @NotNull TypeArgument of(final WildcardType wildcardType) { + final Type[] upper = wildcardType.getUpperBounds(); + final Type[] lower = wildcardType.getLowerBounds(); + + if (upper.length == 1 && upper[0] == Objects.class && lower.length == 0) { + return WildcardArgument.INSTANCE; + } else if (upper.length > 0) { + return BoundedTypeArgument.of( + WildcardBound.UPPER, + (ReferenceTypeSignature) TypeSignature.of(wildcardType.getUpperBounds()[0]) + ); + } else { + return BoundedTypeArgument.of( + WildcardBound.LOWER, + (ReferenceTypeSignature) TypeSignature.of(wildcardType.getLowerBounds()[0]) + ); + } + } +} diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/sig/param/TypeParameter.java b/hypo-types/src/main/java/dev/denwav/hypo/types/sig/param/TypeParameter.java new file mode 100644 index 0000000..edf9b11 --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/sig/param/TypeParameter.java @@ -0,0 +1,221 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types.sig.param; + +import com.google.errorprone.annotations.Immutable; +import dev.denwav.hypo.types.intern.Intern; +import dev.denwav.hypo.types.TypeBindable; +import dev.denwav.hypo.types.TypeRepresentable; +import dev.denwav.hypo.types.TypeVariableBinder; +import dev.denwav.hypo.types.sig.ClassTypeSignature; +import dev.denwav.hypo.types.sig.ReferenceTypeSignature; +import java.util.Collections; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * A type parameter is the type variable binding for a generic type. Type parameters can be present on + * {@link dev.denwav.hypo.types.sig.ClassSignature ClassSignatures} and + * {@link dev.denwav.hypo.types.sig.MethodSignature MethodSignatures}, and may reference other type parameters through + * corresponding type variables. + * + *

Each declared type parameter may be referenced elsewhere in the type signature as a {@link TypeVariable}, + * but that it is not required. For example, the following method declaration declares a type parameter {@code T}, but + * does not use it: + * + *


+ *     static <T> void foo() { ... }
+ * 
+ * + *

Type parameters may define class and interface bounds to be more specific about which types are allowed to be used + * as type arguments for the said parameter. If no class or interface bounds are specified, then the type parameter is + * implied to have a class bound of {@code java/lang/Object}. + */ +@Immutable +public final class TypeParameter extends Intern implements TypeBindable, TypeRepresentable { + + private final @NotNull String name; + private final @Nullable ReferenceTypeSignature classBound; + @SuppressWarnings("Immutable") + private final @NotNull List interfaceBounds; + + /** + * Create a new instance of a {@link TypeParameter}. + * @param name The name of the type parameter, as it appears in the Java source code. + * @param classBound The optional class bound of the type parameter. + * @param interfaceBounds The optional interface bounds of the type parameter. + * @return The new {@link TypeParameter}. + */ + public static @NotNull TypeParameter of( + final @NotNull String name, + final @Nullable ReferenceTypeSignature classBound, + final @NotNull List interfaceBounds + ) { + return new TypeParameter(name, classBound, interfaceBounds).intern(); + } + + /** + * Create a new instance of a {@link TypeParameter} with a specified class bound and no interface bounds. + * @param name The name of the type parameter, as it appears in the Java source code. + * @param classBound The class bound of the type parameter. + * @return The new {@link TypeParameter}. + */ + public static @NotNull TypeParameter of( + final @NotNull String name, + final @NotNull ReferenceTypeSignature classBound + ) { + return new TypeParameter(name, classBound, List.of()).intern(); + } + + /** + * Create a new instance of a {@link TypeParameter} with no class bound and no interface bounds. + * @param name The name of the type parameter, as it appears in the Java source code. + * @return The new {@link TypeParameter}. + */ + public static @NotNull TypeParameter of( + final @NotNull String name + ) { + return new TypeParameter(name, ClassTypeSignature.of("java/lang/Object"), List.of()).intern(); + } + + private TypeParameter( + final @NotNull String name, + final @Nullable ReferenceTypeSignature classBound, + final @NotNull List interfaceBounds + ) { + this.name = name; + this.classBound = classBound; + this.interfaceBounds = List.copyOf(interfaceBounds); + + if (classBound == null && this.interfaceBounds.isEmpty()) { + throw new IllegalArgumentException( + "Cannot construct a type with empty classBound and interfaceBounds, at least one must be set." + ); + } + } + + @Override + public void asReadable(final @NotNull StringBuilder sb) { + sb.append(this.name); + + if (this.classBound != null || !this.interfaceBounds.isEmpty()) { + sb.append(" extends "); + } + + if (this.classBound != null) { + this.classBound.asReadable(sb); + } + + for (int i = 0; i < this.interfaceBounds.size(); i++) { + if (i > 0 || this.classBound != null) { + sb.append(" & "); + } + this.interfaceBounds.get(i).asReadable(sb); + } + } + + @Override + public void asInternal(final @NotNull StringBuilder sb) { + this.asInternal(sb, false); + } + + @Override + public void asInternal(final @NotNull StringBuilder sb, final boolean withBindKey) { + sb.append(this.name); + + sb.append(':'); + if (this.classBound != null) { + this.classBound.asInternal(sb, withBindKey); + } + + for (final ReferenceTypeSignature interfaceBound : this.interfaceBounds) { + sb.append(':'); + interfaceBound.asInternal(sb, withBindKey); + } + } + + @Override + public @NotNull TypeParameter bind(final @NotNull TypeVariableBinder binder) { + return TypeParameter.of( + this.name, + binder.bind(this.classBound), + this.interfaceBounds.stream().map(binder::bind).collect(Collectors.toList()) + ); + } + + @Override + public boolean isUnbound() { + if (this.classBound != null && this.classBound.isUnbound()) { + return true; + } + for (final ReferenceTypeSignature b : this.interfaceBounds) { + if (b.isUnbound()) { + return true; + } + } + return false; + } + + /** + * Get the name of the type parameter, as it appears in the Java source code. + * @return The name of the type parameter. + */ + public @NotNull String getName() { + return this.name; + } + + /** + * Get the optional class bound of the type parameter. + * @return The class bound of the type parameter, or {@code null} if no class bound is specified. + */ + public @Nullable ReferenceTypeSignature getClassBound() { + return this.classBound; + } + + /** + * Get the optional interface bounds of the type parameter. + * @return The interface bounds of the type parameter, or an empty list if no interface bounds are specified. + */ + public @NotNull List getInterfaceBounds() { + return this.interfaceBounds; + } + + @Override + public boolean equals(final Object o) { + if (!(o instanceof final TypeParameter that)) { + return false; + } + return Objects.equals(this.name, that.name) + && Objects.equals(this.classBound, that.classBound) + && Objects.equals(this.interfaceBounds, that.interfaceBounds); + } + + @Override + public int hashCode() { + return Objects.hash(this.name, this.classBound, this.interfaceBounds); + } + + @Override + public String toString() { + return this.asReadable(); + } +} diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/sig/param/TypeVariable.java b/hypo-types/src/main/java/dev/denwav/hypo/types/sig/param/TypeVariable.java new file mode 100644 index 0000000..b8fe983 --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/sig/param/TypeVariable.java @@ -0,0 +1,283 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types.sig.param; + +import com.google.errorprone.annotations.Immutable; +import dev.denwav.hypo.types.intern.Intern; +import dev.denwav.hypo.types.TypeRepresentable; +import dev.denwav.hypo.types.TypeVariableBinder; +import dev.denwav.hypo.types.desc.ClassTypeDescriptor; +import dev.denwav.hypo.types.desc.TypeDescriptor; +import dev.denwav.hypo.types.sig.ReferenceTypeSignature; +import dev.denwav.hypo.types.sig.ThrowsSignature; +import dev.denwav.hypo.types.sig.TypeSignature; +import java.lang.reflect.Type; +import java.util.Arrays; +import java.util.Objects; +import org.jetbrains.annotations.NotNull; + +/** + * A type varaible is a reference to a type parameter in a type signature. Type variables can be used as standalone + * types or as bounds for other type definitions. + * + *

This model only considers a type variable to be fully defined if the corresponding {@link TypeParameter} is also + * specified. The type parameter definition corresponding to a type variable allows the type variable usage to be + * properly replaced when type erasure occurs, such as when transforming a signature into a descriptor. + * + *

When a type variable is initially parsed it will be considered {@link Unbound unbound}, which means no type + * parameter definition has been specified for the type variable. Attempting to call {@link #asDescriptor()} on an + * unbound type variable will fail. + * + *

Type parameter definitions attach to a type variable following scoping rules defined in the JLS. In most code type + * parameter definitions do not shadow other type parameter definitions, but it is possible. Given the following code: + * + *


+ *     public class Foo<T> {
+ *         public <T> void bar(T t) { ... }
+ *     }
+ * 
+ * + *

The type variable {@code T} used as the type for the first parameter of the {@code bar} method will bind to the + * type parmater {@code T} of the method {@code bar}, rather than to the type parameter of the class {@code Foo}. + */ +@Immutable +public final class TypeVariable + extends Intern + implements ReferenceTypeSignature, TypeRepresentable, ThrowsSignature { + + private final @NotNull TypeParameter definition; + + /** + * Create a new bounded {@link TypeParameter} instance. + * @param definition The type parameter bound of this type variable. + * @return The new {@link TypeVariable}. + */ + public static TypeVariable of(final @NotNull TypeParameter definition) { + return new TypeVariable(definition).intern(); + } + + /** + * Create a new {@link TypeParameter} instance from the given {@link java.lang.reflect.TypeVariable}. + * + *

The returned parameter will be bound as long as the given type variable has defined bounds from + * {@link java.lang.reflect.TypeVariable#getBounds()}. + * + * @param typeVar The Java reflection object to create a {@link TypeVariable} from. + * @return The new {@link TypeVariable}. + */ + public static @NotNull TypeVariable of(final @NotNull java.lang.reflect.TypeVariable typeVar) { + final TypeParameter bindingParam; + final Type[] bounds = typeVar.getBounds(); + if (bounds.length == 0) { + bindingParam = TypeParameter.of(typeVar.getName()); + } else if (bounds.length == 1) { + bindingParam = TypeParameter.of(typeVar.getName(), (ReferenceTypeSignature) TypeSignature.of(bounds[0])); + } else { + final ReferenceTypeSignature[] interfaceBounds = new ReferenceTypeSignature[bounds.length - 1]; + for (int i = 1; i < bounds.length; i++) { + interfaceBounds[i - 1] = (ReferenceTypeSignature) TypeSignature.of(bounds[i]); + } + bindingParam = TypeParameter.of( + typeVar.getName(), + (ReferenceTypeSignature) TypeSignature.of(bounds[0]), + Arrays.asList(interfaceBounds) + ); + } + return TypeVariable.of(bindingParam); + } + + /** + * Create a new {@link Unbound unbound} {@link TypeParameter} instance. + * @param name The name of the unbound type variable. + * @return The new {@link Unbound unbound} {@link TypeParameter} instance. + */ + public static Unbound unbound(final @NotNull String name) { + return new TypeVariable.Unbound(name).intern(); + } + + private TypeVariable(final @NotNull TypeParameter definition) { + this.definition = definition; + } + + @Override + public void asReadable(final @NotNull StringBuilder sb) { + sb.append(this.definition.getName()); + } + + @Override + public void asInternal(final @NotNull StringBuilder sb) { + this.asInternal(sb, false); + } + + @Override + public void asInternal(final @NotNull StringBuilder sb, final boolean withBindKey) { + if (withBindKey) { + sb.append("{BIND:"); + this.definition.asInternal(sb, true); + sb.append('}'); + } + sb.append('T'); + sb.append(this.definition.getName()); + sb.append(';'); + } + + @Override + public @NotNull TypeDescriptor asDescriptor() { + { + final ReferenceTypeSignature bound = this.definition.getClassBound(); + if (bound != null) { + return bound.asDescriptor(); + } + } + for (final ReferenceTypeSignature bound : this.definition.getInterfaceBounds()) { + return bound.asDescriptor(); + } + return ClassTypeDescriptor.of("java/lang/Object"); + } + + @Override + public @NotNull TypeVariable bind(final @NotNull TypeVariableBinder binder) { + return this; + } + + @Override + public boolean isUnbound() { + return this.definition.isUnbound(); + } + + /** + * Return the {@link TypeParameter} bound of this type variable. + * @return The {@link TypeParameter} bound of this type variable. + */ + public @NotNull TypeParameter getDefinition() { + return this.definition; + } + + /** + * Return the name of the type variable, which matches the name of the corresponding {@link TypeParameter}. + * @return The name of the type variable. + */ + public @NotNull String getName() { + return this.definition.getName(); + } + + @Override + public boolean equals(final Object o) { + if (!(o instanceof final TypeVariable that)) { + return false; + } + return Objects.equals(this.definition, that.definition); + } + + @Override + public int hashCode() { + return Objects.hashCode(this.definition); + } + + @Override + public String toString() { + return this.asReadable(); + } + + /** + * A {@link TypeParameter} which is not bound to a definint {@link TypeParameter}. This type cannot be properly + * erased into a {@link TypeDescriptor}, so {@link #asDescriptor()} will always fail. Use + * {@link #bind(TypeVariableBinder)} to convert this into a bound {@link TypeVariable}, as long as the + * {@link TypeVariableBinder} given includes a matching {@link TypeParameter} definition for this type variable. + * + *

Create new instances of this class using {@link TypeVariable#unbound(String)}. + */ + public static final class Unbound + extends Intern + implements ReferenceTypeSignature, TypeRepresentable, ThrowsSignature { + + private final @NotNull String name; + + private Unbound(final @NotNull String name) { + this.name = name; + } + + /** + * Return the name of this type variable. + * @return The name of this type variable. + */ + public @NotNull String getName() { + return this.name; + } + + @Override + public @NotNull TypeVariable bind(final @NotNull TypeVariableBinder binder) { + final TypeParameter param = binder.bindingFor(this.name); + if (param == null) { + throw new IllegalStateException("TypeParameter not found for name: " + this.name); + } + return TypeVariable.of(param); + } + + @Override + public boolean isUnbound() { + return true; + } + + @Override + public void asReadable(final @NotNull StringBuilder sb) { + sb.append(this.name); + } + + @Override + public void asInternal(final @NotNull StringBuilder sb) { + this.asInternal(sb, false); + } + + @Override + public void asInternal(final @NotNull StringBuilder sb, final boolean withBindKey) { + if (withBindKey) { + sb.append("{UNBOUND}"); + } + sb.append('T'); + sb.append(this.name); + sb.append(';'); + } + + @Override + public @NotNull TypeDescriptor asDescriptor() { + throw new IllegalStateException( + "TypeVariable must be bounded by calling bind() in order to create a TypeDescriptor" + ); + } + + @Override + public boolean equals(final Object o) { + if (!(o instanceof final Unbound that)) { + return false; + } + return Objects.equals(this.name, that.name); + } + + @Override + public int hashCode() { + return Objects.hashCode(this.name); + } + + @Override + public String toString() { + return this.asReadable(); + } + } +} diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/sig/param/WildcardArgument.java b/hypo-types/src/main/java/dev/denwav/hypo/types/sig/param/WildcardArgument.java new file mode 100644 index 0000000..1f96bbb --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/sig/param/WildcardArgument.java @@ -0,0 +1,68 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types.sig.param; + +import dev.denwav.hypo.types.TypeVariableBinder; +import org.jetbrains.annotations.NotNull; + +/** + * A wildcard type argument with no defined bounds. When erased an unbounded type argument is considered to be + * {@code java/lang/Object}. + * + *

A wildcard type argument is represented by the {@code ?} character in a type signature, without {@code extends} + * or {@code super} following to declare a bound. For a bounded wildcard type, use {@link BoundedTypeArgument}. + */ +public enum WildcardArgument implements TypeArgument { + /** + * The singleton instance of this wildcard argument type. + */ + INSTANCE, + ; + + @Override + public void asReadable(final @NotNull StringBuilder sb) { + sb.append('?'); + } + + @Override + public void asInternal(final @NotNull StringBuilder sb) { + this.asInternal(sb, false); + } + + @Override + public void asInternal(final @NotNull StringBuilder sb, final boolean withBindKey) { + sb.append('*'); + } + + // always binds to Ljava/lang/Object; + @Override + public @NotNull WildcardArgument bind(final @NotNull TypeVariableBinder binder) { + return this; + } + + @Override + public boolean isUnbound() { + return false; + } + + @Override + public String toString() { + return this.asReadable(); + } +} diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/sig/param/WildcardBound.java b/hypo-types/src/main/java/dev/denwav/hypo/types/sig/param/WildcardBound.java new file mode 100644 index 0000000..2e571b5 --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/sig/param/WildcardBound.java @@ -0,0 +1,60 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types.sig.param; + +import dev.denwav.hypo.types.TypeRepresentable; +import org.jetbrains.annotations.NotNull; + +/** + * The bound kind for a bounded wildcard type. This enum is used by {@link BoundedTypeArgument}. + */ +public enum WildcardBound implements TypeRepresentable { + /** + * The singleton instance of the upper bound, represented in code as {@code ? extends}. + */ + UPPER("? extends", '+'), + /** + * The singleton instance of the lower bound, represented in code as {@code ? super}. + */ + LOWER("? super", '-'), + ; + + private final @NotNull String readable; + private final char internal; + + WildcardBound(final @NotNull String readable, final char internal) { + this.readable = readable; + this.internal = internal; + } + + @Override + public void asReadable(final @NotNull StringBuilder sb) { + sb.append(this.readable); + } + + @Override + public void asInternal(final @NotNull StringBuilder sb) { + sb.append(this.internal); + } + + @Override + public String toString() { + return this.asReadable(); + } +} diff --git a/hypo-types/src/main/java/dev/denwav/hypo/types/sig/param/package-info.java b/hypo-types/src/main/java/dev/denwav/hypo/types/sig/param/package-info.java new file mode 100644 index 0000000..9c13e25 --- /dev/null +++ b/hypo-types/src/main/java/dev/denwav/hypo/types/sig/param/package-info.java @@ -0,0 +1,23 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +/** + * This package contains various type implementations around type parameters, + * type arguments, and type variables. + */ +package dev.denwav.hypo.types.sig.param; diff --git a/hypo-types/src/main/java/module-info.java b/hypo-types/src/main/java/module-info.java new file mode 100644 index 0000000..b40ec86 --- /dev/null +++ b/hypo-types/src/main/java/module-info.java @@ -0,0 +1,41 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +/** + * This module contains value classes representing all aspects of the Java type + * system, including descriptors and signatures, and covering value types as + * well as method types. This module also includes Java type parsing utilities + * as well as {@link dev.denwav.hypo.types.pattern pattern matching} utilities + * for inspecting types. + * + * @see dev.denwav.hypo.types + */ +module dev.denwav.hypo.types { + requires static transitive org.jetbrains.annotations; + requires static transitive com.google.errorprone.annotations; + requires jdk.jdi; + + exports dev.denwav.hypo.types; + exports dev.denwav.hypo.types.desc; + exports dev.denwav.hypo.types.intern; + exports dev.denwav.hypo.types.kind; + exports dev.denwav.hypo.types.parsing; + exports dev.denwav.hypo.types.pattern; + exports dev.denwav.hypo.types.sig; + exports dev.denwav.hypo.types.sig.param; +} diff --git a/hypo-types/src/test/java/dev/denwav/hypo/types/ClassSignatureTest.java b/hypo-types/src/test/java/dev/denwav/hypo/types/ClassSignatureTest.java new file mode 100644 index 0000000..49c49ee --- /dev/null +++ b/hypo-types/src/test/java/dev/denwav/hypo/types/ClassSignatureTest.java @@ -0,0 +1,209 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types; + +import dev.denwav.hypo.types.sig.ClassSignature; +import dev.denwav.hypo.types.sig.ClassTypeSignature; +import dev.denwav.hypo.types.sig.param.BoundedTypeArgument; +import dev.denwav.hypo.types.sig.param.TypeParameter; +import dev.denwav.hypo.types.sig.param.TypeVariable; +import dev.denwav.hypo.types.sig.param.WildcardArgument; +import dev.denwav.hypo.types.sig.param.WildcardBound; +import java.util.List; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@SuppressWarnings("unused") +class ClassSignatureTest { + + @Test + void testCollectionGeneric() { + final String s = "Ljava/lang/Object;Ljava/lang/Iterable;"; + + final var expected = ClassSignature.of( + List.of( + TypeParameter.of("E") + ), + ClassTypeSignature.of("java/lang/Object"), + List.of( + ClassTypeSignature.of("java/lang/Iterable", List.of(TypeVariable.unbound("E"))) + ) + ); + + final var actual = ClassSignature.parse(s); + assertEquals(expected, actual); + assertEquals(s, actual.asInternal()); + } + + @Test + void testDoubleGeneric() { + final String s = "Ljava/util/Collections$CheckedSortedMap;Ljava/util/NavigableMap;Ljava/io/Serializable;"; + + final var expected = ClassSignature.of( + List.of( + TypeParameter.of("K"), + TypeParameter.of("V") + ), + ClassTypeSignature.of("java/util/Collections$CheckedSortedMap", List.of(TypeVariable.unbound("K"), TypeVariable.unbound("V"))), + List.of( + ClassTypeSignature.of("java/util/NavigableMap", List.of(TypeVariable.unbound("K"), TypeVariable.unbound("V"))), + ClassTypeSignature.of("java/io/Serializable") + ) + ); + + final var actual = ClassSignature.parse(s); + assertEquals(expected, actual); + assertEquals(s, actual.asInternal()); + } + + @Test + void testTypeParamInterfaceBound() { + final String s = "Ljava/lang/Object;Ljava/time/chrono/ChronoLocalDate;Ljava/time/temporal/Temporal;Ljava/time/temporal/TemporalAdjuster;Ljava/io/Serializable;"; + + final var expected = ClassSignature.of( + List.of( + TypeParameter.of("D", null, List.of(ClassTypeSignature.of("java/time/chrono/ChronoLocalDate"))) + ), + ClassTypeSignature.of("java/lang/Object"), + List.of( + ClassTypeSignature.of("java/time/chrono/ChronoLocalDate"), + ClassTypeSignature.of("java/time/temporal/Temporal"), + ClassTypeSignature.of("java/time/temporal/TemporalAdjuster"), + ClassTypeSignature.of("java/io/Serializable") + ) + ); + + final var actual = ClassSignature.parse(s); + assertEquals(expected, actual); + assertEquals(s, actual.asInternal()); + } + + @Test + void testNestedArgument() { + final String s = "Ljava/lang/Object;Lsun/util/locale/provider/LocaleServiceProviderPool$LocalizedObjectGetter;>;"; + + final var expected = ClassSignature.of( + List.of(), + ClassTypeSignature.of("java/lang/Object"), + List.of( + ClassTypeSignature.of( + "sun/util/locale/provider/LocaleServiceProviderPool$LocalizedObjectGetter", + List.of( + ClassTypeSignature.of("java/util/spi/CalendarNameProvider"), + ClassTypeSignature.of( + "java/util/Map", + List.of(ClassTypeSignature.of("java/lang/String"), + ClassTypeSignature.of("java/lang/Integer")) + ) + ) + ) + ) + ); + + final var actual = ClassSignature.parse(s); + assertEquals(expected, actual); + assertEquals(s, actual.asInternal()); + } + + @Test + void testWildcard() { + final String s = "Ljava/lang/Object;Ljava/io/Serializable;Ljava/lang/reflect/GenericDeclaration;Ljava/lang/reflect/Type;Ljava/lang/reflect/AnnotatedElement;Ljava/lang/invoke/TypeDescriptor$OfField;>;Ljava/lang/constant/Constable;"; + + final var expected = ClassSignature.of( + List.of( + TypeParameter.of("T") + ), + ClassTypeSignature.of("java/lang/Object"), + List.of( + ClassTypeSignature.of("java/io/Serializable"), + ClassTypeSignature.of("java/lang/reflect/GenericDeclaration"), + ClassTypeSignature.of("java/lang/reflect/Type"), + ClassTypeSignature.of("java/lang/reflect/AnnotatedElement"), + ClassTypeSignature.of( + "java/lang/invoke/TypeDescriptor$OfField", + List.of(ClassTypeSignature.of("java/lang/Class", List.of(WildcardArgument.INSTANCE))) + ), + ClassTypeSignature.of("java/lang/constant/Constable") + ) + ); + + final var actual = ClassSignature.parse(s); + assertEquals(expected, actual); + assertEquals(s, actual.asInternal()); + } + + @Test + void testPlus() { + final String s = "Lcom/sun/source/util/SimpleDocTreeVisitor;Ljava/lang/Void;>;"; + + final var expected = ClassSignature.of( + List.of(), + ClassTypeSignature.of( + "com/sun/source/util/SimpleDocTreeVisitor", + List.of( + ClassTypeSignature.of( + "java/util/List", + List.of(BoundedTypeArgument.of(WildcardBound.UPPER, ClassTypeSignature.of("com/sun/source/doctree/DocTree"))) + ), + ClassTypeSignature.of("java/lang/Void") + ) + ), + List.of() + ); + + final var actual = ClassSignature.parse(s); + assertEquals(expected, actual); + assertEquals(s, actual.asInternal()); + } + + @Test + void testMinus() { + final String s = ";>;R:Ljava/lang/Object;>Ljava/lang/Object;Ljdk/internal/net/http/ResponseSubscribers$TrustedSubscriber;"; + + final var expected = ClassSignature.of( + List.of( + TypeParameter.of( + "S", + null, + List.of( + ClassTypeSignature.of( + "java/util/concurrent/Flow$Subscriber", + List.of( + BoundedTypeArgument.of( + WildcardBound.LOWER, + ClassTypeSignature.of("java/util/List", List.of(ClassTypeSignature.of("java/nio/ByteBuffer"))) + ) + ) + ) + ) + ), + TypeParameter.of("R") + ), + ClassTypeSignature.of("java/lang/Object"), + List.of( + ClassTypeSignature.of("jdk/internal/net/http/ResponseSubscribers$TrustedSubscriber", List.of(TypeVariable.unbound("R"))) + ) + ); + + final var actual = ClassSignature.parse(s); + assertEquals(expected, actual); + assertEquals(s, actual.asInternal()); + } +} diff --git a/hypo-types/src/test/java/dev/denwav/hypo/types/InternTest.java b/hypo-types/src/test/java/dev/denwav/hypo/types/InternTest.java new file mode 100644 index 0000000..e5411d2 --- /dev/null +++ b/hypo-types/src/test/java/dev/denwav/hypo/types/InternTest.java @@ -0,0 +1,66 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types; + +import dev.denwav.hypo.types.desc.ClassTypeDescriptor; +import dev.denwav.hypo.types.desc.TypeDescriptor; +import dev.denwav.hypo.types.intern.Intern; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.invoke.MethodType; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; + +class InternTest { + + private static final MethodHandle constructor; + static { + try { + constructor = MethodHandles.privateLookupIn(ClassTypeDescriptor.class, MethodHandles.lookup()) + .findConstructor(ClassTypeDescriptor.class, MethodType.methodType(void.class, String.class)); + } catch (final NoSuchMethodException | IllegalAccessException e) { + throw new RuntimeException(e); + } + } + + @Test + void testIntern() { + final TypeDescriptor first = TypeDescriptor.parse("Ljava/lang/String;"); + final TypeDescriptor second = TypeDescriptor.parse("Ljava/lang/String;"); + final TypeDescriptor third = ClassTypeDescriptor.of("java/lang/String"); + + Assertions.assertSame(first, second); + Assertions.assertSame(first, third); + Assertions.assertEquals(1, Intern.internmentSize(ClassTypeDescriptor.class)); + } + + @Test + void testInternOnSeparate() throws Throwable { + final ClassTypeDescriptor instance1 = (ClassTypeDescriptor) constructor.invoke("java/lang/String"); + final ClassTypeDescriptor instance2 = (ClassTypeDescriptor) constructor.invoke("java/lang/String"); + + // Guaranteed - sanity check + assertNotSame(instance1, instance2); + + assertSame(instance1.intern(), instance2.intern()); + } +} diff --git a/hypo-types/src/test/java/dev/denwav/hypo/types/JvmReflectionTest.java b/hypo-types/src/test/java/dev/denwav/hypo/types/JvmReflectionTest.java new file mode 100644 index 0000000..809e2a6 --- /dev/null +++ b/hypo-types/src/test/java/dev/denwav/hypo/types/JvmReflectionTest.java @@ -0,0 +1,114 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types; + +import com.google.common.reflect.TypeToken; +import dev.denwav.hypo.types.desc.ClassTypeDescriptor; +import dev.denwav.hypo.types.desc.MethodDescriptor; +import dev.denwav.hypo.types.desc.TypeDescriptor; +import dev.denwav.hypo.types.sig.ClassTypeSignature; +import dev.denwav.hypo.types.sig.MethodSignature; +import dev.denwav.hypo.types.sig.TypeSignature; +import dev.denwav.hypo.types.sig.param.TypeParameter; +import java.lang.reflect.Method; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@SuppressWarnings("all") +class JvmReflectionTest { + + @Test + void testTypeDescriptor() { + assertEquals(ClassTypeDescriptor.of("java/lang/String"), TypeDescriptor.of(String.class)); + } + + private static final String someMethodSig = ";:Ldev/denwav/hypo/types/JvmReflectionTest$TwoInterface;>(Ljava/lang/Object;Ljava/util/Map<-Ljava/lang/String;Ljava/util/function/Function;>;[I[[Ljava/lang/String;[Ljava/util/List;TT;)Ljava/util/List;"; + @SuppressWarnings("unused") + private & TwoInterface> List someMethod( + Object a, + Map> func, + int[] i, + String[][] s, + List[] l, + T intersection + ) { + return null; + } + private static Method someMethod() { + try { + return JvmReflectionTest.class.getDeclaredMethod("someMethod", Object.class, Map.class, int[].class, String[][].class, List[].class, OneInterface.class); + } catch (final NoSuchMethodException e) { + throw new RuntimeException(e); + } + } + private static @NotNull MethodSignature someMethodSig() { + final MethodSignature sig = MethodSignature.parse(someMethodSig); + // TODO replace with proper binder when implemented + return sig.bind(var -> { + for (final TypeParameter param : sig.getTypeParameters()) { + if (param.getName().equals(var)) { + return param; + } + } + return null; + }); + } + + @SuppressWarnings("unused") + interface OneInterface {} + interface TwoInterface {} + + @SuppressWarnings("unused") + private static class TypeHolder implements OneInterface { + } + + @Test + void testMethodDescriptor() { + final Method someMethod = someMethod(); + final MethodDescriptor expected = someMethodSig().asDescriptor(); + + assertEquals(expected, MethodDescriptor.of(someMethod)); + } + + @Test + void testTypeSignatureSimple() { + assertEquals(ClassTypeSignature.of("java/lang/String"), TypeSignature.of(String.class)); + assertEquals( + TypeSignature.parse("Ljava/util/List;").bind(TypeVariableBinder.object()), + TypeSignature.of(List.class) + ); + } + + @Test + void testTypeSignatureGeneric() { + final TypeSignature expected = TypeSignature.parse("Ljava/util/List;"); + final TypeSignature actual = TypeSignature.of(new TypeToken>() {}.getType()); + + assertEquals(expected, actual); + } + + @Test + void testTypeSignatureAnnotatedGeneric() { + } +} diff --git a/hypo-types/src/test/java/dev/denwav/hypo/types/MethodDescriptorTest.java b/hypo-types/src/test/java/dev/denwav/hypo/types/MethodDescriptorTest.java new file mode 100644 index 0000000..4065c02 --- /dev/null +++ b/hypo-types/src/test/java/dev/denwav/hypo/types/MethodDescriptorTest.java @@ -0,0 +1,103 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types; + +import dev.denwav.hypo.types.desc.ArrayTypeDescriptor; +import dev.denwav.hypo.types.desc.ClassTypeDescriptor; +import dev.denwav.hypo.types.desc.MethodDescriptor; +import dev.denwav.hypo.types.desc.TypeDescriptor; +import dev.denwav.hypo.types.intern.Intern; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Stream; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +@DisplayName("MethodDescriptor Tests") +class MethodDescriptorTest { + + @Test + @DisplayName("Test MethodDescriptor.parse() and MethodDescriptor.asInternal()") + void testParse() throws ExecutionException, InterruptedException { + final TypeDescriptor[] testTypes = Arrays.stream(new TypeDescriptor[]{ + PrimitiveType.CHAR, + PrimitiveType.BYTE, + PrimitiveType.SHORT, + PrimitiveType.INT, + PrimitiveType.LONG, + PrimitiveType.FLOAT, + PrimitiveType.DOUBLE, + PrimitiveType.BOOLEAN, + ClassTypeDescriptor.of("java/lang/Object"), + ClassTypeDescriptor.of("java/lang/String"), + }).flatMap(t -> { + return Stream.of(t, ArrayTypeDescriptor.of(1, t), ArrayTypeDescriptor.of(2, t)); + }).toArray(TypeDescriptor[]::new); + + final ArrayList returnTypeList = new ArrayList<>(Arrays.asList(testTypes)); + returnTypeList.add(VoidType.INSTANCE); + final TypeDescriptor[] returnTypes = returnTypeList.toArray(new TypeDescriptor[0]); + + final TypeDescriptor[] params = new TypeDescriptor[4]; + final List paramList = Arrays.asList(params); + + final AtomicLong counter = new AtomicLong(0); + + try (final ExecutorService pool = Executors.newWorkStealingPool()) { + final ArrayList> tasks = new ArrayList<>(); + + for (final TypeDescriptor param0 : testTypes) { + tasks.add(pool.submit(() -> { + for (final TypeDescriptor param1 : testTypes) { + for (final TypeDescriptor param2 : testTypes) { + for (final TypeDescriptor param3 : testTypes) { + params[0] = param0; + params[1] = param1; + params[2] = param2; + params[3] = param3; + + for (final TypeDescriptor returnType : returnTypes) { + final MethodDescriptor dec = MethodDescriptor.of(paramList, returnType); + Assertions.assertEquals(dec, MethodDescriptor.parse(dec.asInternal())); + + final long count = counter.incrementAndGet(); + if (count % 1_000_000L == 0L) { + System.out.printf("Tested %,d permutations...%n", count); + System.out.printf("MethodDescriptor internment: %,d %n", Intern.internmentSize(MethodDescriptor.class)); + } + } + } + } + } + })); + } + + for (final Future task : tasks) { + task.get(); + } + } + } +} diff --git a/hypo-types/src/test/java/dev/denwav/hypo/types/MethodSignatureTest.java b/hypo-types/src/test/java/dev/denwav/hypo/types/MethodSignatureTest.java new file mode 100644 index 0000000..8cde579 --- /dev/null +++ b/hypo-types/src/test/java/dev/denwav/hypo/types/MethodSignatureTest.java @@ -0,0 +1,179 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types; + +import dev.denwav.hypo.types.intern.Intern; +import dev.denwav.hypo.types.sig.ArrayTypeSignature; +import dev.denwav.hypo.types.sig.ClassTypeSignature; +import dev.denwav.hypo.types.sig.MethodSignature; +import dev.denwav.hypo.types.sig.ThrowsSignature; +import dev.denwav.hypo.types.sig.TypeSignature; +import dev.denwav.hypo.types.sig.param.TypeParameter; +import dev.denwav.hypo.types.sig.param.TypeVariable; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Stream; +import org.jetbrains.annotations.NotNull; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class MethodSignatureTest { + + @Test + void testMethodSig() throws ExecutionException, InterruptedException { + final var primitives = List.of(PrimitiveType.values()); + + final List> classTypes = List.of( + TypeMapping.of(ClassTypeSignature.of("java/lang/Object")), + TypeMapping.of(ClassTypeSignature.of("org/gradle/internal/execution/history/impl/SerializableFileCollectionFingerprint")), + TypeMapping.of( + TypeParameter.of("T"), + ClassTypeSignature.of("java/util/List", List.of(TypeVariable.unbound("T"))) + ), + TypeMapping.of( + List.of(TypeParameter.of("T1", null, List.of(ClassTypeSignature.of("java/util/Collection"))), TypeParameter.of("T2", TypeVariable.unbound("T1"))), + List.of(TypeVariable.unbound("T1"), TypeVariable.unbound("T2")) + ), + TypeMapping.of( + List.of(TypeParameter.of("K", ClassTypeSignature.of("java/lang/Object")), TypeParameter.of("V", ClassTypeSignature.of("java/lang/CharSequence"))), + ClassTypeSignature.of("java/util/Map", List.of(TypeVariable.unbound("K"), TypeVariable.unbound("V"))) + ) + ); + + final List> parameters = Stream + .concat( + primitives.stream().map(TypeMapping::of), + classTypes.stream() + ) + .flatMap(t -> + Stream.of( + t, + TypeMapping.of(t.params(), t.types().stream().map(t1 -> ArrayTypeSignature.of(1, t1)).toList()), + TypeMapping.of(t.params(), t.types().stream().map(t1 -> ArrayTypeSignature.of(2, t1)).toList()) + ) + ) + .toList(); + + final List> returnTypes = Stream.concat( + Stream + .concat( + primitives.stream().map(TypeMapping::of), + classTypes.stream().filter(t -> t.types().size() == 1) + ), + Stream.of(TypeMapping.of(VoidType.INSTANCE)) + ) + .flatMap(t -> { + if (t.types().getFirst() == VoidType.INSTANCE) { + return Stream.of(t); + } + return Stream.of( + t, + TypeMapping.of(t.params(), t.types().stream().map(t1 -> ArrayTypeSignature.of(1, t1)).toList()), + TypeMapping.of(t.params(), t.types().stream().map(t1 -> ArrayTypeSignature.of(2, t1)).toList()) + ); + }) + .toList(); + + final List> throwsTypes = List.of( + TypeMapping.of(ClassTypeSignature.of("java/lang/Exception")), + TypeMapping.of( + TypeParameter.of("X", ClassTypeSignature.of("java/lang/RuntimeException")), + TypeVariable.unbound("X") + ), + TypeMapping.of( + TypeParameter.of("X", ClassTypeSignature.of("java/lang/RuntimeException")), + List.of(ClassTypeSignature.of("java/sql/SqlException"), TypeVariable.unbound("X")) + ) + ); + + final AtomicLong counter = new AtomicLong(0); + + try (final ExecutorService pool = Executors.newWorkStealingPool()) { + final ArrayList> tasks = new ArrayList<>(); + + for (final TypeMapping param1 : parameters) { + tasks.add(pool.submit(() -> { + for (final TypeMapping param2 : parameters) { + for (final TypeMapping param3 : parameters) { + final var paramTypes = Stream.of(param1, param2, param3) + .flatMap(t -> t.types().stream()) + .toList(); + + for (final TypeMapping returnType : returnTypes) { + for (final TypeMapping throwsType : throwsTypes) { + final var typeParams = Stream.of(param1, param2, param3, returnType, throwsType) + .flatMap(t -> t.params().stream()) + .toList(); + + final MethodSignature sig = MethodSignature.of(typeParams, paramTypes, returnType.types().getFirst(), throwsType.types()); + Assertions.assertEquals(sig, MethodSignature.parse(sig.asInternal()), "Internal " + sig.asInternal()); + + final long count = counter.incrementAndGet(); + if (count % 1_000_000L == 0L) { + System.out.printf("Tested %,d permutations...%n", count); + System.out.printf("MethodSignature internment: %,d %n", Intern.internmentSize(MethodSignature.class)); + } + } + } + } + } + })); + } + + for (final Future task : tasks) { + task.get(); + } + } + } + + @SuppressWarnings("unused") + private record TypeMapping( + @NotNull List params, + @NotNull List types + ) { + public static TypeMapping of(final @NotNull List params, final @NotNull List types) { + return new TypeMapping<>(params, types); + } + + public static TypeMapping of(final @NotNull TypeParameter param, final @NotNull List types) { + return new TypeMapping<>(List.of(param), types); + } + + public static TypeMapping of(final @NotNull List types) { + return new TypeMapping<>(List.of(), types); + } + + public static TypeMapping of(final @NotNull List params, final @NotNull T type) { + return new TypeMapping<>(params, List.of(type)); + } + + public static TypeMapping of(final @NotNull TypeParameter param, final @NotNull T type) { + return new TypeMapping<>(List.of(param), List.of(type)); + } + + public static TypeMapping of(final @NotNull T type) { + return new TypeMapping<>(List.of(), List.of(type)); + } + } +} diff --git a/hypo-types/src/test/java/dev/denwav/hypo/types/MethodSignatureThreadContentionTest.java b/hypo-types/src/test/java/dev/denwav/hypo/types/MethodSignatureThreadContentionTest.java new file mode 100644 index 0000000..93912a7 --- /dev/null +++ b/hypo-types/src/test/java/dev/denwav/hypo/types/MethodSignatureThreadContentionTest.java @@ -0,0 +1,60 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types; + +import dev.denwav.hypo.types.intern.Intern; +import dev.denwav.hypo.types.sig.MethodSignature; +import java.util.ArrayList; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.atomic.AtomicLong; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +public class MethodSignatureThreadContentionTest { + + @Test + void testThreadContention() throws ExecutionException, InterruptedException { + final AtomicLong counter = new AtomicLong(0); + + final ArrayList> tasks = new ArrayList<>(); + try (final ExecutorService pool = Executors.newVirtualThreadPerTaskExecutor()) { + for (int i = 0; i < 64; i++) { + tasks.add(pool.submit(() -> { + for (int j = 0; j < 1_000_000; j++) { + final MethodSignature sig = MethodSignature.parse("()V"); + Assertions.assertEquals(sig, MethodSignature.parse(sig.asInternal()), "Internal " + sig.asInternal()); + + final long count = counter.incrementAndGet(); + if (count % 1_000_000 == 0) { + System.out.printf("Tested %,d iterations...%n", count); + System.out.printf("MethodSignature internment: %,d %n", Intern.internmentSize(MethodSignature.class)); + } + } + })); + } + } + + for (final Future task : tasks) { + task.get(); + } + } +} diff --git a/hypo-types/src/test/java/dev/denwav/hypo/types/ParsingTest.java b/hypo-types/src/test/java/dev/denwav/hypo/types/ParsingTest.java new file mode 100644 index 0000000..607fc55 --- /dev/null +++ b/hypo-types/src/test/java/dev/denwav/hypo/types/ParsingTest.java @@ -0,0 +1,73 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types; + +import dev.denwav.hypo.types.desc.ArrayTypeDescriptor; +import dev.denwav.hypo.types.desc.ClassTypeDescriptor; +import dev.denwav.hypo.types.desc.TypeDescriptor; +import dev.denwav.hypo.types.sig.ArrayTypeSignature; +import dev.denwav.hypo.types.sig.ClassTypeSignature; +import dev.denwav.hypo.types.sig.TypeSignature; +import java.util.List; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class ParsingTest { + + @Test + void testTypeDescriptor() { + Assertions.assertEquals(ClassTypeDescriptor.of("java/lang/Object"), TypeDescriptor.parse("Ljava/lang/Object;")); + Assertions.assertEquals(ArrayTypeDescriptor.of(2, ClassTypeDescriptor.of("java/lang/Object")), TypeDescriptor.parse("[[Ljava/lang/Object;")); + Assertions.assertEquals(VoidType.INSTANCE, TypeDescriptor.parse("V")); + Assertions.assertEquals(PrimitiveType.CHAR, TypeDescriptor.parse("C")); + Assertions.assertEquals(PrimitiveType.BYTE, TypeDescriptor.parse("B")); + Assertions.assertEquals(PrimitiveType.SHORT, TypeDescriptor.parse("S")); + Assertions.assertEquals(PrimitiveType.INT, TypeDescriptor.parse("I")); + Assertions.assertEquals(PrimitiveType.LONG, TypeDescriptor.parse("J")); + Assertions.assertEquals(PrimitiveType.FLOAT, TypeDescriptor.parse("F")); + Assertions.assertEquals(PrimitiveType.DOUBLE, TypeDescriptor.parse("D")); + Assertions.assertEquals(PrimitiveType.BOOLEAN, TypeDescriptor.parse("Z")); + } + + @Test + void testTypeSignature() { + Assertions.assertEquals(ClassTypeSignature.of(null, "java/lang/Object", null), TypeSignature.parse("Ljava/lang/Object;")); + Assertions.assertEquals(ArrayTypeSignature.of(2, ClassTypeSignature.of(null, "java/lang/Object", null)), TypeSignature.parse("[[Ljava/lang/Object;")); + Assertions.assertEquals(VoidType.INSTANCE, TypeSignature.parse("V")); + Assertions.assertEquals(PrimitiveType.CHAR, TypeSignature.parse("C")); + Assertions.assertEquals(PrimitiveType.BYTE, TypeSignature.parse("B")); + Assertions.assertEquals(PrimitiveType.SHORT, TypeSignature.parse("S")); + Assertions.assertEquals(PrimitiveType.INT, TypeSignature.parse("I")); + Assertions.assertEquals(PrimitiveType.LONG, TypeSignature.parse("J")); + Assertions.assertEquals(PrimitiveType.FLOAT, TypeSignature.parse("F")); + Assertions.assertEquals(PrimitiveType.DOUBLE, TypeSignature.parse("D")); + Assertions.assertEquals(PrimitiveType.BOOLEAN, TypeSignature.parse("Z")); + + Assertions.assertEquals( + ClassTypeSignature.of( + null, + "net/minecraft/world/entity/ai/memory/MemoryModuleType", + List.of(ClassTypeSignature.of(null, "net/minecraft/world/entity/LivingEntity", null)) + ), + TypeSignature.parse( + "Lnet/minecraft/world/entity/ai/memory/MemoryModuleType;" + ) + ); + } +} diff --git a/hypo-types/src/test/java/dev/denwav/hypo/types/TestAllTypes.java b/hypo-types/src/test/java/dev/denwav/hypo/types/TestAllTypes.java new file mode 100644 index 0000000..11d83bd --- /dev/null +++ b/hypo-types/src/test/java/dev/denwav/hypo/types/TestAllTypes.java @@ -0,0 +1,217 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types; + +import dev.denwav.hypo.types.desc.MethodDescriptor; +import dev.denwav.hypo.types.desc.TypeDescriptor; +import dev.denwav.hypo.types.sig.ClassSignature; +import dev.denwav.hypo.types.sig.MethodSignature; +import dev.denwav.hypo.types.sig.TypeSignature; +import java.io.BufferedReader; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.function.Supplier; +import javax.xml.namespace.QName; +import javax.xml.stream.XMLEventReader; +import javax.xml.stream.XMLInputFactory; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.events.Attribute; +import javax.xml.stream.events.StartElement; +import javax.xml.stream.events.XMLEvent; +import org.jetbrains.annotations.Nullable; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class TestAllTypes { + + private static FileSystem fs; + private static Path root; + + @BeforeAll + static void setup() throws IOException { + final Path typesZip = Path.of(System.getProperty("hypo.types.zip")); + fs = FileSystems.newFileSystem(typesZip); + root = fs.getPath("/"); + } + + @AfterAll + static void teardown() throws IOException { + fs.close(); + } + + @Test + void testJvmTypes() throws XMLStreamException, IOException { + this.doTest(root.resolve("jvm.xml")); + } + + @Test + void testSpringTypes() throws XMLStreamException, IOException { + this.doTest(root.resolve("spring.xml")); + } + + @Test + void testGuavaTypes() throws XMLStreamException, IOException { + this.doTest(root.resolve("guava.xml")); + } + + @Test + void testEclipseTypes() throws XMLStreamException, IOException { + this.doTest(root.resolve("eclipse.xml")); + } + + + private void doTest(final Path xmlFile) throws IOException, XMLStreamException { + try (final BufferedReader reader = Files.newBufferedReader(xmlFile, StandardCharsets.UTF_8)) { + final XMLInputFactory factory = XMLInputFactory.newFactory(); + final XMLEventReader eventReader = factory.createXMLEventReader(reader); + this.doTest(eventReader); + } + } + + private void doTest(final XMLEventReader reader) throws XMLStreamException { + ClassAttributes currentClass = null; + MemberAttributes currentMethod = null; + + long classCounter = 0L; + long fieldCounter = 0L; + long methodCounter = 0L; + long localCounter = 0L; + + while (reader.hasNext()) { + final XMLEvent event = reader.nextEvent(); + + if (event.isStartElement()) { + final StartElement startElement = event.asStartElement(); + switch (startElement.getName().getLocalPart()) { + case "class": + currentClass = this.getClassAttributes(startElement); + this.testClass(currentClass); + classCounter++; + break; + case "method": + currentMethod = this.getMemberAttributes(startElement); + this.testMethod(currentClass, currentMethod); + methodCounter++; + break; + case "field": + final MemberAttributes field = this.getMemberAttributes(startElement); + this.testField(currentClass, field); + fieldCounter++; + break; + case "local": + final MemberAttributes local = this.getMemberAttributes(startElement); + this.testLocal(currentClass, currentMethod, local); + localCounter++; + break; + } + } + } + + System.out.printf("Tested classes: %,d%n", classCounter); + System.out.printf("Tested fields: %,d%n", fieldCounter); + System.out.printf("Tested methods: %,d%n", methodCounter); + System.out.printf("Tested locals: %,d%n", localCounter); + } + + private void testClass(final ClassAttributes attr) { + if (attr.signature() == null) { + return; + } + final ClassSignature sig = ClassSignature.parse(attr.signature()); + assertEquals(attr.signature(), sig.asInternal(), "Failed for class: " + attr.name()); + } + + private void testField(final ClassAttributes currentClass, final MemberAttributes attr) { + final Supplier failureMsg = () -> "Failed for field: " + attr.name() + " in class: " + currentClass.name(); + final TypeDescriptor desc = TypeDescriptor.parse(attr.descriptor()); + assertEquals(attr.descriptor(), desc.asInternal(), failureMsg); + + if (attr.signature() == null) { + return; + } + + final TypeSignature sig = TypeSignature.parse(attr.signature()); + assertEquals(attr.signature(), sig.asInternal(), failureMsg); + } + + private void testMethod(final ClassAttributes currentClass, final MemberAttributes attr) { + final Supplier failureMsg = () -> "Failed for method: " + attr.name() + " in class: " + currentClass.name(); + final MethodDescriptor desc = MethodDescriptor.parse(attr.descriptor()); + assertEquals(attr.descriptor(), desc.asInternal(), failureMsg); + + if (attr.signature() == null) { + return; + } + + final MethodSignature sig = MethodSignature.parse(attr.signature()); + assertEquals(attr.signature(), sig.asInternal(), failureMsg); + } + + private void testLocal(final ClassAttributes currentClass, final MemberAttributes currentMethod, final MemberAttributes attr) { + final Supplier failureMsg = () -> "Failed for local: " + attr.name() + " in method: " + currentMethod.name() + " in class: " + currentClass.name(); + final TypeDescriptor desc = TypeDescriptor.parse(attr.descriptor()); + assertEquals(attr.descriptor(), desc.asInternal(), failureMsg); + + if (attr.signature() == null) { + return; + } + + final TypeSignature sig = TypeSignature.parse(attr.signature()); + assertEquals(attr.signature(), sig.asInternal(), failureMsg); + } + + private static final QName nameAttr = new QName("name"); + private static final QName descAttr = new QName("descriptor"); + private static final QName sigAttr = new QName("signature"); + + private ClassAttributes getClassAttributes(final StartElement element) { + final String className = element.getAttributeByName(nameAttr).getValue(); + final Attribute classSigAttr = element.getAttributeByName(sigAttr); + final String classSig; + if (classSigAttr != null) { + classSig = classSigAttr.getValue(); + } else { + classSig = null; + } + return new ClassAttributes(className, classSig); + } + + private MemberAttributes getMemberAttributes(final StartElement element) { + final String memberName = element.getAttributeByName(nameAttr).getValue(); + final String memberDesc = element.getAttributeByName(descAttr).getValue(); + final Attribute memberSigAttr = element.getAttributeByName(sigAttr); + final String memberSig; + if (memberSigAttr != null) { + memberSig = memberSigAttr.getValue(); + } else { + memberSig = null; + } + return new MemberAttributes(memberName, memberDesc, memberSig); + } + + private record ClassAttributes(String name, @Nullable String signature) {} + private record MemberAttributes(String name, String descriptor, @Nullable String signature) {} +} diff --git a/hypo-types/src/test/java/dev/denwav/hypo/types/pattern/TypeCaptureTest.java b/hypo-types/src/test/java/dev/denwav/hypo/types/pattern/TypeCaptureTest.java new file mode 100644 index 0000000..9a752b0 --- /dev/null +++ b/hypo-types/src/test/java/dev/denwav/hypo/types/pattern/TypeCaptureTest.java @@ -0,0 +1,35 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types.pattern; + +import java.util.HashSet; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +class TypeCaptureTest { + + @Test + void testKeyGen() { + final HashSet set = new HashSet<>(); + for (int i = 0; i < 250_000; i++) { + assertTrue(set.add(TypeCapture.randomKey()), "After: " + i); + } + } +} diff --git a/hypo-types/src/test/java/dev/denwav/hypo/types/pattern/TypePatternTest.java b/hypo-types/src/test/java/dev/denwav/hypo/types/pattern/TypePatternTest.java new file mode 100644 index 0000000..249c445 --- /dev/null +++ b/hypo-types/src/test/java/dev/denwav/hypo/types/pattern/TypePatternTest.java @@ -0,0 +1,391 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.types.pattern; + +import dev.denwav.hypo.types.PrimitiveType; +import dev.denwav.hypo.types.TypeRepresentable; +import dev.denwav.hypo.types.VoidType; +import dev.denwav.hypo.types.desc.ArrayTypeDescriptor; +import dev.denwav.hypo.types.desc.ClassTypeDescriptor; +import dev.denwav.hypo.types.desc.MethodDescriptor; +import dev.denwav.hypo.types.desc.TypeDescriptor; +import dev.denwav.hypo.types.parsing.JvmTypeParser; +import dev.denwav.hypo.types.sig.ArrayTypeSignature; +import dev.denwav.hypo.types.sig.ClassSignature; +import dev.denwav.hypo.types.sig.ClassTypeSignature; +import dev.denwav.hypo.types.sig.MethodSignature; +import dev.denwav.hypo.types.sig.TypeSignature; +import dev.denwav.hypo.types.sig.param.BoundedTypeArgument; +import dev.denwav.hypo.types.sig.param.TypeParameter; +import dev.denwav.hypo.types.sig.param.TypeVariable; +import dev.denwav.hypo.types.sig.param.WildcardArgument; +import dev.denwav.hypo.types.sig.param.WildcardBound; +import java.lang.invoke.SerializedLambda; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Objects; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Stream; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DynamicContainer; +import org.junit.jupiter.api.DynamicNode; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class TypePatternTest { + + public static final String STRING = "java/lang/String"; + public static final String LIST = "java/util/List"; + public static final ClassTypeDescriptor STRING_DESC = ClassTypeDescriptor.of(STRING); + public static final ClassTypeSignature STRING_SIG = ClassTypeSignature.of(STRING); + public static final ClassTypeSignature LIST_SIG = ClassTypeSignature.of(LIST, List.of(ClassTypeSignature.of(STRING))); + + public static final TypeSignature NESTED_SIG = JvmTypeParser.parseTypeSignature("Ldev/denwav/hypo/types/pattern/A.B;", 0); + + public static final ArrayTypeDescriptor ARRAY_DESC = ArrayTypeDescriptor.of(1, PrimitiveType.BYTE); + public static final ArrayTypeSignature ARRAY_SIG = ArrayTypeSignature.of(1, PrimitiveType.BYTE); + + public static final TypeParameter TYPE_PARAM = TypeParameter.of("T"); + + public static final TypeVariable TYPE_VAR = TypeVariable.of(TYPE_PARAM); + public static final TypeVariable.Unbound TYPE_VAR_UNBOUND = TypeVariable.unbound("T"); + + public static Stream assignableTypes() { + return Stream.of( + PrimitiveType.INT, + STRING_DESC, + ARRAY_DESC, + STRING_SIG, + LIST_SIG, + NESTED_SIG, + ARRAY_SIG, + TYPE_VAR, + TYPE_VAR_UNBOUND + ); + } + + public static Stream returnableTypes() { + return Stream.concat(assignableTypes(), Stream.of(VoidType.INSTANCE)); + } + + public static Stream methodTypes() { + return Stream.of( + MethodDescriptor.of(List.of(), VoidType.INSTANCE), + MethodSignature.of(List.of(), List.of(), VoidType.INSTANCE, List.of()) + ); + } + + public static Stream otherTypes() { + return Stream.of( + TYPE_PARAM, + WildcardArgument.INSTANCE, + BoundedTypeArgument.of(WildcardBound.UPPER, STRING_SIG), + ClassSignature.of(List.of(), STRING_SIG, List.of()) + ); + } + + public static Stream allTypes() { + return Stream.of(returnableTypes(), methodTypes(), otherTypes()).flatMap(Function.identity()); + } + + private final List tests = new ArrayList<>(); + private Stream tests() { + return this.tests.stream().map(TestBuilder::build); + } + + private TestBuilder test(final TypePattern pattern) { + final TestBuilder test = new TestBuilder(pattern); + this.tests.add(test); + return test; + } + + @TestFactory + @DisplayName("Basic Type Pattern") + Stream basicTypes() { + this.test(TypePatterns.isType()) + .matches(returnableTypes()); + + return this.tests(); + } + + @TestFactory + @DisplayName("Primitive Type Patterns") + Stream primitivePatterns() { + this.test(TypePatterns.isBoolean()).matches(PrimitiveType.BOOLEAN); + this.test(TypePatterns.isChar()).matches(PrimitiveType.CHAR); + this.test(TypePatterns.isByte()).matches(PrimitiveType.BYTE); + this.test(TypePatterns.isShort()).matches(PrimitiveType.SHORT); + this.test(TypePatterns.isInt()).matches(PrimitiveType.INT); + this.test(TypePatterns.isLong()).matches(PrimitiveType.LONG); + this.test(TypePatterns.isFloat()).matches(PrimitiveType.FLOAT); + this.test(TypePatterns.isDouble()).matches(PrimitiveType.DOUBLE); + this.test(TypePatterns.isVoid()).matches(VoidType.INSTANCE); + + this.test(TypePatterns.isPrimitive()).matches(PrimitiveType.values()); + + this.test(TypePatterns.isIntegerType()) + .matches(PrimitiveType.BYTE, PrimitiveType.SHORT, PrimitiveType.INT, PrimitiveType.LONG); + this.test(TypePatterns.isFloatingPointType()) + .matches(PrimitiveType.FLOAT, PrimitiveType.DOUBLE); + + this.test(TypePatterns.isWide()) + .matches(PrimitiveType.LONG, PrimitiveType.DOUBLE); + + return this.tests(); + } + + @TestFactory + @DisplayName("Type Assignability Patterns") + Stream testAssignability() { + this.test(TypePatterns.isAssignable()) + .matches(assignableTypes()) + .notMatches(VoidType.INSTANCE) + .notMatches(Stream.concat(methodTypes(), otherTypes())); + + this.test(TypePatterns.isReturnable()) + .matches(returnableTypes()) + .notMatches(Stream.concat(methodTypes(), otherTypes())); + + return this.tests(); + } + + @TestFactory + @DisplayName("Class Patterns") + Stream testClasses() { + this.test(TypePatterns.isClass()) + .matches(STRING_DESC, STRING_SIG, LIST_SIG, NESTED_SIG); + + this.test(TypePatterns.isClassNamed(STRING)) + .matches(STRING_DESC, STRING_SIG); + + this.test(TypePatterns.isClassNamed(LIST)) + .matches(LIST_SIG); + + this.test(TypePatterns.isClassNamed(LIST::equals)) + .matches(LIST_SIG); + + this.test(TypePatterns.isReferenceType()) + .matches(assignableTypes().filter(isNot(PrimitiveType.class))); + + return this.tests(); + } + + @TestFactory + @DisplayName("Array Patterns") + Stream testArray() { + this.test(TypePatterns.isArray()).matches(ARRAY_DESC, ARRAY_SIG); + this.test(TypePatterns.isArray(1)).matches(ARRAY_DESC, ARRAY_SIG); + this.test(TypePatterns.isArray(TypePatterns.isByte())).matches(ARRAY_DESC, ARRAY_SIG); + this.test(TypePatterns.isArray(1, TypePatterns.isByte())).matches(ARRAY_DESC, ARRAY_SIG); + this.test(TypePatterns.isArray(1, TypePatterns.isClass())).notMatches(ARRAY_DESC, ARRAY_SIG); + + return this.tests(); + } + + @TestFactory + @DisplayName("TypeDescriptor Pattern") + Stream testTypeDesc() { + this.test(TypePatterns.isTypeDescriptor()) + .matches(STRING_DESC, ARRAY_DESC) + .matches(PrimitiveType.values()) + .matches(VoidType.INSTANCE); + + return this.tests(); + } + + @TestFactory + @DisplayName("TypeSignature Pattern") + Stream testTypeSig() { + this.test(TypePatterns.isTypeSignature()) + .matches(STRING_SIG, LIST_SIG, NESTED_SIG, ARRAY_SIG, TYPE_VAR, TYPE_VAR_UNBOUND) + .matches(PrimitiveType.values()) + .matches(VoidType.INSTANCE); + + this.test(TypePatterns.isTypeSignature(s -> s.asInternal().equals("L" + STRING + ";"))) + .matches(STRING_SIG); + + return this.tests(); + } + + @TestFactory + @DisplayName("TypeSignature::TypeArgument Patterns") + Stream testTypeArgs() { + this.test(TypePatterns.hasTypeArguments()) + .matches(LIST_SIG, NESTED_SIG); + + this.test(TypePatterns.hasNoTypeArguments()) + .matches(STRING_DESC, STRING_SIG); + + this.test(TypePatterns.hasTypeArguments(TypePatterns.isClass())) + .matches(LIST_SIG); + this.test(TypePatterns.hasTypeArguments(TypePatterns.isClassNamed(STRING))) + .matches(LIST_SIG); + this.test(TypePatterns.hasTypeArguments(TypePatterns.isClassNamed(LIST))) + .notMatches(allTypes()); + + return this.tests(); + } + + @TestFactory + @DisplayName("TypeSignature::Owner Pattern") + Stream testOwnerIs() { + this.test(TypePatterns.ownerIs(TypePattern.any())) + .matches(NESTED_SIG); + + return this.tests(); + } + + private static Predicate is(final Class clazz) { + return t -> clazz.isAssignableFrom(t.getClass()); + } + private static Predicate isNot(final Class clazz) { + return is(clazz).negate(); + } + + static class TestBuilder { + private final TypePattern pattern; + + private final List ensureSafe = new ArrayList<>(); + private final List testMatches = new ArrayList<>(); + private final List testNotMatches = new ArrayList<>(); + + TestBuilder(final TypePattern pattern) { + this.pattern = pattern; + } + + TestBuilder safe(final TypeRepresentable... test) { + return this.safe(Arrays.asList(test)); + } + TestBuilder safe(final Stream test) { + return this.safe(test.toList()); + } + TestBuilder safe(final Iterable test) { + for (final TypeRepresentable t : test) { + this.ensureSafe.add(t); + } + return this; + } + + TestBuilder matches(final TypeRepresentable... test) { + return this.matches(Arrays.asList(test)); + } + TestBuilder matches(final Stream test) { + return this.matches(test.toList()); + } + TestBuilder matches(final Iterable test) { + for (final TypeRepresentable t : test) { + this.testMatches.add(t); + } + return this; + } + + TestBuilder notMatches(final TypeRepresentable... test) { + return this.notMatches(Arrays.asList(test)); + } + TestBuilder notMatches(final Stream test) { + return this.notMatches(test.toList()); + } + TestBuilder notMatches(final Iterable test) { + for (final TypeRepresentable t : test) { + this.testNotMatches.add(t); + } + return this; + } + + enum Expect { + MATCH, + NOT_MATCH, + IGNORE, + } + private Stream test(final List tests, final Expect expect) { + return tests.stream() + .map(t -> { + return DynamicTest.dynamicTest(t.asReadable(), () -> { + switch (expect) { + case MATCH -> assertTrue(this.pattern.match(t).matches(), "Pattern should match: " + t); + case NOT_MATCH -> assertFalse(this.pattern.match(t).matches(), "Pattern should not match: " + t); + case IGNORE -> assertDoesNotThrow(() -> this.pattern.match(t), "Pattern should not fail: " + t); + } + }); + }); + } + + DynamicNode build() { + this.safe(allTypes()); + this.notMatches(allTypes() + .filter(Predicate.not(this.testMatches::contains)) + .toList()); + + return DynamicContainer.dynamicContainer(this.getPatternName(), Stream.of( + DynamicContainer.dynamicContainer("Safe", this.test(this.ensureSafe, Expect.IGNORE)), + DynamicContainer.dynamicContainer("Should Match", this.test(this.testMatches, Expect.MATCH)), + DynamicContainer.dynamicContainer("Should Not Match", this.test(this.testNotMatches, Expect.NOT_MATCH)) + )); + } + + private String getPatternName() { + try { + final Method writeReplace = this.pattern.getClass().getDeclaredMethod("writeReplace"); + writeReplace.setAccessible(true); + final SerializedLambda sl = (SerializedLambda) writeReplace.invoke(this.pattern); + + final String ownedClass = sl.getImplClass(); + final String simpleName = ownedClass.substring(ownedClass.lastIndexOf('/') + 1); + + final String fullMethodName = sl.getImplMethodName(); + final String methodName = fullMethodName.split("\\$", 3)[1]; + + final MethodDescriptor methodImpl = MethodDescriptor.parse(sl.getImplMethodSignature()); + final int captured = methodImpl.getParameters().size() - 2; + final ArrayList capturedTypes = new ArrayList<>(); + for (int i = 0; i < captured; i++) { + capturedTypes.add(methodImpl.getParameters().get(i)); + } + + final var capturedParams = Stream.of(Class.forName(ownedClass.replace('/', '.')).getMethods()) + .map(MethodDescriptor::of) + .map(MethodDescriptor::getParameters) + .filter(capturedTypes::equals) + .findFirst() + .orElseThrow(() -> new IllegalStateException("Could not find matching method")); + + if (capturedParams.isEmpty()) { + // zero-width space `\u200B` is needed for IntelliJ to render the () in the test output window + return simpleName + "." + methodName + "(\u200B)"; + } else { + final List p = capturedParams.stream() + .map(TypeRepresentable::asReadable) + .map(s -> s.substring(s.lastIndexOf('.') + 1)) + .toList(); + final String paramList = String.join(", ", p); + return simpleName + "." + methodName + "(" + paramList + ")"; + } + } catch (final Exception e) { + throw new RuntimeException(e); + } + } + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 033ae16..ca857df 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -6,12 +6,15 @@ include( "hypo-asm:hypo-asm-test-data", "hypo-core", "hypo-hydrate", + "hypo-hydrate:hypo-hydrate-test-data", "hypo-mappings", "hypo-meta:hypo-catalog", "hypo-meta:hypo-platform", "hypo-model", "hypo-test", "hypo-test:hypo-test-data", + "hypo-types", + "types-export", ) enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") diff --git a/types-export/build.gradle.kts b/types-export/build.gradle.kts new file mode 100644 index 0000000..dedb5fe --- /dev/null +++ b/types-export/build.gradle.kts @@ -0,0 +1,82 @@ +plugins { + java + `hypo-java` + `hypo-module` +} + +val spring: Configuration by configurations.creating +val guava: Configuration by configurations.creating +val eclipse: Configuration by configurations.creating + +dependencies { + implementation(projects.hypoAsm) + + implementation(libs.staxUtils) { + isTransitive = false + } + + spring("org.springframework.boot:spring-boot:3.4.0") + spring("org.springframework:spring-webmvc:6.2.1") + spring("org.springframework:spring-web:6.2.1") + + guava("com.google.guava:guava:33.3.1-jre") + + eclipse("org.eclipse.collections:eclipse-collections:12.0.0.M3") + eclipse("org.eclipse.jgit:org.eclipse.jgit:7.1.0.202411261347-r") + eclipse("org.eclipse.collections:eclipse-collections-api:12.0.0.M3") + eclipse("org.eclipse.emf:org.eclipse.emf.ecore:2.38.0") + eclipse("org.eclipse.platform:org.eclipse.core.runtime:3.32.0") + eclipse("org.eclipse.jdt:org.eclipse.jdt.core:3.40.0") +} + +tasks.withType().configureEach { + options.release = 21 +} + +val outputDir = layout.buildDirectory.dir("types-export") +val xmlDir = outputDir.map { it.dir("xmls") } +val runJvm by tasks.registering(JavaExec::class) { + javaLauncher = javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(21) + } + mainClass.set("dev.denwav.hypo.typesexport.Main") + classpath(sourceSets.main.get().runtimeClasspath) + args("jvm", "jvm.xml") + workingDir(xmlDir) + outputs.file(xmlDir.map { it.file("jvm.xml") }) +} + +fun createTypeExportTask(configuration: Configuration) = tasks.registering(JavaExec::class) { + javaLauncher = javaToolchains.launcherFor { + languageVersion = JavaLanguageVersion.of(21) + } + mainClass.set("dev.denwav.hypo.typesexport.Main") + classpath(sourceSets.main.get().runtimeClasspath) + args("jar", "${configuration.name}.xml") + systemProperty("hypo.jar.path", files(configuration).asPath) + workingDir(xmlDir) + outputs.file(xmlDir.map { it.file("${configuration.name}.xml") }) +} + +val runSpring by createTypeExportTask(spring) +val runGuava by createTypeExportTask(guava) +val runEclipse by createTypeExportTask(eclipse) + +val buildTypesExport by tasks.registering(Zip::class) { + dependsOn(runJvm, runSpring, runGuava, runEclipse) + + from(xmlDir) + + destinationDirectory = outputDir + archiveFileName = "types-export.zip" +} + +tasks.withType(DownloadJavadocListFiles::class) { + enabled = false +} +tasks.withType(PatchJavadocList::class) { + enabled = false +} +tasks.javadoc { + enabled = false +} diff --git a/types-export/src/main/java/dev/denwav/hypo/typesexport/Main.java b/types-export/src/main/java/dev/denwav/hypo/typesexport/Main.java new file mode 100644 index 0000000..1f94c6e --- /dev/null +++ b/types-export/src/main/java/dev/denwav/hypo/typesexport/Main.java @@ -0,0 +1,147 @@ +/* + * Hypo, an extensible and pluggable Java bytecode analytical model. + * + * Copyright (C) 2023 Kyle Wood (DenWav) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the Lesser GNU General Public License as published by + * the Free Software Foundation, version 3 of the License only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package dev.denwav.hypo.typesexport; + +import dev.denwav.hypo.asm.AsmClassData; +import dev.denwav.hypo.asm.AsmClassDataProvider; +import dev.denwav.hypo.core.HypoContext; +import dev.denwav.hypo.model.ClassProviderRoot; +import dev.denwav.hypo.model.data.ClassData; +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; +import java.util.stream.Stream; +import javanet.staxutils.IndentingXMLStreamWriter; +import javax.xml.stream.XMLOutputFactory; +import javax.xml.stream.XMLStreamException; +import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.FieldNode; +import org.objectweb.asm.tree.LocalVariableNode; +import org.objectweb.asm.tree.MethodNode; + +public final class Main { + + public static void main(final String[] args) throws IOException, XMLStreamException { + if (args.length < 2) { + throw new RuntimeException("No output file given"); + } + + final String mode = args[0]; + final Path outputFile = Path.of(args[1]).toAbsolutePath(); + Files.createDirectories(outputFile.getParent()); + + final var root = switch (mode) { + case "jvm" -> List.of(ClassProviderRoot.ofJdk()); + case "jar" -> ClassProviderRoot.fromJars(Stream.of(System.getProperty("hypo.jar.path").split(":")).map(Path::of).toArray(Path[]::new)); + default -> throw new IllegalStateException("Unknown mode: " + mode); + }; + + try ( + final BufferedWriter writer = Files.newBufferedWriter(outputFile, StandardCharsets.UTF_8) + ) { + final var factory = XMLOutputFactory.newFactory(); + final var streamWriter = new IndentingXMLStreamWriter(factory.createXMLStreamWriter(writer)); + + streamWriter.writeStartDocument(); + streamWriter.writeStartElement("classes"); + + try (final HypoContext ctx = HypoContext.builder() + .withProvider(AsmClassDataProvider.of(root)) + .build()) { + + final Iterator it = ctx.getProvider().stream() + .sorted(Comparator.comparing(ClassData::name)) + .iterator(); + + while (it.hasNext()) { + final AsmClassData clazz = (AsmClassData) it.next(); + final ClassNode node = clazz.getNode(); + if (node.fields.isEmpty() && node.methods.isEmpty()) { + continue; + } + + streamWriter.writeStartElement("class"); + + streamWriter.writeAttribute("name", node.name); + if (node.signature != null) { + streamWriter.writeAttribute("signature", node.signature); + } + + if (!node.fields.isEmpty()) { + streamWriter.writeStartElement("fields"); + for (final FieldNode field : node.fields) { + streamWriter.writeEmptyElement("field"); + streamWriter.writeAttribute("name", field.name); + streamWriter.writeAttribute("descriptor", field.desc); + if (field.signature != null) { + streamWriter.writeAttribute("signature", field.signature); + } + } + streamWriter.writeEndElement(); // fields + } + + if (!node.methods.isEmpty()) { + streamWriter.writeStartElement("methods"); + for (final MethodNode method : node.methods) { + if (method.localVariables != null && !method.localVariables.isEmpty()) { + streamWriter.writeStartElement("method"); + } else { + streamWriter.writeEmptyElement("method"); + } + streamWriter.writeAttribute("name", method.name); + streamWriter.writeAttribute("descriptor", method.desc); + if (method.signature != null) { + streamWriter.writeAttribute("signature", method.signature); + } + + if (method.localVariables != null) { + streamWriter.writeStartElement("locals"); + for (final LocalVariableNode local : method.localVariables) { + streamWriter.writeEmptyElement("local"); + streamWriter.writeAttribute("name", local.name); + streamWriter.writeAttribute("descriptor", local.desc); + if (local.signature != null) { + streamWriter.writeAttribute("signature", local.signature); + } + } + streamWriter.writeEndElement(); // locals + } + if (method.localVariables != null && !method.localVariables.isEmpty()) { + streamWriter.writeEndElement(); // method + } + } + streamWriter.writeEndElement(); // methods + } + + streamWriter.writeEndElement(); // class + } + } + + streamWriter.writeEndElement(); // classes + + streamWriter.writeEndDocument(); + streamWriter.close(); + } + } +}