App-perl-distrolint
view release on metacpan or search on metacpan
lib/App/perl/distrolint/Check/SubSignatures.pm view on Meta::CPAN
use v5.36;
use Object::Pad 0.807;
class App::perl::distrolint::Check::SubSignatures 0.09;
apply App::perl::distrolint::CheckRole::EachFile;
apply App::perl::distrolint::CheckRole::TreeSitterPerl;
use List::Util 1.29 qw( any first );
use Text::Treesitter 0.07; # child_by_field_name
use constant DESC => "check that Perl subroutines use signatures if enabled";
use constant SORT => 25;
=head1 NAME
C<App::perl::distrolint::Check::SubSignatures> - check that Perl subroutines use signatures
=head1 DESCRIPTION
This checks that for every Perl source file in F<lib/>, that any C<sub> or
C<method> declarations made inside a scope that has the C<signatures> feature
enabled are actually made using signatures. That said, it fails if a
subroutine declaration is found that does not have a signature, if it lies
inside a scope that specifically enables the feature.
The C<signatures> feature is considered enabled by a C<use VERSION>
declaration of a version C<v5.36> or later, or by a C<use feature> declaration
that specifically lists the C<signatures> feature.
=cut
method run ( $app )
{
return $self->run_for_each_perl_file( check_file => );
}
my $QUERY = <<'EOF';
(use_version_statement)
@use_version
(use_statement
module: _ @module (#eq? @module "feature"))
@use_module
(subroutine_declaration_statement (signature) ? @signature)
@sub
(method_declaration_statement (signature) ? @signature)
@method
(anonymous_subroutine_expression (signature) ? @signature)
@sub
(anonymous_method_expression (signature) ? @signature)
@method
EOF
my sub proto_looks_like_signature ( $proto )
{
# prototype must be wrapped in (...)
$proto =~ s/^\((.*)\)$/$1/ or
return 0;
# trim
$proto =~ s/^\s+//;
$proto =~ s/\s+$//;
# Empty string would count as a zero-arg signature
return 1 if $proto eq '';
# A single '$' would count as a single-arg unnamed signature param
# A single '@' or '%' would count as a single slurpy unnamed signature param
return 1 if $proto eq '$' or $proto eq '@' or $proto eq '%';
return 0;
}
method check_file ( $file )
{
my $tree = $self->parse_perl_file( $file );
my $ok = 1;
$self->walk_each_scoped_query_match( $QUERY, $tree->root_node, method ( $capture, $context ) {
my $node;
if( $node = $capture->{use_version} ) {
my $version = version->new( $node->child_by_field_name( 'version' )->text );
$context->{use_feature_signatures} = ( $version ge v5.36 );
}
elsif( $node = $capture->{use_module} ) {
my $sense = ( $node->child_nodes )[0]->type eq "use";
my @features = $self->extract_use_module_imports( $node );
$context->{use_feature_signatures} = $sense
if any { $_ eq "signatures" } @features;
}
elsif( $node = $capture->{sub} or $node = $capture->{method} ) {
my $kw = ( $capture->{sub} ) ? "sub" : "method";
my $has_signature = defined $capture->{signature};
# tree-sitter-perl can't tell the difference between prototypes
# and signatures
if( $context->{use_feature_signatures} and not $has_signature ) {
my $proto_node = first { $_->type eq "prototype" } $node->child_nodes;
$has_signature = proto_looks_like_signature( $proto_node->text ) if $proto_node;
}
if( $context->{use_feature_signatures} and !$has_signature ) {
App->diag( App->format_file( $file, $node->start_row + 1 ), " declares a $kw without signature" );
$ok = 0;
}
}
else {
my @names = sort keys %$capture;
die "TODO: Unsure how to handle captures <@names>\n";
}
return 1; # check all of them
} );
return $ok;
}
=head1 AUTHOR
Paul Evans <leonerd@leonerd.org.uk>
=cut
0x55AA;
( run in 2.543 seconds using v1.01-cache-2.11-cpan-5a3173703d6 )