App-Project-Doctor

 view release on metacpan or  search on metacpan

lib/App/Project/Doctor/Report.pm  view on Meta::CPAN


sub render_text {
	my ($self, %opts) = @_;
	# verbose mode adds per-finding detail lines under each check summary.
	my $verbose = $opts{verbose} // 0;

	# Group findings by check name while preserving the insertion order of
	# the first finding seen for each check.  This keeps the output stable.
	my (%by_check, @order);
	for my $f ($self->all_findings) {
		my $name = $f->check_name;
		unless (exists $by_check{$name}) {
			# First time we see this check name -- record its position.
			push @order, $name;
			$by_check{$name} = [];
		}
		push @{ $by_check{$name} }, $f;
	}

	# Build the output one line at a time and join at the end.
	my @lines;
	for my $name (@order) {
		my @group = @{ $by_check{$name} };
		# Choose the worst severity in this group to pick the icon.
		my $sev   = _worst_severity(\@group);
		my $icon  = $ICON{$sev};    # Severity is always valid; no fallback needed.

		# Use the first non-pass finding as the summary line for mixed groups.
		my ($lead) = grep { $_->severity ne 'pass' } @group;
		my $summary = $lead ? $lead->message : $group[0]->message;

		# Format: icon  check-name (padded)  summary message
		push @lines, sprintf('  %-4s  %-*s  %s', $icon, $LABEL_WIDTH, $name, $summary);

		if ($verbose) {
			# In verbose mode, print each non-pass finding with its detail.
			for my $f (@group) {
				# Skip pass findings in verbose mode -- they have no useful detail.
				next if $f->severity eq 'pass';
				push @lines, sprintf('        -> %s', $f->message);
				# Only print the detail line when there is something to show.
				push @lines, sprintf('           %s', $f->detail) if $f->detail;
			}
		}
	}

	# Print a one-line summary of error/warning counts below the check table.
	my $ec = scalar($self->errors);
	my $wc = scalar($self->warnings);
	push @lines, '';    # Blank line before the summary.
	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

Returns findings as a pretty-printed JSON string (requires L<JSON::MaybeXS>).

=cut

sub render_json {
	my $self = shift;
	# JSON::MaybeXS is loaded lazily so it is only required when --format=json.
	require JSON::MaybeXS;
	# canonical => 1 sorts keys so the output is diff-friendly.
	return JSON::MaybeXS->new(utf8 => 1, pretty => 1, canonical => 1)
	                    ->encode([ map { $_->to_hash } $self->all_findings ]);
}

=head2 render_tap

Returns a TAP-format string for CI pipeline consumption.

=cut

sub render_tap {
	my $self     = shift;
	my @findings = $self->all_findings;
	# TAP header declares how many tests will follow.
	my @lines    = ('1..' . scalar @findings);
	my $n        = 0;
	for my $f (@findings) {
		$n++;
		# pass and info severities are "ok"; error and warning are "not ok".
		my $ok = $f->severity =~ /^(?:pass|info)$/ ? 'ok' : 'not ok';
		push @lines, sprintf('%s %d - [%s] %s', $ok, $n, $f->check_name, $f->message);
	}
	return join("\n", @lines) . "\n";
}

# ---------------------------------------------------------------------------
# Private helpers
# ---------------------------------------------------------------------------

# Purpose:    Find the most severe severity string in a group of findings.
# Entry:      $group is a non-empty arrayref of Finding objects.
# Exit:       String -- one of 'error', 'warning', 'info', 'pass'.
# Side effects: None.
sub _worst_severity {
	my $group = shift;
	# Sort by numeric rank (descending) and take the first (= highest) value.
	return (sort { $SEV_RANK{$b} <=> $SEV_RANK{$a} } map { $_->severity } @{$group})[0];
}

1;



( run in 0.959 second using v1.01-cache-2.11-cpan-0b5f733616e )