File Coverage

blib/lib/Spellunker.pm
Criterion Covered Total %
statement 125 127 98.4
branch 96 110 87.2
condition 13 21 61.9
subroutine 19 19 100.0
pod 6 9 66.6
total 259 286 90.5


line stmt bran cond sub pod time code
1             package Spellunker;
2 11     11   195552 use strict;
  11         25  
  11         443  
3 11     11   48 use warnings FATAL => 'all';
  11         23  
  11         409  
4 11     11   1860 use utf8;
  11         42  
  11         65  
5 11     11   471 use 5.008001;
  11         37  
  11         477  
6              
7 11     11   8853 use version; our $VERSION = version->declare("v0.4.0");
  11         22986  
  11         60  
8              
9 11     11   970 use Scalar::Util ();
  11         20  
  11         170  
10 11     11   55 use File::Spec ();
  11         21  
  11         144  
11 11     11   9705 use File::ShareDir ();
  11         67671  
  11         312  
12 11     11   11221 use Regexp::Common qw /URI/;
  11         52887  
  11         61  
13              
14             # Ref http://www.din.or.jp/~ohzaki/mail_regex.htm#Simplify
15             my $MAIL_REGEX = (
16             q{(?:[-!#-'*+/-9=?A-Z^-~]+(?:\.[-!#-'*+/-9=?A-Z^-~]+)*|"(?:[!#-\[\]-} .
17             q{~]|\\\\[\x09 -~])*")@[-!#-'*+/-9=?A-Z^-~]+(?:\.[-!#-'*+/-9=?A-Z^-~]+} .
18             q{)*}
19             );
20              
21              
22             sub new {
23 10     10 1 87 my $class = shift;
24 10 50       67 my %args = @_==1 ? %{$_[0]} : @_;
  0         0  
25 10         33 my $self = bless {}, $class;
26              
27             # From https://code.google.com/p/dotnetperls-controls/downloads/detail?name=enable1.tx
28 10         75 $self->load_dictionary(File::Spec->catfile(File::ShareDir::dist_dir('Spellunker'), 'enable1.txt'));
29 10         147255 $self->load_dictionary(File::Spec->catfile(File::ShareDir::dist_dir('Spellunker'), 'spellunker-dict.txt'));
30              
31 10 100       538 unless ($ENV{PERL_SPELLUNKER_NO_USER_DICT}) {
32 2         14 $self->_load_user_dict();
33             }
34 10         111 return $self;
35             }
36              
37             sub _load_user_dict {
38 2     2   5 my $self = shift;
39 2         11 my $home = $ENV{HOME};
40 2 50       14 return unless defined $home;
41 2 50       68 return unless -d $home;
42 2         47 my $dictpath = File::Spec->catfile($home, '.spellunker.en');
43 2 50       42 if (-f $dictpath) {
44 0         0 $self->load_dictionary($dictpath);
45             }
46             }
47              
48             sub load_dictionary {
49 24     24 1 4515 my ($self, $filename_or_fh) = @_;
50              
51 24         45 my $fh;
52 24 100       158 if (Scalar::Util::openhandle($filename_or_fh)) {
53 4         14 $fh = $filename_or_fh;
54             }
55             else {
56 20 50       1402 open $fh, '<:utf8', $filename_or_fh
57             or die "Cannot open '$filename_or_fh' for reading: $!";
58             }
59              
60 24         132 local $/;
61 24         101823 my $chunk = <$fh>;
62 24         131142 $chunk =~ s/\#[^\n]*$//xmsg; # remove comments.
63 24         721122 $self->add_stopwords(split ' ', $chunk);
64             }
65              
66             sub add_stopwords {
67 27     27 1 163 my $self = shift;
68 27         164 for (@_) {
69 1733996         4869628 $self->{stopwords}->{$_}++
70             }
71 27         1173 return undef;
72             }
73              
74             sub clear_stopwords {
75 1     1 1 5 my $self = shift;
76 1         4 undef $self->{stopwords};
77             }
78              
79             sub check_word {
80 904     904 1 13037 my ($self, $word) = @_;
81 904 50       1825 return 0 unless defined $word;
82              
83 904 100       1808 return 1 if length($word)==0;
84 896 100       1604 return 1 if length($word)==1;
85              
86             # There is no alphabetical characters.
87 864 100       2289 return 1 if $word !~ /[A-Za-z]/;
88              
89             # git sha1
90 857 100       1457 return 1 if $word =~ /\A[a-z0-9]{40}\z/;
91              
92             # 19xx 2xx
93 856 100       1977 return 1 if $word =~ /^[0-9]+(xx|yy)$/;
94             # 4th
95 853 100       1600 return 1 if $word =~ /^[0-9]+(th)$/;
96              
97             # Method name
98 852 100       1616 return 1 if $word =~ /\A([a-zA-Z0-9]+_)+[a-zA-Z0-9]+\z/;
99              
100             # Extensions
101 843 100       1624 return 1 if $word =~ /\A\.[a-zA-Z0-9]{2,4}\z/;
102              
103             # File name
104 835 100       1404 return 1 if $word =~ /\A[a-zA-Z0-9-]+\.[a-zA-Z0-9]{1,4}\z/;
105              
106 833 50       1274 return 1 if looks_like_domain($word);
107 833 100       1311 return 1 if looks_like_perl_code($word);
108 789 100       1389 return 1 if looks_like_file_path($word);
109              
110             # Ignore capital letter words like RT, RFC, IETF.
111             # And so "IT'S" should be allow.
112             # AUTHORS
113             # APIs
114 782 100       2030 return 1 if $word =~ /\A [A-Z']+ s? \z/x;
115              
116             # good
117 723 100       3958 return 1 if $self->{stopwords}->{$word};
118              
119             # ucfirst-ed word.
120             # 'How'
121             # Dan
122 164 100       570 if ($word =~ /\A[A-Z][a-z]+\z/) {
123 80         284 return 1;
124             }
125              
126             # CamelCase-ed word like "McCamant"
127 84 100       250 if ($word =~ /\A [A-Z][a-z]+ (?:[A-Z][a-z]+)+ \z/x) {
128 1         4 return 1;
129             }
130              
131             # Suffix rules
132 83 100 66     543 return 1 if $word =~ /\A
133             (.*?)
134             (?:
135             's # Dan's
136             | s' # cookies'
137             | 've # You've
138             | 're # We're
139             | 'll # You'll
140             | n't # doesn't
141             | 'd # You'd
142             | -ish # -ish
143             )
144             \z/x && $self->check_word($1);
145              
146             # comE
147             ## Prefixes
148 67 100 66     220 return 1 if $word =~ /\Anon-(.*)\z/ && $self->check_word($1);
149 66 100 66     210 return 1 if $word =~ /\Are-(.*)\z/ && $self->check_word($1);
150              
151             #

152 65 100 66     185 return 1 if $word =~ /\A

(.*)<\/p>\z/ && $self->check_word($1);

153              
154             # :Str - Moose-ish type definition
155 64 100       156 return 1 if $word =~ /\A
156             :
157             (?:[A-Z][a-z]+)+
158             \z/x;
159              
160             # IRC channel name
161 63 100       189 return 1 if $word =~ /\A#[a-z0-9-]+\z/;
162              
163             # Suffix
164 61 100 66     405 return 1 if $word =~ /\A(.*?)[^A-Za-z]+\z/ && $self->check_word($1);
165             # Prefix
166 35 100 66     238 return 1 if $word =~ /\A[^A-Za-z]+(.*?)\z/ && $self->check_word($1);
167              
168 26 100       85 if ($word =~ /[^A-Za-z]+/) {
169 11         70 my @words = split /[^A-Za-z]+/, $word;
170 11         16 my $ok = 0;
171 11         18 for (@words) {
172 31 50       74 if ($self->check_word($_)) {
173 31         56 $ok++;
174             }
175             }
176 11 50       81 return 1 if @words == $ok;
177             }
178              
179 15         80 return 0;
180             }
181              
182             sub looks_like_file_path {
183 797     797 0 2820 my ($word) = @_;
184              
185             # ~/
186             # ~/foo/
187             # ~foo/
188             # /dev/tty
189             # t/01_simple.t
190 797 100       2804 return 1 if $word =~ m{\A
191             (?:
192             ~ [a-zA-Z0-9_.-]* / (?: [a-z0-9A-Z_.-]+ / )* (?: [a-z0-9A-Z_.-]+ )?
193             |
194             / (?: [a-z0-9A-Z_.-]+ / )* (?: [a-z0-9A-Z_.]+ )?
195             |
196             (?: [a-z0-9A-Z_.-]+ / )+ (?: [a-z0-9A-Z_.]+ )?
197             )
198             \z}x;
199 783         1640 return 0;
200             }
201              
202             sub looks_like_domain {
203 833     833 0 945 my ($word) = @_;
204 833 50       1657 return 1 if $word =~ /\A
205             ([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,6}
206             \z/x;
207 833         1789 return 0;
208             }
209              
210             sub check_line {
211 208     208 1 45300 my ($self, $line) = @_;
212 208 50       455 return unless defined $line;
213              
214 208         5933 $line =~ s!<$MAIL_REGEX>|$MAIL_REGEX!!; # Remove E-mail address.
215 208         1026 $line =~ s!$RE{URI}{HTTP}{-scheme => 'https?'}!!g; # Remove HTTPS? URI
216              
217 208         29331 my @bad_words;
218 208         1536 for ( grep /\S/, split /[\|*=\[\]`" \t,?!]+/, $line) {
219 796         1063 s/\n//;
220              
221 796 100       2551 if (/\A'(.*)'\z/) {
    100          
    100          
222 4         17 push @bad_words, $self->check_line($1);
223             } elsif (/\A(.*)\.\z/) { # The word ended by dot
224 54         108 my $word = $1;
225 54 50       107 $self->check_word($word)
226             or push @bad_words, $word;
227             } elsif (/\./) { # The word includes dot
228 14 50       38 $self->check_word($_)
229             or push @bad_words, $_;
230             } else {
231             # Ignore command line options
232 724 100       1306 next if /\A
233             --
234             (?: [a-z]+ - )+
235             [a-z]+
236             \z/x;
237              
238 723 100       1363 $self->check_word($_)
239             or push @bad_words, $_;
240             }
241             }
242 208         805 return @bad_words;
243             }
244              
245             sub looks_like_perl_code {
246 848     848 0 7285 my $PERL_NAME = '[A-Za-z_][A-Za-z0-9_]*';
247              
248             # Class name
249             # Foo::Bar
250             # JSON::PP::
251 848 100       4872 return 1 if $_[0] =~ /\A
252             [\+\$]?
253             (?: $PERL_NAME :: )+
254             $PERL_NAME
255             $PERL_NAME?
256             \z/x;
257              
258             # foo()
259 836 100       4185 return 1 if $_[0] =~ /\A
260             $PERL_NAME
261             \(
262             \s*
263             ( \$ $PERL_NAME \s* , \s* )*
264             ( \$ $PERL_NAME )?
265             \s*
266             \)
267             \z/x;
268              
269             # 5.8.x
270             # 5.10.x
271 830 100       1647 return 1 if $_[0] =~ /\A
272             [0-9]+\.[0-9]+\.x
273             \z/x;
274              
275             # U+002F
276 828 100       1428 return 1 if $_[0] =~ /\A
277             U \+ [0-9a-fA-F]{4,}
278             \z/x;
279              
280             # \x00-\x1f\x22\x2f\x5c
281             # \x2f
282 827 100       1457 return 1 if $_[0] =~ /\A
283             (
284             \\ x [0-9a-fA-F][0-9a-fA-F] -?
285             )+
286             \z/x;
287              
288             # $foo
289             # %foo
290             # @foo
291             # *foo
292             # \$foo
293 825 100       3256 return 1 if $_[0] =~ /\A
294             \\?
295             [\*\@\$\%]
296             $PERL_NAME
297             \z/x;
298              
299             # Spellunker->bar
300             # Foo::Bar->bar
301             # $foo->bar
302             # $foo->bar()
303 806 100       4803 return 1 if $_[0] =~ /\A
304             (?:
305             \$ $PERL_NAME
306             | ( $PERL_NAME :: )* $PERL_NAME
307             )
308             ->
309             $PERL_NAME
310             (?:\([^\)]*\))?
311             \z/x;
312              
313             # hash access
314 791 100       9055 return 1 if $_[0] =~ /\A
315             \$ $PERL_NAME \{ $PERL_NAME \}
316             \z/x;
317              
318             # hashref access
319 790 100       2913 return 1 if $_[0] =~ /\A
320             \$ $PERL_NAME -> \{ $PERL_NAME \}
321             \z/x;
322              
323             # JSON::XS-ish boolean value
324 789 50 33     3480 return 1 if $_[0] eq '\1' || $_[0] eq '\1';
325              
326 789         2579 return 0;
327             }
328              
329             1;
330             __END__