独立解析Perl

在我编程生涯的几年里,我参与了一个为一家庞大的全球IT公司而做的相当不寻常的Web项目。由于一些奇怪的平台问题,我们只能用Perl编写项目内部网络的一半,而几乎完全相同的公共互联网的一半只能用Java编写。

在我努力学习足够的Java以便帮助我的Perl代码与Java团队代码交互的过程中,我偶然发现了一个名为JetBrains IntelliJ IDEA的相对较新的编辑器。

多么愉快!它使学习Java变得绝对愉快,具有全面的选项卡完成、轻量级且简单的API文档、轻松探索Java类库,以及不显眼的指示器,它会告诉我我的错误并建议修复它们。简而言之,它有很多智能和快速、干净的界面。

IntelliPerl在哪里?

尽管我仅重度使用了几个月,但自从那时起,它就成了我的黄金标准,以及我对“优秀”编辑器的定义。我安装了遇到的每一个新的Perl编辑器和IDE,希望能有一天Perl能有一个像Java多年来那样的优秀编辑器。

这些优秀的编辑器正在传播。Java现在有1.5个(Eclipse几乎很棒,但似乎还不够“轻松”)。Dreamweaver多年前就为HTML人员提供了优秀的编辑器,我听说Python现在可能也有类似的。

有趣的是,这些优秀的编辑器似乎有一个共同的主要特点。

如何构建一个优秀的编辑器

而不是依赖于语言的解析器来检查代码,优秀的编辑器似乎实现了自己的特殊解析器。这些解析器将文件视为更通用的文档(碰巧也是代码)。

这是一个关键的区别,提供了两个关键功能。

首先,它创建了一个“往返”功能,将文件解析成内部模型,然后再解析出来,而不会移动一个空格字符。即使文件的一部分是损坏的或格式错误,你仍然可以更改文件的其他部分并正确保存它,而不会更改你没有更改的内容。

其次,它使解析器非常安全和容错。任何在编辑器中打开的代码都有其存在的理由——通常是因为它还没有完成,是损坏的,或者需要更改。文档解析器可以遇到问题,标记它,跌跌撞撞地前进一个字符或几个字符,直到找到它认识的内容,然后继续。

将解析视为代码是完全不同的任务,通常不适合这些类型的错误。

例如,看看以下。

print "Hello World!\n";

}

MyModule->foobar;

对于使用Perl本身的编辑器来理解这段代码,一旦它遇到裸露的闭合花括号,就游戏结束了,因为代码是无效的。如果没有了解括号下面的内容,你将失去解析器所需的所有智能:语法高亮、模块检查、有用的提示,等等。

这根本不是构建编辑器的合理方式,在这种情况下,文件既可以是未完成的,也可能有数十个错误。

构建Perl的文档解析器

即使还没有编辑器来放置它(目前还没有),Perl的文档解析器对于各种任务都非常有用。然而,在当时,我真正想要的只是一个非常准确的HTML语法高亮器。

2002年初的一个下午,无聊之余,我第一次尝试解决这个问题。鉴于我看到其他人尝试同样的方法时出现的模式,结果是可以预见的。它基于正则表达式,对于任何有趣的事情来说都是无用的。

从那时到2004年12月《Perl基金会》资助项目开始,我每个月花一到两天的时间在这个问题上,重写和丢弃代码。我废弃了两个标记化器、一个词法分析器、一个分析包、三个语法高亮器、一个混淆包、一个引用引擎,以及当前对象树中一半的类。

现在,终于,除了一些小功能和测试之外,《PPI》已经完成。它是100%双向安全,并且已经针对CPAN中38,000个(非Acme)模块进行了压力测试,除了28个最损坏和奇怪的外,都能处理。

它做什么?

PPI应该是任何需要解析、分析或操作Perl的任务的基础,它最终提供了一个平台,可以充分发挥这些任务的全部潜力。这涵盖了大量可能的任务;太多以至于无法在此深入讨论。

对于这篇文章,我想展示PPI如何改善现有的工具,这些工具目前只做非常基础的工作,而潜在的可能性如此之大。

其中一个就是PAR应用程序打包模块的一部分。当PAR将模块打包到内部包含目录中时,它会尝试通过删除POD来减小模块的大小。当然,更好的做法是删除所有多余的东西,进一步减小PAR文件的大小。

这是一种压缩形式,但考虑到使用像“Compress::Perl”这样的名字可能会引起混淆,我选择了自己的术语。我宣布将这个术语定为“Squish”。一个压缩后的模块将尽可能少地占用空间,因为已经移除了冗余字符。它将会非常小,尽管看起来可能有点“压缩”的样子 :)

Perl::Squish

与其向您展示最终项目,我更愿意展示压缩单个模块的过程。

# Where is File::Spec on our system?
use Class::Inspector;
my $filename = Class::Inspector->resolved_filename( 'File::Spec' );

# Load File::Spec as a document
use PPI;
my $Document = PPI::Document->new( $filename );

您使用PPI做的一切都始于并结束于PPI::Document对象。如果您直接使用词法分析器,您可能正在做错事。

我该从哪里开始削减冗余?首先,许多核心模块都有一个__END__部分。

# Get the (one and only) __END__ section
my $End = $Document->find_first( 'Statement::End' );

# Delete it from the document
$End->delete if $End;

PPI提供了一组搜索方法,您可以在任何有子元素的对象上使用这些方法。find_first是一个安全的猜测,因为只有一个__END__部分。搜索方法实际上接收像File::Find这样的&wanted函数,所以'Statement::End'实际上是以下语法糖:

sub wanted {
    my ($Document, $Element) = @_;
    $Element->isa('PPI::Statement::End');
}

当然,有一种更快的方法来做同样的事情。prune方法找到并立即删除所有符合特定条件的元素。

# Delete all comments and POD
$Document->prune( 'Token::Pod' );
$Document->prune( 'Token::Comment' );

作为一个更严重的例子,下面是如何从->method()中去除非必需的大括号:

# Remove useless braces
$Document->prune( sub {
    my $Braces = $_[1];
    $Braces->isa('PPI::Structure::List')      or return '';
    $Braces->children == 0                    or return '';
    my $Method = $Braces->sprevious_sibling   or return '';
    $Method->isa('PPI::Token::Word')          or return '';
    $Method->content !~ /:/                   or return '';
    my $Operator = $Method->sprevious_sibling or return '';
    $Operator->isa('PPI::Token::Operator')    or return '';
    $Operator->content eq '->'                or return '';
    return 1;
    } );

虽然有点罗嗦,但写起来相对简单。只需添加条件并逐步丢弃。您可以获取其他元素,进行任何计算或调用子搜索。

完成之后,请务必保存文件。

# Save the file
$Document->save( "$filename.squish" );

总结

现在您需要做的只是将所有这些包裹在典型的模块样板中。

package Perl::Squish;

use strict;
use PPI;

our $VERSION = '0.01';

# Squish a file in place
# Perl::Squish->file( $filename )
sub file {
    my ($class, $file) = @_;
    my $Document = PPI::Document->new( $file ) or return undef;
    $class->document( $Document ) or return undef;
    $Document->save( $file );
}

# Squish a document object
# Perl::Squish->document( $Document );
sub document {
    my ($squish, $Document) = @_;

    # Remove the stuff we did earlier
    $Document->prune('Statement::End');
    $Document->prune('Token::Comment');
    $Document->prune('Token::Pod');

    $Document->prune( sub {
        my $Braces = $_[1];
        $Braces->isa('PPI::Structure::List')      or return '';
        $Braces->elements == 0                    or return '';
        my $Method = $Braces->sprevious_sibling   or return '';
        $Method->isa('PPI::Token::Word')          or return '';
        $Method->content !~ /:/                   or return '';
        my $Operator = $Method->sprevious_sibling or return '';
        $Operator->isa('PPI::Token::Operator')    or return '';
        $Operator->content eq '->'                or return '';
        return 1;
        } );

    # Let's also do some whitespace cleanup
    my @whitespace = $Document->find('Token::Whitespace');
    foreach ( @whitespace ) {
        $_->{content} = $_->{content} =~ /\n/ ? "\n" : " ";
    }

    1;
}

1;

最后,最后一步是将所有这些包裹成一个合适的模块。您可以在CPAN::Squish中看到经过PPI语法高亮器美化的最终产品。我在上述基本代码中添加了一些额外的功能,但您已经明白了。有关更多详细信息,请参阅Perl::Squish

在15分钟内,我创建了一个非常简单的模块,它显著提高了你可以在没有PPI这样的工具的情况下完成的事情。现在想象一下它使可能实现的困难事情。

标签

反馈

这篇文章有什么问题吗?请通过在GitHub上打开一个问题或拉取请求来帮助我们。