Skip to content

Commit

Permalink
php util
Browse files Browse the repository at this point in the history
- added php util module which for now can scan file contents to
  determine the php class type (e.g. interface, enum, class) for
  a list of files. this enables one to in / exclude certain class
  types when for instance reducing a git diff
  • Loading branch information
wickedOne committed Mar 3, 2024
1 parent d7b95be commit c674fd6
Show file tree
Hide file tree
Showing 11 changed files with 348 additions and 1 deletion.
3 changes: 2 additions & 1 deletion Makefile.PL
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ WriteMakefile(
PREREQ_PM => {
"File::Basename" => 0,
"Time::Piece" => 0,
"XML::LibXML" => 0
"XML::LibXML" => 0,
"Cwd" => 0
},
CONFIGURE_REQUIRES => {
"ExtUtils::MakeMaker" => 0
Expand Down
137 changes: 137 additions & 0 deletions lib/GPH/Util/Php.pm
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package GPH::Util::Php;

use strict;
use warnings FATAL => 'all';

use Cwd;
use Data::Dumper;

sub new {
my ($proto) = @_;

return bless {
'types' => {},
}, $proto;
};

sub types {
my ($self, @files) = @_;
my ($fh, $pattern);

foreach my $file (@files) {
chomp $file;
next unless $file =~ '[/]{0,}([^/]+)\.php$';
open($fh, '<', getcwd() . '/' . $file) or die "unable to open file $file : $!";

$pattern = "[ ]{0,}([^ ]+) " . $1 . "(?:[ :]|\$){1,}";

while(<$fh>) {
chomp $_;
next unless $_ =~ $pattern;
my $type = ($1 ne 'enum') ? $1 : $self->resolveEnum(<$fh>);

$self->{'types'}->{$type} = [] unless exists($self->{'types'}->{$type});

push(@{$self->{'types'}->{$type}}, $file);

last();
}

close($fh);
}

return($self);
};

sub resolveEnum {
my ($self, @lines) = @_;

foreach my $line (@lines) {
return 'method_enum' if $line =~ / function [^ ]{1,}[ ]{0,}\(/;
}

return 'enum';
};

sub reduce {
my ($self, %args) = @_;

(exists($args{paths}) and exists($args{excludes})) or die $!;

$self->types(@{$args{paths}});

my %exclude = map{$_ => 1} @{$args{excludes}};
my @result;

foreach my $key (keys %{$self->{types}}) {
push(@result, @{$self->{types}->{$key}}) unless defined $exclude{$key};
}

return(@result);
};

1;

__END__
=head1 NAME
GPH::Util::Php - php related util methods
=head1 SYNOPSIS
use GPH::Util::Php;
my $stats = GPH::Util::Php->new();
$stats->types(<STDIN>);
=head1 METHODS
=over 4
=item C<< -E<gt>new(%args) >>
the C<new> method returns a php object.
=item C<< -E<gt>types(@paths) >>
scans file content of given php files in C<@paths> and tries to determine their type (e.g. class, interface, trait, enum)
=item C<< -E<gt>resolveEnum(@lines) >>
scans C<< @lines >> for the presence of a method and returns the 'method_enum' type if found, otherwise 'enum' is returned.
this for instance can be particularly useful when creating a filter for infection testing as no mutants can be generated
for a (backed) enum, but they can when the enum contains methods.
=item C<< -E<gt>reduce(%args) >>
the reduce method takes a hash of options, valid option keys include:
=over
=item paths B<(required)>
file paths to scan. must be relative to the path where the script is executed
=item excludes B<(required)>
class type(s) to exclude from the list
=back
calls the C<< -E<gt>types(@paths) >> method with given paths after which it returns an array of paths for files with
different types than the excluded types.
=back
=head1 AUTHOR
the GPH::Util::Php module was written by wicliff wolda <wicliff.wolda@gmail.com>
=head1 COPYRIGHT AND LICENSE
this library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
=cut
27 changes: 27 additions & 0 deletions stdin2classtype-filter.pl
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/perl -w
use strict;
use warnings FATAL => 'all';

use File::Basename;
use lib dirname(__FILE__) . '/lib/';

use GPH::Util::Php;
use GPH::Gitlab;

my $owner = $ENV{'DEV_TEAM'} or die "please define owner in DEV_TEAM env var";
my @excludes = split /,/, ($ENV{'EXCLUDE_PATHS'} || '');
my $codeonwers = $ENV{'CODEOWNERS'} || './CODEOWNERS';
my @types = split /,/, ($ENV{'PHP_EXCLUDE_TYPES'} || '');

my %config = (
owner => $owner,
codeowners => $codeonwers,
excludes => \@excludes,
);

my $gitlab = GPH::Gitlab->new(%config);
my $util = GPH::Util::Php->new();

my @paths = $gitlab->intersect(<STDIN>);

print join(",", $util->reduce((paths => \@paths, excludes => @types)));
8 changes: 8 additions & 0 deletions t/share/Php/Bar.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace share\Php;

abstract class Bar
{

}
6 changes: 6 additions & 0 deletions t/share/Php/Foo.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<?php

final readonly class Foo
{

}
4 changes: 4 additions & 0 deletions t/share/Php/FooEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
enum FooEnum: string
{

}
5 changes: 5 additions & 0 deletions t/share/Php/MethodEnum.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
enum MethodEnum: string
{
public function colors(): string
{}
}
4 changes: 4 additions & 0 deletions t/share/Php/SomethingInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
interface SomethingInterface
{

}
4 changes: 4 additions & 0 deletions t/share/Php/SomethingTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
trait SomethingTrait
{

}
1 change: 1 addition & 0 deletions t/share/Php/textfile.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
text file
150 changes: 150 additions & 0 deletions t/unit/GPH/Util/Php.t
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
#!/usr/bin/perl
package t::unit::GPH::Util::Php;

use strict;
use warnings;

use Test2::V0 -target => 'GPH::Util::Php';
use Test2::Tools::Spec;

use Data::Dumper;

my @paths = qw{t/share/Php/Foo.php t/share/Php/FooEnum.php t/share/Php/MethodEnum.php t/share/Php/SomethingInterface.php t/share/Php/SomethingTrait.php t/share/Php/textfile.txt t/share/Php/Bar.php};

describe "class `$CLASS`" => sub {
tests 'it can be instantiated' => sub {
can_ok($CLASS, 'new');
};

tests 'instantation' => sub {
my ($object, $exception, $warnings);

$exception = dies {
$warnings = warns {
$object = $CLASS->new();
};
};

is($exception, undef, 'no exception thrown');
is($warnings, 0, 'no warnings generated');

is(
$object,
object {
field types => {};
end;
},
'object as expected'
) or diag Dumper($object);
};
};

describe "class `$CLASS` types method" => sub {
tests 'types' => sub {
my ($object, $exception, $warnings);

$exception = dies {
$warnings = warns {
$object = $CLASS->new();
$object->types(@paths);
};
};

is($exception, undef, 'no exception thrown');
is($warnings, 0, 'no warnings generated');

is(
$object,
object {
field types => hash {
field interface => array {
item 't/share/Php/SomethingInterface.php';
end;
};
field trait => array {
item 't/share/Php/SomethingTrait.php';
end;
};
field enum => array {
item 't/share/Php/FooEnum.php';
end;
};
field method_enum => array {
item 't/share/Php/MethodEnum.php';
end;
};
field class => array {
item 't/share/Php/Foo.php';
item 't/share/Php/Bar.php';
end;
};
};
end;
},
'object as expected'
) or diag Dumper($object);
};

tests 'die when file not found' => sub {
my $object = $CLASS->new();

ok(dies {$object->types(('non/exiting/file.php'))}, 'died with non existing file') or note($@);
ok(lives {$object->types(('t/share/Php/FooEnum.php'))}, 'lives with existing file') or note($@);
};
};

describe "class `$CLASS` reduce method" => sub {
my (@excludes, $excluded, @result, $object, $exception, $warnings);

case 'exclude enum' => sub {
@excludes = 'enum';
$excluded = 't/share/Php/FooEnum.php';
};

case 'exclude method_enum' => sub {
@excludes = 'method_enum';
$excluded = 't/share/Php/MethodEnum.php';
};

case 'exclude class' => sub {
@excludes = 'class';
$excluded = 't/share/Php/Foo.php';
};

case 'exclude interface' => sub {
@excludes = 'interface';
$excluded = 't/share/Php/SomethingInterface.php';
};

case 'exclude trait' => sub {
@excludes = 'trait';
$excluded = 't/share/Php/SomethingTrait.php';
};

tests 'reduce paths' => sub {
$exception = dies {
$warnings = warns {
$object = $CLASS->new();
@result = $object->reduce((paths => \@paths, excludes => \@excludes));
};
};

is($exception, undef, 'no exception thrown');
is($warnings, 0, 'no warnings generated');
is($excluded, not_in_set(@result), 'excluded file not in set');
};
};

describe "class `$CLASS` reduce options" => sub {
tests 'reduce method call' => sub {
my @excludes = 'trait';
my $object = $CLASS->new();

ok(dies {$object->reduce((paths => \@paths))}, 'died with missing excludes') or note($@);
ok(dies {$object->reduce((excludes => \@excludes))}, 'died with missing paths') or note($@);
ok(lives {$object->reduce((paths => \@paths, excludes => \@excludes))}, 'lived with paths and excludes defined') or note($@);
};
};

done_testing();

0 comments on commit c674fd6

Please sign in to comment.