扩展 MooseX::Declare 时注入代码中的语法错误

发布于 2024-12-12 13:03:10 字数 6369 浏览 2 评论 0原文

这是一件大事,所以请耐心等待。最后有一桶金。

出于主要实验原因,我正在尝试制作 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 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

时间你老了 2024-12-19 13:03:10

好吧,那是一次地狱般的调试过程,但我最终找到了问题并解决了它。破解 MooseX::Declare::ContextDevel::Declare::Context::Simple (前代表)之后,我能够跟踪流程通过大量转储到 STDOUT,我意识到 MooseSetup.pm 中的一些额外处理程序(我认为我已正确地将其组合到我的关键字类中)实际上并不存在。因此,注入的结果代码没有附加适当的阴影/清理内容。

不管怎样,我现在拥有了一个完全可以工作的定制 MooseX::Declare!我对此感到非常兴奋——这意味着我可以输入

use MyApp::Setup; 

class MyApp::Foo { ... }

,并且一个 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 and Devel::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

use MyApp::Setup; 

class MyApp::Foo { ... }

and that one class statement sets up a whole mess of application-specific boilerplate. Rad.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文