Blog

Perl::Critic-Regeln umsetzen

01.12.2020 // Renée Bäcker

Gregor hat vor längerer Zeit das Modul Test::Perl::Critic::Progressive und ich vor kurzem das Modul PPI vorgestellt. In diesem Blogpost zeige ich, wie man mit PPI Perl::Critic-Regeln umsetzen kann, die dann in den Tests verwendet werden.

Nachdem ich eine Anwendung programmiert hatte, hat Gregor den Code angeschaut. Dabei ist er über dieses Stück Code gestolpert und meinte, es sähe schräg aus:

my ($doc) = grep{
    $_->{virtual_path} eq $virt_path
}@{ $app_config->{docs} || [] };

Für mich ist das völlig normal, ich nutze das häufiger. Aber ich gebe zu, dass man das schöner schreiben könnte.

Da man in diesem Beispiel nur an dem ersten Treffer interessiert ist, könnte man statt des grep die Funktion first aus dem Modul List::Util nutzen. Damit würde es dann schon so aussehen:

my $doc = first {
    $_->{virtual_path} eq $virt_path
}@{ $app_config->{docs} || [] };

Natürlich muss man am Anfang noch das Modul laden. Der Unterschied ist nur klein, aber für Einsteiger vielleicht doch einfacher, weil man hier nicht auf den Kontext achten muss. Beim grep gibt es unterschiedliche Ergebnisse, je nachdem ob man das Ergebnis in Skalar- oder im Listenkontext nutzt – einfach mal dieses Skript bei perlbanjo.com ausführen...

Das nächste was man schöner schreiben könnte, ist die Dereferenzierung der Arrayreferenz:

@{ $app_config->{docs} || [] }

Seit der Version 5.20 gibt es das (bis 5.24 experimentelle) Feature postderef . Da sieht die Schreibweise dann etwas schöner aus:

$app_config->{docs}->@*

Auch hier muss man am Anfang des Codes noch etwas einfügen, aber der Lesefluss ist in meinen Augen deutlich besser. Zum Schluss noch ein Leerzeichen zwischen das schließende } des greps und der Dereferenzierung. Insgesamt wäre es dann

my $doc = first {
    $_->{virtual_path} eq $virt_path
} $app_config->{docs}->@*;

Nachdem wir geklärt haben, welchen Code wir beanstanden wollen, und wie er schlussendlich aussehen soll, können wir uns an die Umsetzung der Regeln machen.

In meinen Augen liefert PPI::Dumper einen sehr guten Einstiegspunkt. Einfach den unerwünschten Code in allen möglichen Fassungen in den __DATA__-Bereich des Skriptes packen und ausführen. Damit sieht man, welche PPI-Knoten es gibt und an welchen man ansetzen muss. Der Dump des Perl Document Object Model (POM) in unserem Beispiel sieht auszugsweise folgendermaßen aus:

PPI::Document
  PPI::Token::Whitespace    '\n'
  PPI::Statement::Variable
    PPI::Token::Word    'my'
    PPI::Token::Whitespace      ' '
    PPI::Token::Symbol      '@list'
    PPI::Token::Whitespace      '   '
    PPI::Token::Operator    '='
    PPI::Token::Whitespace      ' '
    PPI::Token::Word    'grep'
    PPI::Token::Whitespace      ' '
    PPI::Structure::Block   { ... }

Bei jeder Perl::Critic-Regel muss man angeben, auf welchen Knoten die Regel angewandt werden soll. In diesem Fall ist das immer ein Knoten vom Typ PPI::Token::Word.

Gehen wir einen Teil nach dem anderen durch. Zuerst wollen wir first statt grep nutzen, wenn auf der linken Seite nur eine einelementige Liste angegeben ist.

Die passenden Beispiele dazu sind:

my @list   = grep { ausdruck() } @liste;           # grep ok
my $list   = grep { ausdruck() } @liste;           # grep ok
my ($list) = grep { ausdruck() } @liste;           # grep not ok
my ($list, $second) = grep { ausdruck() } @liste;  # grep ok
if( grep{ ausdruck() }@liste ) {                   # grep ok
}
# ggf. noch weitere Beispiele

Wenn wir uns den Dump anschauen, merken wir folgendes:

  • Wenn ein PPI::Token::Word mit dem Wert grep als Elternelement ein PPI::Statement::Variable hat, dann ist das potentiell interessant
  • Vergleiche Varianten der Variablenliste:

    PPI::Token::Whitespace      '\n'
    PPI::Statement::Variable
      PPI::Token::Word      'my'
      PPI::Token::Whitespace    ' '
      PPI::Structure::List      ( ... )
        PPI::Statement::Expression
          PPI::Token::Symbol    '$list'
    
    # vs
    
    PPI::Statement::Variable
      PPI::Token::Word      'my'
      PPI::Token::Whitespace    ' '
      PPI::Structure::List      ( ... )
        PPI::Statement::Expression
          PPI::Token::Symbol    '$list'
          PPI::Token::Operator      ','
          PPI::Token::Whitespace    ' '
          PPI::Token::Symbol    '$second'
    

    Es muss also eine Liste mit genau einem PPI::Token::Symbol sein, dessen Sigil ein $ ist.

Schauen wir uns das Grundgerüst für eine Perl::Critic-Regel an:

package Perl::Critic::Policy::PerlAcademy::ProhibitGrepToGetFirstFoundElement;

# ABSTRACT: Use List::Utils 'first' instead of grep if you want to get the first found element

use 5.006001;
use strict;
use warnings;
use Readonly;

use Perl::Critic::Utils qw{ :severities };

use base 'Perl::Critic::Policy';

our $VERSION = '2.02';

#-----------------------------------------------------------------------------

Readonly::Scalar my $DESC => q{Use List::Utils 'first' instead of grep if you want to get the first found element};
Readonly::Scalar my $EXPL => [  ]; 

#-----------------------------------------------------------------------------

sub default_severity     { return $SEVERITY_MEDIUM           }
sub default_themes       { return qw<perl_academy> }
sub applies_to           {
    return qw<
        PPI::Token::Word
    >;
}

Das kann so 1:1 kopiert werden. Der Paketnamen muss angepasst werden sowie die Beschreibungen.

Bei der default_severity muss man sich überlegen, wie wichtig einem diese Regel ist. Regeln können in sogenannte Themes eingeteilt werden.

Mit applies_to legt man eine Liste von Klassennamen fest, auf deren Objekte die Regel angewandt wird. Wir haben oben festgestellt, dass wir auf PPI::Token::Word-Objekte reagieren müssen.

Ob die Regel eingehalten wird, wird dann in der Methode violates geprüft. Ist alles ok, wird undef zurückgegeben, ansonsten ein Objekt, das die Regelmissachtung beschreibt:

sub violates {
    my ( $self, $elem, $doc ) = @_;

    # ... code zur pruefung der regel ...

    return $self->violation( $DESC, $EXPL, $elem );
}

Neben dem Objekt der Regel selbst bekommt man das Element (hier ein Objekt von PPI::Token::Word) und das PPI::Document-Objekt übergeben.

Als erstes prüfen wir, ob es ein grep-Befehl ist:

    # other statements than grep aren't catched
    return if $elem->content ne 'grep';

Dann interessieren wir uns nur für das grep, wenn das Ergebnis nicht in einer Variablen gespeichert wird. Dazu müssen wir in der Hierarchie des POM eine Stufe nach oben gehen:

    my $parent = $elem->parent;

    # grep in boolean or void context isn't checked
    return if !$parent->isa('PPI::Statement::Variable');

Und bei den Variablen muss eine Liste gegeben sein

    my $list = first{ $_->isa('PPI::Structure::List') } $parent->schildren;
    return if !$list;

Die Liste darf nur ein Element haben und das muss ein Scalar sein.

    my $symbols = $list->find('PPI::Token::Symbol');

    return if !$symbols;
    return if 1 != @{ $symbols };

    return if '$' ne substr $symbols->[0], 0, 1;

Sollte das alles zutreffen, ist das ein Regelverstoß:

    return $self->violation( $DESC, $EXPL, $elem );

Diese gesamte Regel ist mittlerweile auch auf CPAN bei meinen Regeln – und zwar als Perl::Critic::Policy::Reneeb::ProhibitGrepToGetFirstFoundElement – zu finden.

Die beiden anderen Punkte zum schöneren Code umzusetzen bleibt dem geneigten Leser überlassen.


Permalink:

Modul des Monats: PPI

21.10.2020 // Renée Bäcker

Only perl can parse Perl

Dieses Zitat von Tom Christiansen gilt in Teilen immer noch. Perl (die Sprache) ist eigentlich alles das, was perl (der Interpreter) sauber ausführen kann. Dass das nicht nur der Code mit der Syntax ist, die man so hinlänglich sieht, kann man an etlichen Acme::-Modulen sehen. So kann man wirklich sauberen Code mit Acme::Bleach schreiben. Das Modul wandelt normalen Perl-Code in Whitespaces um – und das Programm läuft immer noch. Ein weiteres Beispiel: Zu meinen Ausbildungszeiten in der Bioinformatik habe ich (nicht produktiven) Code mit Acme::DoubleHelix in eine Doppelhelix-Darstellung umgewandelt.

Um diese Darstellung von Code geht es aber nicht, wenn wir etwas mit PPI machen. Das Modul dient der statischen Analyse von Perl-Code. Zum Beispiel als Backend für Perl::Critic und das von Gregor letztens vorgestellte Test::Perl::Critic::Progressive. Da PPI eine statische Analyse durchführt, wird der Code nicht ausgeführt. Deshalb können manche Konstrukte aber nicht tiefergehend betrachtet werden:

require $module;

Welches Modul hier geladen wird, kann PPI nicht feststellen. Für viele Anwendungsbereiche ist das aber auch irrelevant. Es ermöglicht aber viele Anwendungen, ohne dass man Perl-Code ausführen muss oder selbst mit mehr oder weniger guten regulären Ausdrücken im Code etwas suchen muss.

Aber nicht nur wegen Perl::Critic ist PPI unser Modul des Monats. Es vereinfacht auch unsere Arbeit bei einigen Anwendungen.

Zum einen bei der Bereitstellung der Perl-API-Dokumentation der ((OTRS)) Community Edition unter https://otrs.perl-services.de aber auch bei der Anwendung zur Messung der Modul-Verbreitung unter https://usage.perl-academy.de .

Bei der OTRS-Dokumentation schauen wir im Code nach, ob es überhaupt POD in der Datei gibt und schmeißt den Rest quasi weg. Damit wird es einfacher, Teile der Dokumentation auseinanderzunehmen, ohne dass der restliche Code in die Quere kommt.

So sieht der entsprechende Code aus:

my @perl_files = $archive->membersMatching('\.(?:pm|pod)$');

FILE:
for my $file ( @perl_files ) {
    my $path = $file->fileName;

    next FILE if $path =~ m{cpan-lib};
    next FILE if $path !~ m{Kernel};

    my $code = $file->contents;
    my $doc  = PPI::Document->new( \$code );
    my $pod  = $doc->find( 'PPI::Token::Pod' );

    next FILE if !$pod;

    my $pod_string = join "\n\n", map{ $_->content }@{ $pod };
    my $package    = $doc->find_first('PPI::Statement::Package');

    $pod_string =~ m{
         ^=head\d+ \s+ DESCRIPTION \s+
         (?<description>.*?) \s+
         ^=(?:head|cut)
    }xms;

    my $description = $+{description} || '';
}

In $archive steckt ein Objekt von Archive::Zip und über das holen wir alle Perl-Module und POD-Dateien, die in der Zip-Datei stecken. Wir holen immer die Zip-Datei, damit wir nicht zig Git-Klone bei uns rumfliegen haben und wir die vielleicht gar nicht mehr brauchen.

Von jeder dieser Dateien holen wir mit ->contents den Inhalt der Datei. Mit PPI::Document->new( \\$code ) erzeugen wir dann das Objekt von PPI::Document und durch die Skalarreferenz weiß PPI, dass nicht eine Datei geparst werden soll, sondern der Inhalt der Skalarvariablen. Soll eine Datei geparst werden, nimmt man einfach PPI::Document->new( $path_to_file ) .

Anschließend suchen wir alle POD-Teile in der Datei (find), und wenn gar kein POD enthalten ist, brauchen wir gar nicht weiterzumachen. Der Methode find muss man den Paketnamen übergeben, nach dessen Objekten man sucht. Welche Klassen es gibt ist, wird in der PPI-Dokumentation gezeigt.

Da wir auch noch den Paketnamen brauchen, der im Perl-Code zu finden ist, suchen wir das erste Element der Klasse PPI::Statement::Package.

Somit haben wir dann alles, um das HTML aus POD zu generieren.

Die zweite Anwendung nutzt PPI, um cpanfiles zu parsen. Warum wir nicht das Modul Module::CPANfile nutzen? Wenn man sich das anschaut, lädt es den Perl-Code mit eval. Und da wir keine Kontrolle darüber haben, was die Besucher in die Dateien schreiben, die hochgeladen werden, nehmen wir lieber einen Parser zur statischen Analyse.

Schauen wir uns also mal an, was PPI aus einem cpanfile macht. Das cpanfile hat diesen Inhalt:

on 'test' => sub {
    requires 'Perl::Critic::RENEEB' => '2.01';
    requires "Proc::Background"     => 0;
    requires 'Test::BDD::Cucumber';
};

on 'runtime' => sub {
    requires 'Class::Unload';
};

on 'develop' => sub {
    requires 'CryptX' => '0.64';
    requires 'DBIx::Class::DeploymentHandler';
    requires q~Dist::Zilla~;
    requires MySQL::Workbench::DBIC => '1.13';
};

Um uns anzuschauen, wie das Perl Document Object Tree dazu aussieht, nutzen wir das folgende Skript (Perl::Critichat ein etwas mächtigeres Skript im Repository, das aber leider nicht installiert wird):

#!/usr/bin/perl
  
use strict;
use warnings;
use PPI;
use PPI::Dumper;
use Mojo::File qw(curfile);

curfile->sibling('examples')->list->first( sub {
    my $code = $_->slurp;
    my $doc  = PPI::Document->new( \$code );
    PPI::Dumper->new( $doc )->print and return;
});

Das erzeugt dann folgende Ausgabe:

PPI::Document
  PPI::Statement
    PPI::Token::Word    'on'
    PPI::Token::Whitespace      ' '
    PPI::Token::Quote::Single   ''test''
    PPI::Token::Whitespace      ' '
    PPI::Token::Operator    '=>'
    PPI::Token::Whitespace      ' '
    PPI::Token::Word    'sub'
    PPI::Token::Whitespace      ' '
    PPI::Structure::Block   { ... }
      PPI::Token::Whitespace    '\n'
      PPI::Token::Whitespace    '    '
      PPI::Statement
        PPI::Token::Word    'requires'
        PPI::Token::Whitespace      ' '
        PPI::Token::Quote::Single   ''Perl::Critic::RENEEB''
        PPI::Token::Whitespace      ' '
        PPI::Token::Operator    '=>'
        PPI::Token::Whitespace      ' '
        PPI::Token::Quote::Single   ''2.01''
        PPI::Token::Structure   ';'
      PPI::Token::Whitespace    '\n'
      PPI::Token::Whitespace    '    '
      PPI::Statement
        PPI::Token::Word    'requires'
        PPI::Token::Whitespace      ' '
        PPI::Token::Quote::Double   '"Proc::Background"'
        PPI::Token::Whitespace      '     '
        PPI::Token::Operator    '=>'
        PPI::Token::Whitespace      ' '
        PPI::Token::Number      '0'
        PPI::Token::Structure   ';'
      PPI::Token::Whitespace    '\n'
[...]

Wir wollen einfach die Modulnamen daraus haben, also der erste String nach dem Literal requires. An dem Dump kann man gut sehen, dass das Literal immer ein Objekt der Klasse PPI::Token::Word ist. Aber nicht jedes Objekt der Klasse enthält das Literal requires. Wir sehen zum Beispiel auch sub. Das müssen wir berücksichtigen. Nach dem Literal kommen beliebige Whitespaces, die uns aber nicht interessieren. PPI kennt da das Prinzip der significant siblings/children. Da in dem Baum die Elemente von requires und der Modulname auf einer Ebene sind, interessieren wir uns für das nächste signifikate Geschwister.

So sieht der Code dann in der Anwendung aus:

            my $code = $file->slurp;
            my $doc  = PPI::Document->new( \$code );

            my $requires = $doc->find(
                sub { $_[1]->isa('PPI::Token::Word') and $_[1]->content eq 'requires' }
            );

            if ( !$requires ) {
                $file->remove;
                return;
            }

            REQUIRED:
            for my $required ( @{ $requires || [] } ) {
                my $value = $required->snext_sibling;
                
                my $can_string = $value->can('string') ? 1 : 0;
                my $prereq     = $can_string ?
                    $value->string :
                    $value->content;
                
                save_prereq( $prereq );
            }

Der Anfang ist ähnlich zu oben. Wir lesen die Datei ein und erzeugen ein Objekt von PPI::Document. Während wir oben der Methode find nur die Klasse angegeben haben, deren Objekte wir alle haben wollen, möchten wir hier die Suche etwas einschränken. Nämlich alle PPI::Token::Word-Objekte, deren Inhalt required ist. Übergibt man eine anonyme Subroutine an find, wird für jedes Element unterhalb des zu durchsuchenden Knoten diese Subroutine aufgerufen.

Als Parameter werden zwei Parameter übergeben. Zum einen das Element auf dem die Suche ausgeführt wird und zum anderen das Element, das geprüft wird.

Die Methode content liefert den Code 1:1.

Für alle gefundenen requires bekommen wir die Objekte der Klasse PPI::Token::Word. Für jedes dieser Objekte wollen wir das erste signifikante Geschwister. Mit next_sibling würde man die Whitespaces bekommen, die interessieren uns aber nicht. Aus diesem Grund müssen wir snext_sibling verwenden.

Schauen wir noch mal kurz auf die verschiedenen Varianten der Modulangaben:

requires 'Perl::Critic::RENEEB' => '2.01';
requires "Proc::Background"     => 0;
requires q~Dist::Zilla~;
requires MySQL::Workbench::DBIC => '1.13';

... und die dazugehörigen PPI-Elemente:

PPI::Token::Quote::Single   ''Perl::Critic::RENEEB''
PPI::Token::Quote::Double   '"Proc::Background"'
PPI::Token::Quote::Literal  'q~Dist::Zilla~'
PPI::Token::Word            'MySQL::Workbench::DBIC'

Da die Whitespace-Knoten übergangen werden bekommen wir einen der hier gezeigten Knoten. Mit der Methode content bekommen wir den Wert wie er hier zu sehen ist, also inklusive der Anführungszeichen bzw. des q\~\~.

Wir müssen uns also auf die unterschiedlichen Methoden stützen, die die Objekte mitliefern. Bei den ganzen Quote::\*-Klassen kann die Methode string genutzt werden, bei PPI::Token::Word die Methode content.

Noch kurz zu den s...-Methoden: Als significant werden alle Teile bezeichnet, die für die Ausführung des Codes wichtig sind. Kommentare, POD-Dokumenation, __END__-Bereiche und Whitespaces gehören nicht dazu.

Die Skripte und Beispiele habe ich auch wieder ins Git-Repository gepackt.


Permalink:

Messung der Modul-Verwendung

17.08.2020 // Renée Bäcker

Mich selbst interessiert auch die Frage, welches Modul vom CPAN wird eigentlich besonders häufig genutzt? Da CPAN keine Daten dazu speichert, wie oft ein Modul heruntergeladen wird ist das nicht so einfach zu beantworten. Ein Tweet hat mich dazu gebracht eine kleine Anwendung zu schreiben, bei der jeder seine/ihre cpanfiles hochladen kann: https://usage.perl-academy.de.

Momentan werden dabei nur die requires ausgewertet.

Ja, so lange es nicht besonders weit verbreitet ist, ist die Datenlage sehr dünn und natürlich kann man das leicht manipulieren in dem man die eigenen cpanfiles mehrfach hochlädt. Um eine Idee von häufig genutzten Modulen zu bekommen, ist die Anwendung ganz nützlich.

Ich plane mit der Zeit noch einige statistische Auswertungen hinzuzufügen. Ich werde dann hier im Blog davon berichten. Bis dahin hoffe ich, dass noch einige cpanfiles hochgeladen werden. Jede/r kann mithelfen!

Mit was haben wir die Anwendung umgesetzt? Natürlich mit Mojolicious. Weiterhin kommen noch Minion als JobQueue und PPI zum Parsen der cpanfiles zum Einsatz. Diese beiden Module werden wir hier im Blog noch genauer vorstellen.


Permalink: