diff --git a/Makefile.PL b/Makefile.PL index 78d01de..625f84c 100644 --- a/Makefile.PL +++ b/Makefile.PL @@ -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 diff --git a/lib/GPH/Util/Php.pm b/lib/GPH/Util/Php.pm new file mode 100644 index 0000000..9f109e9 --- /dev/null +++ b/lib/GPH/Util/Php.pm @@ -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(); + +=head1 METHODS + +=over 4 + +=item C<< -Enew(%args) >> + +the C method returns a php object. + +=item C<< -Etypes(@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<< -EresolveEnum(@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<< -Ereduce(%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<< -Etypes(@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 + +=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 diff --git a/stdin2classtype-filter.pl b/stdin2classtype-filter.pl new file mode 100644 index 0000000..b4ca433 --- /dev/null +++ b/stdin2classtype-filter.pl @@ -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(); + +print join(",", $util->reduce((paths => \@paths, excludes => @types))); \ No newline at end of file diff --git a/t/share/Php/Bar.php b/t/share/Php/Bar.php new file mode 100644 index 0000000..28c9fc7 --- /dev/null +++ b/t/share/Php/Bar.php @@ -0,0 +1,8 @@ + '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(); +