File Coverage

blib/lib/App/GHGen/Detector.pm
Criterion Covered Total %
statement 154 156 98.7
branch 95 164 57.9
condition 11 30 36.6
subroutine 17 17 100.0
pod 2 2 100.0
total 279 369 75.6


line stmt bran cond sub pod time code
1             package App::GHGen::Detector;
2              
3 1     1   394655 use v5.36;
  1         4  
4 1     1   6 use strict;
  1         2  
  1         33  
5 1     1   5 use warnings;
  1         1  
  1         80  
6              
7 1     1   6 use Path::Tiny;
  1         2  
  1         84  
8              
9 1     1   6 use Exporter 'import';
  1         2  
  1         2302  
10             our @EXPORT_OK = qw(
11             detect_project_type
12             get_project_indicators
13             );
14              
15             our $VERSION = '0.03';
16              
17             =head1 NAME
18              
19             App::GHGen::Detector - Detect project type from repository contents
20              
21             =head1 SYNOPSIS
22              
23             use App::GHGen::Detector qw(detect_project_type);
24              
25             my $type = detect_project_type();
26             # Returns: 'perl', 'node', 'python', etc.
27              
28             =head1 FUNCTIONS
29              
30             =head2 detect_project_type()
31              
32             Detect the project type by examining files in the current directory.
33             Returns the detected type string or undef if unable to detect.
34              
35             =cut
36              
37 7     7 1 19936 sub detect_project_type() {
  7         13  
38 7         14 my @detections;
39              
40             # Check for each project type
41 7         24 for my $type (qw(perl node python rust go ruby php java cpp docker)) {
42 70         157 my $detector = "_detect_$type";
43 70         544 my $score = __PACKAGE__->can($detector)->();
44 70 100       292 push @detections, { type => $type, score => $score, indicators => [] } if $score > 0;
45             }
46              
47             # Sort by score (highest first)
48 7         23 @detections = sort { $b->{score} <=> $a->{score} } @detections;
  1         5  
49              
50 7 100       22 return undef unless @detections;
51 6 100       47 return wantarray ? @detections : $detections[0]->{type};
52             }
53              
54             =head2 get_project_indicators($type)
55              
56             Get a list of indicators (files/patterns) that suggest a project type.
57              
58             =cut
59              
60 2     2 1 1517 sub get_project_indicators($type = undef) {
  2         6  
  2         4  
61 2         34 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 100       18 return $type ? $indicators{$type} : \%indicators;
104             }
105              
106             # Detection functions - return a score (0 = not detected, higher = more confident)
107              
108 7     7   16 sub _detect_perl() {
  7         11  
109 7         14 my $score = 0;
110              
111             # Strong indicators
112 7 100       25 $score += 10 if path('cpanfile')->exists;
113 7 50       493 $score += 10 if path('dist.ini')->exists;
114 7 50       379 $score += 8 if path('Makefile.PL')->exists;
115 7 50       323 $score += 8 if path('Build.PL')->exists;
116              
117             # Medium indicators
118 7 50       309 $score += 5 if path('META.json')->exists;
119 7 50       353 $score += 5 if path('META.yml')->exists;
120              
121             # Weak indicators
122 7 100 66     297 $score += 3 if path('lib')->exists && path('lib')->is_dir;
123 7 50 33     392 $score += 2 if path('t')->exists && path('t')->is_dir;
124              
125             # Check for .pm files in lib/
126 7 100       332 if (path('lib')->exists) {
127 1         54 my @pm_files = path('lib')->children(qr/\.pm$/);
128 1 50       213 $score += 2 if @pm_files > 0;
129             }
130              
131             # Check for .t files in t/
132 7 50       245 if (path('t')->exists) {
133 0         0 my @t_files = path('t')->children(qr/\.t$/);
134 0 0       0 $score += 1 if @t_files > 0;
135             }
136              
137 7         306 return $score;
138             }
139              
140 7     7   15 sub _detect_node() {
  7         11  
141 7         24 my $score = 0;
142              
143             # Strong indicators
144 7 100       17 $score += 15 if path('package.json')->exists;
145 7 50       416 $score += 8 if path('package-lock.json')->exists;
146 7 50       306 $score += 8 if path('yarn.lock')->exists;
147 7 50       311 $score += 7 if path('pnpm-lock.yaml')->exists;
148              
149             # Medium indicators
150 7 50 33     384 $score += 5 if path('node_modules')->exists && path('node_modules')->is_dir;
151 7 50       405 $score += 4 if path('tsconfig.json')->exists;
152 7 50       301 $score += 3 if path('.npmrc')->exists;
153              
154             # Weak indicators
155 7 50 33     345 $score += 2 if path('src')->exists && path('src')->is_dir;
156              
157 7         315 return $score;
158             }
159              
160 7     7   16 sub _detect_python() {
  7         13  
161 7         12 my $score = 0;
162              
163             # Strong indicators
164 7 100       19 $score += 12 if path('requirements.txt')->exists;
165 7 100       374 $score += 12 if path('setup.py')->exists;
166 7 50       346 $score += 12 if path('pyproject.toml')->exists;
167 7 50       354 $score += 10 if path('Pipfile')->exists;
168 7 50       343 $score += 8 if path('poetry.lock')->exists;
169              
170             # Medium indicators
171 7 50       304 $score += 5 if path('setup.cfg')->exists;
172 7 50       297 $score += 4 if path('tox.ini')->exists;
173 7 50       303 $score += 3 if path('.python-version')->exists;
174              
175             # Weak indicators
176 7 50 33     302 $score += 2 if path('venv')->exists || path('.venv')->exists;
177              
178             # Check for .py files
179 7         672 my @py_files = path('.')->children(qr/\.py$/);
180 7 100       1124 $score += 2 if @py_files > 0;
181              
182 7         18 return $score;
183             }
184              
185 7     7   13 sub _detect_rust() {
  7         10  
186 7         14 my $score = 0;
187              
188             # Strong indicators
189 7 100       21 $score += 15 if path('Cargo.toml')->exists;
190 7 50       432 $score += 10 if path('Cargo.lock')->exists;
191              
192             # Medium indicators
193 7 50       319 $score += 5 if path('src/main.rs')->exists;
194 7 50       331 $score += 5 if path('src/lib.rs')->exists;
195 7 50 33     280 $score += 3 if path('rust-toolchain.toml')->exists || path('rust-toolchain')->exists;
196              
197             # Weak indicators
198 7 50 33     777 $score += 2 if path('target')->exists && path('target')->is_dir;
199              
200 7         318 return $score;
201             }
202              
203 7     7   14 sub _detect_go() {
  7         11  
204 7         16 my $score = 0;
205              
206             # Strong indicators
207 7 100       19 $score += 15 if path('go.mod')->exists;
208 7 50       340 $score += 10 if path('go.sum')->exists;
209              
210             # Medium indicators
211 7 50       406 $score += 5 if path('main.go')->exists;
212              
213             # Check for .go files
214 7         313 my @go_files = path('.')->children(qr/\.go$/);
215 7 50       953 $score += 3 if @go_files > 0;
216 7 50       20 $score += 1 if @go_files > 3;
217              
218 7         19 return $score;
219             }
220              
221 7     7   15 sub _detect_ruby() {
  7         52  
222 7         13 my $score = 0;
223              
224             # Strong indicators
225 7 50       22 $score += 15 if path('Gemfile')->exists;
226 7 50       392 $score += 10 if path('Gemfile.lock')->exists;
227              
228             # Medium indicators
229 7 50       321 $score += 5 if path('Rakefile')->exists;
230 7 50       319 $score += 4 if path('.ruby-version')->exists;
231 7 50       343 $score += 3 if path('config.ru')->exists;
232              
233             # Check for .rb files
234 7         306 my @rb_files = path('.')->children(qr/\.rb$/);
235 7 50       821 $score += 2 if @rb_files > 0;
236              
237 7         15 return $score;
238             }
239              
240 7     7   13 sub _detect_docker() {
  7         13  
241 7         10 my $score = 0;
242              
243             # Strong indicators
244 7 100       20 $score += 12 if path('Dockerfile')->exists;
245 7 50       369 $score += 8 if path('docker-compose.yml')->exists;
246 7 50       398 $score += 8 if path('docker-compose.yaml')->exists;
247              
248             # Medium indicators
249 7 50       353 $score += 3 if path('.dockerignore')->exists;
250              
251 7         290 return $score;
252             }
253              
254 7     7   22 sub _detect_php() {
  7         12  
255 7         15 my $score = 0;
256              
257             # Strong indicators
258 7 50       18 $score += 15 if path('composer.json')->exists;
259 7 50       407 $score += 10 if path('composer.lock')->exists;
260              
261             # Medium indicators
262 7 50       341 $score += 5 if path('phpunit.xml')->exists;
263 7 50       353 $score += 5 if path('phpunit.xml.dist')->exists;
264 7 50       332 $score += 3 if path('.php-version')->exists;
265              
266             # Weak indicators
267 7 50 33     291 $score += 2 if path('src')->exists && path('src')->is_dir;
268 7 50 33     312 $score += 2 if path('tests')->exists && path('tests')->is_dir;
269              
270             # Check for .php files
271 7         357 my @php_files = path('.')->children(qr/\.php$/);
272 7 50       920 $score += 2 if @php_files > 0;
273              
274 7         17 return $score;
275             }
276              
277 7     7   29 sub _detect_java() {
  7         11  
278 7         14 my $score = 0;
279              
280             # Strong indicators
281 7 50       22 $score += 15 if path('pom.xml')->exists;
282 7 50       379 $score += 15 if path('build.gradle')->exists;
283 7 50       381 $score += 15 if path('build.gradle.kts')->exists;
284              
285             # Medium indicators
286 7 50       328 $score += 5 if path('gradlew')->exists;
287 7 50       308 $score += 5 if path('mvnw')->exists;
288 7 50       303 $score += 4 if path('settings.gradle')->exists;
289 7 50       358 $score += 4 if path('settings.gradle.kts')->exists;
290              
291             # Weak indicators
292 7 50       299 $score += 3 if path('src/main/java')->exists;
293 7 50       294 $score += 2 if path('src/test/java')->exists;
294              
295             # Check for .java files
296 7         274 my @java_files = path('.')->children(qr/\.java$/);
297 7 50       872 $score += 2 if @java_files > 0;
298              
299 7         18 return $score;
300             }
301              
302 7     7   36 sub _detect_cpp() {
  7         12  
303 7         12 my $score = 0;
304              
305             # Strong indicators
306 7 50       22 $score += 12 if path('CMakeLists.txt')->exists;
307 7 50       422 $score += 8 if path('Makefile')->exists;
308 7 50       304 $score += 6 if path('configure.ac')->exists;
309 7 50       344 $score += 6 if path('configure')->exists;
310              
311             # Medium indicators
312 7 50       320 $score += 4 if path('meson.build')->exists;
313 7 50       292 $score += 3 if path('.clang-format')->exists;
314              
315             # Check for C++ files
316 7         291 my @cpp_files = path('.')->children(qr/\.(cpp|cc|cxx|hpp|hxx|h)$/);
317 7 50       931 $score += 3 if @cpp_files > 0;
318 7 50       18 $score += 2 if @cpp_files > 5;
319              
320             # Check for include directory
321 7 50 33     24 $score += 2 if path('include')->exists && path('include')->is_dir;
322              
323 7         531 return $score;
324             }
325              
326             =head1 AUTHOR
327              
328             Nigel Horne Enjh@nigelhorne.comE
329              
330             L
331              
332             =head1 LICENSE
333              
334             This is free software; you can redistribute it and/or modify it under
335             the same terms as the Perl 5 programming language system itself.
336              
337             =cut
338              
339             1;