Monday, May 04, 2009

eigene Perl::Critic Policies

Im Zuge des EPO-Iron Mans werde ich auch vereinzelt Artikel aus früheren Ausgaben von "$foo - Perl-Magazin" posten.

Ich mache hier die Regeln

eigene Policies für Perl::Critic

Jedes Unternehmen hat eigene Programmierrichtlinien und jeder Programmierer hat einen eigenen Stil. Soll der Code auf den eigenen Stil hin überprüft werden, sind die bestehenden Policies von Perl::Critic häufig ungeeignet. Da müssen also eigene Policies geschrieben werden.

Dafür ist es notwendig, dass man sich einigermaßen mit PPI (siehe auch $foo Ausgabe 3) auskennt und einige Regeln von Perl::Critic beachtet. In diesem Artikel soll das Schreiben eigener Policies am Beispiel "öffnende Klammer einer Schleife muss in der Zeile des Schleifenkopfes stehen" gezeigt werden.

Es soll in Zukunft also so etwas erzwungen werden wie

  for my $var ( ... ) {

und nicht

  for my $var ( ... )
{

Big Perl is watching your Code

Wie schon erwähnt, steht hinter Perl::Critic das Modul PPI. PPI zerlegt den Perl-Code in Tokens, erstellt daraus "Nodes" und baut daraus eine Baumstruktur. Perl::Critic ruft bei jedem "Node" die entsprechende Perl::Critic::Policy-Subklasse auf und wenn ein Code vorhanden ist, der nicht den Regeln entspricht, wird ein Perl::Critic::Violation-Objekt zurückgeliefert.

Das Gerüst muss stehen

Die Policies sind Subklassen von Perl::Critic::Policy, und die Klassen müssen dementsprechend auch in diesem Namensraum liegen. Perl::Critic arbeitet mit Module::Pluggable, um die Policies automatisch zu finden.

Die ersten Zeilen einer Policy (nach dem Package-Namen) sollten so aussehen:
  use strict;
use warnings;
use Perl::Critic::Util;
use Perl::Critic::Violation;

use base qw/Perl::Critic::Policy/;

our $VERSION = '0.01';
Die Policy muss einem Nutzer auch sagen, was falsch ist. Dafür gibt es zwei Variablen in jeder Policy: Zum Einen $desc und zum Anderen $expl. $desc ist ein String, der aussagen sollte, was falsch gelaufen ist. Das kann in wenigen Worten geschehen. $expl ist entweder ein String mit einer genauen Beschreibung, was falsch gemacht wurde, oder eine Arrayreferenz mit den Seitenzahlen aus "Perl Best Practices", auf denen das gewünschte Verhalten erklärt ist.

Da in diesem Beispiel kein Programmierstil von "Perl Best Practices" behandelt wird, sind das ganz eigene Sachen, die dort ausgegeben werden.
  my $desc = q~{ nicht in der Zeile des Schleifenkopfes.~;
my $expl = q~Die öffnende Klammer muss in der Zeile des
Zeilenkopfes stehen, also

for my $var ( ... ){
...
}

anstatt

for my $var ( ... )
{
...
}
~;

Die new-Methode sollte nur überschrieben werden, wenn der Nutzer Argumente an die Policy übergeben können soll. Das ist hier aber nicht notwendig.

Mein Profil

Jede Policy hat ein paar Standardsachen, die ein gewisses Profil für die Policy darstellen. Dazu gehören neben der Kurzbeschreibung in $desc und der genauen Beschreibung in $expl auch ein paar Subroutinen, die angeben, für welche PPI-Nodes die Policy angewendet werden soll, zu welchen Themes die Policy gehört und natürlich für welchen Severity-Level die Policy angewendet werden soll.

Die Angabe des PPI-Knotens ist wichtig, damit die Policy auch nur für ganz bestimmte Code-Fragmente angewendet wird. Außerdem bedeutet es einen Geschwindigkeitsvorteil, wenn nicht alle Policies für alle Knoten angewendet werden.

Der Knoten wird durch die Subroutine applies_to bestimmt. Die for-Schleife ist ein PPI::Statement::Compound, also muss dies in der Subroutine eingetragen werden.
  sub applies_to{ return 'PPI::Statement::Compound' }
Über die Angabe des Themes können Anwender bestimmte Policies ein- oder ausschalten. Dies ist ganz praktisch, wenn man für mehrere Kunden arbeitet und alle etwas unterschiedliche Regelungen haben.

Dieses Beispiel soll in den Themes "core" und "foo" enthalten sein. Die Subroutine, die das bestimmt, heißt default_themes.
  sub default_themes{ return qw/core foo/ }
Der Severity-Level bestimmt, wie wichtig die Regel ist. Ist es eine Regel, die in jedem Programm eingehalten werden muss, dann ist das die höchste Prioritätsstufe, ist es jedoch nur eine "Schönheitsregel", dann kann eine niedrige Prioritätsstufe ausgewählt werden. Da die hier gezeigte Regel auf jeden Fall eingehalten werden soll, bekommt sie die höchste Priorität
  sub default_severity{ return $SEVERITY_HIGHEST }
Die Variable $SEVERITY_HIGHEST wird vom Modul Perl::Critic::Utils exportiert.

Jetzt geht's los!

Bevor das große Coden losgeht, am besten nochmal genau darüber Gedanken machen, was die Policy machen soll. In diesem Fall müssen wir bedenken, dass kein Fehler ausgegeben werden soll, wenn der Schleifenkopf nachgestellt ist, also so etwas wie
  print $_ for @array;
Als ganz nützlich hat sich herausgestellt, vorher verschiedene Varianten von Code, die der Nutzer schreiben könnte, mit PPI zu testen und sich die Baumstruktur anzuschauen.

In den Beispielcodes auf der Webseite ist ein Beispielprogramm mit verschiedenen Schleifenvarianten enthalten. In Listing 2 ist die PDOM-Struktur einer for-Schleife, wie sie gültig ist, zu sehen.

  PPI::Statement::Compound
PPI::Token::Word 'for'
PPI::Token::Whitespace ' '
PPI::Token::Word 'my'
PPI::Token::Whitespace ' '
PPI::Token::Symbol '$var'
PPI::Token::Whitespace ' '
PPI::Structure::ForLoop ( ... )
PPI::Statement
PPI::Token::Symbol '@array'
PPI::Structure::Block { ... }

Ein kleiner Teil ist bei einer ungültigen Schleife unterschiedlich. Zwischen der ForLoop und dem Block taucht noch ein Newline auf:

  PPI::Structure::ForLoop     (... )
PPI::Statement
PPI::Token::Symbol '@array'
PPI::Token::Whitespace '\n'
PPI::Structure::Block { ... }
Die einfachste Variante, eine solche Policy zu entwickeln ist, sich alle möglichen Varianten der zu untersuchenden Struktur aufzuschreiben und mit dem Dumper von PPI anzuschauen. Dann können die Unterschiede besser herausgearbeitet werden. Für diesen Artikel gibt es bei den Codes auf der Webseite das Skript show_pdoms.pl, das genau dieses zeigt.

Bei dem hier gezeigten Beispiel fällt auf, dass bei einer Regelverletzung immer ein \n zwischen der ForLoop und dem Block erscheint.

In Listing 3 ist die Methode zu sehen, die für die eigentliche Regelüberprüfung
zuständig ist.

1 sub violates{
2 my ($self,$elem,$doc) = @_;
3
4 my $base = $elem->schild(0);
5 return unless $base eq 'for';
6
7 my ($list,$newline,$block);
8
9 my @children = $elem->children;
10
11 for my $i ( 0 .. $#children ){
12 my $child = $children[$i];
13
14 if( $child->isa( 'PPI::Structure::Block' ) ){
15 $block = $i;
16 }
17 elsif( $child->isa( 'PPI::Structure::ForLoop' ) ){
18 $list = $i;
19 }
20 elsif( $child->isa( 'PPI::Token::Whitespace' ) and
21 $child eq "\n" and
22 not $newline){
23 $newline = $i;
24 }
25 }
26
27 if( $newline and $newline > $list and $newline < $block){ 28 my $sev = $self->get_severity;
29 return Perl::Critic::Violation->new( $desc,$expl,$elem,$sev );
30 }
31
32 return;
33 }


Die Methode, die die Überprüfung enthält, heißt violates und bekommt von Perl::Critic automatisch drei Parameter übergeben: das Perl::Critic-Objekt, das entsprechende PPI-Element, auf das die Regel angewendet wird und das komplette PPI-Dokument (die Datei oder das Snippet, auf das Perl::Critic angewendet wird).

Die Policy soll nur auf for-Schleifen angewendet werden. Deshalb muss als erstes überprüft werden, ob es sich tatsächlich um eine solche Schleife handelt. Dazu wird überprüft, ob das erste child-Element ein 'for' ist.

Danach werden die Positionen von Block, ForLoop und Newline festgestellt. Nur in dem Fall, in dem ein Newline zwischen Block und ForLoop ist, wird ein Fehler ausgegeben.

Und schon ist die erste eigene Regel implementiert. Bei einer for-Schleifen, die gegen die Regel verstößt, wird in Zukunft ein Fehler ausgegeben, wie er in Listing 4 zu sehen ist.
oeffnende Klammer muss in Zeile mit Schleifenkopf sein at line 20, column 5. Es
soll in Zukunft also so etwas erzwungen werden wie

for my $var ( ... ) {

und nicht

for my $var ( ... )
{
.

3 comments:

jotr said...

Probably less painful to use Perl::Tidy.

ReneeB said...

Yes, in this case. But I wanted to show how to write your own Perl::Critic policies with an simple example.

Elliot Shank said...

In ancient times, you had to instantiate P::C::Violation objects yourself. Nowadays, you should use "$self->violation(...)" instead. This adds other stuff to the Violation object for you and is the supported way of creating them.

Thanks for showing people how to program Perl::Critic!