| File: | blib/lib/App/GHGen/Detector.pm |
| Coverage: | 75.5% |
| line | stmt | bran | cond | sub | time | code |
|---|---|---|---|---|---|---|
| 1 | package 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'; | |||
| 10 | our @EXPORT_OK = qw( | |||||
| 11 | detect_project_type | |||||
| 12 | get_project_indicators | |||||
| 13 | ); | |||||
| 14 | ||||||
| 15 | our $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 | ||||||
| 339 | 1; | |||||