Login
Rename the "--script-extensions" parameter to "--program-extensions"
[gknop/Perl-Critic.git] / lib / Perl / Critic / Document.pm
CommitLineData
6036a254 1##############################################################################
a73f4a71
JRT
2# $URL$
3# $Date$
4# $Author$
5# $Revision$
6036a254 6##############################################################################
5bf96118
CD
7
8package Perl::Critic::Document;
9
df6dee2b 10use 5.006001;
5bf96118 11use strict;
58a9e587 12use warnings;
267b39b4 13
d5835ca8 14use Carp qw< confess >;
2d2fd196 15
5bf96118 16use PPI::Document;
d5835ca8
JRT
17use PPI::Document::File;
18
81e74a91 19use List::Util qw< reduce >;
d5835ca8 20use Scalar::Util qw< blessed weaken >;
267b39b4 21use version;
5bf96118 22
d5835ca8 23use Perl::Critic::Annotation;
d533eee5
ES
24use Perl::Critic::Exception::Parse qw< throw_parse >;
25use Perl::Critic::Utils qw < :characters shebang_line >;
26use Perl::Critic::Utils::Constants qw< :document_type >;
d5835ca8 27
6036a254 28#-----------------------------------------------------------------------------
58a9e587 29
9c4f4d28 30our $VERSION = '1.105';
5bf96118 31
6036a254 32#-----------------------------------------------------------------------------
5bf96118
CD
33
34our $AUTOLOAD;
937b8de0 35sub AUTOLOAD { ## no critic (ProhibitAutoloading,ArgUnpacking)
6e7d6c9f
CD
36 my ( $function_name ) = $AUTOLOAD =~ m/ ([^:\']+) \z /xms;
37 return if $function_name eq 'DESTROY';
38 my $self = shift;
39 return $self->{_doc}->$function_name(@_);
5bf96118
CD
40}
41
6036a254 42#-----------------------------------------------------------------------------
5bf96118 43
58a9e587 44sub new {
d5835ca8 45 my ($class, @args) = @_;
937b8de0 46 my $self = bless {}, $class;
d5835ca8
JRT
47 return $self->_init(@args);
48}
49
50#-----------------------------------------------------------------------------
51
d533eee5 52sub _init { ## no critic (Subroutines::RequireArgUnpacking)
d5835ca8 53
d533eee5
ES
54 my $self = shift;
55 my %args;
56 if (@_ == 1) {
57 warnings::warnif(
58 'deprecated',
59 'Perl::Critic::Document->new($source) deprecated, use Perl::Critic::Document->new(-source => $source) instead.' ## no critic (ValuesAndExpressions::RequireInterpolationOfMetachars)
60 );
61 %args = ('-source' => shift);
62 } else {
63 %args = @_;
64 }
65 my $source_code = $args{'-source'};
d5835ca8
JRT
66
67 # $source_code can be a file name, or a reference to a
68 # PPI::Document, or a reference to a scalar containing source
69 # code. In the last case, PPI handles the translation for us.
70
71 my $doc = _is_ppi_doc( $source_code ) ? $source_code
72 : ref $source_code ? PPI::Document->new($source_code)
73 : PPI::Document::File->new($source_code);
74
75 # Bail on error
76 if ( not defined $doc ) {
77 my $errstr = PPI::Document::errstr();
78 my $file = ref $source_code ? undef : $source_code;
79 throw_parse
80 message => qq<Can't parse code: $errstr>,
81 file_name => $file;
82 }
83
937b8de0 84 $self->{_doc} = $doc;
d5835ca8
JRT
85 $self->{_annotations} = [];
86 $self->{_suppressed_violations} = [];
87 $self->{_disabled_line_map} = {};
88 $self->index_locations();
89 $self->_disable_shebang_fix();
d533eee5 90 $self->{_document_type} = $self->_compute_document_type(\%args);
d5835ca8 91
937b8de0 92 return $self;
5bf96118
CD
93}
94
6036a254 95#-----------------------------------------------------------------------------
58a9e587 96
d5835ca8
JRT
97sub _is_ppi_doc {
98 my ($ref) = @_;
99 return blessed($ref) && $ref->isa('PPI::Document');
100}
101
102#-----------------------------------------------------------------------------
103
2b6293b2
CD
104sub ppi_document {
105 my ($self) = @_;
106 return $self->{_doc};
107}
108
109#-----------------------------------------------------------------------------
110
47e1ff34 111sub isa {
6e7d6c9f
CD
112 my ($self, @args) = @_;
113 return $self->SUPER::isa(@args)
114 || ( (ref $self) && $self->{_doc} && $self->{_doc}->isa(@args) );
47e1ff34
CD
115}
116
6036a254 117#-----------------------------------------------------------------------------
47e1ff34 118
5bf96118 119sub find {
6e7d6c9f 120 my ($self, $wanted, @more_args) = @_;
5bf96118 121
58a9e587
JRT
122 # This method can only find elements by their class names. For
123 # other types of searches, delegate to the PPI::Document
5bf96118 124 if ( ( ref $wanted ) || !$wanted || $wanted !~ m/ \A PPI:: /xms ) {
6e7d6c9f 125 return $self->{_doc}->find($wanted, @more_args);
5bf96118 126 }
58a9e587
JRT
127
128 # Build the class cache if it doesn't exist. This happens at most
129 # once per Perl::Critic::Document instance. %elements of will be
130 # populated as a side-effect of calling the $finder_sub coderef
131 # that is produced by the caching_finder() closure.
5bf96118 132 if ( !$self->{_elements_of} ) {
389109ec 133
58a9e587 134 my %cache = ( 'PPI::Document' => [ $self ] );
389109ec
JRT
135
136 # The cache refers to $self, and $self refers to the cache. This
137 # creates a circular reference that leaks memory (i.e. $self is not
138 # destroyed until execution is complete). By weakening the reference,
139 # we allow perl to collect the garbage properly.
140 weaken( $cache{'PPI::Document'}->[0] );
141
58a9e587
JRT
142 my $finder_coderef = _caching_finder( \%cache );
143 $self->{_doc}->find( $finder_coderef );
144 $self->{_elements_of} = \%cache;
145 }
146
147 # find() must return false-but-defined on fail
148 return $self->{_elements_of}->{$wanted} || q{};
149}
150
6036a254 151#-----------------------------------------------------------------------------
58a9e587 152
fb21e21e 153sub find_first {
6e7d6c9f 154 my ($self, $wanted, @more_args) = @_;
fb21e21e
CD
155
156 # This method can only find elements by their class names. For
157 # other types of searches, delegate to the PPI::Document
158 if ( ( ref $wanted ) || !$wanted || $wanted !~ m/ \A PPI:: /xms ) {
6e7d6c9f 159 return $self->{_doc}->find_first($wanted, @more_args);
fb21e21e
CD
160 }
161
162 my $result = $self->find($wanted);
163 return $result ? $result->[0] : $result;
164}
165
6036a254 166#-----------------------------------------------------------------------------
fb21e21e 167
f5eeac3b 168sub find_any {
6e7d6c9f 169 my ($self, $wanted, @more_args) = @_;
f5eeac3b
CD
170
171 # This method can only find elements by their class names. For
172 # other types of searches, delegate to the PPI::Document
173 if ( ( ref $wanted ) || !$wanted || $wanted !~ m/ \A PPI:: /xms ) {
6e7d6c9f 174 return $self->{_doc}->find_any($wanted, @more_args);
f5eeac3b
CD
175 }
176
177 my $result = $self->find($wanted);
178 return $result ? 1 : $result;
179}
180
6036a254 181#-----------------------------------------------------------------------------
f5eeac3b 182
60108aef
CD
183sub filename {
184 my ($self) = @_;
c73ba7f3
JRT
185 my $doc = $self->{_doc};
186 return $doc->can('filename') ? $doc->filename() : undef;
60108aef
CD
187}
188
6036a254 189#-----------------------------------------------------------------------------
60108aef 190
267b39b4
ES
191sub highest_explicit_perl_version {
192 my ($self) = @_;
193
194 my $highest_explicit_perl_version =
195 $self->{_highest_explicit_perl_version};
196
197 if ( not exists $self->{_highest_explicit_perl_version} ) {
198 my $includes = $self->find( \&_is_a_version_statement );
199
200 if ($includes) {
81e74a91
ES
201 # Note: this doesn't use List::Util::max() because that function
202 # doesn't use the overloaded ">=" etc of a version object. The
203 # reduce() style lets version.pm take care of all comparing.
df9f8d80
ES
204 #
205 # For reference, max() ends up looking at the string converted to
206 # an NV, or something like that. An underscore like "5.005_04"
207 # provokes a warning and is chopped off at "5.005" thus losing the
208 # minor part from the comparison.
209 #
210 # An underscore "5.005_04" is supposed to mean an alpha release
81e74a91
ES
211 # and shouldn't be used in a perl version. But it's shown in
212 # perlfunc under "use" (as a number separator), and appears in
213 # several modules supplied with perl 5.10.0 (like version.pm
214 # itself!). At any rate if version.pm can understand it then
215 # that's enough for here.
267b39b4 216 $highest_explicit_perl_version =
81e74a91 217 reduce { $a >= $b ? $a : $b }
901273dd
ES
218 map { version->new( $_->version() ) }
219 @{$includes};
267b39b4
ES
220 }
221 else {
222 $highest_explicit_perl_version = undef;
223 }
224
225 $self->{_highest_explicit_perl_version} =
226 $highest_explicit_perl_version;
227 }
228
229 return $highest_explicit_perl_version if $highest_explicit_perl_version;
230 return;
231}
232
937b8de0
JRT
233#-----------------------------------------------------------------------------
234
d5835ca8
JRT
235sub process_annotations {
236 my ($self) = @_;
937b8de0 237
d5835ca8
JRT
238 my @annotations = Perl::Critic::Annotation->create_annotations($self);
239 $self->add_annotation(@annotations);
937b8de0
JRT
240 return $self;
241}
242
243#-----------------------------------------------------------------------------
244
d5835ca8
JRT
245sub line_is_disabled_for_policy {
246 my ($self, $line, $policy) = @_;
247 my $policy_name = ref $policy || $policy;
2d2fd196
JRT
248
249 # HACK: This Policy is special. If it is active, it cannot be
d1237298 250 # disabled by a "## no critic" annotation. Rather than create a general
2d2fd196 251 # hook in Policy.pm for enabling this behavior, we chose to hack
d5835ca8 252 # it here, since this isn't the kind of thing that most policies do
2f4b6b33
JRT
253
254 return 0 if $policy_name eq
2d2fd196
JRT
255 'Perl::Critic::Policy::Miscellanea::ProhibitUnrestrictedNoCritic';
256
d5835ca8
JRT
257 return 1 if $self->{_disabled_line_map}->{$line}->{$policy_name};
258 return 1 if $self->{_disabled_line_map}->{$line}->{ALL};
937b8de0
JRT
259 return 0;
260}
261
262#-----------------------------------------------------------------------------
263
d5835ca8
JRT
264sub add_annotation {
265 my ($self, @annotations) = @_;
266
267 # Add annotation to our private map for quick lookup
268 for my $annotation (@annotations) {
269
270 my ($start, $end) = $annotation->effective_range();
271 my @affected_policies = $annotation->disables_all_policies ?
272 qw(ALL) : $annotation->disabled_policies();
273
274 # TODO: Find clever way to do this with hash slices
275 for my $line ($start .. $end) {
276 for my $policy (@affected_policies) {
277 $self->{_disabled_line_map}->{$line}->{$policy} = 1;
278 }
279 }
280 }
281
282 push @{ $self->{_annotations} }, @annotations;
2d2fd196
JRT
283 return $self;
284}
4880392e 285
2d2fd196 286#-----------------------------------------------------------------------------
4880392e 287
d5835ca8 288sub annotations {
2d2fd196 289 my ($self) = @_;
d5835ca8
JRT
290 return @{ $self->{_annotations} };
291}
4880392e 292
d5835ca8 293#-----------------------------------------------------------------------------
4880392e 294
d5835ca8
JRT
295sub add_suppressed_violation {
296 my ($self, $violation) = @_;
297 push @{$self->{_suppressed_violations}}, $violation;
298 return $self;
299}
4880392e 300
d5835ca8 301#-----------------------------------------------------------------------------
2d2fd196 302
d5835ca8
JRT
303sub suppressed_violations {
304 my ($self) = @_;
305 return @{ $self->{_suppressed_violations} };
95ebf9b0
JRT
306}
307
308#-----------------------------------------------------------------------------
d533eee5
ES
309
310sub document_type {
311 my ($self) = @_;
312 return $self->{_document_type};
313}
314
315#-----------------------------------------------------------------------------
316
1b936936 317sub is_program {
d533eee5 318 my ($self) = @_;
1b936936 319 return $self->{_document_type} eq $DOCUMENT_TYPE_PROGRAM;
d533eee5
ES
320}
321
322#-----------------------------------------------------------------------------
323
324sub is_module {
325 my ($self) = @_;
326 return $self->{_document_type} eq $DOCUMENT_TYPE_MODULE;
327}
328
329#-----------------------------------------------------------------------------
d5835ca8 330# PRIVATE functions & methods
95ebf9b0 331
267b39b4
ES
332sub _is_a_version_statement {
333 my (undef, $element) = @_;
334
335 return 0 if not $element->isa('PPI::Statement::Include');
336 return 1 if $element->version();
337 return 0;
338}
339
340#-----------------------------------------------------------------------------
341
58a9e587
JRT
342sub _caching_finder {
343
344 my $cache_ref = shift; # These vars will persist for the life
345 my %isa_cache = (); # of the code ref that this sub returns
346
347
348 # Gather up all the PPI elements and sort by @ISA. Note: if any
349 # instances used multiple inheritance, this implementation would
350 # lead to multiple copies of $element in the $elements_of lists.
351 # However, PPI::* doesn't do multiple inheritance, so we are safe
352
353 return sub {
6e7d6c9f 354 my (undef, $element) = @_;
58a9e587
JRT
355 my $classes = $isa_cache{ref $element};
356 if ( !$classes ) {
357 $classes = [ ref $element ];
358 # Use a C-style loop because we append to the classes array inside
359 for ( my $i = 0; $i < @{$classes}; $i++ ) { ## no critic(ProhibitCStyleForLoops)
360 no strict 'refs'; ## no critic(ProhibitNoStrict)
361 push @{$classes}, @{"$classes->[$i]::ISA"};
362 $cache_ref->{$classes->[$i]} ||= [];
5bf96118 363 }
58a9e587
JRT
364 $isa_cache{$classes->[0]} = $classes;
365 }
5bf96118 366
58a9e587
JRT
367 for my $class ( @{$classes} ) {
368 push @{$cache_ref->{$class}}, $element;
369 }
5bf96118 370
58a9e587
JRT
371 return 0; # 0 tells find() to keep traversing, but not to store this $element
372 };
5bf96118
CD
373}
374
6036a254 375#-----------------------------------------------------------------------------
58a9e587 376
d5835ca8 377sub _disable_shebang_fix {
2d2fd196
JRT
378 my ($self) = @_;
379
1b936936 380 # When you install a program using ExtUtils::MakeMaker or Module::Build, it
937b8de0 381 # inserts some magical code into the top of the file (just after the
1b936936 382 # shebang). This code allows people to call your program using a shell,
937b8de0 383 # like `sh my_script`. Unfortunately, this code causes several Policy
d1237298 384 # violations, so we disable them as if they had "## no critic" annotations.
937b8de0 385
d5835ca8 386 my $first_stmnt = $self->schild(0) || return;
937b8de0
JRT
387
388 # Different versions of MakeMaker and Build use slightly different shebang
389 # fixing strings. This matches most of the ones I've found in my own Perl
390 # distribution, but it may not be bullet-proof.
391
66d796fa 392 my $fixin_rx = qr<^eval 'exec .* \$0 \${1\+"\$@"}'\s*[\r\n]\s*if.+;>ms; ## no critic (ExtendedFormatting)
937b8de0 393 if ( $first_stmnt =~ $fixin_rx ) {
d5835ca8
JRT
394 my $line = $first_stmnt->location->[0];
395 $self->{_disabled_line_map}->{$line}->{ALL} = 1;
396 $self->{_disabled_line_map}->{$line + 1}->{ALL} = 1;
937b8de0
JRT
397 }
398
2d2fd196 399 return $self;
937b8de0
JRT
400}
401
402#-----------------------------------------------------------------------------
403
d533eee5
ES
404sub _compute_document_type {
405 my ($self, $args) = @_;
406
407 my $file_name = $self->filename();
1b936936
ES
408 if (
409 defined $file_name
410 and ref $args->{'-program-extensions'} eq 'ARRAY'
411 ) {
412 foreach my $ext ( @{ $args->{'-program-extensions'} } ) {
413 my $regex =
414 ref $ext eq 'Regexp'
415 ? $ext
416 : qr< @{ [ quotemeta $ext ] } \z >xms;
417
418 return $DOCUMENT_TYPE_PROGRAM if $file_name =~ m/$regex/smx;
d533eee5
ES
419 }
420 }
421
1b936936 422 return $DOCUMENT_TYPE_PROGRAM if shebang_line($self);
d533eee5 423
1b936936 424 return $DOCUMENT_TYPE_PROGRAM
d533eee5
ES
425 if defined $file_name && $file_name =~ m/ [.] PL \z /smx;
426
427 return $DOCUMENT_TYPE_MODULE;
428}
429
430#-----------------------------------------------------------------------------
431
5bf96118 4321;
58a9e587 433
5bf96118
CD
434__END__
435
a73f4a71
JRT
436=pod
437
438=for stopwords pre-caches
439
5bf96118
CD
440=head1 NAME
441
c728943a 442Perl::Critic::Document - Caching wrapper around a PPI::Document.
5bf96118 443
267b39b4 444
5bf96118
CD
445=head1 SYNOPSIS
446
447 use PPI::Document;
448 use Perl::Critic::Document;
449 my $doc = PPI::Document->new('Foo.pm');
d533eee5 450 $doc = Perl::Critic::Document->new(-source => $doc);
5bf96118
CD
451 ## Then use the instance just like a PPI::Document
452
267b39b4 453
5bf96118
CD
454=head1 DESCRIPTION
455
456Perl::Critic does a lot of iterations over the PPI document tree via
457the C<PPI::Document::find()> method. To save some time, this class
458pre-caches a lot of the common C<find()> calls in a single traversal.
459Then, on subsequent requests we return the cached data.
460
461This is implemented as a facade, where method calls are handed to the
462stored C<PPI::Document> instance.
463
267b39b4 464
5bf96118
CD
465=head1 CAVEATS
466
467This facade does not implement the overloaded operators from
11f53956
ES
468L<PPI::Document|PPI::Document> (that is, the C<use overload ...>
469work). Therefore, users of this facade must not rely on that syntactic
470sugar. So, for example, instead of C<my $source = "$doc";> you should
471write C<my $source = $doc->content();>
5bf96118
CD
472
473Perhaps there is a CPAN module out there which implements a facade
474better than we do here?
475
267b39b4 476
4444d94d
ES
477=head1 INTERFACE SUPPORT
478
479This is considered to be a public class. Any changes to its interface
480will go through a deprecation cycle.
481
482
267b39b4
ES
483=head1 CONSTRUCTOR
484
485=over
486
1b936936 487=item C<< new(-source => $source_code, '-program-extensions' => [program_extensions]) >>
267b39b4 488
d5835ca8
JRT
489Create a new instance referencing a PPI::Document instance. The
490C<$source_code> can be the name of a file, a reference to a scalar
fcb2381b 491containing actual source code, or a L<PPI::Document> or
d5835ca8 492L<PPI::Document::File>.
267b39b4 493
1b936936 494The '-program-extensions' argument is optional, and is a reference to a list of
d533eee5
ES
495strings and/or regexps. The strings will be made into regexps matching the end
496of a file name, and any document whose file name matches one of the regexps
1b936936 497will be considered a program.
d533eee5 498
1b936936
ES
499If -program-extensions is not specified, or if it does not determine the
500document type, the document type will be 'program' if the source has a shebang
d533eee5
ES
501line or its file name (if any) matches C<< m/ [.] PL \z /smx >>, or 'module'
502otherwise.
503
504Be aware that the document type influences not only the value returned by the
1b936936 505C<document_type()> method, but also the value returned by the C<is_program()>
d533eee5
ES
506and C<is_module()> methods.
507
267b39b4
ES
508=back
509
5bf96118
CD
510=head1 METHODS
511
512=over
513
267b39b4 514=item C<< ppi_document() >>
2b6293b2 515
11f53956
ES
516Accessor for the wrapped PPI::Document instance. Note that altering
517this instance in any way can cause unpredictable failures in
518Perl::Critic's subsequent analysis because some caches may fall out of
519date.
2b6293b2 520
5bf96118 521
267b39b4
ES
522=item C<< find($wanted) >>
523
524=item C<< find_first($wanted) >>
fb21e21e 525
267b39b4 526=item C<< find_any($wanted) >>
f5eeac3b 527
fb21e21e 528If C<$wanted> is a simple PPI class name, then the cache is employed.
f5eeac3b
CD
529Otherwise we forward the call to the corresponding method of the
530C<PPI::Document> instance.
5bf96118 531
267b39b4
ES
532
533=item C<< filename() >>
e7f2d995
CD
534
535Returns the filename for the source code if applicable
536(PPI::Document::File) or C<undef> otherwise (PPI::Document).
537
267b39b4
ES
538
539=item C<< isa( $classname ) >>
242f7b08 540
11f53956
ES
541To be compatible with other modules that expect to get a
542PPI::Document, the Perl::Critic::Document class masquerades as the
543PPI::Document class.
242f7b08 544
267b39b4
ES
545
546=item C<< highest_explicit_perl_version() >>
547
11f53956
ES
548Returns a L<version|version> object for the highest Perl version
549requirement declared in the document via a C<use> or C<require>
550statement. Returns nothing if there is no version statement.
267b39b4 551
d5835ca8 552=item C<< process_annotations() >>
267b39b4 553
d5835ca8
JRT
554Causes this Document to scan itself and mark which lines &
555policies are disabled by the C<"## no critic"> annotations.
937b8de0 556
d5835ca8 557=item C<< line_is_disabled_for_policy($line, $policy_object) >>
937b8de0 558
fcb2381b 559Returns true if the given C<$policy_object> or C<$policy_name> has
d5835ca8 560been disabled for at C<$line> in this Document. Otherwise, returns false.
937b8de0 561
d5835ca8 562=item C<< add_annotation( $annotation ) >>
937b8de0 563
d5835ca8 564Adds an C<$annotation> object to this Document.
2d2fd196 565
d5835ca8 566=item C<< annotations() >>
2d2fd196 567
d5835ca8
JRT
568Returns a list containing all the L<Perl::Critic::Annotation> that
569were found in this Document.
2d2fd196 570
d5835ca8 571=item C<< add_suppressed_violation($violation) >>
2d2fd196 572
fcb2381b 573Informs this Document that a C<$violation> was found but not reported
d5835ca8
JRT
574because it fell on a line that had been suppressed by a C<"## no critic">
575annotation. Returns C<$self>.
95ebf9b0 576
d5835ca8 577=item C<< suppressed_violations() >>
95ebf9b0 578
d5835ca8
JRT
579Returns a list of references to all the L<Perl::Critic::Violation>s
580that were found in this Document but were suppressed.
937b8de0 581
d533eee5
ES
582=item C<< document_type() >>
583
584Returns the current value of the C<document_type> attribute. When the
585C<Perl::Critic::Document> object is instantiated, it will be set based on the
1b936936
ES
586value '-program-extensions' argument (if any) and/or the contents of the file
587to L<Perl::Critic::Utils::Constants/"$DOCUMENT_TYPE_PROGRAM"> or
d533eee5
ES
588L<Perl::Critic::Utils::Constants/"$DOCUMENT_TYPE_MODULE">. See the C<new()>
589documentation for the details. This attribute exists to support
590L<Perl::Critic|Perl::Critic>.
591
1b936936
ES
592=item C<< is_program() >>
593
594Returns whether this document is considered to be a program.
d533eee5 595
d533eee5
ES
596
597=item C<< is_module() >>
598
1b936936 599Returns whether this document is considered to be a Perl module.
d533eee5 600
5bf96118
CD
601=back
602
603=head1 AUTHOR
604
2f4b6b33 605Chris Dolan <cdolan@cpan.org>
5bf96118
CD
606
607=head1 COPYRIGHT
608
f4cb844a 609Copyright (c) 2006-2009 Chris Dolan.
5bf96118
CD
610
611This program is free software; you can redistribute it and/or modify
612it under the same terms as Perl itself. The full text of this license
613can be found in the LICENSE file included with this module.
614
615=cut
737d3b65 616
d5835ca8 617##############################################################################
737d3b65
CD
618# Local Variables:
619# mode: cperl
620# cperl-indent-level: 4
621# fill-column: 78
622# indent-tabs-mode: nil
623# c-indentation-style: bsd
624# End:
96fed375 625# ex: set ts=8 sts=4 sw=4 tw=78 ft=perl expandtab shiftround :