File Coverage

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

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