From 56cac2318ec45351e0eb806b1e348e2c7af71135 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Wed, 29 Apr 2026 11:52:44 +0200 Subject: [PATCH] fix(parser+autoclean): ${'Pkg::'} treats ' as string literal; guard get_method_list Two fixes surfaced by `jcpan -t MooseX::Types`: 1. Parser: inside ${...}, a leading single quote starts a string literal, not the legacy $'foo => $main::foo package separator. Previously, \%{'Foo::'} parsed as the bogus identifier ::Foo::' and resolved to a different (empty) stash from \%{"Foo::"}. Net effect: keys %Foo:: listed entries that exists ${'Foo::'}{name} could not find, and delete ${'Pkg::'}{name} did not invalidate Pkg->can("name"). This broke MooseX::Types' t/16_introspection.t. Fix in IdentifierParser.parseComplexIdentifierInner: when insideBraces and the first token is ', return null so parseBracedVariable falls through to parseBlock and evaluates the string literal. 2. namespace::autoclean: _method_check unconditionally called $meta->get_method_list whenever Class::MOP::class_of returned a metaobject. For non-class packages that yields a bare Class::MOP::Package (no HasMethods mixin), producing a "Can't locate object method get_method_list" warning on every on_scope_end callback. Guard with $meta->can('get_method_list') and fall through to the plain-class detection otherwise. After: MooseX-Types 0.51 goes from Files=20, Tests=250, FAIL (1 fail + warning spam after every test) to PASS with no warnings. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- src/main/java/org/perlonjava/core/Configuration.java | 4 ++-- .../perlonjava/frontend/parser/IdentifierParser.java | 10 +++++++++- src/main/perl/lib/namespace/autoclean.pm | 7 ++++++- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index d8b6e0dfd..8fc6664d6 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,7 +33,7 @@ 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 = "cb3dcd790"; + public static final String gitCommitId = "36ce11560"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). @@ -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 29 2026 11:15:25"; + public static final String buildTimestamp = "Apr 29 2026 11:48:26"; // Prevent instantiation private Configuration() { diff --git a/src/main/java/org/perlonjava/frontend/parser/IdentifierParser.java b/src/main/java/org/perlonjava/frontend/parser/IdentifierParser.java index f36d8aeed..e23776ba2 100644 --- a/src/main/java/org/perlonjava/frontend/parser/IdentifierParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/IdentifierParser.java @@ -213,7 +213,11 @@ public static String parseComplexIdentifierInner(Parser parser, boolean insideBr return null; // Force fallback to expression parsing for unary plus + hash constructor } // Check if this is a leading single quote followed by an identifier ($'foo means $main::foo) - if (firstChar == '\'' && (nextToken.type == LexerTokenType.IDENTIFIER || nextToken.type == LexerTokenType.NUMBER)) { + // BUT: inside ${...}, a leading ' starts a string literal expression (e.g. ${'Foo::'}) + // and must not be treated as the legacy package separator. Returning null here forces + // parseBracedVariable to fall back to parseBlock, which evaluates the string literal. + if (firstChar == '\'' && !insideBraces + && (nextToken.type == LexerTokenType.IDENTIFIER || nextToken.type == LexerTokenType.NUMBER)) { // This is $'foo which means $main::foo // We convert it to ::foo internally (leading :: means main::) variableName.append("::"); @@ -221,6 +225,10 @@ public static String parseComplexIdentifierInner(Parser parser, boolean insideBr token = parser.tokens.get(parser.tokenIndex); nextToken = parser.tokens.get(parser.tokenIndex + 1); // Continue to parse the rest of the identifier - fall through to main loop + } else if (firstChar == '\'' && insideBraces) { + // Inside ${...}: the ' starts a string literal — fail identifier parsing so the + // caller falls through to parseBlock and evaluates 'Foo::' as a normal expression. + return null; } else { // Either it's a special variable like $' (postmatch), $| (autoflush), etc. // Consume the character from the token (which might be "|=" or just "|") diff --git a/src/main/perl/lib/namespace/autoclean.pm b/src/main/perl/lib/namespace/autoclean.pm index 72435220e..f89f481f4 100644 --- a/src/main/perl/lib/namespace/autoclean.pm +++ b/src/main/perl/lib/namespace/autoclean.pm @@ -118,7 +118,12 @@ sub _method_check { # For Moose/Moo classes, use the metaclass if available if (defined &Class::MOP::class_of) { my $meta = Class::MOP::class_of($package); - if ($meta) { + # Only metaclasses that mix in HasMethods (Class::MOP::Class and friends) + # implement get_method_list. A bare Class::MOP::Package instance does not, + # so guard with can() to avoid "Can't locate object method" errors during + # on_scope_end callbacks (seen with MooseX::Types loading namespace::autoclean + # in non-class packages). + if ($meta && $meta->can('get_method_list')) { my %methods = map +($_ => 1), $meta->get_method_list; $methods{meta} = 1 if $meta->isa('Moose::Meta::Role')