#####################################################################
#
# the Perl::Tidy::FileWriter class writes the output file
#
#####################################################################
package Perl::Tidy::FileWriter;
use strict;
use warnings;
our $VERSION = '20230912';
use constant DEVEL_MODE => 0;
use constant EMPTY_STRING => q{};
sub AUTOLOAD {
# Catch any undefined sub calls so that we are sure to get
# some diagnostic information. This sub should never be called
# except for a programming error.
our $AUTOLOAD;
return if ( $AUTOLOAD =~ /\bDESTROY$/ );
my ( $pkg, $fname, $lno ) = caller();
my $my_package = __PACKAGE__;
print {*STDERR} <<EOM;
======================================================================
Error detected in package '$my_package', version $VERSION
Received unexpected AUTOLOAD call for sub '$AUTOLOAD'
Called from package: '$pkg'
Called from File '$fname' at line '$lno'
This error is probably due to a recent programming change
======================================================================
EOM
exit 1;
} ## end sub AUTOLOAD
sub DESTROY {
# required to avoid call to AUTOLOAD in some versions of perl
}
my $input_stream_name = EMPTY_STRING;
# Maximum number of little messages; probably need not be changed.
use constant MAX_NAG_MESSAGES => 6;
BEGIN {
# Array index names for variables.
# Do not combine with other BEGIN blocks (c101).
my $i = 0;
use constant {
_logger_object_ => $i++,
_rOpts_ => $i++,
_output_line_number_ => $i++,
_consecutive_blank_lines_ => $i++,
_consecutive_nonblank_lines_ => $i++,
_consecutive_new_blank_lines_ => $i++,
_first_line_length_error_ => $i++,
_max_line_length_error_ => $i++,
_last_line_length_error_ => $i++,
_first_line_length_error_at_ => $i++,
_max_line_length_error_at_ => $i++,
_last_line_length_error_at_ => $i++,
_line_length_error_count_ => $i++,
_max_output_line_length_ => $i++,
_max_output_line_length_at_ => $i++,
_rK_checklist_ => $i++,
_K_arrival_order_matches_ => $i++,
_K_sequence_error_msg_ => $i++,
_K_last_arrival_ => $i++,
_save_logfile_ => $i++,
_routput_string_ => $i++,
};
} ## end BEGIN
sub Die {
my ($msg) = @_;
Perl::Tidy::Die($msg);
return;
}
sub Fault {
my ($msg) = @_;
# This routine is called for errors that really should not occur
# except if there has been a bug introduced by a recent program change.
# Please add comments at calls to Fault to explain why the call
# should not occur, and where to look to fix it.
my ( $package0, $filename0, $line0, $subroutine0 ) = caller(0);
my ( $package1, $filename1, $line1, $subroutine1 ) = caller(1);
my ( $package2, $filename2, $line2, $subroutine2 ) = caller(2);
my $pkg = __PACKAGE__;
Die(<<EOM);
==============================================================================
While operating on input stream with name: '$input_stream_name'
A fault was detected at line $line0 of sub '$subroutine1'
in file '$filename1'
which was called from line $line1 of sub '$subroutine2'
Message: '$msg'
This is probably an error introduced by a recent programming change.
$pkg reports VERSION='$VERSION'.
==============================================================================
EOM
# This return is to keep Perl-Critic from complaining.
return;
} ## end sub Fault
sub warning {
my ( $self, $msg ) = @_;
my $logger_object = $self->[_logger_object_];
if ($logger_object) { $logger_object->warning($msg); }
return;
} ## end sub warning
sub write_logfile_entry {
my ( $self, $msg ) = @_;
my $logger_object = $self->[_logger_object_];
if ($logger_object) {
$logger_object->write_logfile_entry($msg);
}
return;
} ## end sub write_logfile_entry
sub new {
my ( $class, $line_sink_object, $rOpts, $logger_object ) = @_;
my $self = [];
$self->[_logger_object_] = $logger_object;
$self->[_rOpts_] = $rOpts;
$self->[_output_line_number_] = 1;
$self->[_consecutive_blank_lines_] = 0;
$self->[_consecutive_nonblank_lines_] = 0;
$self->[_consecutive_new_blank_lines_] = 0;
$self->[_first_line_length_error_] = 0;
$self->[_max_line_length_error_] = 0;
$self->[_last_line_length_error_] = 0;
$self->[_first_line_length_error_at_] = 0;
$self->[_max_line_length_error_at_] = 0;
$self->[_last_line_length_error_at_] = 0;
$self->[_line_length_error_count_] = 0;
$self->[_max_output_line_length_] = 0;
$self->[_max_output_line_length_at_] = 0;
$self->[_rK_checklist_] = [];
$self->[_K_arrival_order_matches_] = 0;
$self->[_K_sequence_error_msg_] = EMPTY_STRING;
$self->[_K_last_arrival_] = -1;
$self->[_save_logfile_] = defined($logger_object);
$self->[_routput_string_] = undef;
# '$line_sink_object' is a SCALAR ref which receives the lines.
my $ref = ref($line_sink_object);
if ( !$ref ) {
Fault("FileWriter expects line_sink_object to be a ref\n");
}
elsif ( $ref eq 'SCALAR' ) {
$self->[_routput_string_] = $line_sink_object;
}
else {
my $str = $ref;
if ( length($str) > 63 ) { $str = substr( $str, 0, 60 ) . '...' }
Fault(<<EOM);
FileWriter expects 'line_sink_object' to be ref to SCALAR but it is ref to:
$str
EOM
}
# save input stream name for local error messages
$input_stream_name = EMPTY_STRING;
if ($logger_object) {
$input_stream_name = $logger_object->get_input_stream_name();
}
bless $self, $class;
return $self;
} ## end sub new
sub setup_convergence_test {
my ( $self, $rlist ) = @_;
if ( @{$rlist} ) {
# We are going to destroy the list, so make a copy
# and put in reverse order so we can pop values
my @list = @{$rlist};
if ( $list[0] < $list[-1] ) {
@list = reverse @list;
}
$self->[_rK_checklist_] = \@list;
}
$self->[_K_arrival_order_matches_] = 1;
$self->[_K_sequence_error_msg_] = EMPTY_STRING;
$self->[_K_last_arrival_] = -1;
return;
} ## end sub setup_convergence_test
sub get_convergence_check {
my ($self) = @_;
my $rlist = $self->[_rK_checklist_];
# converged if all K arrived and in correct order
return $self->[_K_arrival_order_matches_] && !@{$rlist};
} ## end sub get_convergence_check
sub get_output_line_number {
return $_[0]->[_output_line_number_];
}
sub decrement_output_line_number {
$_[0]->[_output_line_number_]--;
return;
}
sub get_consecutive_nonblank_lines {
return $_[0]->[_consecutive_nonblank_lines_];
}
sub get_consecutive_blank_lines {
return $_[0]->[_consecutive_blank_lines_];
}
sub reset_consecutive_blank_lines {
$_[0]->[_consecutive_blank_lines_] = 0;
return;
}
# This sub call allows termination of logfile writing for efficiency when we
# know that the logfile will not be saved.
sub set_save_logfile {
my ( $self, $save_logfile ) = @_;
$self->[_save_logfile_] = $save_logfile;
return;
}
sub want_blank_line {
my $self = shift;
if ( !$self->[_consecutive_blank_lines_] ) {
$self->write_blank_code_line();
}
return;
} ## end sub want_blank_line
sub require_blank_code_lines {
# write out the requested number of blanks regardless of the value of -mbl
# unless -mbl=0. This allows extra blank lines to be written for subs and
# packages even with the default -mbl=1
my ( $self, $count ) = @_;
my $need = $count - $self->[_consecutive_blank_lines_];
my $rOpts = $self->[_rOpts_];
my $forced = $rOpts->{'maximum-consecutive-blank-lines'} > 0;
foreach ( 0 .. $need - 1 ) {
$self->write_blank_code_line($forced);
}
return;
} ## end sub require_blank_code_lines
sub write_blank_code_line {
my ( $self, $forced ) = @_;
# Write a blank line of code, given:
# $forced = optional flag which, if set, forces the blank line
# to be written. This allows the -mbl flag to be temporarily
# exceeded.
my $rOpts = $self->[_rOpts_];
return
if (!$forced
&& $self->[_consecutive_blank_lines_] >=
$rOpts->{'maximum-consecutive-blank-lines'} );
$self->[_consecutive_nonblank_lines_] = 0;
# Balance old blanks against new (forced) blanks instead of writing them.
# This fixes case b1073.
if ( !$forced && $self->[_consecutive_new_blank_lines_] > 0 ) {
$self->[_consecutive_new_blank_lines_]--;
return;
}
${ $self->[_routput_string_] } .= "\n";
$self->[_output_line_number_]++;
$self->[_consecutive_blank_lines_]++;
$self->[_consecutive_new_blank_lines_]++ if ($forced);
return;
} ## end sub write_blank_code_line
use constant MAX_PRINTED_CHARS => 80;
sub write_code_line {
my ( $self, $str, $K ) = @_;
# Write a line of code, given
# $str = the line of code
# $K = an optional check integer which, if if given, must
# increase monotonically. This was added to catch cache
# sequence errors in the vertical aligner.
$self->[_consecutive_blank_lines_] = 0;
$self->[_consecutive_new_blank_lines_] = 0;
$self->[_consecutive_nonblank_lines_]++;
$self->[_output_line_number_]++;
${ $self->[_routput_string_] } .= $str;
if ( $self->[_save_logfile_] ) { $self->check_line_lengths($str) }
#----------------------------
# Convergence and error check
#----------------------------
if ( defined($K) ) {
# Convergence check: we are checking if all defined K values arrive in
# the order which was defined by the caller. Quit checking if any
# unexpected K value arrives.
if ( $self->[_K_arrival_order_matches_] ) {
my $Kt = pop @{ $self->[_rK_checklist_] };
if ( !defined($Kt) || $Kt != $K ) {
$self->[_K_arrival_order_matches_] = 0;
}
}
# Check for out-of-order arrivals of index K. The K values are the
# token indexes of the last token of code lines, and they should come
# out in increasing order. Otherwise something is seriously wrong.
# Most likely a recent programming change to VerticalAligner.pm has
# caused lines to go out in the wrong order. This could happen if
# either the cache or buffer that it uses are emptied in the wrong
# order.
if ( $K < $self->[_K_last_arrival_]
&& !$self->[_K_sequence_error_msg_] )
{
my $K_prev = $self->[_K_last_arrival_];
chomp $str;
if ( length($str) > MAX_PRINTED_CHARS ) {
$str = substr( $str, 0, MAX_PRINTED_CHARS ) . "...";
}
my $msg = <<EOM;
While operating on input stream with name: '$input_stream_name'
Lines have arrived out of order in sub 'write_code_line'
as detected by token index K=$K arriving after index K=$K_prev in the following line:
$str
This is probably due to a recent programming change and needs to be fixed.
EOM
# Always die during development, this needs to be fixed
if (DEVEL_MODE) { Fault($msg) }
# Otherwise warn if string is not empty (added for b1378)
$self->warning($msg) if ( length($str) );
# Only issue this warning once
$self->[_K_sequence_error_msg_] = $msg;
}
$self->[_K_last_arrival_] = $K;
}
return;
} ## end sub write_code_line
sub write_line {
my ( $self, $str ) = @_;
# Write a line directly to the output, without any counting of blank or
# non-blank lines.
${ $self->[_routput_string_] } .= $str;
if ( chomp $str ) { $self->[_output_line_number_]++; }
if ( $self->[_save_logfile_] ) { $self->check_line_lengths($str) }
return;
} ## end sub write_line
sub check_line_lengths {
my ( $self, $str ) = @_;
# collect info on line lengths for logfile
# This calculation of excess line length ignores any internal tabs
my $rOpts = $self->[_rOpts_];
chomp $str;
my $len_str = length($str);
my $exceed = $len_str - $rOpts->{'maximum-line-length'};
if ( $str && substr( $str, 0, 1 ) eq "\t" && $str =~ /^\t+/g ) {
$exceed += pos($str) * $rOpts->{'indent-columns'};
}
# Note that we just incremented output line number to future value
# so we must subtract 1 for current line number
if ( $len_str > $self->[_max_output_line_length_] ) {
$self->[_max_output_line_length_] = $len_str;
$self->[_max_output_line_length_at_] =
$self->[_output_line_number_] - 1;
}
if ( $exceed > 0 ) {
my $output_line_number = $self->[_output_line_number_];
$self->[_last_line_length_error_] = $exceed;
$self->[_last_line_length_error_at_] = $output_line_number - 1;
if ( $self->[_line_length_error_count_] == 0 ) {
$self->[_first_line_length_error_] = $exceed;
$self->[_first_line_length_error_at_] = $output_line_number - 1;
}
if ( $self->[_last_line_length_error_] >
$self->[_max_line_length_error_] )
{
$self->[_max_line_length_error_] = $exceed;
$self->[_max_line_length_error_at_] = $output_line_number - 1;
}
if ( $self->[_line_length_error_count_] < MAX_NAG_MESSAGES ) {
$self->write_logfile_entry(
"Line length exceeded by $exceed characters\n");
}
$self->[_line_length_error_count_]++;
}
return;
} ## end sub check_line_lengths
sub report_line_length_errors {
my $self = shift;
# Write summary info about line lengths to the log file
my $rOpts = $self->[_rOpts_];
my $line_length_error_count = $self->[_line_length_error_count_];
if ( $line_length_error_count == 0 ) {
$self->write_logfile_entry(
"No lines exceeded $rOpts->{'maximum-line-length'} characters\n");
my $max_output_line_length = $self->[_max_output_line_length_];
my $max_output_line_length_at = $self->[_max_output_line_length_at_];
$self->write_logfile_entry(
" Maximum output line length was $max_output_line_length at line $max_output_line_length_at\n"
);
}
else {
my $word = ( $line_length_error_count > 1 ) ? "s" : EMPTY_STRING;
$self->write_logfile_entry(
"$line_length_error_count output line$word exceeded $rOpts->{'maximum-line-length'} characters:\n"
);
$word = ( $line_length_error_count > 1 ) ? "First" : EMPTY_STRING;
my $first_line_length_error = $self->[_first_line_length_error_];
my $first_line_length_error_at = $self->[_first_line_length_error_at_];
$self->write_logfile_entry(
" $word at line $first_line_length_error_at by $first_line_length_error characters\n"
);
if ( $line_length_error_count > 1 ) {
my $max_line_length_error = $self->[_max_line_length_error_];
my $max_line_length_error_at = $self->[_max_line_length_error_at_];
my $last_line_length_error = $self->[_last_line_length_error_];
my $last_line_length_error_at =
$self->[_last_line_length_error_at_];
$self->write_logfile_entry(
" Maximum at line $max_line_length_error_at by $max_line_length_error characters\n"
);
$self->write_logfile_entry(
" Last at line $last_line_length_error_at by $last_line_length_error characters\n"
);
}
}
return;
} ## end sub report_line_length_errors
1;