File Coverage

lib/App/PRT/Command/RenameClass.pm
Criterion Covered Total %
statement 124 124 100.0
branch 46 52 88.4
condition 3 3 100.0
subroutine 17 17 100.0
pod 0 6 0.0
total 190 202 94.0


line stmt bran cond sub pod time code
1             package App::PRT::Command::RenameClass;
2 3     3   3573 use strict;
  3         6  
  3         91  
3 3     3   16 use warnings;
  3         5  
  3         80  
4 3     3   505 use PPI;
  3         102639  
  3         97  
5 3     3   1141 use App::PRT::Util::DestinationFile;
  3         10  
  3         98  
6 3     3   30 use Path::Class;
  3         6  
  3         3729  
7              
8             sub new {
9 16     16 0 88463 my ($class) = @_;
10 16         77 bless {
11             rule => undef,
12             }, $class;
13             }
14              
15             # parse arguments from CLI
16             # arguments:
17             # @arguments
18             # returns:
19             # @rest_arguments
20             sub parse_arguments {
21 3     3 0 180 my ($self, @arguments) = @_;
22              
23 3 100       31 die "source and destination class are required" unless @arguments >= 2;
24              
25 1         6 $self->register(shift @arguments => shift @arguments);
26              
27 1         4 @arguments;
28             }
29              
30              
31             # register a replacing rule
32             # arguments:
33             # $source: source class name
34             # $dest: destination class name
35             sub register {
36 13     13 0 71 my ($self, $source_class_name, $destination_class_name) = @_;
37              
38 13         42 $self->{source_class_name} = $source_class_name;
39 13         31 $self->{destination_class_name} = $destination_class_name;
40             }
41              
42             sub source_class_name {
43 2241     2241 0 7617 my ($self) = @_;
44              
45 2241         4414 $self->{source_class_name};
46             }
47              
48             sub destination_class_name {
49 67     67 0 97 my ($self) = @_;
50              
51 67         242 $self->{destination_class_name};
52             }
53              
54             # refactor a file
55             # argumensts:
56             # $file: filename for refactoring
57             sub execute {
58 26     26 0 18759 my ($self, $file) = @_;
59              
60 26         44 my $replaced = 0;
61              
62 26         131 my $document = PPI::Document->new($file);
63              
64             # When parse failed
65 26 100       290606 return unless $document;
66              
67 25         69 my $should_rename = $self->_try_rename_package_statement($document);
68              
69 25         197 $replaced += $self->_try_rename_includes($document);
70              
71 25         64 $replaced += $self->_try_rename_parent_class($document);
72              
73 25         74 $replaced += $self->_try_rename_quotes($document);
74              
75 25         56 $replaced += $self->_try_rename_tokens($document);
76              
77 25         58 $replaced += $self->_try_rename_symbols($document);
78              
79 25 100       61 if ($should_rename) {
80 7         18 my $dest_file = App::PRT::Util::DestinationFile::destination_file($self->source_class_name, $self->destination_class_name, $file);
81 7         805 my $dest_dir = file($dest_file)->dir;
82 7         568 $dest_dir->mkpath;
83 7         915 $document->save($dest_file);
84 7         9484 unlink($file);
85 7         77 $dest_file;
86             } else {
87 18 100       61 return unless $replaced;
88 10         35 $document->save($file);
89 10         11831 $file;
90             }
91             }
92              
93             # returns: should rename this document?
94             sub _try_rename_package_statement {
95 25     25   49 my ($self, $document) = @_;
96              
97 25         80 my $package = $document->find_first('PPI::Statement::Package');
98              
99 25 100       19352 return unless $package;
100 19 100       86 return unless $package->namespace eq $self->source_class_name;
101              
102 9         21 my $namespace = $package->schild(1);
103              
104 9 50       139 return unless $namespace->isa('PPI::Token::Word');
105              
106 9         23 $namespace->set_content($self->destination_class_name);
107              
108             # Rename this file when the first significant token is package (heuristic).
109             # This skips comments and POD before the package statement.
110 9         45 return $document->schild(0)->DOES('PPI::Statement::Package');
111             }
112              
113             sub _try_rename_includes {
114 25     25   51 my ($self, $document) = @_;
115              
116 25         35 my $replaced = 0;
117              
118 25         60 my $statements = $document->find('PPI::Statement::Include');
119 25 100       62718 return 0 unless $statements;
120              
121 23         58 for my $statement (@$statements) {
122 59 50       148 next unless defined $statement->module;
123 59 100       1387 next unless $statement->module eq $self->source_class_name;
124              
125 3         10 my $module = $statement->schild(1);
126              
127 3 50       50 return unless $module->isa('PPI::Token::Word');
128              
129 3         9 $module->set_content($self->destination_class_name);
130 3         11 $replaced++;
131             }
132              
133 23         53 $replaced;
134             }
135              
136             sub _try_rename_quotes {
137 25     25   49 my ($self, $document) = @_;
138              
139 25         41 my $replaced = 0;
140              
141 25         56 my $quotes = $document->find('PPI::Token::Quote');
142 25 100       62441 return 0 unless $quotes;
143              
144 21         51 for my $quote (@$quotes) {
145 68 100       160 next unless $quote->string eq $self->source_class_name;
146 10         19 $quote->set_content("'@{[ $self->destination_class_name ]}'");
  10         18  
147              
148 10         35 $replaced++;
149             }
150              
151 21         48 $replaced;
152             }
153              
154             # TODO: too complicated
155             sub _try_rename_parent_class {
156 25     25   54 my ($self, $document) = @_;
157              
158 25         29 my $replaced = 0;
159              
160 25         54 my $includes = $document->find('PPI::Statement::Include');
161 25 100       62731 return 0 unless $includes;
162              
163 23         55 for my $statement (@$includes) {
164 59 50       884 next unless defined $statement->pragma;
165 59 100       1770 next unless $statement->pragma =~ /^parent|base$/; # only 'use parent' and 'use base' are supported
166              
167             # schild(2) is 'Foo' of use parent Foo
168 10         281 my $parent = $statement->schild(2);
169              
170 10 100       200 if ($parent->isa('PPI::Token::Quote')) {
    50          
171             # The 'literal' method is not implemented by ::Quote::Double or ::Quote::Interpolate.
172 6 100       54 my $string = $parent->can('literal') ? $parent->literal : $parent->string;
173              
174 6 100       91 if ($string eq $self->source_class_name) {
175 5         9 $parent->set_content("'@{[ $self->destination_class_name ]}'");
  5         9  
176 5         19 $replaced++;
177             }
178             } elsif ($parent->isa('PPI::Token::QuoteLike::Words')) {
179             # use parent qw(A B C) pattern
180             # literal is array when QuoteLike::Words
181 4         8 my $_replaced = 0;
182             my @new_literal = map {
183 4 100       21 if ($_ eq $self->source_class_name) {
  6         304  
184 1         2 $_replaced++;
185 1         3 $self->destination_class_name;
186             } else {
187 5         14 $_;
188             }
189             } $parent->literal;
190 4 100       16 if ($_replaced) {
191 1         17 $parent->set_content('qw(' . join(' ', @new_literal) . ')');
192 1         5 $replaced++;
193             }
194             }
195             }
196              
197 23         600 $replaced;
198             }
199              
200             # discussions:
201             # seems too wild
202             sub _try_rename_tokens {
203 25     25   49 my ($self, $document) = @_;
204              
205 25         32 my $replaced = 0;
206              
207 25         56 my $tokens = $document->find('PPI::Token');
208 25 50       63576 return 0 unless $tokens;
209              
210 25         66 for my $token (@$tokens) {
211 2057 100       3004 next unless $token->content eq $self->source_class_name;
212 13         27 $token->set_content($self->destination_class_name);
213 13         36 $replaced++;
214             }
215              
216 25         91 $replaced;
217             }
218              
219             # e.g.
220             # $Foo::Bar::GLOBAL_VAR
221             # ~~~~~~~~ Rename here
222             #
223             # $Foo::Bar::Buz::GLOBAL_VAR <= Don't rename because it's not the same class
224             sub _try_rename_symbols {
225 25     25   50 my ($self, $document) = @_;
226              
227 25         84 my $replaced = 0;
228              
229 25         66 my $symbols = $document->find('PPI::Token::Symbol');
230 25 100       63209 return 0 unless $symbols;
231              
232 17         49 my $source_class_name = $self->source_class_name;
233 17         35 my $destination_class_name = $self->destination_class_name;
234              
235 17         39 for my $symbol (@$symbols) {
236 113         243 my $content = $symbol->content;
237 113         374 my $sigil = substr $content, 0, 1, '';
238              
239 113 100 100     454 if ($content =~ s/\A${source_class_name}::// && scalar(split /::/, $content) == 1) {
240 5         33 $symbol->set_content($sigil . $destination_class_name . '::' . $content);
241 5         22 $replaced++;
242             }
243             }
244              
245 17         43 $replaced;
246             }
247              
248             1;
249