From Code to Community: Sponsoring The Perl and Raku Conference 2025 Learn more

package App::perlminlint; sub MY () {__PACKAGE__}
# -*- coding: utf-8 -*-
use 5.009;
use strict;
use warnings FATAL => 'all';
our $VERSION = '0.24';
use Carp;
use autodie;
use Encode qw/is_utf8/;
use open qw/:utf8 :std/;
sub CFGFILE () {'.perlminlint.yml'}
[fields => qw/no_stderr
help
verbose
dryrun
no_auto_libdir
no_widechar
no_force_strict
_plugins
_lib_list _lib_dict
_perl_opts
/];
require lib;
use Module::Pluggable require => 1, sub_name => '_plugins';
sub usage {
(my MY $app) = @_;
die <<END;
Usage: @{[$app->basename($0)]} [opts..] YOUR_SCRIPT
Options:
-v --verbose
-n --dryrun
-w -c -wc (just ignored)
Pass-through Options:
-IDIR
-Mmodule
-mmodule
-dDEBUG
END
}
sub run {
my ($pack, $argv) = @_;
my MY $app = $pack->new($pack->parse_argv
($argv, {h => 'help'
# Just to ignore -w -c -wc
, w => '', c => '', wc => ''
, v => 'verbose'
, n => 'dryrun'
}
, qr{^-[ImMd]}, my $perl_opts = []
));
# -IDIR, -mmod, -MMod
push @{$app->{_perl_opts}}, @$perl_opts;
if ($app->{help} or not @$argv) {
$app->usage;
}
$app->find_and_load_config_from(@$argv);
if ($app->{no_stderr}) {
close STDERR;
open STDERR, '>&STDOUT';
}
$app->add_lib_to_inc_for(@$argv) if not $app->{no_auto_libdir};
my @res = $app->lint(@$argv);
if (@res) {
print join("\n", @res), "\n" unless @res == 1 and ($res[0] // '') eq '';
} else {
print "OK\n";
}
}
sub after_new {
(my MY $self) = @_;
foreach my $lib (@INC) {
$self->{_lib_dict}{$lib}++;
}
}
sub upward_first_file_from (&@) {
my ($code, $lookfor, $startFn) = @_;
my @dirs = MY->splitdir(MY->rel2abs($startFn));
pop @dirs;
local $_;
while (@dirs) {
-e (my $fn = MY->catdir(@dirs, $lookfor))
or next;
$code->($_ = $fn)
and last;
} continue {
pop @dirs;
}
}
sub add_lib_to_inc_for {
(my MY $self, my $fn) = @_;
my $adder = sub {
my ($libdir) = @_;
if (not $self->{_lib_dict}{$libdir}) {
import lib $libdir;
push @{$self->{_lib_list}}, $libdir;
}
};
upward_first_file_from {
my ($libdir) = @_;
if (-d $libdir) {
$adder->($libdir);
# Auto add carton's local/lib/perl5 too.
my $carton = $self->catdir($self->dirname($self->rel2abs($libdir))
, qw(local lib perl5));
if (-d $carton) {
$adder->($carton);
}
1;
}
} lib => $fn;
}
sub find_and_load_config_from {
(my MY $self, my $fn) = @_;
upward_first_file_from {
$self->load_config($_);
} CFGFILE, $fn;
}
sub load_config {
(my MY $self, my $fn) = @_;
if ($self->{verbose}) {
print STDERR "# loading config: $fn\n";
}
eval {require YAML::Tiny};
if ($@) {
die "Can't load '$fn'. Please install YAML::Tiny\n";
}
my $yaml = YAML::Tiny->read($fn);
if (not $yaml->[0] and ref $yaml->[0] eq 'HASH') {
die "Invalid data in $fn. Only HASH is allowed\n";
}
$self->configure($yaml->[0]);
}
sub lint {
(my MY $self, my $fn) = @_;
if ($fn =~ /\P{ASCII}/ and not is_utf8($fn)) {
Encode::_utf8_on($fn);
}
my @fallback;
foreach my $plugin ($self->plugins) {
if (my $obj = $self->apply_to($plugin, handle_match => $fn)) {
#
my @res = $obj->handle_test($fn)
or next;
return @res;
} elsif ($plugin->is_generic) {
push @fallback, $plugin;
}
}
unless (@fallback) {
die "Don't know how to lint $fn\n";
}
foreach my $plugin (@fallback) {
my @res = $self->apply_to($plugin, handle_test => $fn)
or next;
return @res;
}
return "";
}
sub apply_to {
(my MY $self, my ($plugin, $method, @args)) = @_;
$plugin->new(app => $self)->$method(@args);
}
sub plugins {
(my MY $self) = @_;
my $plugins = $self->{_plugins}
//= [sort {$b->priority <=> $a->priority} $self->_plugins];
wantarray ? @$plugins : $plugins;
}
sub run_perl {
my MY $self = shift;
my @opts;
push @opts, '-C' unless $self->{no_widechar};
push @opts, '-Mstrict' unless $self->{no_force_strict};
push @opts, lexpand($self->{_perl_opts});
push @opts, map {"-I$_"} lexpand($self->{_lib_list});
if ($self->{verbose} || $self->{dryrun}) {
print STDERR join(" ", "#", $^X, @opts, @_), "\n";
}
if ($self->{dryrun}) {
return;
}
system($^X, @opts, @_) == 0
or exit $? >> 8;
}
sub read_file {
(my MY $self, my $fn) = @_;
open my $fh, '<:utf8', $fn;
local $/;
scalar <$fh>;
}
sub basename {
shift; File::Basename::basename(@_);
}
sub dirname {
shift; File::Basename::dirname(@_);
}
sub rootname {
shift;
my $fn = shift;
$fn =~ s/\.\w+$//;
join "", $fn, @_;
}
sub lexpand {
if (not defined $_[0]) {
wantarray ? () : 0;
} elsif (not ref $_[0]) {
$_[0]
} else {
@{$_[0]};
}
}
sub inc_opt {
my ($app, $file, $modname) = @_;
(my $no_pm = $file) =~ s/\.\w+$//;
my @filepath = $app->splitdir($app->rel2abs($no_pm));
my @modpath = grep {$_ ne ''} split "::", $modname;
my @popped;
while (@modpath and @filepath and $modpath[-1] eq $filepath[-1]) {
unshift @popped, pop @modpath;
pop @filepath;
}
if (@modpath) {
die "Can't find library root directory of $modname in file $file\n@modpath\n";
}
'-I' . $app->catdir(@filepath);
}
sub read_shbang_opts {
(my MY $app, my $fn) = @_;
my @opts;
my $body = $app->read_file($fn);
my (@shbang) = $app->parse_shbang($body);
if (grep {$_ eq "-T"} @shbang) {
push @opts, "-T";
}
@opts;
}
sub parse_shbang {
my MY $app = shift;
my ($shbang) = $_[0] =~ m{^(\#![^\n]+)}
or return;
split " ", $shbang;
}
# XXX: Real new and options...
sub parse_argv {
my ($pack, $list, $alias, $special_re, $special_list) = @_;
my @opts;
while (@$list) {
if ($special_re and $list->[0] =~ $special_re) {
push @$special_list, $list->[0]
} elsif (my ($k, $v) = $list->[0] =~ /^--?(\w[-\w]*)(?:=(.*))?/) {
$k =~ s/-/_/g;
my $opt = $alias->{$k} // $k;
next if $opt eq ''; # To drop compat-only option.
push @opts, $opt => ($v // 1);
} else {
last;
}
} continue {
shift @$list;
}
@opts;
}
sub parse_perl_opts {
(my MY $self, my $list) = @_;
my @opts;
while (@$list and defined $list->[0]
and $list->[0] =~ m{^-[ImMd]}) {
push @opts, shift @$list;
}
@opts;
}
1; # End of App::perlminlint
__END__
=head1 NAME
App::perlminlint - minimalistic lint for perl
=head1 SYNOPSIS
% perlminlint myscript.pl
# => This tests "perl -wc myscript.pl"
% perlminlint MyModule.pm
# => This tests "perl -we 'require MyModule'"
% perlminlint MyInnerModule.pm
# => This tests "perl -I.. -we 'require MyApp::MyInnerModule'"
% perlminlint cpanfile
# => This tests Module::CPANfile->load
=head1 DESCRIPTION
Perl has had long support for L<compile only mode|perlrun/-c>,
but it is not so trivial to use this mode to check scripts
so that to integrate automatic check into editors like Emacs and Vim.
Because most real-world perl scripts consist of many other modules,
and to load them correctly, you must give correct search path for perl
as L<-I$DIR|perlrun/-I> and/or L<-Mlib=$DIR|lib>.
Also, to test modules, "perl -we 'require THE_MODULE'" is better than "perl -wc".
C<perlminlint> wraps all such details so that you can just run C<perlminlint $yourfile> to test your script.
=head1 SEE ALSO
L<Module::Pluggable>
=head1 COPYRIGHT
Copyright 2014- KOBAYASHI, Hiroaki