扩展 MooseX::Declare 时注入代码中的语法错误
这是一件大事,所以请耐心等待。最后有一桶金。
出于主要实验原因,我正在尝试制作 MooseX::Declare 的自定义扩展,它可以实现一些对特定爱好项目有用的额外魔力。例如,我想让 class
关键字注入一些额外的东西,比如从 List::Util 等导入有用的实用程序,打开各种额外的编译指示(除了 strict 和
warnings
) ,自动导入我的全局 Config 对象,等等。
所以我编写了以下测试并开始看看是否可以让它工作。令人惊讶的是,我能够完成 99% 的任务,但现在我遇到了一个我无法解决的问题。我的自定义 class
关键字因注入代码中的语法错误而终止。
#!/usr/bin/env perl
use MyApp::Setup;
class Foo {
use Test::More tests => 1;
has beer => ( is => 'ro', default => 'delicious' );
method something {
is $self->beer, 'delicious';
}
}
Foo->new->something;
MyApp::Setup
如下所示。将来它会做更多的事情,但现在它只是在我的 MX::D 子类上调用 import
:
package MyApp::Setup;
use strict;
use warnings;
use MyApp::MooseX::Declare;
sub import {
goto &MyApp::MooseX::Declare::import;
}
1;
并且该类看起来像这样:
package MyApp::MooseX::Declare;
use Moose;
use MyApp::MooseX::Declare::Syntax::Keyword::Class;
use MyApp::MooseX::Declare::Syntax::Keyword::Role;
use MyApp::MooseX::Declare::Syntax::Keyword::Namespace;
extends 'MooseX::Declare';
sub import {
my ($class, %args) = @_;
my $caller = caller;
for my $keyword ( __PACKAGE__->keywords ) {
warn sprintf 'setting up keyword %s', $keyword->identifier;
$keyword->setup_for($caller, %args, provided_by => __PACKAGE__ );
}
}
sub keywords {
# override the 'class' keyword with our own
return
( MyApp::MooseX::Declare::Syntax::Keyword::Class->new( identifier => 'class' ),
MyApp::MooseX::Declare::Syntax::Keyword::Role->new( identifier => 'role' ),
MyApp::MooseX::Declare::Syntax::Keyword::Namespace->new( identifier => 'namespace' ) );
}
1;
我设置了三个关键字类以仅包含一个替代 MX::D::Syntax::NamespaceHandling 的额外角色。
package MyApp::MooseX::Declare::Syntax::Keyword::Class;
use Moose;
extends 'MooseX::Declare::Syntax::Keyword::Class';
with 'MyApp::MooseX::Declare::Syntax::NamespaceHandling';
1;
(其他两个是相同的。)
在真正的 MX::D 中,NamespaceHandling 内容被组合成一个名为 MooseSetup 的单独角色,该角色本身组合成关键字类。在一个地方完成这一切似乎是可行的;不过,我不知道结构上的轻微偏差是否是我的问题的根源。曾经我有自己的 MooseSetup 版本,但这导致了我无法弄清楚的组合冲突。
最后,最重要的是我的 NamespaceHandling 版本,它重写了 parse
方法。其中大部分内容只是从原始内容中复制粘贴的。
package MyApp::MooseX::Declare::Syntax::NamespaceHandling;
use Moose::Role;
use Carp 'croak';
use Moose::Util 'does_role';
use MooseX::Declare::Util 'outer_stack_peek';
with 'MooseX::Declare::Syntax::NamespaceHandling';
# this is where the meat is!
sub parse {
my ($self, $ctx) = @_;
# keyword comes first
$ctx->skip_declarator;
# read the name and unwrap the options
$self->parse_specification($ctx);
my $name = $ctx->namespace;
my ($package, $anon);
# we have a name in the declaration, which will be used as package name
if (defined $name) {
$package = $name;
# there is an outer namespace stack item, meaning we namespace below
# it, if the name starts with ::
if (my $outer = outer_stack_peek $ctx->caller_file) {
$package = $outer . $package
if $name =~ /^::/;
}
}
# no name, no options, no block. Probably { class => 'foo' }
elsif (not(keys %{ $ctx->options }) and $ctx->peek_next_char ne '{') {
return;
}
# we have options and/or a block, but not name
else {
$anon = $self->make_anon_metaclass
or croak sprintf 'Unable to create an anonymized %s namespace', $self->identifier;
$package = $anon->name;
}
warn "setting up package [$package]";
# namespace and mx:d initialisations
$ctx->add_preamble_code_parts(
"package ${package}",
sprintf(
"use %s %s => '%s', file => __FILE__, stack => [ %s ]",
$ctx->provided_by,
outer_package => $package,
$self->generate_inline_stack($ctx),
),
);
# handle imports and setup here (TODO)
# allow consumer to provide specialisations
$self->add_namespace_customizations($ctx, $package);
# make options a separate step
$self->add_optional_customizations($ctx, $package);
# finish off preamble with a namespace cleanup
# we'll use namespace::sweep instead
#$ctx->add_preamble_code_parts(
# $ctx->options->{is}->{dirty}
# ? 'use namespace::clean -except => [qw( meta )]'
# : 'use namespace::autoclean'
#);
# clean up our stack afterwards, if there was a name
$ctx->add_cleanup_code_parts(
['BEGIN',
'MooseX::Declare::Util::outer_stack_pop __FILE__',
],
);
# actual code injection
$ctx->inject_code_parts(
missing_block_handler => sub { $self->handle_missing_block(@_) },
);
# a last chance to change things
$self->handle_post_parsing($ctx, $package, defined($name) ? $name : $anon);
}
1;
当我运行测试时,一切似乎都很顺利——我收到警告消息,表明正在调用正确的方法并且正在设置包“Foo”。然后它就死了:
t/default.t 第 5 行“{package Foo”附近的语法错误
因此,似乎有东西在导致 package
声明之前或之后注入了一些代码语法错误,但我不知道是什么。我尝试过随机使用 parse
子项中的各个项目(我实际上不知道它们此时都做什么),但我似乎无法消除甚至更改错误。当然,(据我所知)没有办法实际检查生成的代码,这可能会产生线索。
感谢您的帮助。
一些更新: 在查看 MooseX::Declare::Context 内部之后,我添加了一些 print
语句来准确查看通过什么注射对 inject_code_parts
的调用。这是生成(整理)的实际代码:
package Foo;
use MyApp::MooseX::Declare outer_package => 'Foo', file => __FILE__, stack => [
MooseX::Declare::StackItem->new(q(identifier), q(class), q(handler),
q(MyApp::MooseX::Declare::Syntax::Keyword::Class), q(is_dirty), q(0),
q(is_parameterized), q(0), q(namespace), q(Foo)) ];;
BEGIN { Devel::Declare::Context::Simple->inject_scope('BEGIN {
MooseX::Declare::Util::outer_stack_pop __FILE__ }') }; ;
我不能说我知道所有这些代码的作用(尤其是 outer_stack_pop
),但对我来说,这一切在语法上看起来都没有问题。我仍然认为在这一切之前注入代码会导致语法错误。
This is a big one, so please bear with me. There's a pot of gold at the end.
For mostly experimental reasons, I'm trying to make a custom extension of MooseX::Declare that does some extra magic that is useful for a specific hobby project. For example, I want to make the class
keyword inject a bit of extra stuff, like importing useful utilities from List::Util and the like, turning on various extra pragmas (besides strict
and warnings
) , automatically import my global Config object, and so on.
So I wrote the following test and set off to see if I could get it to work. Amazingly, I was able to get 99% of the way there, but now I've run into a problem that I can't figure out. My custom class
keyword dies with a syntax error in the injected code.
#!/usr/bin/env perl
use MyApp::Setup;
class Foo {
use Test::More tests => 1;
has beer => ( is => 'ro', default => 'delicious' );
method something {
is $self->beer, 'delicious';
}
}
Foo->new->something;
MyApp::Setup
looks like the following. In the future it will do some more stuff, but right now it just calls import
on my MX::D subclass:
package MyApp::Setup;
use strict;
use warnings;
use MyApp::MooseX::Declare;
sub import {
goto &MyApp::MooseX::Declare::import;
}
1;
And that class looks like this:
package MyApp::MooseX::Declare;
use Moose;
use MyApp::MooseX::Declare::Syntax::Keyword::Class;
use MyApp::MooseX::Declare::Syntax::Keyword::Role;
use MyApp::MooseX::Declare::Syntax::Keyword::Namespace;
extends 'MooseX::Declare';
sub import {
my ($class, %args) = @_;
my $caller = caller;
for my $keyword ( __PACKAGE__->keywords ) {
warn sprintf 'setting up keyword %s', $keyword->identifier;
$keyword->setup_for($caller, %args, provided_by => __PACKAGE__ );
}
}
sub keywords {
# override the 'class' keyword with our own
return
( MyApp::MooseX::Declare::Syntax::Keyword::Class->new( identifier => 'class' ),
MyApp::MooseX::Declare::Syntax::Keyword::Role->new( identifier => 'role' ),
MyApp::MooseX::Declare::Syntax::Keyword::Namespace->new( identifier => 'namespace' ) );
}
1;
I set up the three keyword classes to just include an extra role that replaces MX::D::Syntax::NamespaceHandling
.
package MyApp::MooseX::Declare::Syntax::Keyword::Class;
use Moose;
extends 'MooseX::Declare::Syntax::Keyword::Class';
with 'MyApp::MooseX::Declare::Syntax::NamespaceHandling';
1;
(The other two are identical.)
In the real MX::D, the NamespaceHandling stuff is composed into a separate role called MooseSetup, which is itself composed into the keyword class. Doing it all in one place seems to work; I don't know if the slight deviation in structure is the source of my problem, though. At one point I had my own version of MooseSetup, but that led to composition conflicts that I couldn't figure out.
Finally, the meat and potatoes is my version of NamespaceHandling, which overrides the parse
method. The bulk of it is just copy-and-pasted from the original.
package MyApp::MooseX::Declare::Syntax::NamespaceHandling;
use Moose::Role;
use Carp 'croak';
use Moose::Util 'does_role';
use MooseX::Declare::Util 'outer_stack_peek';
with 'MooseX::Declare::Syntax::NamespaceHandling';
# this is where the meat is!
sub parse {
my ($self, $ctx) = @_;
# keyword comes first
$ctx->skip_declarator;
# read the name and unwrap the options
$self->parse_specification($ctx);
my $name = $ctx->namespace;
my ($package, $anon);
# we have a name in the declaration, which will be used as package name
if (defined $name) {
$package = $name;
# there is an outer namespace stack item, meaning we namespace below
# it, if the name starts with ::
if (my $outer = outer_stack_peek $ctx->caller_file) {
$package = $outer . $package
if $name =~ /^::/;
}
}
# no name, no options, no block. Probably { class => 'foo' }
elsif (not(keys %{ $ctx->options }) and $ctx->peek_next_char ne '{') {
return;
}
# we have options and/or a block, but not name
else {
$anon = $self->make_anon_metaclass
or croak sprintf 'Unable to create an anonymized %s namespace', $self->identifier;
$package = $anon->name;
}
warn "setting up package [$package]";
# namespace and mx:d initialisations
$ctx->add_preamble_code_parts(
"package ${package}",
sprintf(
"use %s %s => '%s', file => __FILE__, stack => [ %s ]",
$ctx->provided_by,
outer_package => $package,
$self->generate_inline_stack($ctx),
),
);
# handle imports and setup here (TODO)
# allow consumer to provide specialisations
$self->add_namespace_customizations($ctx, $package);
# make options a separate step
$self->add_optional_customizations($ctx, $package);
# finish off preamble with a namespace cleanup
# we'll use namespace::sweep instead
#$ctx->add_preamble_code_parts(
# $ctx->options->{is}->{dirty}
# ? 'use namespace::clean -except => [qw( meta )]'
# : 'use namespace::autoclean'
#);
# clean up our stack afterwards, if there was a name
$ctx->add_cleanup_code_parts(
['BEGIN',
'MooseX::Declare::Util::outer_stack_pop __FILE__',
],
);
# actual code injection
$ctx->inject_code_parts(
missing_block_handler => sub { $self->handle_missing_block(@_) },
);
# a last chance to change things
$self->handle_post_parsing($ctx, $package, defined($name) ? $name : $anon);
}
1;
When I run the test, everything seems to go great -- I get the warning messages indicating that the right methods are being called and that the package "Foo" is being set up. Then it dies with:
syntax error at t/default.t line 5, near "{package Foo"
So it seems like something is injecting some code right before or after the package
declaration that is causing a syntax error, but I can't figure out what. I've tried randomly playing with the various items in the parse
sub (I don't actually know what they all do at this point) but I can't seem to eliminate or even change the error. And of course there's no way (that I know of) to actually inspect the generated code, which might yield a clue.
Thanks for your help.
Some updates: After looking around inside MooseX::Declare::Context, I added some print
statements to see exactly what was being injected via the call to inject_code_parts
. This is the actual code that gets generated (tidied):
package Foo;
use MyApp::MooseX::Declare outer_package => 'Foo', file => __FILE__, stack => [
MooseX::Declare::StackItem->new(q(identifier), q(class), q(handler),
q(MyApp::MooseX::Declare::Syntax::Keyword::Class), q(is_dirty), q(0),
q(is_parameterized), q(0), q(namespace), q(Foo)) ];;
BEGIN { Devel::Declare::Context::Simple->inject_scope('BEGIN {
MooseX::Declare::Util::outer_stack_pop __FILE__ }') }; ;
I can't say I know what all that does (especially the outer_stack_pop
thing), but it all looks syntactically OK to me. I still think something is injecting code before all this that causes the syntax error.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(1)
好吧,那是一次地狱般的调试过程,但我最终找到了问题并解决了它。破解
MooseX::Declare::Context
和Devel::Declare::Context::Simple
(前代表)之后,我能够跟踪流程通过大量转储到 STDOUT,我意识到 MooseSetup.pm 中的一些额外处理程序(我认为我已正确地将其组合到我的关键字类中)实际上并不存在。因此,注入的结果代码没有附加适当的阴影/清理内容。不管怎样,我现在拥有了一个完全可以工作的定制 MooseX::Declare!我对此感到非常兴奋——这意味着我可以输入
,并且一个
class
语句会设置一整套特定于应用程序的样板文件。拉德。Well, that was a hell of a debugging session, but I finally traced the problem and got it figured out. After cracking open both
MooseX::Declare::Context
andDevel::Declare::Context::Simple
(to which the former delegates) I was able to trace the flow and through copious dumping to STDOUT I realized that some of the extra handlers from MooseSetup.pm, which I thought I had correctly composed into my keyword classes, were not actually there. The resulting code being injected thus did not have the proper shadow/cleanup stuff attached.Anyway, I now have what appears to be a fully working customized MooseX::Declare! I'm really psyched about this -- it means I can type
and that one
class
statement sets up a whole mess of application-specific boilerplate. Rad.