diff --git a/.github/RELEASING.md b/.github/RELEASING.md index f9d5607..abe60c2 100644 --- a/.github/RELEASING.md +++ b/.github/RELEASING.md @@ -91,17 +91,25 @@ Update those four in one commit: | `Writerside/v.list` | `` | | `aether-weaver-engine/…/engine/Weaver.java` | `static final String VERSION = "x.y.z";` — it is stamped into every weave record | +Two more the release workflow does not read, but that must move with these or the gate fails: the +agent's own `aether-weaver-agent/…/agent/WeaverAgent.java` `VERSION` — its start-up banner — and the +built-in plugin id in `aether-weaver-engine/…/engine/inject/CorePlugin.java`. `ExplainReportTest`, +`WeaverAgentEndToEndTest` and `WeaveMojoTest` pin the printed version, so they move with it too. + The poms are **not** touched. They stay on `-SNAPSHOT`; the workflow stamps the release version in from the tag. That way a build from `main` can never overwrite a released artefact. -**The IDE plugin has two more, and nothing checks either.** It is a separate Gradle build, so the -release workflow never sees it — but it ships to the Marketplace beside the release, and both of -these decide what it ships as. +**The IDE plugin has three more, and they are set in [step 6](#step-6--the-ide-plugin), not here.** +It is a separate Gradle build the release workflow never sees, but the `IntelliJ plugin` CI check +builds it, and it resolves `aether-weaver-api` and `-engine` as ordinary published dependencies. +Bumping these before the release is on Central makes that check fail to resolve the version — so set +them once Central serves it, in step 6, where the plugin is actually built. | File | What to change | What it breaks if you forget | |---|---|---| | `aether-weaver-ide/aether-weaver-idea/build.gradle.kts` | `version = "x.y.z"` | The archive uploads as `x.y.z-SNAPSHOT` | | `aether-weaver-ide/aether-weaver-idea/gradle.properties` | `aetherWeaverVersion=x.y.z` | The plugin bundles the API and engine it resolves, so it ships the `-SNAPSHOT` jars from whoever built it rather than the published ones | +| `aether-weaver-ide/aether-weaver-idea/sample/pom.xml` | `x.y.z` | `checkSampleVersion` fails the Gradle build: the sample would resolve a different API than the plugin was built against | Also worth doing: `Writerside/versions.json`, if the site is to offer a version switcher. @@ -158,7 +166,16 @@ shortly after. ordinary dependency and bundles it, so a build started before Central serves it either fails to resolve or quietly bundles something else. -With the two versions from step 1 set: +Set the three IDE-plugin versions now — they are left alone during step 1 because this build +resolves the published artefacts, so bumping them earlier fails the `IntelliJ plugin` CI check: + +| File | What to change | +|---|---| +| `aether-weaver-ide/aether-weaver-idea/build.gradle.kts` | `version = "x.y.z"` | +| `aether-weaver-ide/aether-weaver-idea/gradle.properties` | `aetherWeaverVersion=x.y.z` | +| `aether-weaver-ide/aether-weaver-idea/sample/pom.xml` | `x.y.z` | + +Then: ```bash cd aether-weaver-ide/aether-weaver-idea @@ -224,6 +241,8 @@ switcher offers. | Central rejects the deployment | Read the job log; it carries Central's own validation messages. Nothing was published | | Signing fails, or the job hangs | The passphrase secret or the key secret is wrong. The key must be armoured, whole, including its BEGIN and END lines | | The deployment sits in `VALIDATED` | `autoPublish` did not take effect. Publish it by hand in the portal, and fix the configuration before the next release | +| The publish job fails but the portal says `PUBLISHING` | The plugin stopped waiting; Central did not stop publishing. Nothing was lost and nothing may be re-uploaded. Wait for `PUBLISHED`, then create the GitHub Release by hand — the `github-release` job cannot be re-run alone, because it needs `publish`, and re-running failed jobs would deploy a second time | +| The deployment sits in `PUBLISHING` for over an hour | Sonatype's side. Check , then open a support ticket with the deployment id. Do not delete the tag and do not deploy again | | The release published, the GitHub Release did not | Re-run only the `github-release` job. Central is already done and cannot be redone | A released version is never re-released. If a release is wrong, release the next patch version. diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d75e4c..2ac8e59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,19 @@ reads it out of this file, so there is one copy of it and it cannot drift. Nothing yet. +## [0.1.1] - 2026-09-15 + +### Fixed + +- **Load-time weaving of application types now verifies.** On the load-time path, stack-map + generation and verification resolved the class hierarchy through the system class loader, so + a weave into a class whose method frames merge the application's own types was rejected — + dropped silently under `verification=report`, or reported as `AW4001` (or a + `Could not resolve class` from the Class-File API) under `verification=strict`. The defining + class loader is now threaded from the agent through the injection, merge and verification + steps, all of which build through `FrameSupport.forLoadTime(loader)`. Weaves whose signatures + reference only JDK types were unaffected, which is why the failure looked selective. + ## [0.1.0] - 2026-08-31 The first release. Everything below is new, so this entry describes the shape of the framework @@ -147,5 +160,6 @@ Stated here because a first release that hides its edges is worse than one that - **There is no Gradle plugin.** A Gradle build can declare and compile-check weaves; applying them means the agent or the runtime driver. -[Unreleased]: https://github.com/aether-framework/aether-weaver/compare/v0.1.0...HEAD +[Unreleased]: https://github.com/aether-framework/aether-weaver/compare/v0.1.1...HEAD +[0.1.1]: https://github.com/aether-framework/aether-weaver/compare/v0.1.0...v0.1.1 [0.1.0]: https://github.com/aether-framework/aether-weaver/releases/tag/v0.1.0 diff --git a/Writerside/v.list b/Writerside/v.list index f7d7302..dfb51a7 100644 --- a/Writerside/v.list +++ b/Writerside/v.list @@ -2,7 +2,7 @@ - + diff --git a/Writerside/writerside.cfg b/Writerside/writerside.cfg index 417a590..7886df6 100644 --- a/Writerside/writerside.cfg +++ b/Writerside/writerside.cfg @@ -9,5 +9,5 @@ - + diff --git a/aether-weaver-agent/src/main/java/de/splatgames/aether/weaver/agent/WeaverAgent.java b/aether-weaver-agent/src/main/java/de/splatgames/aether/weaver/agent/WeaverAgent.java index 599c712..b95cb5e 100644 --- a/aether-weaver-agent/src/main/java/de/splatgames/aether/weaver/agent/WeaverAgent.java +++ b/aether-weaver-agent/src/main/java/de/splatgames/aether/weaver/agent/WeaverAgent.java @@ -98,7 +98,7 @@ public final class WeaverAgent { /** The version this agent prints on its startup line. */ - private static final String VERSION = "0.1.0"; + private static final String VERSION = "0.1.1"; /** * Refuses instantiation. diff --git a/aether-weaver-agent/src/main/java/de/splatgames/aether/weaver/agent/WeavingTransformer.java b/aether-weaver-agent/src/main/java/de/splatgames/aether/weaver/agent/WeavingTransformer.java index 8c9649f..7fa7d00 100644 --- a/aether-weaver-agent/src/main/java/de/splatgames/aether/weaver/agent/WeavingTransformer.java +++ b/aether-weaver-agent/src/main/java/de/splatgames/aether/weaver/agent/WeavingTransformer.java @@ -135,7 +135,11 @@ final class WeavingTransformer implements ClassFileTransformer { return null; } try { - final byte[] woven = this.weaver.weave(className, buffer); + // The loader that defines this class is what lets the engine resolve the application's + // own types through their class-file resources when it regenerates stack maps. Dropping + // it here forces the frame builder onto a resolver that reflects over the system loader + // and cannot see host classes, which fails to verify any target whose frames merge them. + final byte[] woven = this.weaver.weave(className, buffer, loader); if (woven == null) { // Contract point 1. Returning `buffer` here would be correct-looking and would make // the JVM re-verify every class in the application. diff --git a/aether-weaver-agent/src/test/java/de/splatgames/aether/weaver/agent/WeaverAgentEndToEndTest.java b/aether-weaver-agent/src/test/java/de/splatgames/aether/weaver/agent/WeaverAgentEndToEndTest.java index 0082dd9..54eccf6 100644 --- a/aether-weaver-agent/src/test/java/de/splatgames/aether/weaver/agent/WeaverAgentEndToEndTest.java +++ b/aether-weaver-agent/src/test/java/de/splatgames/aether/weaver/agent/WeaverAgentEndToEndTest.java @@ -65,7 +65,7 @@ void theStartupLineIsPrinted() throws Exception { assertThat(result.output()) .as("a run whose configuration is not visible in its own log is a run whose " + "behaviour has to be reconstructed from the deployment") - .contains("Aether Weaver 0.1.0") + .contains("Aether Weaver 0.1.1") .contains("1 weave") .contains("fingerprint ") .contains("verification=strict"); diff --git a/aether-weaver-engine/src/main/java/de/splatgames/aether/weaver/engine/Weaver.java b/aether-weaver-engine/src/main/java/de/splatgames/aether/weaver/engine/Weaver.java index 8c7019e..55fe657 100644 --- a/aether-weaver-engine/src/main/java/de/splatgames/aether/weaver/engine/Weaver.java +++ b/aether-weaver-engine/src/main/java/de/splatgames/aether/weaver/engine/Weaver.java @@ -238,7 +238,7 @@ private DiagnosticListener onceFor(@NotNull final String key, } /** The weaver version, written into every weave record and printed by the explain report. */ - static final String VERSION = "0.1.0"; + static final String VERSION = "0.1.1"; /** * Every kind this release declares, in one place. @@ -300,6 +300,28 @@ public static WeaverBuilder builder() { */ public byte @Nullable [] weave(@NotNull final String internalName, final byte @NotNull [] original) { + return weave(internalName, original, null); + } + + /** + * Weaves a class whose bytes are already in hand, resolving its type hierarchy through the loader + * that defines it. + * + *

The loader is what lets load-time stack-map generation resolve the application's own types + * through their class-file resources without loading them. {@code null} stands for the bootstrap + * loader, for which the platform loader is used, exactly as an agent is handed {@code null} for a + * class the bootstrap loader defined. + * + * @param internalName the class's internal name, such as {@code com/acme/Ledger} + * @param original the class as it stands; not modified + * @param loader the loader that defines the class, whose resources describe the hierarchy, + * or {@code null} for the bootstrap loader + * @return the woven class, or {@code null} when it is to be used unchanged + * @throws NullPointerException if {@code internalName} or {@code original} is {@code null} + */ + public byte @Nullable [] weave(@NotNull final String internalName, + final byte @NotNull [] original, + @Nullable final ClassLoader loader) { Objects.requireNonNull(internalName, "internalName"); Objects.requireNonNull(original, "original"); this.statistics.seen(); @@ -307,9 +329,9 @@ public static WeaverBuilder builder() { final List entries = this.plan.entriesFor(internalName); final List dissolving = this.plan.structuralFor(internalName); if (entries.isEmpty() && dissolving.isEmpty()) { - return extensionsOnly(internalName, original); + return extensionsOnly(internalName, original, loader); } - return timed(internalName, () -> original, entries, dissolving); + return timed(internalName, () -> original, entries, dissolving, loader); } /** @@ -335,10 +357,10 @@ public static WeaverBuilder builder() { final List entries = this.plan.entriesFor(internalName); final List dissolving = this.plan.structuralFor(internalName); if (entries.isEmpty() && dissolving.isEmpty()) { - return this.extensions.isEmpty() ? null : extensionsOnly(internalName, original.get()); + return this.extensions.isEmpty() ? null : extensionsOnly(internalName, original.get(), null); } - return timed(internalName, original, entries, dissolving); + return timed(internalName, original, entries, dissolving, null); } /** @@ -353,12 +375,14 @@ public static WeaverBuilder builder() { * * @param internalName the class's internal name * @param original the class as it stands + * @param loader the loader that defines the class, or {@code null} for the bootstrap loader * @return the rewritten class, or {@code null} when neither rewrite applied; also the original * bytes, unrewritten, when a rewrite applied but the verifier refused the result under * {@code REPORT} */ private byte @Nullable [] extensionsOnly(@NotNull final String internalName, - final byte @NotNull [] original) { + final byte @NotNull [] original, + @Nullable final ClassLoader loader) { if (this.extensions.isEmpty()) { return null; } @@ -373,7 +397,7 @@ public static WeaverBuilder builder() { // the original, and a caller that wrote the returned bytes unconditionally still gets a // class that loads. return this.verifier.check(internalName, original, - guarded == null ? rewritten : guarded); + guarded == null ? rewritten : guarded, loader); } /** @@ -386,15 +410,17 @@ public static WeaverBuilder builder() { * @param original supplies the class as it stands * @param entries the injections planned for it * @param dissolving the weaves to merge into it + * @param loader the loader that defines the class, or {@code null} for the bootstrap loader * @return whatever {@link #apply} returned */ private byte @Nullable [] timed(@NotNull final String internalName, @NotNull final ByteSupplier original, @NotNull final List entries, - @NotNull final List dissolving) { + @NotNull final List dissolving, + @Nullable final ClassLoader loader) { final long started = System.nanoTime(); try { - return apply(internalName, original, entries, dissolving, started); + return apply(internalName, original, entries, dissolving, started, loader); } finally { this.statistics.spent(System.nanoTime() - started); } @@ -428,6 +454,7 @@ public static WeaverBuilder builder() { * @param entries the injections planned for it * @param dissolving the weaves to merge into it * @param started when timing began, for the JFR event + * @param loader the loader that defines the class, or {@code null} for the bootstrap loader * @return the woven class, {@code null} when it is to be used unchanged, or the original bytes * when a verifier refusal under {@code REPORT} left {@code checked != woven} */ @@ -435,7 +462,8 @@ public static WeaverBuilder builder() { @NotNull final ByteSupplier original, @NotNull final List entries, @NotNull final List dissolving, - final long started) { + final long started, + @Nullable final ClassLoader loader) { final byte[] bytes = original.get(); final ClassModel model = ClassFile.of().parse(bytes); @@ -478,7 +506,7 @@ public static WeaverBuilder builder() { final Reporter reporter = this.listener::report; byte[] current = bytes; if (!entries.isEmpty()) { - final byte[] woven = this.pipeline.weave(model, entries, this.groups, reporter); + final byte[] woven = this.pipeline.weave(model, entries, this.groups, reporter, loader); if (woven == null) { // Nothing resolved, or accounting refused it. The pipeline has already said why, // and the original class stands — never a half-woven one, because the pipeline @@ -489,7 +517,7 @@ public static WeaverBuilder builder() { } if (!dissolving.isEmpty()) { final byte[] merged = this.structural.apply( - ClassFile.of().parse(current), dissolving, reporter); + ClassFile.of().parse(current), dissolving, reporter, loader); if (merged == null && current == bytes) { return null; } @@ -520,7 +548,7 @@ public static WeaverBuilder builder() { // Gate 5: verify. Under REPORT this hands back the ORIGINAL, never the broken class. final byte[] woven = current; - final byte[] checked = this.verifier.check(internalName, bytes, woven); + final byte[] checked = this.verifier.check(internalName, bytes, woven, loader); if (checked != woven) { // Counted, not returned as a success. The class the caller gets back is the original // one, so a run that reported "42 classes woven" while a verifier had quietly handed diff --git a/aether-weaver-engine/src/main/java/de/splatgames/aether/weaver/engine/inject/CorePlugin.java b/aether-weaver-engine/src/main/java/de/splatgames/aether/weaver/engine/inject/CorePlugin.java index e2ceb1f..f4951e5 100644 --- a/aether-weaver-engine/src/main/java/de/splatgames/aether/weaver/engine/inject/CorePlugin.java +++ b/aether-weaver-engine/src/main/java/de/splatgames/aether/weaver/engine/inject/CorePlugin.java @@ -42,7 +42,7 @@ public final class CorePlugin implements WeaverPlugin { /** Identifies the plugin. The version is a literal here and is not read from the build. */ private static final PluginId ID = new PluginId( - PluginId.BUILT_IN_NAMESPACE, "Aether Weaver", "0.1.0"); + PluginId.BUILT_IN_NAMESPACE, "Aether Weaver", "0.1.1"); /** * Creates the plugin. diff --git a/aether-weaver-engine/src/main/java/de/splatgames/aether/weaver/engine/inject/WeavingPipeline.java b/aether-weaver-engine/src/main/java/de/splatgames/aether/weaver/engine/inject/WeavingPipeline.java index 1afc2c9..f3d4041 100644 --- a/aether-weaver-engine/src/main/java/de/splatgames/aether/weaver/engine/inject/WeavingPipeline.java +++ b/aether-weaver-engine/src/main/java/de/splatgames/aether/weaver/engine/inject/WeavingPipeline.java @@ -25,12 +25,12 @@ import de.splatgames.aether.weaver.engine.inject.point.ModelViews; import de.splatgames.aether.weaver.engine.inject.point.PointResolver; import de.splatgames.aether.weaver.engine.inject.point.Targets; +import de.splatgames.aether.weaver.engine.internal.transform.FrameSupport; import de.splatgames.aether.weaver.engine.internal.transform.LocalTable; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -import java.lang.classfile.ClassFile; import java.lang.classfile.ClassModel; import java.lang.classfile.ClassTransform; import java.lang.classfile.CodeElement; @@ -105,6 +105,28 @@ public WeavingPipeline(@NotNull final Function points, this.observer = Objects.requireNonNull(observer, "observer"); } + /** + * Weaves one class, resolving its type hierarchy through the bootstrap loader. + * + *

Equivalent to {@link #weave(ClassModel, List, List, Reporter, ClassLoader)} with a + * {@code null} loader: a caller with no defining loader to offer, such as a build-time driver, + * lets stack-map generation resolve through class-file resources on the bootstrap loader. + * + * @param model the class to weave; must not be {@code null} + * @param entries the declarations to apply, in the order they should be offered each element; + * must not be {@code null} + * @param groups the groups the entries are accounted against; must not be {@code null} + * @param reporter where to report; must not be {@code null} + * @return the woven bytes, or {@code null} when the class was not woven + * @throws NullPointerException if any argument is {@code null} + */ + public byte @Nullable [] weave(@NotNull final ClassModel model, + @NotNull final List entries, + @NotNull final List groups, + @NotNull final Reporter reporter) { + return weave(model, entries, groups, reporter, null); + } + /** * Weaves one class. * @@ -128,13 +150,16 @@ public WeavingPipeline(@NotNull final Function points, * must not be {@code null} * @param groups the groups the entries are accounted against; must not be {@code null} * @param reporter where to report; must not be {@code null} + * @param loader the loader that defines the target, whose resources describe the hierarchy the + * stack-map generator merges over; {@code null} for the bootstrap loader * @return the woven bytes, or {@code null} when the class was not woven - * @throws NullPointerException if any argument is {@code null} + * @throws NullPointerException if any argument but {@code loader} is {@code null} */ public byte @Nullable [] weave(@NotNull final ClassModel model, @NotNull final List entries, @NotNull final List groups, - @NotNull final Reporter reporter) { + @NotNull final Reporter reporter, + @Nullable final ClassLoader loader) { Objects.requireNonNull(model, "model"); Objects.requireNonNull(entries, "entries"); Objects.requireNonNull(groups, "groups"); @@ -226,10 +251,10 @@ public WeavingPipeline(@NotNull final Function points, final String contributed = contributedKinds(byMethod); if (contributed.isEmpty()) { - return emit(model, internalName, byMethod, reporter); + return emit(model, internalName, byMethod, reporter, loader); } return PluginIsolation.call(contributed, PluginIsolation.Phase.APPLY, reporter, - () -> emit(model, internalName, byMethod, reporter)).orElse(null); + () -> emit(model, internalName, byMethod, reporter, loader)).orElse(null); } /** @@ -247,12 +272,15 @@ public WeavingPipeline(@NotNull final Function points, * @param byMethod the declarations to apply, grouped by target method name; must not be * {@code null} * @param reporter where to report; must not be {@code null} + * @param loader the loader that defines the target, whose resources describe the hierarchy, + * or {@code null} for the bootstrap loader * @return the woven bytes, or {@code null} when the writer refused the result */ private byte @Nullable [] emit(@NotNull final ClassModel model, @NotNull final String internalName, @NotNull final Map> byMethod, - @NotNull final Reporter reporter) { + @NotNull final Reporter reporter, + @Nullable final ClassLoader loader) { ClassTransform transform = ClassTransform.ACCEPT_ALL; for (final Map.Entry> group : byMethod.entrySet()) { final String methodName = group.getKey(); @@ -265,7 +293,7 @@ public WeavingPipeline(@NotNull final Function points, CodeTransform.ofStateful(() -> compose(emissions, ranges)))); } try { - return ClassFile.of().transformClass(model, transform); + return FrameSupport.forLoadTime(loader).transformClass(model, transform); } catch (final IllegalArgumentException refused) { // The Class-File API validates while it writes, so the one limit an injection can // realistically cross — 65535 bytes of code in one method — surfaces here as an diff --git a/aether-weaver-engine/src/main/java/de/splatgames/aether/weaver/engine/merge/StructuralWeaver.java b/aether-weaver-engine/src/main/java/de/splatgames/aether/weaver/engine/merge/StructuralWeaver.java index 9ce8ecc..bff9b51 100644 --- a/aether-weaver-engine/src/main/java/de/splatgames/aether/weaver/engine/merge/StructuralWeaver.java +++ b/aether-weaver-engine/src/main/java/de/splatgames/aether/weaver/engine/merge/StructuralWeaver.java @@ -6,6 +6,7 @@ import de.splatgames.aether.weaver.api.model.HandlerRef; import de.splatgames.aether.weaver.api.spi.Reporter; import de.splatgames.aether.weaver.engine.internal.transform.ClassRemapper; +import de.splatgames.aether.weaver.engine.internal.transform.FrameSupport; import de.splatgames.aether.weaver.engine.model.WeaveClass; import de.splatgames.aether.weaver.engine.model.WeaveMember; import org.jetbrains.annotations.NotNull; @@ -63,6 +64,27 @@ public StructuralWeaver(@NotNull final WeaveBytes bytes) { this.bytes = Objects.requireNonNull(bytes, "bytes"); } + /** + * Applies every weave that dissolves into the given class, resolving the hierarchy through the + * bootstrap loader. + * + *

Equivalent to {@link #apply(ClassModel, List, Reporter, ClassLoader)} with a {@code null} + * loader, which is what a caller with no defining loader to offer, such as a build-time driver, + * passes. + * + * @param model the class to weave, as it stands; must not be {@code null} + * @param weaves the weaves that dissolve into it; must not be {@code null} + * @param reporter where refusals are reported; must not be {@code null} + * @return the rebuilt class, or {@code null} when a weave was refused or there was nothing to + * emit + * @throws NullPointerException if any argument is {@code null} + */ + public byte @Nullable [] apply(@NotNull final ClassModel model, + @NotNull final List weaves, + @NotNull final Reporter reporter) { + return apply(model, weaves, reporter, null); + } + /** * Applies every weave that dissolves into the given class. * @@ -75,13 +97,16 @@ public StructuralWeaver(@NotNull final WeaveBytes bytes) { * @param model the class to weave, as it stands; must not be {@code null} * @param weaves the weaves that dissolve into it; must not be {@code null} * @param reporter where refusals are reported; must not be {@code null} + * @param loader the loader that defines the target, whose resources describe the hierarchy the + * stack-map generator merges over; {@code null} for the bootstrap loader * @return the rebuilt class, or {@code null} when a weave was refused or there was nothing to * emit - * @throws NullPointerException if any argument is {@code null} + * @throws NullPointerException if any argument but {@code loader} is {@code null} */ public byte @Nullable [] apply(@NotNull final ClassModel model, @NotNull final List weaves, - @NotNull final Reporter reporter) { + @NotNull final Reporter reporter, + @Nullable final ClassLoader loader) { Objects.requireNonNull(model, "model"); Objects.requireNonNull(weaves, "weaves"); Objects.requireNonNull(reporter, "reporter"); @@ -103,7 +128,7 @@ public StructuralWeaver(@NotNull final WeaveBytes bytes) { return null; } - return ClassFile.of().build(model.thisClass().asSymbol(), builder -> { + return FrameSupport.forLoadTime(loader).build(model.thisClass().asSymbol(), builder -> { // Everything the target had, first and unchanged — except a field whose final flag a // @Shadow(mutable = true) asked to drop. A merged member that collided was already // refused, so nothing here can be overwritten by what follows. diff --git a/aether-weaver-engine/src/main/java/de/splatgames/aether/weaver/engine/verify/Verifier.java b/aether-weaver-engine/src/main/java/de/splatgames/aether/weaver/engine/verify/Verifier.java index b0e0ac3..246e70e 100644 --- a/aether-weaver-engine/src/main/java/de/splatgames/aether/weaver/engine/verify/Verifier.java +++ b/aether-weaver-engine/src/main/java/de/splatgames/aether/weaver/engine/verify/Verifier.java @@ -4,8 +4,10 @@ import de.splatgames.aether.weaver.api.diagnostic.DiagnosticCode; import de.splatgames.aether.weaver.api.diagnostic.WeaveException; import de.splatgames.aether.weaver.api.spi.DiagnosticListener; +import de.splatgames.aether.weaver.engine.internal.transform.FrameSupport; import org.jetbrains.annotations.Contract; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.lang.classfile.ClassFile; import java.util.List; @@ -92,6 +94,37 @@ public Verifier(@NotNull final VerificationPolicy policy, public byte[] check(@NotNull final String internalName, final byte @NotNull [] original, final byte @NotNull [] woven) { + return check(internalName, original, woven, null); + } + + /** + * Checks a woven class against the type hierarchy the given loader describes. + * + *

Identical to {@link #check(String, byte[], byte[])} except that {@link ClassFile#verify( + * byte[])} is run through a {@link FrameSupport#forLoadTime(ClassLoader)} context, so the + * assignability questions the verifier asks at frame merges are answered from the loader that + * defines the target rather than the system loader. Without it a woven class that merges the + * application's own types is reported as unverifiable purely because those types cannot be + * resolved, which is a false refusal rather than a real one. + * + * @param internalName the class's internal name, which the message names; must not be + * {@code null} + * @param original the class as it arrived, handed back when a refusal is not fatal; must + * not be {@code null} + * @param woven the class as weaving left it; must not be {@code null} + * @param loader the loader that defines the target, whose resources describe the + * hierarchy; {@code null} for the bootstrap loader + * @return {@code woven} when the policy checks nothing or nothing refuses it, and + * {@code original} when a refusal is not fatal + * @throws NullPointerException if {@code internalName}, {@code original} or {@code woven} is + * {@code null} + * @throws WeaveException if the policy is fatal and either check refuses the class + */ + @NotNull + public byte[] check(@NotNull final String internalName, + final byte @NotNull [] original, + final byte @NotNull [] woven, + @Nullable final ClassLoader loader) { Objects.requireNonNull(internalName, "internalName"); Objects.requireNonNull(original, "original"); Objects.requireNonNull(woven, "woven"); @@ -106,7 +139,10 @@ public byte[] check(@NotNull final String internalName, if (!structural.isEmpty()) { return refuse(structuralDiagnostic(internalName, structural), original); } - final List errors = ClassFile.of().verify(woven); + // Through the load-time context, so the verifier resolves the application's own types the + // same way stack-map generation did; a plain ClassFile.of() here uses the system loader and + // reports unresolvable application types as spurious verification errors. + final List errors = FrameSupport.forLoadTime(loader).verify(woven); if (errors.isEmpty()) { return woven; } diff --git a/aether-weaver-engine/src/test/java/de/splatgames/aether/weaver/engine/explain/ExplainReportTest.java b/aether-weaver-engine/src/test/java/de/splatgames/aether/weaver/engine/explain/ExplainReportTest.java index f2c89b5..467d642 100644 --- a/aether-weaver-engine/src/test/java/de/splatgames/aether/weaver/engine/explain/ExplainReportTest.java +++ b/aether-weaver-engine/src/test/java/de/splatgames/aether/weaver/engine/explain/ExplainReportTest.java @@ -40,8 +40,8 @@ void header() { final Weaver weaver = explaining(audit()); assertThat(weaver.explain().lines().findFirst().orElseThrow()) - .startsWith("Aether Weaver 0.1.0 — plan ") - .hasSizeGreaterThan("Aether Weaver 0.1.0 — plan ".length()); + .startsWith("Aether Weaver 0.1.1 — plan ") + .hasSizeGreaterThan("Aether Weaver 0.1.1 — plan ".length()); } @Test diff --git a/aether-weaver-maven-plugin/src/test/java/de/splatgames/aether/weaver/maven/WeaveMojoTest.java b/aether-weaver-maven-plugin/src/test/java/de/splatgames/aether/weaver/maven/WeaveMojoTest.java index 75e6c7b..a58e69e 100644 --- a/aether-weaver-maven-plugin/src/test/java/de/splatgames/aether/weaver/maven/WeaveMojoTest.java +++ b/aether-weaver-maven-plugin/src/test/java/de/splatgames/aether/weaver/maven/WeaveMojoTest.java @@ -161,7 +161,7 @@ void onGreet() { .as("this is the one driver whose report can be complete: the goal knows every " + "class it will ever be asked about, so by the time it prints, every " + "point has been resolved against real bytes") - .anyMatch(line -> line.startsWith("Aether Weaver 0.1.0 — plan ")) + .anyMatch(line -> line.startsWith("Aether Weaver 0.1.1 — plan ")) .anyMatch(line -> line.contains("Weaves (1):")) .anyMatch(line -> line.contains("1 site @")) .noneMatch(line -> line.contains("not woven yet"));