File Coverage

File:blib/lib/App/GHGen/Detector.pm
Coverage:75.5%

linestmtbrancondsubtimecode
1package App::GHGen::Detector;
2
3
1
1
123701
2
use v5.36;
4
1
1
1
2
1
16
use strict;
5
1
1
1
2
0
17
use warnings;
6
7
1
1
1
2
0
24
use Path::Tiny;
8
9
1
1
1
2
0
697
use Exporter 'import';
10our @EXPORT_OK = qw(
11        detect_project_type
12        get_project_indicators
13);
14
15our $VERSION = '0.03';
16
17 - 35
=head1 NAME

App::GHGen::Detector - Detect project type from repository contents

=head1 SYNOPSIS

        use App::GHGen::Detector qw(detect_project_type);

        my $type = detect_project_type();
        # Returns: 'perl', 'node', 'python', etc.

=head1 FUNCTIONS

=head2 detect_project_type()

Detect the project type by examining files in the current directory.
Returns the detected type string or undef if unable to detect.

=cut
36
37
7
7
6251
6
sub detect_project_type() {
38
7
4
        my @detections;
39
40        # Check for each project type
41
7
6
        for my $type (qw(perl node python rust go ruby php java cpp docker)) {
42
70
47
                my $detector = "_detect_$type";
43
70
125
                my $score = __PACKAGE__->can($detector)->();
44
70
74
                push @detections, { type => $type, score => $score, indicators => [] } if $score > 0;
45        }
46
47        # Sort by score (highest first)
48
7
1
8
2
        @detections = sort { $b->{score} <=> $a->{score} } @detections;
49
50
7
7
        return undef unless @detections;
51
6
14
        return wantarray ? @detections : $detections[0]->{type};
52}
53
54 - 58
=head2 get_project_indicators($type)

Get a list of indicators (files/patterns) that suggest a project type.

=cut
59
60
2
2
2
480
2
1
sub get_project_indicators($type = undef) {
61
2
11
        my %indicators = (
62                perl => [
63                        'cpanfile', 'dist.ini', 'Makefile.PL', 'Build.PL',
64                        'lib/*.pm', 't/*.t', 'META.json', 'META.yml'
65                ],
66                node => [
67                        'package.json', 'package-lock.json', 'yarn.lock',
68                        'node_modules/', 'tsconfig.json', '.npmrc'
69                ],
70                python => [
71                        'requirements.txt', 'setup.py', 'pyproject.toml',
72                        'Pipfile', 'poetry.lock', 'setup.cfg', 'tox.ini'
73                ],
74                rust => [
75                        'Cargo.toml', 'Cargo.lock', 'src/main.rs',
76                        'src/lib.rs', 'rust-toolchain.toml'
77                ],
78                go => [
79                        'go.mod', 'go.sum', 'main.go', '*.go'
80                ],
81                ruby => [
82                        'Gemfile', 'Gemfile.lock', 'Rakefile',
83                        '.ruby-version', 'config.ru'
84                ],
85                php => [
86                        'composer.json', 'composer.lock', 'phpunit.xml',
87                        'phpunit.xml.dist', 'src/', 'tests/'
88                ],
89                java => [
90                        'pom.xml', 'build.gradle', 'build.gradle.kts',
91                        'gradlew', 'mvnw', 'src/main/java/'
92                ],
93                cpp => [
94                        'CMakeLists.txt', 'Makefile', 'configure.ac',
95                        '*.cpp', '*.hpp', '*.cc', '*.h'
96                ],
97                docker => [
98                        'Dockerfile', 'docker-compose.yml', 'docker-compose.yaml',
99                        '.dockerignore'
100                ],
101        );
102
103
2
6
        return $type ? $indicators{$type} : \%indicators;
104}
105
106# Detection functions - return a score (0 = not detected, higher = more confident)
107
108
7
7
6
3
sub _detect_perl() {
109
7
5
        my $score = 0;
110
111        # Strong indicators
112
7
7
        $score += 10 if path('cpanfile')->exists;
113
7
130
        $score += 10 if path('dist.ini')->exists;
114
7
95
        $score += 8 if path('Makefile.PL')->exists;
115
7
90
        $score += 8  if path('Build.PL')->exists;
116
117        # Medium indicators
118
7
89
        $score += 5  if path('META.json')->exists;
119
7
120
        $score += 5  if path('META.yml')->exists;
120
121        # Weak indicators
122
7
98
        $score += 3  if path('lib')->exists && path('lib')->is_dir;
123
7
108
        $score += 2  if path('t')->exists && path('t')->is_dir;
124
125        # Check for .pm files in lib/
126
7
122
        if (path('lib')->exists) {
127
1
16
                my @pm_files = path('lib')->children(qr/\.pm$/);
128
1
55
                $score += 2 if @pm_files > 0;
129        }
130
131        # Check for .t files in t/
132
7
73
        if (path('t')->exists) {
133
0
0
                my @t_files = path('t')->children(qr/\.t$/);
134
0
0
                $score += 1 if @t_files > 0;
135        }
136
137
7
89
        return $score;
138}
139
140
7
7
5
3
sub _detect_node() {
141
7
8
        my $score = 0;
142
143        # Strong indicators
144
7
4
        $score += 15 if path('package.json')->exists;
145
7
94
        $score += 8  if path('package-lock.json')->exists;
146
7
90
        $score += 8  if path('yarn.lock')->exists;
147
7
86
        $score += 7  if path('pnpm-lock.yaml')->exists;
148
149        # Medium indicators
150
7
85
        $score += 5  if path('node_modules')->exists && path('node_modules')->is_dir;
151
7
88
        $score += 4  if path('tsconfig.json')->exists;
152
7
87
        $score += 3  if path('.npmrc')->exists;
153
154        # Weak indicators
155
7
87
        $score += 2  if path('src')->exists && path('src')->is_dir;
156
157
7
87
        return $score;
158}
159
160
7
7
5
1
sub _detect_python() {
161
7
6
        my $score = 0;
162
163        # Strong indicators
164
7
9
        $score += 12 if path('requirements.txt')->exists;
165
7
92
        $score += 12 if path('setup.py')->exists;
166
7
88
        $score += 12 if path('pyproject.toml')->exists;
167
7
87
        $score += 10 if path('Pipfile')->exists;
168
7
86
        $score += 8  if path('poetry.lock')->exists;
169
170        # Medium indicators
171
7
87
        $score += 5  if path('setup.cfg')->exists;
172
7
84
        $score += 4  if path('tox.ini')->exists;
173
7
108
        $score += 3  if path('.python-version')->exists;
174
175        # Weak indicators
176
7
90
        $score += 2  if path('venv')->exists || path('.venv')->exists;
177
178        # Check for .py files
179
7
181
        my @py_files = path('.')->children(qr/\.py$/);
180
7
319
        $score += 2 if @py_files > 0;
181
182
7
5
        return $score;
183}
184
185
7
7
4
3
sub _detect_rust() {
186
7
4
        my $score = 0;
187
188        # Strong indicators
189
7
6
        $score += 15 if path('Cargo.toml')->exists;
190
7
119
        $score += 10 if path('Cargo.lock')->exists;
191
192        # Medium indicators
193
7
91
        $score += 5  if path('src/main.rs')->exists;
194
7
85
        $score += 5  if path('src/lib.rs')->exists;
195
7
83
        $score += 3  if path('rust-toolchain.toml')->exists || path('rust-toolchain')->exists;
196
197        # Weak indicators
198
7
174
        $score += 2  if path('target')->exists && path('target')->is_dir;
199
200
7
88
        return $score;
201}
202
203
7
7
4
3
sub _detect_go() {
204
7
4
        my $score = 0;
205
206        # Strong indicators
207
7
4
        $score += 15 if path('go.mod')->exists;
208
7
90
        $score += 10 if path('go.sum')->exists;
209
210        # Medium indicators
211
7
92
        $score += 5  if path('main.go')->exists;
212
213        # Check for .go files
214
7
83
        my @go_files = path('.')->children(qr/\.go$/);
215
7
213
        $score += 3 if @go_files > 0;
216
7
5
        $score += 1 if @go_files > 3;
217
218
7
4
        return $score;
219}
220
221
7
7
4
3
sub _detect_ruby() {
222
7
5
        my $score = 0;
223
224        # Strong indicators
225
7
5
        $score += 15 if path('Gemfile')->exists;
226
7
102
        $score += 10 if path('Gemfile.lock')->exists;
227
228        # Medium indicators
229
7
88
        $score += 5  if path('Rakefile')->exists;
230
7
87
        $score += 4  if path('.ruby-version')->exists;
231
7
86
        $score += 3  if path('config.ru')->exists;
232
233        # Check for .rb files
234
7
86
        my @rb_files = path('.')->children(qr/\.rb$/);
235
7
233
        $score += 2 if @rb_files > 0;
236
237
7
3
        return $score;
238}
239
240
7
7
3
5
sub _detect_docker() {
241
7
2
        my $score = 0;
242
243        # Strong indicators
244
7
5
        $score += 12 if path('Dockerfile')->exists;
245
7
90
        $score += 8  if path('docker-compose.yml')->exists;
246
7
86
        $score += 8  if path('docker-compose.yaml')->exists;
247
248        # Medium indicators
249
7
88
        $score += 3  if path('.dockerignore')->exists;
250
251
7
84
        return $score;
252}
253
254
7
7
6
2
sub _detect_php() {
255
7
5
        my $score = 0;
256
257        # Strong indicators
258
7
3
        $score += 15 if path('composer.json')->exists;
259
7
102
        $score += 10 if path('composer.lock')->exists;
260
261        # Medium indicators
262
7
87
        $score += 5  if path('phpunit.xml')->exists;
263
7
85
        $score += 5  if path('phpunit.xml.dist')->exists;
264
7
87
        $score += 3  if path('.php-version')->exists;
265
266        # Weak indicators
267
7
85
        $score += 2  if path('src')->exists && path('src')->is_dir;
268
7
86
        $score += 2  if path('tests')->exists && path('tests')->is_dir;
269
270        # Check for .php files
271
7
86
        my @php_files = path('.')->children(qr/\.php$/);
272
7
220
        $score += 2 if @php_files > 0;
273
274
7
3
        return $score;
275}
276
277
7
7
5
3
sub _detect_java() {
278
7
4
        my $score = 0;
279
280        # Strong indicators
281
7
4
        $score += 15 if path('pom.xml')->exists;
282
7
101
        $score += 15 if path('build.gradle')->exists;
283
7
86
        $score += 15 if path('build.gradle.kts')->exists;
284
285        # Medium indicators
286
7
126
        $score += 5  if path('gradlew')->exists;
287
7
99
        $score += 5  if path('mvnw')->exists;
288
7
102
        $score += 4  if path('settings.gradle')->exists;
289
7
86
        $score += 4  if path('settings.gradle.kts')->exists;
290
291        # Weak indicators
292
7
85
        $score += 3  if path('src/main/java')->exists;
293
7
83
        $score += 2  if path('src/test/java')->exists;
294
295        # Check for .java files
296
7
83
        my @java_files = path('.')->children(qr/\.java$/);
297
7
207
        $score += 2 if @java_files > 0;
298
299
7
5
        return $score;
300}
301
302
7
7
6
7
sub _detect_cpp() {
303
7
5
        my $score = 0;
304
305        # Strong indicators
306
7
5
        $score += 12 if path('CMakeLists.txt')->exists;
307
7
100
        $score += 8  if path('Makefile')->exists;
308
7
86
        $score += 6  if path('configure.ac')->exists;
309
7
87
        $score += 6  if path('configure')->exists;
310
311        # Medium indicators
312
7
85
        $score += 4  if path('meson.build')->exists;
313
7
85
        $score += 3  if path('.clang-format')->exists;
314
315        # Check for C++ files
316
7
85
        my @cpp_files = path('.')->children(qr/\.(cpp|cc|cxx|hpp|hxx|h)$/);
317
7
221
        $score += 3 if @cpp_files > 0;
318
7
5
        $score += 2 if @cpp_files > 5;
319
320        # Check for include directory
321
7
6
        $score += 2 if path('include')->exists && path('include')->is_dir;
322
323
7
100
        return $score;
324}
325
326 - 337
=head1 AUTHOR

Nigel Horne E<lt>njh@nigelhorne.comE<gt>

L<https://github.com/nigelhorne>

=head1 LICENSE

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut
338
3391;