File Coverage

File:blib/lib/App/Test/Generator/Model/Method.pm
Coverage:97.8%

linestmtbrancondsubtimecode
1package App::Test::Generator::Model::Method;
2
3
25
25
25
130364
23
324
use strict;
4
25
25
25
41
20
542
use warnings;
5
6
25
25
25
39
23
486
use Carp qw(croak);
7
25
25
25
4049
32987
12471
use Readonly;
8
9Readonly my $HIGH_CONFIDENCE_THRESHOLD   => 40;
10Readonly my $MEDIUM_CONFIDENCE_THRESHOLD => 20;
11
12our $VERSION = '0.36';
13
14 - 18
=head1 VERSION

Version 0.36

=cut
19
20sub new {
21
376
265540
        my ($class, %args) = @_;
22
376
461
        croak 'name required'   unless defined $args{name};
23
374
380
        croak 'source required' unless defined $args{source};
24
25        my $self = {
26                name          => $args{name},
27                source        => $args{source},
28                # parameters    => [],
29
372
872
                evidence      => [],
30                return_type   => undef,
31                classification => undef,
32                confidence    => undef,
33        };
34
35
372
597
        return bless $self, $class;
36}
37
38# Read-only accessors — name and source are immutable after construction
39
9
332
sub name   { $_[0]->{name}   }
40
275
333
sub source { $_[0]->{source} }
41
42sub return_type {
43
26
32
        my ($self, $val) = @_;
44
26
27
        $self->{return_type} = $val if @_ > 1;
45
26
37
        return $self->{return_type};
46}
47
48sub classification {
49
281
319
        my ($self, $val) = @_;
50
281
339
        $self->{classification} = $val if @_ > 1;
51
281
399
        return $self->{classification};
52}
53
54sub confidence {
55
280
245
        my ($self, $val) = @_;
56
280
230
        $self->{confidence} = $val if @_ > 1;
57
280
500
        return $self->{confidence};
58}
59
60sub add_evidence {
61
538
3510
        my ($self, %args) = @_;
62
63        # Validate category — must be one of the three recognised kinds
64
538
1614
874
1540
        my %valid_categories = map { $_ => 1 } qw(return input effect);
65
66
538
635
        my $cat = $args{category} // '';
67
538
510
        croak "Invalid evidence category '$cat'" unless $valid_categories{$cat};
68
69        # Validate signal — must be a known signal name to catch typos early.
70        # Signals are per-category; we validate the full set across all categories.
71
534
5874
532
4668
        my %valid_signals = map { $_ => 1 } qw(
72                returns_property returns_constant returns_self
73                legacy_type context_aware error_pattern
74                input_validated input_typed input_optional
75                has_side_effect no_side_effect
76        );
77
78
534
619
        my $sig = $args{signal} // '';
79
534
499
        croak "Invalid evidence signal '$sig'" unless $valid_signals{$sig};
80
81
530
1814
        push @{ $self->{evidence} }, {
82                category => $args{category},
83                signal   => $args{signal},
84                value    => $args{value},
85
530
310
                weight   => defined $args{weight} ? $args{weight} : 1,
86        };
87}
88
89sub evidence {
90
33
598
        my $self = $_[0];
91
33
33
17
49
        return @{ $self->{evidence} };
92}
93
94sub evidence_ref {
95
4
585
        my $self = $_[0];
96
4
4
        return $self->{evidence};
97}
98
99sub resolve_return_type {
100
296
225
        my $self = $_[0];
101
296
449
        my %score = (property => 0, constant => 0, object => 0);
102
103
296
296
211
324
        for my $ev (@{ $self->{evidence} }) {
104
480
521
                next unless $ev->{category} eq 'return';
105
476
717
                if($ev->{signal} eq 'returns_property') {
106
32
32
                        $score{property} += $ev->{weight};
107                } elsif($ev->{signal} eq 'returns_constant') {
108
152
159
                        $score{constant} += $ev->{weight};
109                } elsif($ev->{signal} eq 'returns_self') {
110
21
16
                        $score{object} += $ev->{weight};
111                } elsif($ev->{signal} eq 'legacy_type') {
112                        # Legacy type hint — map to nearest score bucket if recognisable
113
251
327
                        my $t = $ev->{value} // '';
114
251
24
289
30
                        if($t eq 'object')   { $score{object}   += $ev->{weight} }
115
1
1
                        elsif($t eq 'self')  { $score{object}   += $ev->{weight} }
116
226
251
                        else                 { $score{property} += $ev->{weight} }
117                } elsif($ev->{signal} eq 'context_aware') {
118                        # Context-aware return suggests getter behaviour
119
6
5
                        $score{property} += $ev->{weight};
120                } elsif($ev->{signal} eq 'error_pattern') {
121                        # Error pattern return doesn't strongly imply a type —
122                        # give a small nudge toward property (scalar return)
123
14
13
                        $score{property} += $ev->{weight};
124                }
125                # Unknown signals are ignored — they may be used by external consumers
126        }
127
128        # Tie-break alphabetically — deterministic but arbitrary
129
296
801
548
1612
        my ($winner) = sort { ($score{$b} || 0) <=> ($score{$a} || 0) || $a cmp $b } keys %score;
130
131
296
378
        $self->{return_type} = $winner || 'unknown';
132
296
410
        return $self->{return_type};
133}
134
135sub resolve_confidence {
136
280
195
        my $self = $_[0];
137
138
280
202
        my $total = 0;
139
280
280
222
447
        $total += $_->{weight} for @{ $self->{evidence} };
140
141
280
379
        my $level = $total >= $HIGH_CONFIDENCE_THRESHOLD ? 'high' : $total >= $MEDIUM_CONFIDENCE_THRESHOLD ? 'medium' : 'low';
142
143
280
1239
        $self->{confidence} = { score => $total, level => $level };
144
145
280
279
        return $self->{confidence};
146}
147
148sub resolve_classification {
149
279
202
        my $self = $_[0];
150
151        # Return_type must be resolved before classification can be determined
152
279
313
        $self->resolve_return_type() unless defined $self->{return_type};
153
154
279
399
        if($self->{return_type} eq 'object') {
155
28
31
                $self->{classification} = 'chainable';
156        } elsif ($self->{return_type} eq 'property') {
157
230
230
                $self->{classification} = 'getter';
158        } elsif ($self->{return_type} eq 'constant') {
159
19
19
                $self->{classification} = 'constant';
160        } else {
161
2
2
                $self->{classification} = 'unknown';
162        }
163
164
279
252
        return $self->{classification};
165}
166
167sub absorb_legacy_output {
168
285
332
        my ($self, $output) = @_;
169
170
285
508
        return unless $output && ref $output eq 'HASH';
171
172
281
317
        if ($output->{type}) {
173                $self->add_evidence(
174                        category => 'return',
175                        signal   => 'legacy_type',
176                        value    => $output->{type},
177
252
337
                        weight   => 20,
178                );
179        }
180
181
281
315
        if ($output->{_returns_self}) {
182
9
7
                $self->add_evidence(
183                        category => 'return',
184                        signal   => 'returns_self',
185                        weight   => 25,
186                );
187        }
188
189
281
305
        if ($output->{_context_aware}) {
190
9
10
                $self->add_evidence(
191                        category => 'return',
192                        signal   => 'context_aware',
193                        weight   => 15,
194                );
195        }
196
197
281
359
        if ($output->{_error_return}) {
198                $self->add_evidence(
199                        category => 'return',
200                        signal   => 'error_pattern',
201                        value    => $output->{_error_return},
202
17
16
                        weight   => 15,
203                );
204        }
205}
206
2071;