package Perl::Critic::Violation; use 5.010001; use strict; use warnings; use Readonly; use File::Basename qw< basename >; use Scalar::Util qw< blessed >; use String::Format qw< stringf >; use overload ( q{""} => 'to_string', cmp => '_compare' ); use Perl::Critic::Utils qw< :characters :internal_lookup >; use Perl::Critic::Utils::POD qw< get_pod_section_for_module trim_pod_section >; use Perl::Critic::Exception::Fatal::Internal qw< throw_internal >; our $VERSION = '1.152'; Readonly::Scalar my $NO_EXCEPTION_NO_SPLIT_LIMIT => -1; Readonly::Scalar my $LOCATION_LINE_NUMBER => 0; Readonly::Scalar my $LOCATION_COLUMN_NUMBER => 1; Readonly::Scalar my $LOCATION_VISUAL_COLUMN_NUMBER => 2; Readonly::Scalar my $LOCATION_LOGICAL_LINE_NUMBER => 3; Readonly::Scalar my $LOCATION_LOGICAL_FILENAME => 4; # Class variables... my $format = "%m at line %l, column %c. %e.\n"; # Default stringy format my %diagnostics; #----------------------------------------------------------------------------- Readonly::Scalar my $CONSTRUCTOR_ARG_COUNT => 5; sub new { my ( $class, $desc, $expl, $elem, $sev ) = @_; # Check arguments to help out developers who might # be creating new Perl::Critic::Policy modules. if ( @_ != $CONSTRUCTOR_ARG_COUNT ) { throw_internal 'Wrong number of args to Violation->new()'; } if ( eval { $elem->isa( 'Perl::Critic::Document' ) } ) { # break the facade, return the real PPI::Document $elem = $elem->ppi_document(); } if ( not eval { $elem->isa( 'PPI::Element' ) } ) { throw_internal '3rd arg to Violation->new() must be a PPI::Element'; } # Strip punctuation. These are controlled by the user via the # formats. He/She can use whatever makes sense to them. ($desc, $expl) = _chomp_periods($desc, $expl); # Create object my $self = bless {}, $class; $self->{_description} = $desc; $self->{_explanation} = $expl; $self->{_severity} = $sev; $self->{_policy} = caller; # PPI eviscerates the Elements in a Document when the Document gets # DESTROY()ed, and thus they aren't useful after it is gone. So we have # to preemptively grab everything we could possibly want. $self->{_element_class} = blessed $elem; my $top = $elem->top(); $self->{_filename} = $top->can('filename') ? $top->filename() : undef; $self->{_source} = _line_containing_violation( $elem ); $self->{_location} = $elem->location() || [ 0, 0, 0, 0, $self->filename() ]; return $self; } #----------------------------------------------------------------------------- sub set_format { return $format = verbosity_to_format( $_[0] ); } ## no critic(ArgUnpacking) sub get_format { return $format; } #----------------------------------------------------------------------------- sub sort_by_location { ## no critic(ArgUnpacking) ref $_[0] || shift; # Can call as object or class method return scalar @_ if ! wantarray; # In case we are called in scalar context ## TODO: What if $a and $b are not Violation objects? return map {$_->[0]} sort { ($a->[1] <=> $b->[1]) || ($a->[2] <=> $b->[2]) } map {[$_, $_->location->[0] || 0, $_->location->[1] || 0]} @_; } #----------------------------------------------------------------------------- sub sort_by_severity { ## no critic(ArgUnpacking) ref $_[0] || shift; # Can call as object or class method return scalar @_ if ! wantarray; # In case we are called in scalar context ## TODO: What if $a and $b are not Violation objects? return map {$_->[0]} sort { $a->[1] <=> $b->[1] } map {[$_, $_->severity() || 0]} @_; } #----------------------------------------------------------------------------- sub location { my $self = shift; return $self->{_location}; } #----------------------------------------------------------------------------- sub line_number { my ($self) = @_; return $self->location()->[$LOCATION_LINE_NUMBER]; } #----------------------------------------------------------------------------- sub logical_line_number { my ($self) = @_; return $self->location()->[$LOCATION_LOGICAL_LINE_NUMBER]; } #----------------------------------------------------------------------------- sub column_number { my ($self) = @_; return $self->location()->[$LOCATION_COLUMN_NUMBER]; } #----------------------------------------------------------------------------- sub visual_column_number { my ($self) = @_; return $self->location()->[$LOCATION_VISUAL_COLUMN_NUMBER]; } #----------------------------------------------------------------------------- sub diagnostics { my ($self) = @_; my $policy = $self->policy(); if ( not $diagnostics{$policy} ) { eval { ## no critic (RequireCheckingReturnValueOfEval) my $module_name = ref $policy || $policy; $diagnostics{$policy} = trim_pod_section( get_pod_section_for_module( $module_name, 'DESCRIPTION' ) ); }; $diagnostics{$policy} ||= " No diagnostics available\n"; } return $diagnostics{$policy}; } #----------------------------------------------------------------------------- sub description { my $self = shift; return $self->{_description}; } #----------------------------------------------------------------------------- sub explanation { my $self = shift; my $expl = $self->{_explanation}; if ( !$expl ) { $expl = '(no explanation)'; } if ( ref $expl eq 'ARRAY' ) { my $page = @{$expl} > 1 ? 'pages' : 'page'; $page .= $SPACE . join $COMMA, @{$expl}; $expl = "See $page of PBP"; } return $expl; } #----------------------------------------------------------------------------- sub severity { my $self = shift; return $self->{_severity}; } #----------------------------------------------------------------------------- sub policy { my $self = shift; return $self->{_policy}; } #----------------------------------------------------------------------------- sub filename { my $self = shift; return $self->{_filename}; } #----------------------------------------------------------------------------- sub logical_filename { my ($self) = @_; return $self->location()->[$LOCATION_LOGICAL_FILENAME]; } #----------------------------------------------------------------------------- sub source { my $self = shift; return $self->{_source}; } #----------------------------------------------------------------------------- sub element_class { my ($self) = @_; return $self->{_element_class}; } #----------------------------------------------------------------------------- sub to_string { my $self = shift; my $long_policy = $self->policy(); (my $short_policy = $long_policy) =~ s/ \A Perl::Critic::Policy:: //xms; # Wrap the more expensive ones in sub{} to postpone evaluation my %fspec = ( 'f' => sub { $self->logical_filename() }, 'F' => sub { basename( $self->logical_filename() ) }, 'g' => sub { $self->filename() }, 'G' => sub { basename( $self->filename() ) }, 'l' => sub { $self->logical_line_number() }, 'L' => sub { $self->line_number() }, 'c' => sub { $self->visual_column_number() }, 'C' => sub { $self->element_class() }, 'm' => $self->description(), 'e' => $self->explanation(), 's' => $self->severity(), 'd' => sub { $self->diagnostics() }, 'r' => sub { $self->source() }, 'P' => $long_policy, 'p' => $short_policy, ); return stringf($format, %fspec); } #----------------------------------------------------------------------------- # Apparently, some perls do not implicitly stringify overloading # objects before doing a comparison. This causes a couple of our # sorting tests to fail. To work around this, we overload C to # do it explicitly. # # 20060503 - More information: This problem has been traced to # Test::Simple versions <= 0.60, not perl itself. Upgrading to # Test::Simple v0.62 will fix the problem. But rather than forcing # everyone to upgrade, I have decided to leave this workaround in # place. sub _compare { return "$_[0]" cmp "$_[1]" } #----------------------------------------------------------------------------- sub _line_containing_violation { my ( $elem ) = @_; my $stmnt = $elem->statement() || $elem; my $code_string = $stmnt->content() || $EMPTY; # Split into individual lines # From `perldoc -f split`: # If LIMIT is negative, it is treated as if it were instead # arbitrarily large; as many fields as possible are produced. # # If it's omitted, it's the same except trailing empty fields, so we need # without a limit for the split and without an exception my @lines = split qr{ \n }xms, $code_string, $NO_EXCEPTION_NO_SPLIT_LIMIT; # Take the line containing the element that is in violation my $inx = ( $elem->line_number() || 0 ) - ( $stmnt->line_number() || 0 ); $inx > @lines and return $EMPTY; return $lines[$inx]; } #----------------------------------------------------------------------------- sub _chomp_periods { my @args = @_; for (@args) { next if not defined or ref; s{ [.]+ \z }{}xms; } return @args; } #----------------------------------------------------------------------------- 1; #----------------------------------------------------------------------------- __END__ =head1 NAME Perl::Critic::Violation - A violation of a Policy found in some source code. =head1 SYNOPSIS use PPI; use Perl::Critic::Violation; my $elem = $doc->child(0); # $doc is a PPI::Document object my $desc = 'Offending code'; # Describe the violation my $expl = [1,45,67]; # Page numbers from PBP my $sev = 5; # Severity level of this violation my $vio = Perl::Critic::Violation->new($desc, $expl, $node, $sev); =head1 DESCRIPTION Perl::Critic::Violation is the generic representation of an individual Policy violation. Its primary purpose is to provide an abstraction layer so that clients of L don't have to know anything about L. The C method of all L subclasses must return a list of these Perl::Critic::Violation objects. =head1 INTERFACE SUPPORT This is considered to be a public class. Any changes to its interface will go through a deprecation cycle. =head1 CONSTRUCTOR =over =item C Returns a reference to a new C object. The arguments are a description of the violation (as string), an explanation for the policy (as string) or a series of page numbers in PBP (as an ARRAY ref), a reference to the L element that caused the violation, and the severity of the violation (as an integer). =back =head1 METHODS =over =item C Returns a brief description of the specific violation. In other words, this value may change on a per violation basis. =item C Returns an explanation of the policy as a string or as reference to an array of page numbers in PBP. This value will generally not change based upon the specific code violating the policy. =item C Don't use this method. Use the C, C, C, C, and C methods instead. Returns a five-element array reference containing the line and real & virtual column and logical numbers and logical file name where this Violation occurred, as in L. =item C Returns the physical line number that the violation was found on. =item C Returns the logical line number that the violation was found on. This can differ from the physical line number when there were C<#line> directives in the code. =item C Returns the physical column that the violation was found at. This means that hard tab characters count as a single character. =item C Returns the column that the violation was found at, as it would appear if hard tab characters were expanded, based upon the value of L. =item C Returns the path to the file where this Violation occurred. In some cases, the path may be undefined because the source code was not read directly from a file. =item C Returns the logical path to the file where the Violation occurred. This can differ from C when there was a C<#line> directive in the code. =item C Returns the severity of this Violation as an integer ranging from 1 to 5, where 5 is the "most" severe. =item C If you need to sort Violations by severity, use this handy routine: @sorted = Perl::Critic::Violation::sort_by_severity(@violations); =item C If you need to sort Violations by location, use this handy routine: @sorted = Perl::Critic::Violation::sort_by_location(@violations); =item C Returns a formatted string containing a full discussion of the motivation for and details of the Policy module that created this Violation. This information is automatically extracted from the C section of the Policy module's POD. =item C Returns the name of the L that created this Violation. =item C Returns the string of source code that caused this exception. If the code spans multiple lines (e.g. multi-line statements, subroutines or other blocks), then only the line containing the violation will be returned. =item C Returns the L subclass of the code that caused this exception. =item C Class method. Sets the format for all Violation objects when they are evaluated in string context. The default is C<'%d at line %l, column %c. %e'>. See L<"OVERLOADS"> for formatting options. =item C Class method. Returns the current format for all Violation objects when they are evaluated in string context. =item C Returns a string representation of this violation. The content of the string depends on the current value of the C<$format> package variable. See L<"OVERLOADS"> for the details. =back =head1 OVERLOADS Perl::Critic::Violation overloads the C<""> operator to produce neat little messages when evaluated in string context. Formats are a combination of literal and escape characters similar to the way C works. If you want to know the specific formatting capabilities, look at L. Valid escape characters are: Escape Meaning ------- ---------------------------------------------------------------- %c Column number where the violation occurred %d Full diagnostic discussion of the violation (DESCRIPTION in POD) %e Explanation of violation or page numbers in PBP %F Just the name of the logical file where the violation occurred. %f Path to the logical file where the violation occurred. %G Just the name of the physical file where the violation occurred. %g Path to the physical file where the violation occurred. %l Logical line number where the violation occurred %L Physical line number where the violation occurred %m Brief description of the violation %P Full name of the Policy module that created the violation %p Name of the Policy without the Perl::Critic::Policy:: prefix %r The string of source code that caused the violation %C The class of the PPI::Element that caused the violation %s The severity level of the violation Explanation of the C<%F>, C<%f>, C<%G>, C<%G>, C<%l>, and C<%L> formats: Using C<#line> directives, you can affect what perl thinks the current line number and file name are; see L for the details. Under normal circumstances, the values of C<%F>, C<%f>, and C<%l> will match the values of C<%G>, C<%g>, and C<%L>, respectively. In the presence of a C<#line> directive, the values of C<%F>, C<%f>, and C<%l> will change to take that directive into account. The values of C<%G>, C<%g>, and C<%L> are unaffected by those directives. Here are some examples: Perl::Critic::Violation::set_format("%m at line %l, column %c.\n"); # looks like "Mixed case variable name at line 6, column 23." Perl::Critic::Violation::set_format("%m near '%r'\n"); # looks like "Mixed case variable name near 'my $theGreatAnswer = 42;'" Perl::Critic::Violation::set_format("%l:%c:%p\n"); # looks like "6:23:NamingConventions::Capitalization" Perl::Critic::Violation::set_format("%m at line %l. %e. \n%d\n"); # looks like "Mixed case variable name at line 6. See page 44 of PBP. Conway's recommended naming convention is to use lower-case words separated by underscores. Well-recognized acronyms can be in ALL CAPS, but must be separated by underscores from other parts of the name." =head1 AUTHOR Jeffrey Ryan Thalhammer =head1 COPYRIGHT Copyright (c) 2005-2023 Imaginative Software Systems. All rights reserved. This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself. The full text of this license can be found in the LICENSE file included with this module. =cut # Local Variables: # mode: cperl # cperl-indent-level: 4 # fill-column: 78 # indent-tabs-mode: nil # c-indentation-style: bsd # End: # ex: set ts=8 sts=4 sw=4 tw=78 ft=perl expandtab shiftround :