File Coverage

blib/lib/Test/MixedScripts.pm
Criterion Covered Total %
statement 85 96 88.5
branch 20 26 76.9
condition 5 10 50.0
subroutine 19 24 79.1
pod 2 2 100.0
total 131 158 82.9


line stmt bran cond sub pod time code
1             package Test::MixedScripts;
2              
3 4     4   396327 use utf8;
  4         584  
  4         18  
4              
5             # ABSTRACT: test text for mixed and potentially confusable Unicode scripts
6              
7 4     4   168 use v5.16;
  4         10  
8 4     4   13 use warnings;
  4         18  
  4         186  
9              
10 4     4   17 use Carp qw( croak );
  4         6  
  4         197  
11 4     4   16 use Exporter 5.57 qw( import );
  4         90  
  4         119  
12 4     4   15 use File::Basename qw( basename );
  4         4  
  4         232  
13 4     4   16 use File::Spec;
  4         15  
  4         77  
14 4     4   1455 use IO qw( File );
  4         2915  
  4         18  
15 4     4   25013 use List::Util qw( first );
  4         5  
  4         207  
16 4     4   3827 use Unicode::UCD qw( charinfo charscripts );
  4         129822  
  4         339  
17              
18 4     4   32 use Test2::API 1.302200 qw( context );
  4         74  
  4         192  
19 4     4   1547 use Test2::Util::DistFiles v0.2.0 qw( manifest_files is_perl_file );
  4         56967  
  4         4172  
20              
21             our @EXPORT_OK = qw( all_perl_files_scripts_ok file_scripts_ok );
22              
23             our $VERSION = 'v0.6.5';
24              
25              
26             sub file_scripts_ok {
27 40     40 1 469798 my ( $file, @args ) = @_;
28              
29 40 100 100     188 my $options = @args == 1 && ref( $args[0] ) eq "HASH" ? $args[0] : { scripts => \@args };
30 40   50     93 $options->{scripts} //= [];
31 40 100       98 push @{ $options->{scripts} }, qw( Latin Common ) unless defined $options->{scripts}[0];
  5         32  
32              
33 40         98 my $ctx = context();
34              
35 40 100       7984 if ( my $error = _check_file_scripts( $file, $options ) ) {
36              
37 3         108 my ( $lino, $pre, $char ) = @{$error};
  3         9  
38              
39             # Ideally we would use charprop instead of charscript, since that supports Script_Extensions, but Unicode::UCD
40             # is not dual life and charprop is only available after v5.22.0.
41              
42 3         13 my $info = charinfo( ord($char) );
43             my $message = sprintf(
44             'Unexpected %s character %s on line %u character %u in %s',
45             $info->{script}, #
46 3   50     176429 $info->{name} || "NO NAME", #
47             $lino, #
48             length($pre) + 1, #
49             "$file"
50             );
51              
52 3         20 $ctx->fail( $file, $message );
53              
54             }
55             else {
56 36         128 $ctx->pass( $file );
57             }
58              
59 39         5008 $ctx->release;
60             }
61              
62             sub _check_file_scripts {
63 40     40   62 my ( $file, $options ) = @_;
64              
65 40         48 my @scripts = @{ $options->{scripts} };
  40         83  
66 40         77 my $default = _make_regex(@scripts);
67              
68 39 50       200 my $fh = IO::File->new( $file, "r" ) or croak "Cannot open ${file}: $!";
69              
70 39         3598 $fh->binmode(":utf8");
71              
72 39         346 my $current = $default;
73              
74 39         789 while ( my $line = $fh->getline ) {
75 1546         1454 my $re = $current;
76             # TODO custom comment prefix based on the file type
77 1546 100       2523 if ( $line =~ s/\s*##\s+Test::MixedScripts\s+(\w+(?:,\w+)*).*$// ) {
    100          
78 6         50 $re = _make_regex( split /,\s*/, $1 );
79             }
80             elsif ( $line =~ /^=for\s+Test::MixedScripts\s+(\w+(?:,\w+)*)$/ ) {
81 3 100       18 $current = $1 eq "default" ? $default : _make_regex( split /,\s*/, $1 );
82 3         11 next;
83             }
84              
85 1543 100       6067 unless ( $line =~ $re ) {
86 3         8 my $fail = _make_negative_regex(@scripts);
87 3         17 $line =~ $fail;
88 3         14 return [ $fh->input_line_number, ${^PREMATCH}, ${^MATCH} ];
89             }
90             }
91              
92 36         123 $fh->close;
93              
94 36         567 return 0;
95             }
96              
97             sub _make_regex_set {
98 51     51   68 state $scripts = { ASCII => undef, map { $_ => 1 } keys %{ charscripts() } };
  680         183040  
  4         20  
99 51 100   109   844 if ( my $err = first { !exists $scripts->{$_} } @_ ) {
  109         240  
100 1         163 croak "Unknown script ${err}";
101             }
102 50 100       161 return join( "", map { $_ eq "ASCII" ? '\x00-\x7f' : sprintf( '\p{scx=%s}', $_ ) } @_ );
  108         310  
103             }
104              
105             sub _make_regex {
106 48     48   81 my $set = _make_regex_set(@_);
107 47         1502 return qr/^[${set}]*$/u;
108             }
109              
110             sub _make_negative_regex {
111 3     3   5 my $set = _make_regex_set(@_);
112 3         62 return qr/([^${set}])/up;
113             }
114              
115              
116             sub all_perl_files_scripts_ok {
117 1     1 1 144157 my $options = { };
118 1 50       5 $options = shift if ref $_[0] eq 'HASH';
119 1         7 my @files = manifest_files( \&is_perl_file);
120 1         4290 foreach my $file (@files) {
121 27         893 file_scripts_ok( $file, $options );
122             }
123             }
124              
125             sub _is_perl_file {
126 0     0     my ($file) = @_;
127 0   0       return is_perl_file($file) || _is_pod_file($file) || _is_perl_config($file) || _is_xs_file($file) || _is_template($file);
128             }
129              
130             sub _is_pod_file {
131 0     0     $_[0] =~ /\.pod$/i;
132             }
133              
134             sub _is_perl_config {
135 0     0     my ($file) = @_;
136 0           my $name = basename($file);
137 0 0         return 1 if $name =~ /^(?:Rexfile|cpanfile)$/;
138 0           return;
139             }
140              
141             sub _is_xs_file {
142 0     0     $_[0] =~ /\.(c|h|xs)$/i;
143             }
144              
145             sub _is_template {
146 0     0     my ($file) = @_;
147 0           my $name = basename($file);
148 0 0         return 1 if $name =~ /\.(?:epl?|inc|mc|psp|tal|tm?pl|tt)$/;
149             }
150              
151             1;
152              
153             __END__