File Coverage

blib/lib/Lang/Go/Mod.pm
Criterion Covered Total %
statement 100 102 98.0
branch 77 88 87.5
condition 5 12 41.6
subroutine 9 9 100.0
pod 2 2 100.0
total 193 213 90.6


line stmt bran cond sub pod time code
1             # You may distribute under the terms of either the GNU General Public License
2             # or the Artistic License (the same terms as Perl itself)
3             #
4             # (C) Brad Clawsie, 2023 -- brad.clawsie@gmail.com
5              
6             package Lang::Go::Mod;
7 3     3   715492 use warnings;
  3         21  
  3         115  
8 3     3   27 use strict;
  3         7  
  3         69  
9 3     3   12 use Carp qw(croak);
  3         12  
  3         136  
10 3     3   2040 use English qw(-no_match_vars);
  3         11782  
  3         18  
11 3     3   1135 use Exporter qw(import);
  3         6  
  3         93  
12 3     3   2710 use Path::Tiny qw(path);
  3         42027  
  3         5199  
13              
14             # ABSTRACT: parse and model go.mod files
15              
16             our $VERSION = '0.007';
17             our $AUTHORITY = 'cpan:bclawsie';
18              
19             our @EXPORT_OK = qw(read_go_mod parse_go_mod _parse_retract);
20              
21             sub read_go_mod {
22 1     1 1 141 my $use_msg = 'use: read_go_mod(go_mod_path)';
23 1   33     5 my $go_mod_path = shift || croak $use_msg;
24              
25 1   33     7 my $go_mod_content = path($go_mod_path)->slurp_utf8 || croak "$ERRNO";
26              
27 1         2343 return parse_go_mod($go_mod_content);
28             }
29              
30             sub parse_go_mod {
31 13   33 13 1 9563 my $go_mod_content = shift || croak 'use: parse_go_mod(go_mod_content)';
32              
33 13         23 my $m = {};
34 13         29 for my $k ( 'exclude', 'replace', 'require', 'retracts' ) {
35 52         110 $m->{$k} = {};
36             }
37 13         37 my ( $excludes, $replaces, $requires, $retracts ) = ( 0, 0, 0, 0 );
38              
39 13         61 LINE: for my $line ( split /\n/x, $go_mod_content ) {
40 77 100       266 next LINE if ( $line =~ /^\s*$/x );
41 60 100       106 if ($excludes) {
42 3 100       53 if ( $line =~ /^\s*[)]\s*$/x ) {
    100          
43 1         3 $excludes = 0;
44             }
45             elsif ( $line =~ /\s*(\S+)\s+(\S+)/x ) {
46 1 50       7 $m->{exclude}->{$1} = [] unless ( defined $m->{exclude}->{$1} );
47 1         2 push @{ $m->{exclude}->{$1} }, $2;
  1         5  
48             }
49             else {
50 1         122 croak "malformed exclude line $line";
51             }
52 2         4 next LINE;
53             }
54 57 100       93 if ($replaces) {
55 4 100       25 if ( $line =~ /^\s*[)]\s*$/x ) {
    100          
56 1         1 $replaces = 0;
57             }
58             elsif ( $line =~ /^\s*(\S+)\s+=>\s+(\S+)\s*$/x ) {
59             croak "duplicate replace for $1"
60 2 50       6 if ( defined $m->{replace}->{$1} );
61 2         14 $m->{replace}->{$1} = $2;
62             }
63             else {
64 1         94 croak "malformed replace line $line";
65             }
66 3         7 next LINE;
67             }
68 53 100       87 if ($requires) {
69 5 100       47 if ( $line =~ /^\s*[)]\s*$/x ) {
    100          
70 1         2 $requires = 0;
71             }
72             elsif ( $line =~ /^\s*(\S+)\s+(\S+).*$/x ) {
73             croak "duplicate require for $1"
74 3 50       12 if ( defined $m->{'require'}->{$1} );
75 3         11 $m->{'require'}->{$1} = $2;
76             }
77             else {
78 1         94 croak "malformed require line $line";
79             }
80 4         6 next LINE;
81             }
82 48 100       79 if ($retracts) {
83 3 100       14 if ( $line =~ /^\s*[)]\s*$/x ) {
    50          
84 1         2 $retracts = 0;
85             }
86             elsif ( $line =~ /^\s*(\S+)(.*)$/x ) {
87 2         11 my $retract = _parse_retract( $1 . $2 );
88 2 50       7 croak "unparseable retract content: $line"
89             unless ( defined($retract) );
90             croak "duplicate retract for $retract"
91 2 50       6 if ( defined $m->{retract}->{$retract} );
92 2         15 $m->{retract}->{$retract} = 1;
93             }
94             else {
95 0         0 croak "malformed retract line $line";
96             }
97 3         7 next LINE;
98             }
99              
100             # single-line directives
101 45 100       328 if ( $line =~ /^module\s+(\S+)$/x ) {
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
    100          
102 12         44 $m->{module} = $1;
103             }
104             elsif ( $line =~ /^go\s+(\S+)$/x ) {
105 12         31 $m->{go} = $1;
106             }
107              
108             # multi-line directive
109             elsif ( $line =~ /^exclude\s+[(]\s*$/x ) {
110              
111             # toggle beginning of exclude block (and negate the other block checks)
112 2         14 ( $excludes, $replaces, $requires, $retracts ) = ( 1, 0, 0, 0 );
113             }
114             elsif ( $line =~ /^replace\s+[(]\s*$/x ) {
115              
116             # toggle beginning of replace block (and negate the other block checks)
117 2         10 ( $excludes, $replaces, $requires, $retracts ) = ( 0, 1, 0, 0 );
118             }
119             elsif ( $line =~ /^require\s+[(]\s*$/x ) {
120              
121             # toggle beginning of require block (and negate the other block checks)
122 2         24 ( $excludes, $replaces, $requires, $retracts ) = ( 0, 0, 1, 0 );
123             }
124             elsif ( $line =~ /^retract\s+[(]\s*$/x ) {
125              
126             # toggle beginning of retract block (and negate the other block checks)
127 1         6 ( $excludes, $replaces, $requires, $retracts ) = ( 0, 0, 0, 1 );
128             }
129              
130             # single-line forms of multi-line directives
131             elsif ( $line =~ /^exclude\s+(\S+)\s+(\S+)\s*$/x ) {
132              
133             # single exclude
134 3 100       19 $m->{$1} = [] unless ( defined $m->{exclude}->{$1} );
135 3         6 push @{ $m->{exclude}->{$1} }, $2;
  3         11  
136             }
137             elsif ( $line =~ /^replace\s+(\S+)\s+=>\s+(\S+)\s*$/x ) {
138              
139             # single replace
140             croak "duplicate replace for $1"
141 1 50       8 if ( defined $m->{replace}->{$1} );
142 1         5 $m->{replace}->{$1} = $2;
143             }
144             elsif ( $line =~ /^require\s+(\S+)+\s+(\S+).*$/x ) {
145              
146             # single require
147             croak "duplicate require for $1"
148 1 50       6 if ( defined $m->{'require'}->{$1} );
149 1         5 $m->{'require'}->{$1} = $2;
150             }
151             elsif ( $line =~ /^retract\s+(.+)/x ) {
152              
153             # single retract
154 2         6 my $retract = _parse_retract($1);
155 2 50       6 croak "unparseable retract content: $line"
156             unless ( defined($retract) );
157             croak "duplicate retract for $retract"
158 2 50       8 if ( defined $m->{retract}->{$retract} );
159 2         7 $m->{retract}->{$retract} = 1;
160             }
161             elsif ( $line =~ m{^\s*//.*$}mx ) {
162              
163             # comment - strip
164             # (can also be part of a multi-line retract rationale)
165              
166             }
167             else {
168 6         573 croak "unparseable line content: $line";
169             }
170             }
171              
172 4 100       221 croak 'missing module line' unless ( defined $m->{module} );
173 3 100       126 croak 'missing go line' unless ( defined $m->{go} );
174              
175 2         10 return $m;
176             }
177              
178             # 'private' sub to extract individual retract lines and strip off the rationale comments
179             # see: https://go.dev/ref/mod#go-mod-file-retract
180             #
181             # rationale comments are stripped out
182             #
183             # this sub should only see one line; if a retract rational had multiple lines, like:
184             # retract v1.0.0 // why
185             # // oh why
186             #
187             # then the second comment line is caught by the comment match in the loop of parse_go_mod
188             sub _parse_retract {
189 23   66 23   7187 my $retract = shift || croak 'missing retract string';
190              
191 22 100       170 if ( $retract =~ /^\s*\[(.+?)\](.*)$/x ) { # version-range
    50          
192 11         41 my $range = $1;
193 11         39 my $rest = $2;
194              
195             # trim whitespace from range
196 11         51 $range =~ s/\s+//gx;
197 11         52 my @versions = split( /,/x, $range );
198 11         21 my $count = 0;
199 11         22 for my $version (@versions) {
200 20 100       60 return undef unless ( $version =~ /\S+/x );
201 19         33 $count++;
202             }
203 10 100       45 return undef if ( $count != 2 );
204              
205             # if there is a comment, it must be properly formatted
206 7 100       22 if ( $rest =~ /\S/x ) {
207 5 100       34 return undef unless ( $rest =~ m{^\s+//}ox );
208             }
209 5         31 return '[' . $range . ']';
210             }
211             elsif ( $retract =~ /^\s*(\S+)(.*)$/x ) { # single version
212 11         32 my $version = $1;
213 11         24 my $rest = $2;
214              
215             # if there is a comment, it must be properly formatted
216 11 100       38 if ( $rest =~ /\S/x ) {
217 9 100       62 return undef unless ( $rest =~ m{^\s+//}ox );
218             }
219 5         23 return $version;
220             }
221              
222             # unparseable retract string
223 0           return undef;
224             }
225              
226             1;
227              
228             __END__