File Coverage

blib/lib/String/License.pm
Criterion Covered Total %
statement 386 469 82.3
branch 168 226 74.3
condition 97 133 72.9
subroutine 35 48 72.9
pod 2 5 40.0
total 688 881 78.0


line stmt bran cond sub pod time code
1 10     10   10343617 use Feature::Compat::Class 0.04;
  10         4230  
  10         51  
2              
3 10     10   120314 use v5.12;
  10         41  
4 10     10   71 use utf8;
  10         21  
  10         99  
5 10     10   279 use warnings;
  10         29  
  10         588  
6              
7             =head1 NAME
8              
9             String::License - detect source code license statements in a text string
10              
11             =head1 VERSION
12              
13             Version v0.0.5
14              
15             =head1 SYNOPSIS
16              
17             use String::License;
18             use String::License::Naming::Custom;
19              
20             my $string = 'Licensed under same terms as Perl itself';
21              
22             my $expressed = String::License->new( string => $string );
23             my $expression = $expressed->as_text; # => "Perl"
24              
25             my $desc = String::License::Naming::Custom->new;
26             my $described = String::License->new( string => $string, naming => $desc );
27             my $description = $described->as_text; # => "The Perl 5 License"
28              
29             =head1 DESCRIPTION
30              
31             L identifies license statements in a string
32             and serializes them in a normalized format.
33              
34             =cut
35              
36             package String::License v0.0.5;
37              
38 10     10   68 use Carp qw(croak);
  10         30  
  10         472  
39 10     10   4531 use Log::Any ();
  10         88931  
  10         343  
40 10     10   78 use Scalar::Util qw(blessed);
  10         34  
  10         660  
41 10     10   4976 use List::SomeUtils qw(nsort_by uniq);
  10         137800  
  10         896  
42 10     10   4798 use Array::IntSpan;
  10         31633  
  10         418  
43 10     10   5065 use Regexp::Pattern::License 3.4.0;
  10         1941139  
  10         1030  
44 10     10   5798 use Regexp::Pattern 0.2.12;
  10         14446  
  10         67  
45 10     10   4965 use String::License::Naming::Custom;
  10         28  
  10         361  
46 10     10   3924 use String::License::Naming::SPDX;
  10         40  
  10         425  
47              
48 10     10   81 use namespace::clean;
  10         30  
  10         53  
49              
50             class Trait {
51             field $log;
52             field $name :param;
53             field $begin :param;
54             field $end :param;
55             field $file :param;
56              
57             ADJUST {
58             $log = Log::Any->get_logger;
59              
60             $log->tracef(
61             'located trait: %s: %d-%d "%s"',
62             $name, $begin, $end,
63             $file ? substr( $file->string, $begin, $end - $begin ) : '',
64             );
65             }
66              
67 3363     3363   5986 method name { return $name }
  3363         9511  
68 611     611   1701 method begin { return $begin }
  611         2883  
69 8383     8383   15820 method end { return $end }
  8383         20138  
70 7     7   15 method file { return $file }
  7         81  
71             }
72              
73             class Exception {
74             field $log;
75             field $id :param;
76             field $begin :param;
77             field $end :param;
78             field $file :param;
79              
80             ADJUST {
81             $log = Log::Any->get_logger;
82              
83             $log->tracef(
84             'detected exception: %s: %d-%d',
85             $id->{caption}, $begin, $end
86             );
87             }
88              
89 55     55   135 method id { return $id }
  55         215  
90 0     0   0 method begin { return $begin }
  0         0  
91 0     0   0 method end { return $end }
  0         0  
92 0     0   0 method file { return $file }
  0         0  
93             }
94              
95             class Flaw {
96             field $log;
97             field $id :param;
98             field $begin :param;
99             field $end :param;
100             field $file :param;
101              
102             ADJUST {
103             $log = Log::Any->get_logger;
104              
105             $log->tracef(
106             'detected flaw: %s: %d-%d',
107             $id->{caption}, $begin, $end
108             );
109             }
110              
111 28     28   78 method id { return $id }
  28         120  
112 0     0   0 method begin { return $begin }
  0         0  
113 0     0   0 method end { return $end }
  0         0  
114 0     0   0 method file { return $file }
  0         0  
115             }
116              
117             class Licensing {
118             field $log;
119             field $name :param;
120              
121             ADJUST {
122             $log = Log::Any->get_logger;
123              
124             $log->debugf(
125             'collected some licensing: %s',
126             $name
127             );
128             }
129              
130 333     333   1113 method name { return $name }
  333         1836  
131             }
132              
133             class Fulltext {
134             field $log;
135             field $name :param;
136             field $begin :param;
137             field $end :param;
138             field $file :param;
139             field $traits :param = undef;
140              
141             ADJUST {
142             $log = Log::Any->get_logger;
143              
144             $log->debugf(
145             'collected fulltext: %s: %d-%d',
146             $name, $begin, $end
147             );
148             }
149              
150 0     0   0 method name { return $name }
  0         0  
151 0     0   0 method begin { return $begin }
  0         0  
152 0     0   0 method end { return $end }
  0         0  
153 0     0   0 method file { return $file }
  0         0  
154 0     0   0 method traits { return $traits }
  0         0  
155             }
156              
157             class Grant {
158             field $log;
159             field $name :param;
160             field $begin :param;
161             field $end :param;
162             field $file :param;
163             field $traits :param = undef;
164              
165             ADJUST {
166             $log = Log::Any->get_logger;
167              
168             $log->debugf(
169             'collected grant: %s: %d-%d "%s"',
170             $name, $begin, $end,
171             $file ? substr( $file->string, $begin, $end - $begin ) : '',
172             );
173             }
174              
175 5     5   21 method name { return $name }
  5         21  
176 2     2   7 method begin { return $begin }
  2         9  
177 2     2   6 method end { return $end }
  2         9  
178 0     0   0 method file { return $file }
  0         0  
179 0     0   0 method traits { return $file }
  0         0  
180             }
181              
182             class String::License;
183              
184             # try enable RE2 engine
185             eval { require re::engine::RE2 };
186             my @OPT_RE2 = $@ ? () : ( engine => 'RE2' );
187              
188             field $log;
189              
190             =head1 CONSTRUCTOR
191              
192             =over
193              
194             =item new
195              
196             my $licensed = String::License->new( string => 'Licensed under GPLv2' );
197              
198             Accepts named arguments,
199             and constructs and returns a String::License object.
200              
201             The following options are recognized:
202              
203             =over
204              
205             =item string => STRING
206              
207             The scalar string to parse for licensing information.
208              
209             =cut
210              
211             field $string :param = undef;
212              
213             =item naming => OBJ
214              
215             A L object,
216             used to define license naming conventions.
217              
218             By default uses L.
219              
220             Since instantiation of naming schemes is expensive,
221             there can be a significant speed boost
222             in passing a pre-initialized naming object
223             when processing multiple strings.
224              
225             =cut
226              
227             field $naming :param = undef;
228              
229             field $license = '';
230             field $expr = '';
231              
232             =back
233              
234             =back
235              
236             =cut
237              
238             ADJUST {
239             $log = Log::Any->get_logger;
240              
241             if ( defined $naming ) {
242             croak $log->fatal(
243             'parameter "naming" must be a String::License::Naming object')
244             unless defined blessed($naming)
245             and $naming->isa('String::License::Naming');
246             }
247             else {
248             $naming = String::License::Naming::SPDX->new;
249             }
250             }
251              
252             =head1 METHODS
253              
254             =over
255              
256             =cut
257              
258             method string
259 2669     2669 1 6679 {
260 2669         10508 return $string;
261             }
262              
263             my $any = '[A-Za-z_][A-Za-z0-9_]*';
264             my $str = '[A-Za-z][A-Za-z0-9_]*';
265             my $re_prop_attrs = qr/
266             \A(?'prop'$str)\.alt(?:
267             \.org\.(?'org'$str)|
268             \.version\.(?'version'$str)|
269             \.since\.date_(?'since_date'\d{8})|
270             \.until\.date_(?'until_date'\d{8})|
271             \.synth\.$any|
272             (?'other'\.$any)
273             )*\z/x;
274              
275             method best_value
276 11363     11363 0 25711 {
277 11363         24353 my ( $hashref, @props ) = @_;
278 11363         14281 my $value;
279              
280             PROPERTY:
281 11363         17847 for my $prop (@props) {
282 11391         27268 for my $org ( $naming->list_schemes ) {
283 16711         93725 for ( keys %$hashref ) {
284 310732         1249557 /$re_prop_attrs/;
285 10 100 100 10   66112 next unless $+{prop} and $+{prop} eq $prop;
  10         4375  
  10         112518  
  310732         1730128  
286 30995 100 100     199892 next unless $+{org} and $+{org} eq $org;
287 3825 50       14900 next if $+{version};
288 3825 100       12886 next if $+{other};
289 3655 100       11982 next if $+{until_date};
290              
291 3254         11751 $value = $hashref->{$_};
292 3254         6264 last PROPERTY;
293             }
294             }
295 8137   100     35607 $value ||= $hashref->{$prop};
296             }
297              
298 11363         47416 return $value;
299             }
300              
301             my $type_re
302             = qr/^type:([a-z][a-z0-9_]*)(?::([a-z][a-z0-9_]*))?(?::([a-z][a-z0-9_]*))?/;
303              
304             our %RE;
305             my ( %L, @RE_EXCEPTION, @RE_LICENSE, @RE_NAME );
306              
307             method init_licensepatterns
308 331     331 0 628 {
309             # reuse if already resolved
310 331 100       1121 return %L if exists $L{re_trait};
311              
312 10         357 Regexp::Pattern->import(
313             're',
314             'License::*' => (
315             @OPT_RE2,
316             subject => 'trait',
317             -prefix => 'EXCEPTION_',
318             -has_tag_matching => '^type:trait:exception(?:\z|:)',
319             -lacks_tag_matching => '^type:trait:exception:prefix(?:\z|:)',
320             ),
321             'License::*' => (
322             @OPT_RE2,
323             capture => 'named',
324             subject => 'trait',
325             -prefix => 'TRAIT_',
326             -has_tag_matching => '^type:trait(?:\z|:)',
327             -lacks_tag_matching => '^type:trait:exception(?!:prefix)(?:\z|:)',
328             ),
329             'License::version' => (
330             @OPT_RE2,
331             capture => 'named',
332             subject => 'trait',
333             anchorleft => 1,
334             -prefix => 'ANCHORLEFT_NAMED_',
335             ),
336             'License::version_later' => (
337             @OPT_RE2,
338             capture => 'named',
339             subject => 'trait',
340             anchorleft => 1,
341             -prefix => 'ANCHORLEFT_NAMED_',
342             ),
343             'License::any_of' => (
344             subject => 'trait',
345             -prefix => 'LOCAL_TRAIT_',
346             ),
347             'License::by_fsf' => (
348             subject => 'trait',
349             -prefix => 'LOCAL_TRAIT_',
350             ),
351             'License::fsf_unlimited' => (
352             subject => 'trait',
353             -prefix => 'LOCAL_TRAIT_',
354             ),
355             'License::fsf_unlimited_retention' => (
356             subject => 'trait',
357             -prefix => 'LOCAL_TRAIT_',
358             ),
359             'License::licensed_under' => (
360             subject => 'trait',
361             -prefix => 'LOCAL_TRAIT_',
362             ),
363             'License::or_at_option' => (
364             subject => 'trait',
365             -prefix => 'LOCAL_TRAIT_',
366             ),
367             'License::version' => (
368             capture => 'numbered',
369             subject => 'trait',
370             -prefix => 'LOCAL_TRAIT_KEEP_',
371             ),
372             'License::version_numberstring' => (
373             capture => 'numbered',
374             subject => 'trait',
375             -prefix => 'LOCAL_TRAIT_KEEP_',
376             ),
377             'License::apache' => (
378             subject => 'name',
379             -prefix => 'LOCAL_NAME_',
380             ),
381             'License::gpl' => (
382             subject => 'name',
383             -prefix => 'LOCAL_NAME_',
384             ),
385             'License::lgpl' => (
386             subject => 'name',
387             -prefix => 'LOCAL_NAME_',
388             ),
389             'License::mit' => (
390             subject => 'name',
391             -prefix => 'LOCAL_NAME_',
392             ),
393             'License::*' => (
394             @OPT_RE2,
395             subject => 'name',
396             -prefix => 'NAME_',
397             anchorleft => 1,
398             -lacks_tag_matching => '^type:trait(?:\z|:)',
399             ),
400             'License::*' => (
401             @OPT_RE2,
402             subject => 'grant',
403             -prefix => 'GRANT_',
404             -lacks_tag_matching => '^type:trait(?:\z|:)',
405             ),
406             'License::*' => (
407             @OPT_RE2,
408             subject => 'license',
409             -prefix => 'LICENSE_',
410             -lacks_tag_matching => '^type:trait(?:\z|:)',
411             ),
412             );
413              
414 10         16386343 @RE_EXCEPTION = sort map /^EXCEPTION_(.*)/, keys(%RE);
415 10         9542 @RE_LICENSE = sort map /^LICENSE_(.*)/, keys(%RE);
416 10         9514 @RE_NAME = sort map /^NAME_(.*)/, keys(%RE);
417              
418 10         2313 foreach my $key ( grep {/^[a-z]/} keys(%Regexp::Pattern::License::RE) ) {
  5640         9693  
419 5640         16628 my $val = $Regexp::Pattern::License::RE{$key};
420 5640   66     11115 $L{name}{$key} = $self->best_value( $val, 'name' ) || $key;
421             $L{caption}{$key}
422 5640   66     11752 = $self->best_value( $val, 'caption' ) || $val->{name} || $key;
423 5640         8433 foreach ( @{ $val->{tags} } ) {
  5640         16654  
424 10670 100       44738 /$type_re/ or next;
425 5650         16179 $L{type}{$1}{$key} = 1;
426 5650 100 100     18788 if ( $2 and $1 eq 'singleversion' ) {
427 2130         5317 $L{series}{$key} = $2;
428             }
429 5650 100 100     15896 if ( $2 and $1 eq 'usage' ) {
430 270         698 $L{usage}{$key} = $2;
431             }
432              
433             # TODO: simplify, and require Regexp::Pattern::License v3.9.0
434 5650 100 100     16171 if ( $3 and $1 eq 'trait' ) {
435 230 100       1090 if ( substr( $key, 0, 14 ) eq 'except_prefix_' ) {
436 50         201 $L{TRAITS_exception_prefix}{$key} = undef;
437             }
438             else {
439 180         1037 $L{"TRAITS_$2_$3"}{$key} = undef;
440             }
441             }
442             }
443             }
444              
445             # FIXME: drop when perl doesn't mysteriously freak out over it
446 10         415 foreach (qw(any_of)) {
447 10         82 $L{re_trait}{$_} = '';
448             }
449              
450             #<<< do not let perltidy touch this (keep long regex on one line)
451 10         20898 $L{multi_1} = qr/$RE{LOCAL_TRAIT_licensed_under}$RE{LOCAL_TRAIT_any_of}(?:[^.]|\.\S)*$RE{LOCAL_NAME_lgpl}$RE{LOCAL_TRAIT_KEEP_version}?/i;
452 10         8135 $L{multi_2} = qr/$RE{LOCAL_TRAIT_licensed_under}$RE{LOCAL_TRAIT_any_of}(?:[^.]|\.\S)*$RE{LOCAL_NAME_gpl}$RE{LOCAL_TRAIT_KEEP_version}?/i;
453 10         23372 $L{lgpl_5} = qr/$RE{LOCAL_TRAIT_licensed_under}$RE{LOCAL_NAME_lgpl}(?:$RE{LOCAL_TRAIT_by_fsf})?[,;:]?(?: either)? ?$RE{LOCAL_TRAIT_KEEP_version_numberstring},? $RE{LOCAL_TRAIT_or_at_option} $RE{LOCAL_TRAIT_KEEP_version_numberstring}/i;
454 10         22070 $L{gpl_7} = qr/either $RE{LOCAL_NAME_gpl}$RE{LOCAL_TRAIT_KEEP_version}?(?: \((?:the )?"?GPL"?\))?, or $RE{LOCAL_NAME_lgpl}$RE{LOCAL_TRAIT_KEEP_version}?/i;
455 10         243 $L{bsd_1} = qr/THIS SOFTWARE IS PROVIDED (?:BY (?:\S+ ){1,15})?AS IS AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY/;
456 10         8521 $L{apache_1} = qr/$RE{LOCAL_NAME_apache}$RE{LOCAL_TRAIT_KEEP_version}?(?:(?: or)? [^ ,]*?apache[^ ,]*| \([^(),]+\))*,? or $RE{LOCAL_NAME_gpl}$RE{LOCAL_TRAIT_KEEP_version}?/i;
457 10         3038 $L{apache_2} = qr/$RE{LOCAL_NAME_apache}$RE{LOCAL_TRAIT_KEEP_version}?(?:(?: or)? [^ ,]*?apache[^ ,]*| \([^(),]\))*,? or(?: the)? bsd(?:[ -](\d)-clause)?\b/i;
458 10         2997 $L{apache_4} = qr/$RE{LOCAL_NAME_apache}$RE{LOCAL_TRAIT_KEEP_version}?(?:(?: or)? [^ ,]*?apache[^ ,]*| \([^(),]\))*,? or $RE{LOCAL_NAME_mit}\b/i;
459 10         544 $L{fsful} = qr/This (\w+)(?: (?:file|script))? is free software; $RE{LOCAL_TRAIT_fsf_unlimited}/i;
460 10         517 $L{fsfullr} = qr/This (\w+)(?: (?:file|script))? is free software; $RE{LOCAL_TRAIT_fsf_unlimited_retention}/i;
461 10         65 $L{trailing_space} = qr/\s+$/;
462 10         82 $L{LEFTANCHOR_version_of} = qr/^ of /;
463             #>>>
464             }
465              
466             # license objects where atomic scan must always be applied
467             my %L_grant_stepwise_incomplete = (
468              
469             # usage
470              
471             # singleversion
472             apache_2 => 1,
473              
474             # versioned
475             gpl => 1,
476             lgpl => 1,
477              
478             # other
479             mit_new => 1, # misdetects ambiguous "MIT X11" grant
480             public_domain => 1,
481             );
482              
483             # license objects where stepwise scan cannot be skipped
484             my %L_grant_atomic_incomplete = (
485             afl_1_1 => 1,
486             afl_1_2 => 1,
487             afl_2 => 1,
488             afl_2_1 => 1,
489             afl_3 => 1,
490             apache_1_1 => 1,
491             artistic_1 => 1,
492             artistic_2 => 1,
493             bsl_1 => 1,
494             cc_by_2_5 => 1,
495             cc_by_sa => 1,
496             cpl_1 => 1,
497             mpl => 1,
498             mpl_1 => 1,
499             mpl_1_1 => 1,
500             mpl_2 => 1,
501             openssl => 1,
502             postgresql => 1,
503             zpl_2_1 => 1,
504             );
505              
506             # scan for grants first stepwise and if not found then also atomic
507             # flip either of these flags to test stepwise/atomic pattern coverage
508             my $skip_stepwise = 0;
509             my $force_atomic = 0;
510              
511             my $contains_bsd2_re = qr/^license:contains:license:bsd_2_clause/;
512             my @L_contains_bsd = grep {
513             $Regexp::Pattern::License::RE{$_}{tags}
514             and grep /$contains_bsd2_re/,
515             @{ $Regexp::Pattern::License::RE{$_}{tags} }
516             } keys(%Regexp::Pattern::License::RE);
517              
518             my $id2patterns_re = qr/(.*)(?:_(\d+(?:\.\d+)*)(_or_later)?)?/;
519              
520             method resolve
521 331     331 0 766 {
522 331         1187 $self->init_licensepatterns;
523              
524 331         668 my @L_type_usage = sort keys %{ $L{type}{usage} };
  331         6861  
525 331         1074 my @L_type_singleversion = sort keys %{ $L{type}{singleversion} };
  331         44103  
526 331         3060 my @L_type_versioned = sort keys %{ $L{type}{versioned} };
  331         18107  
527 331         1796 my @L_type_unversioned = sort keys %{ $L{type}{unversioned} };
  331         26142  
528 331         2128 my @L_type_combo = sort keys %{ $L{type}{combo} };
  331         1576  
529 331         760 my @L_type_group = sort keys %{ $L{type}{group} };
  331         1564  
530              
531 331         654 my @spdx_gplver;
532              
533 331         858 my @agpl = qw(agpl agpl_1 agpl_2 agpl_3);
534 331         754 my @gpl = qw(gpl gpl_1 gpl_2 gpl_3);
535 331         762 my @lgpl = qw(lgpl lgpl_2 lgpl_2_1 lgpl_3);
536              
537 331         2690 my $coverage = Array::IntSpan->new();
538 331         5245 my %match;
539 331         1590 my ( %grant, %license );
540              
541             # @clues, @expressions, and @exceptions contains DEP-5 or SPDX identifiers,
542             # and @flaws contains non-SPDX notes.
543 331         0 my ( @clues, @expressions, @exceptions, @flaws );
544              
545             my $patterns2id = sub {
546 3     3   12 my ( $id, $ver ) = @_;
547 3 50       9 return $id
548             unless ($ver);
549 3         8 $_ = $ver;
550 3         22 s/\.0$//g;
551 3         12 s/\./_/g;
552 3         16 return "${id}_$_";
553 331         1946 };
554             my $id2patterns = sub {
555 222     222   3186 return $_[0] =~ /$id2patterns_re/;
556 331         1094 };
557             my $gen_license = sub {
558 333     333   1156 my ( $id, $v, $later, $id2, $v2, $later2 ) = @_;
559 333         621 my @spdx;
560 333   33     1671 my $name = $L{name}{$id} || $id;
561 333   33     1387 my $desc = $L{caption}{$id} || $id;
562 333 100       788 if ($v) {
563 5 100       25 push @spdx, $later ? "$name-$v+" : "$name-$v";
564 5 100       19 $v .= ' or later' if ($later);
565             }
566             else {
567 328         809 push @spdx, $name;
568             }
569 333         639 my ( $name2, $desc2 );
570 333 100       835 if ($id2) {
571 5   33     22 $name2 = $L{name}{$id2} || $id2;
572 5   33     21 $desc2 = $L{caption}{$id2} || $id2;
573 5 100       17 if ($v2) {
574 4 100       18 push @spdx, $later2 ? "$name2-$v2+" : "$name2-$v2";
575 4 100       13 $v2 .= ' or later' if ($later2);
576             }
577             else {
578 1         3 push @spdx, $name2;
579             }
580             }
581 333 100       2148 my $legacy = join(
    100          
    100          
582             ' ',
583             $desc,
584             $v ? "(v$v)" : (),
585             $desc2 ? "or $desc2" : (),
586             $v2 ? "(v$v2)" : (),
587             );
588 333         1120 my $expr = join( ' or ', sort @spdx );
589 333         3657 push @expressions, Licensing->new( name => $expr );
590 333   66     4605 $license = join( ' ', $L{caption}{$legacy} || $legacy, $license );
591 331         1595 };
592              
593             # fulltext
594 331         1403 $log->trace('scan for license fulltext');
595 331         1200 my %pos_license;
596 331         949 foreach my $id (@RE_LICENSE) {
597 161528 100       433792 next unless ( $RE{"LICENSE_$id"} );
598 118498         2825442 while ( $string =~ /$RE{"LICENSE_$id"}/g ) {
599 288         25852 $pos_license{ $-[0] }{$id} = Trait->new(
600             name => "license($id)",
601             begin => $-[0],
602             end => $+[0],
603             file => $self,
604             );
605             }
606             }
607              
608 331         888 foreach my $trait ( keys %{ $L{TRAITS_exception_prefix} } ) {
  331         2160  
609              
610 1655 100       40260 next unless ( $string =~ /$RE{"TRAIT_$trait"}/ );
611 42         950 while ( $string =~ /$RE{"TRAIT_$trait"}/g ) {
612             next
613             if (
614             defined(
615 44 50       368 $coverage->get_range( $-[0], $+[0] )->get_element(0)
616             )
617             );
618 44         2687 push @clues,
619             Trait->new(
620             name => $trait,
621             begin => $-[0],
622             end => $+[0],
623             file => $self,
624             );
625             }
626             }
627 331         2129 foreach my $pos ( sort { $a <=> $b } keys %pos_license ) {
  118         436  
628              
629             # pick longest or most specific among matched license fulltexts
630 288     288   2888 my @licenses = nsort_by { $pos_license{$pos}{$_}->end }
631 261 100       2537 grep { $pos_license{$pos}{$_} ? $pos_license{$pos}{$_}->end : () } (
  126846         206063  
632             @L_type_group,
633             @L_type_combo,
634             @L_type_unversioned,
635             @L_type_versioned,
636             @L_type_singleversion,
637             @L_type_usage,
638             );
639 261         2583 my $license = pop @licenses;
640 261 50       866 next unless ($license);
641             next
642             if defined(
643 261 100       886 $coverage->get_range( $pos, $pos_license{$pos}{$license}->end )
644             ->get_element(0) );
645             $coverage->set_range(
646             $pos_license{$pos}{$license}->begin,
647             $pos_license{$pos}{$license}->end,
648 246         11654 $pos_license{$pos}{$license}
649             );
650 246         7622 $license{$license} = 1;
651             }
652              
653             # grant, stepwise
654 331         3490 $log->trace('scan stepwise for license grant');
655 331         1243 foreach my $trait ( keys %{ $L{TRAITS_grant_prefix} } ) {
  331         1599  
656              
657 1324         44011 while ( $string =~ /$RE{"TRAIT_$trait"}/g ) {
658             next
659             if (
660             defined(
661 1640 100       59495 $coverage->get_range( $-[0], $+[0] )->get_element(0)
662             )
663             );
664 1565         77077 push @clues,
665             Trait->new(
666             name => $trait,
667             begin => $-[0],
668             end => $+[0],
669             file => $self,
670             );
671             }
672             }
673             LICENSED_UNDER:
674 331         4033 foreach my $licensed_under (
675 2388         4069 sort { $a->end <=> $b->end }
676 1609         3426 grep { exists $L{TRAITS_grant_prefix}{ $_->name } } @clues
677             )
678             {
679 1565         4800 my $pos = $licensed_under->end;
680              
681             # possible grant names
682 1565         62671 my @grant_types = (
683             @L_type_combo,
684             @L_type_unversioned,
685             @L_type_versioned,
686             @L_type_singleversion,
687             @L_type_usage,
688             );
689              
690             # optional grant version
691 1565         3107 my ( $version, $later );
692              
693             # scan for prepended version
694 1565         33999 substr( $string, $pos ) =~ $RE{ANCHORLEFT_NAMED_version};
695 1565 100       11829 if ( $+{version_number} ) {
696 11         175 push @clues,
697             Trait->new(
698             name => 'version',
699             begin => $pos + $-[0],
700             end => $pos + $+[0],
701             file => $self,
702             );
703 11         176 $version = $+{version_number};
704 11 50       83 if ( $+{version_later} ) {
705 0         0 push @clues,
706             Trait->new(
707             name => 'or_later',
708             begin => $pos + $-[2],
709             end => $pos + $+[2],
710             file => $self,
711             );
712 0         0 $later = $+{version_later};
713             }
714 11 50       305 if (
715             substr( $string, $pos + $+[0] ) =~ $L{LEFTANCHOR_version_of} )
716             {
717 0         0 push @clues,
718             Trait->new(
719             name => 'version_of',
720             begin => $pos + $-[0],
721             end => $pos + $+[0],
722             file => $self,
723             );
724 0         0 $pos += $+[0];
725 0         0 @grant_types = @L_type_versioned;
726             }
727             else {
728 11         38 $version = '';
729             }
730             }
731              
732             # scan for name
733 1565         4609 foreach my $id (@RE_NAME) {
734 763720 100       5557475 if ( substr( $string, $pos ) =~ $RE{"NAME_$id"} ) {
735 539         6977 $match{$id}{name}{ $pos + $-[0] } = Trait->new(
736             name => "name($id)",
737             begin => $pos + $-[0],
738             end => $pos + $+[0],
739             file => $self,
740             );
741             }
742             }
743              
744             # pick longest matched license name
745             # TODO: include all of most specific type when more are longest
746 401     401   2782 my @names = nsort_by { $match{$_}{name}{$pos}->end }
747 1565 50 66     11580 grep { $match{$_} and $match{$_}{name} and $match{$_}{name}{$pos} }
  754330         1238391  
748             @grant_types;
749 1565         15157 my $name = pop @names;
750 1565 50 66     26206 if ( $name
      66        
      33        
      66        
751             and $match{$name}{name}{$pos}
752             and !defined(
753             $coverage->get_range( $pos, $match{$name}{name}{$pos}->end )
754             ->get_element(0)
755             )
756             and ( !$skip_stepwise or $L_grant_atomic_incomplete{$name} )
757             )
758             {
759 192         9481 my $pos_end = $pos = $match{$name}{name}{$pos}->end;
760              
761             # may include version
762 192 100 66     825 if ( !$version and grep { $_ eq $name } @L_type_versioned ) {
  19392 100 66     29737  
763 38         815 substr( $string, $pos ) =~ $RE{ANCHORLEFT_NAMED_version};
764 38 100       377 if ( $+{version_number} ) {
765 4         49 push @clues, Trait->new(
766             name => 'version',
767             begin => $pos + $-[0],
768             end => $pos + $+[0],
769             file => $self,
770             );
771 4         57 $version = $+{version_number};
772 4         18 $pos_end = $pos + $+[1];
773 4 50       36 if ( $+{version_later} ) {
774 0         0 push @clues, Trait->new(
775             name => 'or_later',
776             begin => $pos + $-[2],
777             end => $pos + $+[2],
778             file => $self,
779             );
780 0         0 $later = $+{version_later};
781 0         0 $pos_end = $pos + $+[2];
782             }
783             }
784             }
785 32802         48272 elsif ( !$version and grep { $_ eq $name } @L_type_singleversion )
786             {
787             substr( $string, $pos )
788 81         1849 =~ $RE{ANCHORLEFT_NAMED_version_later};
789 81 100       828 if ( $+{version_later} ) {
790 3         41 push @clues, Trait->new(
791             name => 'or_later',
792             begin => $pos + $-[1],
793             end => $pos + $+[1],
794             file => $self,
795             );
796 3         71 $later = $+{version_later};
797 3         15 $pos_end = $pos + $+[1];
798             }
799             }
800 192 100       771 if ($version) {
801 4         28 $version =~ s/(?:\.0)+$//;
802 4         17 $version =~ s/\./_/g;
803 4         17 $name .= "_$version";
804             }
805 192 100       569 if ($later) {
806 3         13 my $latername = "${name}_or_later";
807 3         12 push @clues, Trait->new(
808             name => $latername,
809             begin => $licensed_under->begin,
810             end => $pos_end,
811             file => $self,
812             );
813 3         27 $grant{$latername} = $clues[-1];
814 3 50       10 next LICENSED_UNDER if grep { $grant{$_} } @RE_NAME;
  1464         1988  
815             }
816 189         1261 $grant{$name} = Trait->new(
817             name => "grant($name)",
818             begin => $licensed_under->begin,
819             end => $pos_end,
820             file => $self,
821             );
822 189         6166 push @clues, $grant{$name};
823             }
824             }
825              
826             # GNU oddities
827 331 100       1586 if ( grep { $match{$_}{name} } @agpl, @gpl, @lgpl ) {
  3972         8702  
828 96         479 $log->trace('scan for GNU oddities');
829              
830             # address in AGPL/GPL/LGPL
831 96         6745 while ( $string =~ /$RE{TRAIT_addr_fsf}/g ) {
832 41         152 foreach (
833             qw(addr_fsf_franklin_steet addr_fsf_mass addr_fsf_temple))
834             {
835 123 100       1382 if ( defined $+{$_} ) {
836             push @flaws, Flaw->new(
837 14         258 id => $Regexp::Pattern::License::RE{$_},
838             begin => $-[0],
839             end => $+[0],
840             file => $self,
841             );
842             }
843             }
844             }
845             }
846              
847             # exceptions
848             # TODO: conditionally limit to AGPL/GPL/LGPL
849 331         1226 foreach (@RE_EXCEPTION) {
850 11916 100       328767 if ( $string =~ $RE{"EXCEPTION_$_"} ) {
851             my $exception = Exception->new(
852 55         1003 id => $Regexp::Pattern::License::RE{$_},
853             begin => $-[0],
854             end => $+[0],
855             file => $self,
856             );
857 55         676 $coverage->set_range( $-[0], $+[0], $exception );
858 55         1738 push @exceptions, $exception;
859             }
860             }
861              
862             # oddities
863 331         1990 $log->trace('scan for oddities');
864              
865             # generated file
866 331 100       21344 if ( $string =~ $RE{TRAIT_generated} ) {
867             push @flaws, Flaw->new(
868             id => $Regexp::Pattern::License::RE{generated},
869 14         235 begin => $-[0],
870             end => $+[0],
871             file => $self,
872             );
873             }
874              
875             # multi-licensing
876 331         947 my @multilicenses;
877              
878             # LGPL, dual-licensed
879             # FIXME: add test covering this pattern
880 331 100       849 if ( grep { $match{$_}{name} } @lgpl ) {
  1324         3281  
881 29         170 $log->trace('scan for LGPL dual-license grant');
882 29 50       831 if ( $string =~ $L{multi_1} ) {
883 0         0 my $meta = Trait->new(
884             name => 'grant(multi#1)',
885             begin => $-[0],
886             end => $+[0],
887             file => $self,
888             );
889 0         0 $log->tracef(
890             'detected custom pattern multi#1: %s %s %s: %s',
891             'lgpl', $1, $2, $-[0]
892             );
893 0         0 push @multilicenses, 'lgpl', $1, $2;
894             }
895             }
896              
897             # GPL, dual-licensed
898             # FIXME: add test covering this pattern
899 331 100       744 if ( grep { $match{$_}{name} } @gpl ) {
  1324         2794  
900 68         244 $log->trace('scan for GPL dual-license grant');
901 68 50       1463 if ( $string =~ $L{multi_2} ) {
902 0         0 $log->tracef(
903             'detected custom pattern multi#2: %s %s %s: %s',
904             'gpl', $1, $2, $-[0]
905             );
906 0         0 push @multilicenses, 'gpl', $1, $2;
907             }
908             }
909              
910 331 50       950 $gen_license->(@multilicenses) if (@multilicenses);
911              
912             # LGPL
913 331 100       638 if ( grep { $match{$_}{name} } @lgpl ) {
  1324         2538  
914 29         103 $log->trace('scan for LGPL fulltext/grant');
915              
916             # LGPL, dual versions last
917 29 100       61912 if ( $string =~ $L{lgpl_5} ) {
918 5         58 my $grant = Trait->new(
919             name => 'grant(lgpl#5)',
920             begin => $-[0],
921             end => $+[0],
922             file => $self,
923             );
924 5         94 $license = "LGPL (v$1 or v$2) $license";
925 5         29 my $expr = "LGPL-$1 or LGPL-$2";
926 5         18 push @expressions,
927             Grant->new(
928             name => $expr,
929             begin => $grant->begin,
930             end => $grant->end,
931             file => $grant->file,
932             );
933 5         69 $match{ 'lgpl_' . $1 =~ tr/./_/r }{custom} = 1;
934 5         38 $match{ 'lgpl_' . $2 =~ tr/./_/r }{custom} = 1;
935 5         29 $match{lgpl}{custom} = 1;
936             }
937             }
938              
939             # GPL or LGPL
940 331 100       712 if ( grep { $match{$_}{name} } @gpl ) {
  1324         2565  
941 68         243 $log->trace('scan for GPL or LGPL dual-license grant');
942 68 100       7340 if ( $string =~ $L{gpl_7} ) {
943 2         27 my $grant = Trait->new(
944             name => "grant(gpl#7)",
945             begin => $-[0],
946             end => $+[0],
947             file => $self,
948             );
949 2         24 $gen_license->( 'gpl', $1, $2, 'lgpl', $3, $4 );
950 2         8 $match{gpl}{custom} = 1;
951 2         10 $match{lgpl}{custom} = 1;
952             }
953             }
954              
955             # BSD
956 331 50 66     792 if ( grep { $match{$_}{name} } @L_contains_bsd
  2979         6487  
957             and $string =~ $L{bsd_1} )
958             {
959 0         0 $log->trace('scan for BSD fulltext');
960 0         0 my $grant = Trait->new(
961             name => 'license(bsd#1)',
962             begin => $-[0],
963             end => $+[0],
964             file => $self,
965             );
966 0         0 for ($string) {
967 0 0       0 next if ( $license{bsd_4_clause} );
968 0 0       0 if ( $string =~ $RE{TRAIT_clause_advertising} ) {
969 0         0 my $grant = Trait->new(
970             name => 'clause_advertising',
971             begin => $-[0],
972             end => $+[0],
973             file => $self,
974             );
975 0         0 $gen_license->('bsd_4_clause');
976 0         0 next;
977             }
978 0 0       0 next if ( $license{bsd_3_clause} );
979 0 0       0 if ( $string =~ $RE{TRAIT_clause_non_endorsement} ) {
980 0         0 my $grant = Trait->new(
981             name => 'clause_non_endorsement',
982             begin => $-[0],
983             end => $+[0],
984             file => $self,
985             );
986 0         0 $gen_license->('bsd_3_clause');
987 0         0 next;
988             }
989 0 0       0 next if ( $license{bsd_2_clause} );
990 0 0       0 if ( $string =~ $RE{TRAIT_clause_reproduction} ) {
991             next
992             if (
993             defined(
994 0 0       0 $coverage->get_range( $-[0], $+[0] )->get_element(0)
995             )
996             );
997 0         0 my $grant = Trait->new(
998             name => 'clause_reproduction',
999             begin => $-[0],
1000             end => $+[0],
1001             file => $self,
1002             );
1003 0         0 $gen_license->('bsd_2_clause');
1004 0         0 next;
1005             }
1006 0         0 $gen_license->('bsd');
1007             }
1008             }
1009              
1010             # Apache dual-licensed with GPL/BSD/MIT
1011 331 100       1310 if ( $match{apache}{name} ) {
1012 11         56 $log->trace('scan for Apache license grant');
1013 11         57 for ($string) {
1014 11 100       11271 if ( $string =~ $L{apache_1} ) {
1015 2         23 my $grant = Trait->new(
1016             name => 'grant(apache#1)',
1017             begin => $-[0],
1018             end => $+[0],
1019             file => $self,
1020             );
1021 2         37 $gen_license->( 'apache', $1, $2, 'gpl', $3, $4 );
1022 2         11 $match{ $patterns2id->( 'apache', $1 ) }{custom} = 1;
1023 2         9 next;
1024             }
1025 9 50       10819 if ( $string =~ $L{apache_2} ) {
1026 0         0 my $grant = Trait->new(
1027             name => 'grant(apache#2)',
1028             begin => $-[0],
1029             end => $+[0],
1030             file => $self,
1031             );
1032 0 0       0 $gen_license->(
1033             'apache', $1, $2,
1034             $3 ? "bsd_${3}_clause" : ''
1035             );
1036 0         0 $match{ $patterns2id->( 'apache', $1 ) }{custom} = 1;
1037 0         0 next;
1038             }
1039 9 100       158 if ( $string =~ $L{apache_4} ) {
1040 1         12 my $grant = Trait->new(
1041             name => 'grant(apache#4)',
1042             begin => $-[0],
1043             end => $+[0],
1044             file => $self,
1045             );
1046 1         15 $gen_license->( 'apache', $1, $2, 'mit', $3, $4 );
1047 1         19 $match{ $patterns2id->( 'apache', $1 ) }{custom} = 1;
1048 1         6 next;
1049             }
1050             }
1051             }
1052              
1053             # FSFUL
1054             # FIXME: add test covering this pattern
1055 331         1266 $log->trace('scan for FSFUL fulltext');
1056 331 100       1608 if ( not $license{fsful} ) {
1057 330 50       4940 if ( $string =~ $L{fsful} ) {
1058 0         0 my $grant = Trait->new(
1059             name => 'grant(fsful#1)',
1060             begin => $-[0],
1061             end => $+[0],
1062             file => $self,
1063             );
1064 0         0 $license = "FSF Unlimited ($1 derivation) $license";
1065 0         0 my $expr = "FSFUL~$1";
1066 0         0 push @expressions,
1067             Fulltext->new(
1068             name => $expr,
1069             begin => $grant->begin,
1070             end => $grant->end,
1071             file => $grant->file,
1072             );
1073 0         0 $match{fsful}{custom} = 1;
1074             }
1075             }
1076              
1077             # FSFULLR
1078             # FIXME: add test covering this pattern
1079 331         1210 $log->trace('scan for FSFULLR fulltext');
1080 331 100       1570 if ( not $license{fsfullr} ) {
1081 330 50       4294 if ( $string =~ $L{fsfullr} ) {
1082 0         0 my $grant = Trait->new(
1083             name => 'grant(fsfullr#1)',
1084             begin => $-[0],
1085             end => $+[0],
1086             file => $self,
1087             );
1088 0         0 $license
1089             = "FSF Unlimited (with Retention, $1 derivation) $license";
1090 0         0 my $expr = "FSFULLR~$1";
1091 0         0 push @expressions,
1092             Fulltext->new(
1093             name => $expr,
1094             begin => $grant->begin,
1095             end => $grant->end,
1096             file => $grant->file,
1097             );
1098 0         0 $match{fsfullr}{custom} = 1;
1099             }
1100             }
1101              
1102             # usage
1103 331         1362 $log->trace('scan atomic for singleversion usage license grant');
1104 331         1385 foreach my $id (@L_type_usage) {
1105 8937 50       19992 next if ( $match{$id}{custom} );
1106 8937 50 33     28749 if ( !$grant{$id}
      66        
1107             and ( $L_grant_stepwise_incomplete{$id} or $force_atomic ) )
1108             {
1109 0 0       0 if ( $string =~ $RE{"GRANT_$id"} ) {
1110 0         0 my $grant = Trait->new(
1111             name => "grant($id)",
1112             begin => $-[0],
1113             end => $+[0],
1114             file => $self,
1115             );
1116 0 0       0 unless (
1117             defined(
1118             $coverage->get_range( $-[0], $+[0] )->get_element(0)
1119             )
1120             )
1121             {
1122 0         0 $grant{$id} = Grant->new(
1123             name => $id,
1124             begin => $grant->begin,
1125             end => $grant->end,
1126             file => $grant->file,
1127             );
1128             }
1129             }
1130             }
1131              
1132 8937 100       15816 if ( $grant{$id} ) {
1133             $coverage->set_range(
1134             $grant{$id}->begin, $grant{$id}->end,
1135 63         263 $grant{$id}
1136             );
1137 63         2110 $gen_license->( $id2patterns->($id) );
1138              
1139             # skip singleversion and unversioned equivalents
1140 63 50       345 if ( $L{usage}{$id} ) {
1141 63         223 $log->tracef( 'flagged license object: %s', $id );
1142 63         298 $match{ $L{usage}{$id} }{custom} = 1;
1143 63 50       274 if ( $L{series}{ $L{usage}{$id} } ) {
1144             $log->tracef(
1145             'flagged license object: %s',
1146 63         211 $L{usage}{$id}
1147             );
1148 63         328 $match{ $L{series}{ $L{usage}{$id} } }{custom} = 1;
1149             }
1150             }
1151             }
1152             }
1153              
1154             # singleversion
1155 331         1580 $log->trace('scan atomic for singleversion license grant');
1156 331         1735 foreach my $id (@L_type_singleversion) {
1157 70503 100 100     384477 if ( !$license{$id}
      66        
      66        
1158             and !$grant{$id}
1159             and !$match{$id}{custom}
1160             and ( $L_grant_stepwise_incomplete{$id} or $force_atomic ) )
1161             {
1162 321 50       44440 if ( $string =~ $RE{"GRANT_$id"} ) {
1163 0         0 my $grant = Trait->new(
1164             name => "grant($id)",
1165             begin => $-[0],
1166             end => $+[0],
1167             file => $self,
1168             );
1169 0 0       0 unless (
1170             defined(
1171             $coverage->get_range( $-[0], $+[0] )->get_element(0)
1172             )
1173             )
1174             {
1175 0         0 $grant{$id} = Grant->new(
1176             name => $id,
1177             begin => $grant->begin,
1178             end => $grant->end,
1179             file => $grant->file,
1180             );
1181             }
1182             }
1183             }
1184              
1185 70503 100 100     182215 if ( $license{$id} or $grant{$id} ) {
1186             $coverage->set_range(
1187             $grant{$id}->begin, $grant{$id}->end,
1188             $grant{$id}
1189 170 100       844 ) if $grant{$id};
1190             $gen_license->( $id2patterns->($id) )
1191 170 100       3716 unless ( $match{$id}{custom} );
1192              
1193             # skip unversioned equivalent
1194 170 50       1022 if ( $L{series}{$id} ) {
1195 170         747 $log->tracef( 'flagged license object: %s', $id );
1196 170         1107 $match{ $L{series}{$id} }{custom} = 1;
1197             }
1198             }
1199             }
1200              
1201             # versioned
1202 331         1918 $log->trace('scan atomic for versioned license grant');
1203 331         1665 foreach my $id (@L_type_versioned) {
1204 33431 100       75005 next if ( $match{$id}{custom} );
1205              
1206             # skip name part of another name detected as grant
1207             # TODO: use less brittle method than name of clue
1208             next
1209             if ( $id eq 'cc_by'
1210 33200 100 100     57537 and grep { $_->name eq 'grant(cc_by_sa_3)' } @clues );
  1754         3819  
1211              
1212             # skip embedded or referenced licenses
1213 33198 100 100     55875 next if ( $license{rpsl_1} and grep { $id eq $_ } qw(mpl python) );
  400         850  
1214              
1215 33196 50       51121 next if ( $license{$id} );
1216 33196 100 66     105198 if ( !$grant{$id}
      66        
1217             and ( $L_grant_stepwise_incomplete{$id} or $force_atomic ) )
1218             {
1219 554 50       2895 if ( $RE{"GRANT_$id"} ) {
1220 554 100       84405 if ( $string =~ $RE{"GRANT_$id"} ) {
1221 6         110 my $grant = Trait->new(
1222             name => "grant($id)",
1223             begin => $-[0],
1224             end => $+[0],
1225             file => $self,
1226             );
1227 6 100       96 unless (
1228             defined(
1229             $coverage->get_range( $-[0], $+[0] )
1230             ->get_element(0)
1231             )
1232             )
1233             {
1234 2         99 $grant{$id} = Grant->new(
1235             name => $id,
1236             begin => $grant->begin,
1237             end => $grant->end,
1238             file => $grant->file,
1239             );
1240             }
1241             }
1242             }
1243             }
1244              
1245 33196 100       60451 if ( $grant{$id} ) {
1246             $coverage->set_range(
1247             $grant{$id}->begin, $grant{$id}->end,
1248 16         97 $grant{$id}
1249             );
1250 16         824 $gen_license->($id);
1251             }
1252             }
1253              
1254             # other
1255             # TODO: add @L_type_group
1256 331         1865 $log->trace('scan atomic for misc fulltext/grant');
1257 331         1876 foreach my $id ( @L_type_unversioned, @L_type_combo ) {
1258 46671 50 66     145863 next if ( !$license{$id} and $match{$id}{custom} );
1259              
1260             next
1261             unless ( $license{$id}
1262             or $grant{$id}
1263 46671 100 100     201246 or $L_grant_stepwise_incomplete{$id}
      100        
      66        
1264             or $force_atomic );
1265              
1266             # skip embedded or referenced licenses
1267 748 50 33     2382 next if ( $license{caldera} and $id eq 'bsd' );
1268 748 50 66     1752 next if ( $license{cube} and $id eq 'zlib' );
1269 748 50 66     1690 next if ( $license{dsdp} and $id eq 'ntp' );
1270 748 50 66     1726 next if ( $license{mit_cmu} and $id eq 'ntp_disclaimer' );
1271 748 50 66     1644 next if ( $license{ntp_disclaimer} and $id eq 'ntp' );
1272              
1273 748 50 100     67684 if ( !$license{$id}
      66        
1274             and !$grant{$id}
1275             and $string =~ $RE{"GRANT_$id"} )
1276             {
1277 0         0 my $grant = Trait->new(
1278             name => "grant($id)",
1279             begin => $-[0],
1280             end => $+[0],
1281             file => $self,
1282             );
1283 0 0       0 unless (
1284             defined(
1285             $coverage->get_range( $-[0], $+[0] )->get_element(0)
1286             )
1287             )
1288             {
1289 0         0 $grant{$id} = Grant->new(
1290             name => $id,
1291             begin => $grant->begin,
1292             end => $grant->end,
1293             file => $grant->file,
1294             );
1295             }
1296             }
1297 748 100 100     3434 if ( $license{$id} or $grant{$id} ) {
1298             $coverage->set_range(
1299             $grant{$id}->begin, $grant{$id}->end,
1300             $grant{$id}
1301 90 100       314 ) if $grant{$id};
1302 90         873 $gen_license->($id);
1303             }
1304             }
1305              
1306 331         5106 $license =~ s/$L{trailing_space}//;
1307 331         1183 $expr = join( ' and/or ', sort map { $_->name } @expressions );
  338         1235  
1308 331   100     1054 $expr ||= 'UNKNOWN';
1309 331   100     823 $license ||= 'UNKNOWN';
1310 331 100       841 if (@exceptions) {
1311 53 100       184 $expr = "($expr)"
1312             if ( @expressions > 1 );
1313             $expr .= ' with ' . join(
1314             '_AND_',
1315 53         158 sort map { $self->best_value( $_->id, 'name' ) } @exceptions
  55         187  
1316             ) . ' exception';
1317             }
1318 331 100       836 if (@flaws) {
1319             $license .= ' [' . join(
1320             ', ',
1321 28         77 sort map { $self->best_value( $_->id, qw(caption name) ) } @flaws
  28         96  
1322             ) . ']';
1323             }
1324 331         1570 $log->infof( 'resolved license expression: %s', $expr );
1325              
1326 331         35274 return $self;
1327             }
1328              
1329             =item as_text
1330              
1331             Returns identified licensing patterns as a string,
1332             either structured as SPDX License Expressions,
1333             or with scheme-less naming as a short description.
1334              
1335             =cut
1336              
1337             method as_text
1338 331     331 1 1232 {
1339 331 100       1499 if ( $naming->list_schemes ) {
1340 311 50       1755 $self->resolve
1341             unless $expr;
1342              
1343 311         2071 return $expr;
1344             }
1345              
1346             $self->resolve
1347 20 50       84 unless $license;
1348              
1349 20         84 return $license;
1350             }
1351              
1352             =back
1353              
1354             =encoding UTF-8
1355              
1356             =head1 AUTHOR
1357              
1358             Jonas Smedegaard C<< >>
1359              
1360             =head1 COPYRIGHT AND LICENSE
1361              
1362             This program is based on the script "licensecheck" from the KDE SDK,
1363             originally introduced by Stefan Westerfeld C<< >>.
1364              
1365             Copyright © 2007, 2008 Adam D. Barratt
1366              
1367             Copyright © 2016-2023 Jonas Smedegaard
1368              
1369             Copyright © 2017-2022 Purism SPC
1370              
1371             This program is free software:
1372             you can redistribute it and/or modify it
1373             under the terms of the GNU Affero General Public License
1374             as published by the Free Software Foundation,
1375             either version 3, or (at your option) any later version.
1376              
1377             This program is distributed in the hope that it will be useful,
1378             but WITHOUT ANY WARRANTY;
1379             without even the implied warranty
1380             of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
1381             See the GNU Affero General Public License for more details.
1382              
1383             You should have received a copy
1384             of the GNU Affero General Public License along with this program.
1385             If not, see .
1386              
1387             =cut
1388              
1389             1;