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   10112855 use Feature::Compat::Class 0.04;
  10         4156  
  10         53  
2              
3 10     10   116176 use v5.12;
  10         39  
4 10     10   58 use utf8;
  10         22  
  10         102  
5 10     10   271 use warnings;
  10         21  
  10         513  
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.4
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.4;
37              
38 10     10   57 use Carp qw(croak);
  10         24  
  10         430  
39 10     10   4179 use Log::Any ();
  10         86031  
  10         324  
40 10     10   76 use Scalar::Util qw(blessed);
  10         21  
  10         576  
41 10     10   4759 use List::SomeUtils qw(nsort_by uniq);
  10         134570  
  10         864  
42 10     10   4593 use Array::IntSpan;
  10         30809  
  10         432  
43 10     10   4690 use Regexp::Pattern::License 3.4.0;
  10         1943551  
  10         1228  
44 10     10   5302 use Regexp::Pattern 0.2.12;
  10         13645  
  10         62  
45 10     10   4769 use String::License::Naming::Custom;
  10         31  
  10         441  
46 10     10   3945 use String::License::Naming::SPDX;
  10         29  
  10         425  
47              
48 10     10   76 use namespace::clean;
  10         21  
  10         34  
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 3375     3375   6136 method name { return $name }
  3375         9063  
68 621     621   1890 method begin { return $begin }
  621         3060  
69 8439     8439   16510 method end { return $end }
  8439         20347  
70 7     7   16 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   148 method id { return $id }
  55         286  
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   85 method id { return $id }
  28         128  
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 335     335   1618 method name { return $name }
  335         2198  
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   18 method name { return $name }
  5         20  
176 2     2   4 method begin { return $begin }
  2         7  
177 2     2   6 method end { return $end }
  2         6  
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 2686     2686 1 6349 {
260 2686         11979 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 25472 {
277 11363         22764 my ( $hashref, @props ) = @_;
278 11363         13688 my $value;
279              
280             PROPERTY:
281 11363         17233 for my $prop (@props) {
282 11391         29980 for my $org ( $naming->list_schemes ) {
283 16711         80560 for ( keys %$hashref ) {
284 311679         1218671 /$re_prop_attrs/;
285 10 100 100 10   66661 next unless $+{prop} and $+{prop} eq $prop;
  10         4501  
  10         110569  
  311679         1697449  
286 31168 100 100     196266 next unless $+{org} and $+{org} eq $org;
287 3886 50       14361 next if $+{version};
288 3886 100       12634 next if $+{other};
289 3696 100       11858 next if $+{until_date};
290              
291 3254         9008 $value = $hashref->{$_};
292 3254         6126 last PROPERTY;
293             }
294             }
295 8137   100     29864 $value ||= $hashref->{$prop};
296             }
297              
298 11363         45829 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 730 {
309             # reuse if already resolved
310 331 100       1425 return %L if exists $L{re_trait};
311              
312 10         349 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         13131183 @RE_EXCEPTION = sort map /^EXCEPTION_(.*)/, keys(%RE);
415 10         11065 @RE_LICENSE = sort map /^LICENSE_(.*)/, keys(%RE);
416 10         10623 @RE_NAME = sort map /^NAME_(.*)/, keys(%RE);
417              
418 10         2555 foreach my $key ( grep {/^[a-z]/} keys(%Regexp::Pattern::License::RE) ) {
  5640         9405  
419 5640         15020 my $val = $Regexp::Pattern::License::RE{$key};
420 5640   66     10964 $L{name}{$key} = $self->best_value( $val, 'name' ) || $key;
421             $L{caption}{$key}
422 5640   66     11234 = $self->best_value( $val, 'caption' ) || $val->{name} || $key;
423 5640         8250 foreach ( @{ $val->{tags} } ) {
  5640         14643  
424 10670 100       42323 /$type_re/ or next;
425 5650         16742 $L{type}{$1}{$key} = 1;
426 5650 100 100     18701 if ( $2 and $1 eq 'singleversion' ) {
427 2130         5303 $L{series}{$key} = $2;
428             }
429 5650 100 100     15234 if ( $2 and $1 eq 'usage' ) {
430 270         821 $L{usage}{$key} = $2;
431             }
432              
433             # TODO: simplify, and require Regexp::Pattern::License v3.9.0
434 5650 100 100     16020 if ( $3 and $1 eq 'trait' ) {
435 230 100       726 if ( substr( $key, 0, 14 ) eq 'except_prefix_' ) {
436 50         196 $L{TRAITS_exception_prefix}{$key} = undef;
437             }
438             else {
439 180         935 $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         611 foreach (qw(any_of)) {
447 10         90 $L{re_trait}{$_} = '';
448             }
449              
450             #<<< do not let perltidy touch this (keep long regex on one line)
451 10         23600 $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         8838 $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         26491 $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         25263 $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         269 $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         10598 $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         3770 $L{apache_2} = qr/$RE{LOCAL_NAME_apache}$RE{LOCAL_TRAIT_KEEP_version}?(?:(?: or)? [^ ,]*?apache[^ ,]*| \([^(),]\))*,? or(?: the)? bsd(?:[ -](\d)-clause)?\b/i;
458 10         3522 $L{apache_4} = qr/$RE{LOCAL_NAME_apache}$RE{LOCAL_TRAIT_KEEP_version}?(?:(?: or)? [^ ,]*?apache[^ ,]*| \([^(),]\))*,? or $RE{LOCAL_NAME_mit}\b/i;
459 10         573 $L{fsful} = qr/This (\w+)(?: (?:file|script))? is free software; $RE{LOCAL_TRAIT_fsf_unlimited}/i;
460 10         492 $L{fsfullr} = qr/This (\w+)(?: (?:file|script))? is free software; $RE{LOCAL_TRAIT_fsf_unlimited_retention}/i;
461 10         115 $L{trailing_space} = qr/\s+$/;
462 10         78 $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 806 {
522 331         1408 $self->init_licensepatterns;
523              
524 331         827 my @L_type_usage = sort keys %{ $L{type}{usage} };
  331         13012  
525 331         1070 my @L_type_singleversion = sort keys %{ $L{type}{singleversion} };
  331         46704  
526 331         3008 my @L_type_versioned = sort keys %{ $L{type}{versioned} };
  331         18920  
527 331         1885 my @L_type_unversioned = sort keys %{ $L{type}{unversioned} };
  331         27505  
528 331         2230 my @L_type_combo = sort keys %{ $L{type}{combo} };
  331         1723  
529 331         768 my @L_type_group = sort keys %{ $L{type}{group} };
  331         1715  
530              
531 331         661 my @spdx_gplver;
532              
533 331         1000 my @agpl = qw(agpl agpl_1 agpl_2 agpl_3);
534 331         816 my @gpl = qw(gpl gpl_1 gpl_2 gpl_3);
535 331         794 my @lgpl = qw(lgpl lgpl_2 lgpl_2_1 lgpl_3);
536              
537 331         2993 my $coverage = Array::IntSpan->new();
538 331         5323 my %match;
539 331         1145 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   14 my ( $id, $ver ) = @_;
547 3 50       12 return $id
548             unless ($ver);
549 3         8 $_ = $ver;
550 3         26 s/\.0$//g;
551 3         10 s/\./_/g;
552 3         19 return "${id}_$_";
553 331         2094 };
554             my $id2patterns = sub {
555 224     224   2993 return $_[0] =~ /$id2patterns_re/;
556 331         1308 };
557             my $gen_license = sub {
558 335     335   1316 my ( $id, $v, $later, $id2, $v2, $later2 ) = @_;
559 335         578 my @spdx;
560 335   33     1808 my $name = $L{name}{$id} || $id;
561 335   33     1526 my $desc = $L{caption}{$id} || $id;
562 335 100       907 if ($v) {
563 5 100       28 push @spdx, $later ? "$name-$v+" : "$name-$v";
564 5 100       21 $v .= ' or later' if ($later);
565             }
566             else {
567 330         962 push @spdx, $name;
568             }
569 335         747 my ( $name2, $desc2 );
570 335 100       993 if ($id2) {
571 5   33     27 $name2 = $L{name}{$id2} || $id2;
572 5   33     25 $desc2 = $L{caption}{$id2} || $id2;
573 5 100       19 if ($v2) {
574 4 100       21 push @spdx, $later2 ? "$name2-$v2+" : "$name2-$v2";
575 4 100       19 $v2 .= ' or later' if ($later2);
576             }
577             else {
578 1         1947 push @spdx, $name2;
579             }
580             }
581 335 100       2035 my $legacy = join(
    100          
    100          
582             ' ',
583             $desc,
584             $v ? "(v$v)" : (),
585             $desc2 ? "or $desc2" : (),
586             $v2 ? "(v$v2)" : (),
587             );
588 335         1283 my $expr = join( ' or ', sort @spdx );
589 335         5024 push @expressions, Licensing->new( name => $expr );
590 335   66     4638 $license = join( ' ', $L{caption}{$legacy} || $legacy, $license );
591 331         1847 };
592              
593             # fulltext
594 331         1537 $log->trace('scan for license fulltext');
595 331         1205 my %pos_license;
596 331         954 foreach my $id (@RE_LICENSE) {
597 161528 100       547506 next unless ( $RE{"LICENSE_$id"} );
598 118498         26431226 while ( $string =~ /$RE{"LICENSE_$id"}/g ) {
599 290         54284 $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         961 foreach my $trait ( keys %{ $L{TRAITS_exception_prefix} } ) {
  331         2800  
609              
610 1655 100       754976 next unless ( $string =~ /$RE{"TRAIT_$trait"}/ );
611 42         45487 while ( $string =~ /$RE{"TRAIT_$trait"}/g ) {
612             next
613             if (
614             defined(
615 44 50       862 $coverage->get_range( $-[0], $+[0] )->get_element(0)
616             )
617             );
618 44         4976 push @clues,
619             Trait->new(
620             name => $trait,
621             begin => $-[0],
622             end => $+[0],
623             file => $self,
624             );
625             }
626             }
627 331         12617 foreach my $pos ( sort { $a <=> $b } keys %pos_license ) {
  119         419  
628              
629             # pick longest or most specific among matched license fulltexts
630 290     290   3232 my @licenses = nsort_by { $pos_license{$pos}{$_}->end }
631 263 100       2988 grep { $pos_license{$pos}{$_} ? $pos_license{$pos}{$_}->end : () } (
  127818         198942  
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 263         2373 my $license = pop @licenses;
640 263 50       869 next unless ($license);
641             next
642             if defined(
643 263 100       808 $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 248         12803 $pos_license{$pos}{$license}
649             );
650 248         7508 $license{$license} = 1;
651             }
652              
653             # grant, stepwise
654 331         3912 $log->trace('scan stepwise for license grant');
655 331         1553 foreach my $trait ( keys %{ $L{TRAITS_grant_prefix} } ) {
  331         1946  
656              
657 1324         522122 while ( $string =~ /$RE{"TRAIT_$trait"}/g ) {
658             next
659             if (
660             defined(
661 1646 100       1263630 $coverage->get_range( $-[0], $+[0] )->get_element(0)
662             )
663             );
664 1570         189683 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         52660 foreach my $licensed_under (
675 2399         3735 sort { $a->end <=> $b->end }
676 1614         3465 grep { exists $L{TRAITS_grant_prefix}{ $_->name } } @clues
677             )
678             {
679 1570         6014 my $pos = $licensed_under->end;
680              
681             # possible grant names
682 1570         78768 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 1570         3008 my ( $version, $later );
692              
693             # scan for prepended version
694 1570         29997 substr( $string, $pos ) =~ $RE{ANCHORLEFT_NAMED_version};
695 1570 100       12911 if ( $+{version_number} ) {
696 7         104 push @clues,
697             Trait->new(
698             name => 'version',
699             begin => $pos + $-[0],
700             end => $pos + $+[0],
701             file => $self,
702             );
703 7         98 $version = $+{version_number};
704 7 50       51 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 7 50       204 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 7         25 $version = '';
729             }
730             }
731              
732             # scan for name
733 1570         4421 foreach my $id (@RE_NAME) {
734 766160 100       9283750 if ( substr( $string, $pos ) =~ $RE{"NAME_$id"} ) {
735 547         6805 $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 409     409   2969 my @names = nsort_by { $match{$_}{name}{$pos}->end }
747 1570 50 66     13371 grep { $match{$_} and $match{$_}{name} and $match{$_}{name}{$pos} }
  756740         1167621  
748             @grant_types;
749 1570         15633 my $name = pop @names;
750 1570 50 66     25347 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 197         10207 my $pos_end = $pos = $match{$name}{name}{$pos}->end;
760              
761             # may include version
762 197 100 66     891 if ( !$version and grep { $_ eq $name } @L_type_versioned ) {
  19897 100 66     29324  
763 40         584 substr( $string, $pos ) =~ $RE{ANCHORLEFT_NAMED_version};
764 40 100       383 if ( $+{version_number} ) {
765 5         61 push @clues, Trait->new(
766             name => 'version',
767             begin => $pos + $-[0],
768             end => $pos + $+[0],
769             file => $self,
770             );
771 5         73 $version = $+{version_number};
772 5         21 $pos_end = $pos + $+[1];
773 5 50       43 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 33441         46774 elsif ( !$version and grep { $_ eq $name } @L_type_singleversion )
786             {
787             substr( $string, $pos )
788 84         1284 =~ $RE{ANCHORLEFT_NAMED_version_later};
789 84 100       761 if ( $+{version_later} ) {
790 3         35 push @clues, Trait->new(
791             name => 'or_later',
792             begin => $pos + $-[1],
793             end => $pos + $+[1],
794             file => $self,
795             );
796 3         79 $later = $+{version_later};
797 3         20 $pos_end = $pos + $+[1];
798             }
799             }
800 197 100       869 if ($version) {
801 5         27 $version =~ s/(?:\.0)+$//;
802 5         19 $version =~ s/\./_/g;
803 5         20 $name .= "_$version";
804             }
805 197 100       637 if ($later) {
806 3         16 my $latername = "${name}_or_later";
807 3         14 push @clues, Trait->new(
808             name => $latername,
809             begin => $licensed_under->begin,
810             end => $pos_end,
811             file => $self,
812             );
813 3         26 $grant{$latername} = $clues[-1];
814 3 50       14 next LICENSED_UNDER if grep { $grant{$_} } @RE_NAME;
  1464         2096  
815             }
816 194         1160 $grant{$name} = Trait->new(
817             name => "grant($name)",
818             begin => $licensed_under->begin,
819             end => $pos_end,
820             file => $self,
821             );
822 194         6417 push @clues, $grant{$name};
823             }
824             }
825              
826             # GNU oddities
827 331 100       1281 if ( grep { $match{$_}{name} } @agpl, @gpl, @lgpl ) {
  3972         9461  
828 96         547 $log->trace('scan for GNU oddities');
829              
830             # address in AGPL/GPL/LGPL
831 96         37754 while ( $string =~ /$RE{TRAIT_addr_fsf}/g ) {
832 43         173 foreach (
833             qw(addr_fsf_franklin_steet addr_fsf_mass addr_fsf_temple))
834             {
835 129 100       15741 if ( defined $+{$_} ) {
836             push @flaws, Flaw->new(
837 14         259 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         2103 foreach (@RE_EXCEPTION) {
850 11916 100       2912734 if ( $string =~ $RE{"EXCEPTION_$_"} ) {
851             my $exception = Exception->new(
852 55         939 id => $Regexp::Pattern::License::RE{$_},
853             begin => $-[0],
854             end => $+[0],
855             file => $self,
856             );
857 55         673 $coverage->set_range( $-[0], $+[0], $exception );
858 55         5424 push @exceptions, $exception;
859             }
860             }
861              
862             # oddities
863 331         2358 $log->trace('scan for oddities');
864              
865             # generated file
866 331 100       400904 if ( $string =~ $RE{TRAIT_generated} ) {
867             push @flaws, Flaw->new(
868             id => $Regexp::Pattern::License::RE{generated},
869 14         231 begin => $-[0],
870             end => $+[0],
871             file => $self,
872             );
873             }
874              
875             # multi-licensing
876 331         993 my @multilicenses;
877              
878             # LGPL, dual-licensed
879             # FIXME: add test covering this pattern
880 331 100       969 if ( grep { $match{$_}{name} } @lgpl ) {
  1324         3687  
881 29         124 $log->trace('scan for LGPL dual-license grant');
882 29 50       724 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       868 if ( grep { $match{$_}{name} } @gpl ) {
  1324         3281  
900 68         272 $log->trace('scan for GPL dual-license grant');
901 68 50       1511 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       1108 $gen_license->(@multilicenses) if (@multilicenses);
911              
912             # LGPL
913 331 100       714 if ( grep { $match{$_}{name} } @lgpl ) {
  1324         2544  
914 29         93 $log->trace('scan for LGPL fulltext/grant');
915              
916             # LGPL, dual versions last
917 29 100       54868 if ( $string =~ $L{lgpl_5} ) {
918 5         63 my $grant = Trait->new(
919             name => 'grant(lgpl#5)',
920             begin => $-[0],
921             end => $+[0],
922             file => $self,
923             );
924 5         72 $license = "LGPL (v$1 or v$2) $license";
925 5         25 my $expr = "LGPL-$1 or LGPL-$2";
926 5         21 push @expressions,
927             Grant->new(
928             name => $expr,
929             begin => $grant->begin,
930             end => $grant->end,
931             file => $grant->file,
932             );
933 5         62 $match{ 'lgpl_' . $1 =~ tr/./_/r }{custom} = 1;
934 5         24 $match{ 'lgpl_' . $2 =~ tr/./_/r }{custom} = 1;
935 5         22 $match{lgpl}{custom} = 1;
936             }
937             }
938              
939             # GPL or LGPL
940 331 100       991 if ( grep { $match{$_}{name} } @gpl ) {
  1324         2769  
941 68         280 $log->trace('scan for GPL or LGPL dual-license grant');
942 68 100       7018 if ( $string =~ $L{gpl_7} ) {
943 2         33 my $grant = Trait->new(
944             name => "grant(gpl#7)",
945             begin => $-[0],
946             end => $+[0],
947             file => $self,
948             );
949 2         26 $gen_license->( 'gpl', $1, $2, 'lgpl', $3, $4 );
950 2         8 $match{gpl}{custom} = 1;
951 2         11 $match{lgpl}{custom} = 1;
952             }
953             }
954              
955             # BSD
956 331 50 66     909 if ( grep { $match{$_}{name} } @L_contains_bsd
  2979         7016  
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       1510 if ( $match{apache}{name} ) {
1012 11         57 $log->trace('scan for Apache license grant');
1013 11         59 for ($string) {
1014 11 100       10721 if ( $string =~ $L{apache_1} ) {
1015 2         26 my $grant = Trait->new(
1016             name => 'grant(apache#1)',
1017             begin => $-[0],
1018             end => $+[0],
1019             file => $self,
1020             );
1021 2         48 $gen_license->( 'apache', $1, $2, 'gpl', $3, $4 );
1022 2         9 $match{ $patterns2id->( 'apache', $1 ) }{custom} = 1;
1023 2         8 next;
1024             }
1025 9 50       10399 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       2288 if ( $string =~ $L{apache_4} ) {
1040 1         14 my $grant = Trait->new(
1041             name => 'grant(apache#4)',
1042             begin => $-[0],
1043             end => $+[0],
1044             file => $self,
1045             );
1046 1         20 $gen_license->( 'apache', $1, $2, 'mit', $3, $4 );
1047 1         6 $match{ $patterns2id->( 'apache', $1 ) }{custom} = 1;
1048 1         7 next;
1049             }
1050             }
1051             }
1052              
1053             # FSFUL
1054             # FIXME: add test covering this pattern
1055 331         1360 $log->trace('scan for FSFUL fulltext');
1056 331 100       1825 if ( not $license{fsful} ) {
1057 330 50       4849 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         1326 $log->trace('scan for FSFULLR fulltext');
1080 331 100       1528 if ( not $license{fsfullr} ) {
1081 330 50       4257 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         1212 $log->trace('scan atomic for singleversion usage license grant');
1104 331         1423 foreach my $id (@L_type_usage) {
1105 8937 50       19905 next if ( $match{$id}{custom} );
1106 8937 50 33     28071 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       15233 if ( $grant{$id} ) {
1133             $coverage->set_range(
1134             $grant{$id}->begin, $grant{$id}->end,
1135 63         310 $grant{$id}
1136             );
1137 63         2332 $gen_license->( $id2patterns->($id) );
1138              
1139             # skip singleversion and unversioned equivalents
1140 63 50       389 if ( $L{usage}{$id} ) {
1141 63         303 $log->tracef( 'flagged license object: %s', $id );
1142 63         359 $match{ $L{usage}{$id} }{custom} = 1;
1143 63 50       288 if ( $L{series}{ $L{usage}{$id} } ) {
1144             $log->tracef(
1145             'flagged license object: %s',
1146 63         258 $L{usage}{$id}
1147             );
1148 63         388 $match{ $L{series}{ $L{usage}{$id} } }{custom} = 1;
1149             }
1150             }
1151             }
1152             }
1153              
1154             # singleversion
1155 331         1365 $log->trace('scan atomic for singleversion license grant');
1156 331         1429 foreach my $id (@L_type_singleversion) {
1157 70503 100 100     370436 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       1934098 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     174625 if ( $license{$id} or $grant{$id} ) {
1186             $coverage->set_range(
1187             $grant{$id}->begin, $grant{$id}->end,
1188             $grant{$id}
1189 172 100       947 ) if $grant{$id};
1190             $gen_license->( $id2patterns->($id) )
1191 172 100       4059 unless ( $match{$id}{custom} );
1192              
1193             # skip unversioned equivalent
1194 172 50       965 if ( $L{series}{$id} ) {
1195 172         813 $log->tracef( 'flagged license object: %s', $id );
1196 172         1196 $match{ $L{series}{$id} }{custom} = 1;
1197             }
1198             }
1199             }
1200              
1201             # versioned
1202 331         1952 $log->trace('scan atomic for versioned license grant');
1203 331         1753 foreach my $id (@L_type_versioned) {
1204 33431 100       73900 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 33198 100 100     54681 and grep { $_->name eq 'grant(cc_by_sa_3)' } @clues );
  1761         3733  
1211              
1212             # skip embedded or referenced licenses
1213 33196 100 100     52674 next if ( $license{rpsl_1} and grep { $id eq $_ } qw(mpl python) );
  400         790  
1214              
1215 33194 50       47064 next if ( $license{$id} );
1216 33194 100 66     100301 if ( !$grant{$id}
      66        
1217             and ( $L_grant_stepwise_incomplete{$id} or $force_atomic ) )
1218             {
1219 554 50       2992 if ( $RE{"GRANT_$id"} ) {
1220 554 100       3409467 if ( $string =~ $RE{"GRANT_$id"} ) {
1221 6         91 my $grant = Trait->new(
1222             name => "grant($id)",
1223             begin => $-[0],
1224             end => $+[0],
1225             file => $self,
1226             );
1227 6 100       73 unless (
1228             defined(
1229             $coverage->get_range( $-[0], $+[0] )
1230             ->get_element(0)
1231             )
1232             )
1233             {
1234 2         89 $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 33194 100       57696 if ( $grant{$id} ) {
1246             $coverage->set_range(
1247             $grant{$id}->begin, $grant{$id}->end,
1248 15         80 $grant{$id}
1249             );
1250 15         796 $gen_license->($id);
1251             }
1252             }
1253              
1254             # other
1255             # TODO: add @L_type_group
1256 331         2463 $log->trace('scan atomic for misc fulltext/grant');
1257 331         2156 foreach my $id ( @L_type_unversioned, @L_type_combo ) {
1258 46671 50 66     145417 next if ( !$license{$id} and $match{$id}{custom} );
1259              
1260             next
1261             unless ( $license{$id}
1262             or $grant{$id}
1263 46671 100 100     191415 or $L_grant_stepwise_incomplete{$id}
      100        
      66        
1264             or $force_atomic );
1265              
1266             # skip embedded or referenced licenses
1267 749 50 33     2205 next if ( $license{caldera} and $id eq 'bsd' );
1268 749 50 66     2174 next if ( $license{cube} and $id eq 'zlib' );
1269 749 50 66     1943 next if ( $license{dsdp} and $id eq 'ntp' );
1270 749 50 66     1866 next if ( $license{mit_cmu} and $id eq 'ntp_disclaimer' );
1271 749 50 66     1886 next if ( $license{ntp_disclaimer} and $id eq 'ntp' );
1272              
1273 749 50 100     2468601 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 749 100 100     4155 if ( $license{$id} or $grant{$id} ) {
1298             $coverage->set_range(
1299             $grant{$id}->begin, $grant{$id}->end,
1300             $grant{$id}
1301 91 100       297 ) if $grant{$id};
1302 91         1042 $gen_license->($id);
1303             }
1304             }
1305              
1306 331         4947 $license =~ s/$L{trailing_space}//;
1307 331         1462 $expr = join( ' and/or ', sort map { $_->name } @expressions );
  340         1670  
1308 331   100     1155 $expr ||= 'UNKNOWN';
1309 331   100     981 $license ||= 'UNKNOWN';
1310 331 100       1054 if (@exceptions) {
1311 53 100       194 $expr = "($expr)"
1312             if ( @expressions > 1 );
1313             $expr .= ' with ' . join(
1314             '_AND_',
1315 53         136 sort map { $self->best_value( $_->id, 'name' ) } @exceptions
  55         245  
1316             ) . ' exception';
1317             }
1318 331 100       931 if (@flaws) {
1319             $license .= ' [' . join(
1320             ', ',
1321 28         71 sort map { $self->best_value( $_->id, qw(caption name) ) } @flaws
  28         135  
1322             ) . ']';
1323             }
1324 331         2012 $log->infof( 'resolved license expression: %s', $expr );
1325              
1326 331         52011 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 1124 {
1339 331 100       1747 if ( $naming->list_schemes ) {
1340 311 50       1690 $self->resolve
1341             unless $expr;
1342              
1343 311         2232 return $expr;
1344             }
1345              
1346             $self->resolve
1347 20 50       113 unless $license;
1348              
1349 20         107 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;