Repository navigation
Releases: SalyyS1/foliascan
Release list
v0.1.7 - reflectively chosen platform alternates are explained, not muted
Scanning FoliaLib itself reports 44 criticals, and every one of them is the correct non-Folia
branch. Neither provable guard shape can see why.
FoliaLib.createServerImpl does Class.forName(basePackage + implName), so nothing in the jar ever
writes new SpigotImplementation(), and no method of that class tests for Folia either — the test
lives in the factory, one indirection away, keyed by a string. That is the reflective-instantiation
limit this tool already documented, firing on the library a Folia-targeting developer is most likely
to point it at.
What is still visible is the shape: a class this jar never constructs, sitting next to a sibling
implementation of the same interface that does use the region API. Those findings now carry an
explanation naming the sibling and saying plainly that the analysis cannot prove the branch is
unreachable:
CRITICAL FS010 Legacy BukkitScheduler is dead on Folia (3 sites)
com.tcoded.folialib.impl.LegacySpigotImplementation#<init> (LegacySpigotImplementation.java:47)
never constructed in this jar, and FoliaImplementation implements the same interface using
the region API - if a factory picks between them at runtime this branch may be unreachable
on Folia, which this analysis cannot prove
Severity is deliberately unchanged. This is suggestive, not proof, and the last three releases
each showed that a wrong all-clear costs more than a wrong warning.
Verified on an eight-jar corpus of real plugins: identical finding counts, identical severity
breakdowns, and the note fires on none of them. It appears only where a jar really does pick
implementations reflectively — 42 findings in FoliaLib, zero everywhere else.
v0.1.6 - a metrics plugin's own metrics package is not a vendored library
A plugin with 539 stars, MIT, that declares folia-supported: true scanned completely clean on
v0.1.5. It should not have.
Its own package is de.sldk.mc.metrics — it is a Prometheus exporter, so metrics is its entire
domain — and metrics was on the list of names that mean "someone relocated a library here". All
32 of its collector classes were excluded from blame, hiding four criticals, including the
BukkitScheduler.scheduleSyncRepeatingTask call at Tps.java:38 that throws unconditionally on
Folia.
Removed metrics, plus external and internal from the build-folder list for the same reason:
no build tool generates either name, and plugins use both for their own code
(mythic.lib.glow.external). Relocated bStats is still caught by bstats.
Measured cost on the eleven-jar corpus: three extra findings, all inside relocated bStats
classes literally named Metrics, so they are attributable noise rather than mystery. AuraSkills —
the jar that justifies keeping a name list at all, since it relocates to .acf and .kyori with
no marker folder anywhere — stays at zero. Nothing else moved.
This is the third instance of one defect class, so it is worth naming plainly: a name-keyed
allowlist cannot tell "a copy of library X" from "my own package that happens to be about X".
Every generic word on such a list buys one relocation and silently exonerates every plugin that
uses the same word for its own code. The rule now is that an entry must be a library identity
(kyori, geantyref, nbtapi, bstats) and never a domain noun.
v0.1.5 - the relocation filter no longer hides your own code
The filter that keeps relocated dependencies out of the report decides what you never see, so a
wrong answer there is invisible in the output. This release measures it instead of trusting it:
a variant with the library-name list emptied, run over eleven real plugin jars, diffed
finding-by-finding.
The list earns its place. Without it, one jar produces 74 findings on clean code — it
relocates to dev.aurelium.auraskills.acf and .kyori with no marker folder anywhere. Deleting
the list is not the answer.
But four entries were domain nouns rather than library identities, and only ever matched
plugins' own packages: nbt hid mythiccrucible.skills.mechanics.nbt, annotations hid
mythicdungeons.api.annotations, and mysql/sqlite hid CoreTools' own database backends.
Removed, along with internal from the build-folder list — no build tool generates it and
plugins use it constantly. The distinctive name for each of those libraries is still listed
(nbtapi, tr7zw, querz, jetbrains), and a shaded JDBC driver is cheap to blame because a
driver never touches the Bukkit API.
A name match is now skipped when the segment before it says "adapter" (comp, compat,
hooks, integration, bridge, …). Plugins name their own integration packages after the
library they integrate with, so comp.protocollib is the plugin's code, not a copy of
ProtocolLib. The search continues past it, so a library genuinely relocated deeper
(compat/libs/kyori/…) is still found.
Net on the corpus: three findings recovered that were being silently hidden — including a
real Bukkit.getScheduler() call at io/lumine/mythic/lib/comp/protocollib/DamageParticleCap:46
— and no jar regressed. The remaining gap to a list-free run is three findings inside genuinely
relocated bStats, which is correct to exclude.
New unit tests cover each package layout, taken from the real jars they came from.
v0.1.4 - scanning a library no longer exonerates it
Supersedes v0.1.3 if you scan libraries. Plugin scanning is unaffected.
Scanning IF-0.12.0.jar reported nothing at all. The relocation filter matched
inventoryframework — a name plugins really do shade — against the jar's own root package and
excluded all 916 classes from blame. The same held for every library on the curated list:
pointing the scanner at cloud, Configurate or FoliaLib produced a clean bill of health by
construction, which is the worst possible failure mode for a tool whose output you are meant to
trust.
That filter exists to separate a plugin's own code from dependencies relocated inside it. It
now only matches segments below the plugin's root, and does not run at all on a jar with no
plugin descriptor — that jar is the library, not a plugin carrying one.
IF now reports the two Bukkit.getScheduler().runTask calls in GuiListener at lines 93 and
279, on the cancelled-click and close-navigation paths. Both throw on Folia, and both sit on the
common path of a GUI-heavy plugin.
Relocation exclusion is unchanged on real plugins, verified against the same jars as before:
com.magecore.libs, AuraSkills' five relocated subtrees and com.salyvn.slmmoenchant.libs are
still skipped, and every jar's finding counts are identical.
v0.1.3 - dual-platform plugins are no longer reported as broken
Accuracy fixes found by scanning real plugin jars
A plugin that supports both Paper and Folia is no longer told its working code will throw.
Dual-platform plugins have to contain legacy scheduler calls, and those calls are correct
precisely because they never run on Folia. Reporting them as critical was the most damaging
thing this tool could get wrong, and it fired on every well-built Folia plugin.
Both shapes real plugins use are now recognised:
- per method — one class, an
isFoliaflag, a branch in every method. Qualified by
requiring a region-scheduler call in the same method, so unconditional legacy code has
nothing to pair with the test. - per class — one interface, two implementations picked once at startup. Qualified by
requiring everynewof the class to sit in a method that tests for Folia, with the class
under test excluded from that search so a class namedFoliaSchedulercannot vacuously
guard itself.
Lambdas inherit the guard from the method that creates them, because a task built in the
legacy branch compiles into its own synthetic method and would otherwise look unguarded.
A guarded critical drops to info and names the guard rather than disappearing, so you can
still see the call and judge it yourself.
Verified against ten real plugin jars: one plugin's seven sites downgrade — its isFolia
field and per-method branch confirmed in the shipped bytecode with javap — and the other
nine jars are completely untouched, so the downgrade discriminates instead of muting.
Two limits are documented rather than hidden: a class built reflectively or by a DI container
has no visible new and stays reported, and there is no control-flow analysis, so a method
that tests for Folia and also calls the legacy scheduler outside the branch is treated as
guarded.
FS070 is now counted per class instead of per call site. An NMS adapter touches server
internals thousands of times by design; one jar alone produced 2457 findings and buried
everything else. Each finding now carries the distinct internal-type count.
FoliaScan 0.1.2
Fixes a false negative that made clean results untrustworthy on real plugins.
The scan root was derived from the main class's package. Plugins commonly park the entry class in a platform subpackage while keeping logic in siblings — AuraSkills declares dev.aurelium.auraskills.bukkit.AuraSkills but lives in common and api — so 325 of its 2751 classes were analysed and the tool reported zero Folia API usage on a plugin that bundles FoliaLib.
The root is now the longest package prefix shared by every class in the jar, cut back to whatever still lies on main's path, with a two-segment floor. The report prints the root it chose and how much of the jar it covers.
Because relocation frequently leaves no marker folder (dev.aurelium.auraskills.kyori), vendored detection now also matches a curated list of library leaf names. Every matched subtree is named in the notes, --exclude adds your own, --all turns it off. AuraSkills now scans 2745 of 2751 classes, names its 11 relocated subtrees, reports 51 Folia API call sites, and stays clean. Results for the other six real jars checked are unchanged.
18 rules, 16 tests. Requires Java 21+.
sha256 530610685a86162d8d55dd884b330414bbab2769a8e84654e15c50705f3ee963 foliascan-0.1.2.jar
FoliaScan 0.1.1
Adds FS002: reports when folia-supported: true was written into a jar by an automated patcher rather than by the plugin author.
That flag is the only thing Folia checks at load time, and a patcher can set it on the author's behalf without proving anything. FoliaPhantom / pasta's PluginPatcher#modifyPluginDescriptor appends or rewrites it unconditionally, and its own README disclaims thread-safety guarantees. FoliaScan now detects the runtime classes such a patcher bundles and says the flag is unproven instead of showing it as declared. Verified against 8 local jars that legitimately declare support: none are flagged.
Also in this release: the version the CLI prints now comes from the build rather than a duplicated constant, and a jar with no descriptor reports folia-supported as not applicable instead of as undeclared.
18 rules. Requires Java 21+.
java -jar foliascan-0.1.1.jar MyPlugin.jar
java -jar foliascan-0.1.1.jar /srv/mc/plugins --fail-on blockersha256 39f1b58c4406f05aa214bb60227bd3ff4d9542cd94d0eec8bdc89ba3e5aee9d8 foliascan-0.1.1.jar
FoliaScan 0.1.0
First release.
Reads a compiled Minecraft plugin jar and reports where it breaks under Folia's regionised threading. Requires Java 21+.
java -jar foliascan-0.1.0.jar MyPlugin.jar
java -jar foliascan-0.1.0.jar /srv/mc/plugins --fail-on blocker
java -jar foliascan-0.1.0.jar MyPlugin.jar --format sarif -o foliascan.sarif17 rules, each citing the file and class member in Folia's own patch set that justifies it — run --rules to read them. Highlights that catch people out: the legacy BukkitScheduler throws unconditionally on Folia, and the entire Scoreboard API is unimplemented.
Off-region analysis seeds on async task submission and propagates across in-jar call edges, so a helper called from an executor lambda is reported with its entry point and call depth. Relocated dependency packages are analysed but not blamed, so a bundled FoliaLib-style shim does not generate noise.
Console, JSON and SARIF output, CI exit codes, and line-independent baseline suppression.
A clean result means no enabled rule matched — it is not a proof of correctness. See the README for the documented limits.
sha256 04965e5641ebcd9941c681a345059d1d5ae955a4b9d24bd12fb29e34c8988663 foliascan-0.1.0.jar