From acfcd68c8afee7952ca019d69706e4786f537d2a Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Wed, 29 Apr 2026 09:33:40 +0200 Subject: [PATCH] fix(filters): scope source filters per compilation unit Source filters installed via Filter::Util::Call::filter_add lived on a thread-local global stack and the "wasFilterInstalled" flag was never reset between files. As a result, a filter installed by file A leaked into any file A required. Concretely, `jcpan -t URI::git` failed because: use Spiffy -Base; # in Test/Base.pm calls Spiffy::spiffy_filter (which installs a regex filter that prepends "my $self = shift;" to every sub), then Spiffy's import calls Exporter::export(...), which lazily requires Exporter::Heavy. While Heavy.pm was being parsed, the Spiffy filter was still active and got applied to Heavy.pm's source, mangling heavy_export and producing the spurious 'my variable $self masks earlier declaration' warning at Exporter/Heavy.pm line 237. With heavy_export broken, `field` was never installed in Test::Base, so the next line field _filters => [qw(norm trim)]; parsed as two consecutive barewords and died with "syntax error near => [qw". Fix: treat the source-filter stack and the "installed during use" flag as part of the per-file compilation context. ModuleOperators now snapshots and resets that state before compiling a required/do'd file and restores it in finally, so filters installed by the caller cannot leak into the required file (and vice versa). Test plan: - `jcpan -t URI::git` now passes all 3 subtests - `make` (full unit-test suite) is green Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../org/perlonjava/core/Configuration.java | 6 +-- .../runtime/operators/ModuleOperators.java | 14 +++++- .../runtime/perlmodule/FilterUtilCall.java | 46 +++++++++++++++++++ 3 files changed, 62 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index 5831c4256..d73ae9cb5 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,14 +33,14 @@ public final class Configuration { * Automatically populated by Gradle/Maven during build. * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String gitCommitId = "d7eacf972"; + public static final String gitCommitId = "82e5e452d"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). * Automatically populated by Gradle/Maven during build. * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String gitCommitDate = "2026-04-28"; + public static final String gitCommitDate = "2026-04-29"; /** * Build timestamp in Perl 5 "Compiled at" format (e.g., "Apr 7 2026 11:20:00"). @@ -48,7 +48,7 @@ public final class Configuration { * Parsed by App::perlbrew and other tools via: perl -V | grep "Compiled at" * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String buildTimestamp = "Apr 28 2026 21:49:56"; + public static final String buildTimestamp = "Apr 29 2026 09:31:10"; // Prevent instantiation private Configuration() { diff --git a/src/main/java/org/perlonjava/runtime/operators/ModuleOperators.java b/src/main/java/org/perlonjava/runtime/operators/ModuleOperators.java index 4a345f932..edc2dec08 100644 --- a/src/main/java/org/perlonjava/runtime/operators/ModuleOperators.java +++ b/src/main/java/org/perlonjava/runtime/operators/ModuleOperators.java @@ -711,7 +711,14 @@ else if (code == null) { // Notify B::Hooks::EndOfScope that we're starting to load a file // This enables on_scope_end callbacks to know which file they belong to BHooksEndOfScope.beginFileLoad(parsedArgs.fileName); - + + // Source filters installed in the caller's file must not leak into + // the file being required/do'd. Save the outer filter state and start + // with a clean state for this compilation unit; restore on the way out. + org.perlonjava.runtime.perlmodule.FilterUtilCall.FilterStateSnapshot + filterSnapshot = org.perlonjava.runtime.perlmodule.FilterUtilCall + .saveAndResetFilterState(); + try { featureManager = new FeatureFlags(); @@ -755,6 +762,11 @@ else if (code == null) { // Restore the caller's hints hash hintHash.elements.clear(); hintHash.elements.putAll(savedHintHash); + + // Restore the caller's source-filter state (filters installed in + // the required file must not leak back to the caller). + org.perlonjava.runtime.perlmodule.FilterUtilCall + .restoreFilterState(filterSnapshot); } // Return result based on context diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/FilterUtilCall.java b/src/main/java/org/perlonjava/runtime/perlmodule/FilterUtilCall.java index a21fc7b44..440224c96 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/FilterUtilCall.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/FilterUtilCall.java @@ -468,6 +468,52 @@ public static void clearFilters() { context.currentLine = 0; } + /** + * Snapshot of filter state (stack + "installed during use" flag). + * Source filters are scoped to the file/compilation unit in which + * they were installed; when we begin parsing a new file (e.g. via + * {@code require}/{@code do}), we save the outer state, start with + * a clean state, and restore on the way out. + */ + public static class FilterStateSnapshot { + final RuntimeList filterStack; + final boolean installedDuringUse; + + FilterStateSnapshot(RuntimeList filterStack, boolean installedDuringUse) { + this.filterStack = filterStack; + this.installedDuringUse = installedDuringUse; + } + } + + /** + * Save the current filter state and reset to a clean state. + * Call this before compiling a new file (require/do); pair with + * {@link #restoreFilterState(FilterStateSnapshot)}. + */ + public static FilterStateSnapshot saveAndResetFilterState() { + FilterContext context = filterContext.get(); + FilterStateSnapshot snapshot = + new FilterStateSnapshot(context.filterStack, filterInstalledDuringUse.get()); + context.filterStack = new RuntimeList(); + context.sourceLines = null; + context.currentLine = 0; + filterInstalledDuringUse.set(false); + return snapshot; + } + + /** + * Restore filter state previously saved by + * {@link #saveAndResetFilterState()}. + */ + public static void restoreFilterState(FilterStateSnapshot snapshot) { + if (snapshot == null) return; + FilterContext context = filterContext.get(); + context.filterStack = snapshot.filterStack; + context.sourceLines = null; + context.currentLine = 0; + filterInstalledDuringUse.set(snapshot.installedDuringUse); + } + /** * Context for managing active source filters. */