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