App-Project-Doctor
view release on metacpan or search on metacpan
lib/App/Project/Doctor/Fixer.pm view on Meta::CPAN
package App::Project::Doctor::Fixer;
# The Fixer presents a numbered menu of auto-fixable findings, reads the
# user's answer from STDIN, and calls each selected fix coderef with the
# current Context. In non-interactive mode (--fix flag) it applies all
# fixes immediately without prompting.
use strict;
use warnings;
use autodie qw(:all);
# croak dies at the caller's location; carp warns there.
use Carp qw(croak carp);
# Params::Get normalises @_ into a hashref before validate_strict sees it.
use Params::Get;
# validate_strict enforces parameter schemas and throws immediately on failure.
lib/App/Project/Doctor/Fixer.pm view on Meta::CPAN
}
# ---------------------------------------------------------------------------
# Accessors (read-only after construction)
# ---------------------------------------------------------------------------
# The Report whose fixable findings will be presented to the user.
sub report { $_[0]->{report} }
# The Context passed to each fix coderef so it can find files.
sub context { $_[0]->{context} }
# When true, all fixes are applied immediately without user prompting.
sub non_interactive { $_[0]->{non_interactive} }
# ---------------------------------------------------------------------------
# Public interface
# ---------------------------------------------------------------------------
=head2 run
Presents fixable findings, prompts (or auto-applies in non-interactive mode),
and calls each selected C<fix> coderef. Returns the count of fixes applied.
=cut
sub run {
my $self = shift;
# Collect only the findings that have an associated fix coderef.
my @fixable = $self->report->fixable;
# Nothing to do if no fixable findings were found.
return 0 unless @fixable;
# Choose the right mode: silent auto-apply vs. interactive prompt.
return $self->non_interactive
? $self->_apply_all(\@fixable)
: $self->_interactive_loop(\@fixable);
}
# ---------------------------------------------------------------------------
# Private helpers
# ---------------------------------------------------------------------------
# Purpose: Print a numbered list of fixes to STDOUT.
lib/App/Project/Doctor/Fixer.pm view on Meta::CPAN
context => $ctx,
);
my $count = $fixer->run;
=head1 DESCRIPTION
Presents fixable findings from a report, reads the user's choice from STDIN
(C<Y> all, C<n> none, or C<1,3> index list), and calls each selected
finding's C<fix> coderef with the current context.
Set C<non_interactive =E<gt> 1> to apply all fixes without prompting
(C<--fix> mode).
=head1 CONSTRUCTOR
=head2 new( %args )
=head3 API SPECIFICATION
=head4 Input
lib/App/Project/Doctor/Fixer.pm view on Meta::CPAN
-----|-----------------------|---------------------------------------
F001 | A fix coderef throws | Fix skipped; error logged via carp
=head3 FORMAL SPECIFICATION
run : Fixer -> N
run fixer ==
let fixable = { f in findings (report fixer) | is_fixable f }
in if non_interactive fixer
then apply_all fixable
else apply_chosen fixable (prompt fixable)
=head1 LIMITATIONS
Reads from STDIN; use C<non_interactive =E<gt> 1> in automated pipelines.
Encapsulation of C<_interactive_loop>, C<_apply_all>, and C<_print_fix_list>
is enforced by convention only; a future migration to C<Sub::Private> in
enforce mode is tracked as a TODO.
=head1 AUTHOR
lib/App/Project/Doctor/Report.pm view on Meta::CPAN
push @lines, $ec || $wc
? join(' - ', ($ec ? "$ec error(s)" : ()), ($wc ? "$wc warning(s)" : ()))
: 'No errors or warnings.';
# If there are fixable findings, show them and hint that the user can apply them.
my @fixable = $self->fixable;
if (@fixable) {
push @lines, '';
push @lines, 'Suggested fixes:';
my $i = 0;
# Number each fix starting at 1 for the interactive prompt that follows.
push @lines, sprintf(' [%d] %s', ++$i, $_->message) for @fixable;
push @lines, '';
push @lines, 'Would you like me to apply them? [Y/n]';
}
# Join with newlines and add a trailing newline for clean shell output.
return join("\n", @lines) . "\n";
}
=head2 render_json
script/project-doctor view on Meta::CPAN
# Default values for options that may or may not be provided on the command line.
my %opts = (
format => 'text', # Output format: text (default), json, or tap.
fix => undef, # undef = ask interactively; 1 = auto-apply; 0 = never.
);
# Map each CLI flag to its storage location in %opts.
GetOptions(
'check=s' => \$opts{check}, # Comma-separated list of checks to run.
'skip=s' => \$opts{skip}, # Comma-separated list of checks to skip.
'fix!' => \$opts{fix}, # --fix applies all; --no-fix skips prompts.
'format=s' => \$opts{format}, # Output format selection.
'verbose|v' => \$opts{verbose}, # Show extra per-finding detail.
'quiet|q' => \$opts{quiet}, # Suppress the "Checking..." banner.
'help|h' => sub { pod2usage(-verbose => 1, -exitval => 0) },
'man' => sub { pod2usage(-verbose => 2, -exitval => 0) },
) or pod2usage(-verbose => 0, -exitval => 2);
# Any remaining argument after the options is the path to check.
my $path = shift @ARGV // '.';
script/project-doctor view on Meta::CPAN
# ---------------------------------------------------------------------------
# Apply fixes
# ---------------------------------------------------------------------------
# Only offer fixes when using text output and there is at least one fixable finding.
# JSON and TAP consumers handle fixes differently (or not at all).
if ($opts{format} eq 'text' && $report->fixable) {
# --fix -> apply all fixes immediately without asking
# --no-fix -> opts{fix} == 0, so skip the entire block
# (default) -> opts{fix} is undef, so prompt the user interactively
unless (defined $opts{fix} && !$opts{fix}) {
require App::Project::Doctor::Context;
# Build a Context from $path so fix coderefs can locate files.
# Note: $path may be a subdirectory; Doctor used the detected root internally.
App::Project::Doctor::Fixer->new(
report => $report,
context => App::Project::Doctor::Context->new(root => $path),
non_interactive => ($opts{fix} ? 1 : 0),
)->run();
}
script/project-doctor view on Meta::CPAN
0.02
=head1 SYNOPSIS
project-doctor [options] [PATH]
Options:
--check NAMES Run only the named checks (comma-separated)
--skip NAMES Skip the named checks (comma-separated)
--fix Apply all fixes non-interactively
--no-fix Report only; never prompt for fixes
--format FORMAT Output format: text (default), json, tap
--verbose, -v Show per-finding detail for all checks
--quiet, -q Suppress banner and passing checks
--help, -h Show this help
--man Show full manual page
=head1 DESCRIPTION
Runs a suite of diagnostics against a Perl CPAN distribution and reports
on tests, CI configuration, META validity, POD coverage, dependency
declarations, licensing, GitHub Actions workflows, security hygiene, and
CPAN upload readiness.
Fixable issues are listed at the end; the user is prompted (or fixes are
applied automatically with C<--fix>).
=head1 EXAMPLES
# Check the current directory
project-doctor
# Check a specific distribution
project-doctor ~/src/My-Dist
t/function.t view on Meta::CPAN
message => 'Fixable problem',
check_name => 'CI',
fix => sub {},
),
);
like $with_fix->render_text,
qr/Suggested fixes:/,
'fix list appears when fixable findings present';
like $with_fix->render_text,
qr/Would you like me to apply them/,
'prompt appears';
my $no_fix = _make_report(
_make_finding(severity => $PASS_SEV, message => 'ok', check_name => 'CI'),
);
unlike $no_fix->render_text,
qr/Suggested fixes:/,
'no fix list when all findings are non-fixable';
};
subtest 'Report::render_text -- summary line formatting' => sub {
message => 'Problem',
detail => 'Extended explanation',
check_name => 'Pod',
));
unlike $r->render_text(verbose => 0), qr/Extended explanation/,
'detail hidden in non-verbose mode';
like $r->render_text(verbose => 1), qr/Extended explanation/,
'detail shown in verbose mode';
};
subtest 'Report::render_text -- fix prompt only when fixable findings exist' => sub {
my $r_fix = App::Project::Doctor::Report->new;
$r_fix->add_findings(_f(
severity => $SEV_ERROR,
message => 'Fixme',
check_name => 'CI',
fix => sub {},
));
like $r_fix->render_text, qr/Suggested fixes:/, 'fix list header present';
like $r_fix->render_text, qr/Would you like me to apply/, 'prompt present';
my $r_nof = App::Project::Doctor::Report->new;
$r_nof->add_findings(_f(severity => $SEV_PASS, message => 'ok', check_name => 'CI'));
unlike $r_nof->render_text, qr/Suggested fixes:/, 'no fix list when unfixable';
};
subtest 'Report::render_text -- "No errors or warnings" when fully clean' => sub {
my $r = App::Project::Doctor::Report->new;
$r->add_findings(_f(severity => $SEV_PASS, message => 'ok', check_name => 'A'));
like $r->render_text, qr/No errors or warnings/, 'clean summary text';
( run in 1.309 second using v1.01-cache-2.11-cpan-6aa56a78535 )