332 lines
11 KiB
Python
332 lines
11 KiB
Python
"""Project type detection from directory structure."""
|
|
import os
|
|
from pathlib import Path
|
|
from typing import Dict, List, Optional, Set
|
|
|
|
|
|
class ProjectDetector:
|
|
"""Detect project type from directory structure and files."""
|
|
|
|
PATTERNS: Dict[str, List[str]] = {
|
|
"python": [
|
|
"requirements.txt",
|
|
"setup.py",
|
|
"setup.cfg",
|
|
"pyproject.toml",
|
|
"Pipfile",
|
|
"pyvenv.cfg",
|
|
"__pycache__",
|
|
],
|
|
"javascript": [
|
|
"package.json",
|
|
"package-lock.json",
|
|
"yarn.lock",
|
|
"npm-debug.log",
|
|
"node_modules",
|
|
".npmrc",
|
|
],
|
|
"typescript": [
|
|
"tsconfig.json",
|
|
"tsconfig.app.json",
|
|
"tsconfig.spec.json",
|
|
"package.json",
|
|
],
|
|
"java": [
|
|
"pom.xml",
|
|
"build.gradle",
|
|
"settings.gradle",
|
|
"gradlew",
|
|
"mvnw",
|
|
".mvn",
|
|
],
|
|
"go": ["go.mod", "go.sum", "Gopkg.toml", "Gopkg.lock", "main.go"],
|
|
"rust": ["Cargo.toml", "Cargo.lock", "rust-toolchain", "src/main.rs"],
|
|
"dotnet": [
|
|
"*.csproj",
|
|
"*.fsproj",
|
|
"*.vbproj",
|
|
"*.sln",
|
|
"*.cshtml",
|
|
],
|
|
"php": [
|
|
"composer.json",
|
|
"composer.lock",
|
|
"wp-config.php",
|
|
"vendor",
|
|
"index.php",
|
|
],
|
|
"ruby": ["Gemfile", "Gemfile.lock", "Rakefile", "config.ru"],
|
|
"django": ["manage.py", "django-admin.py", "settings.py", "wsgi.py"],
|
|
"flask": ["app.py", "wsgi.py", "application.py"],
|
|
"react": ["package.json", "react.config.js", "vite.config.ts"],
|
|
"vue": ["package.json", "vue.config.js", "vite.config.ts"],
|
|
"angular": [
|
|
"angular.json",
|
|
"tsconfig.json",
|
|
"karma.conf.js",
|
|
"package.json",
|
|
],
|
|
"rails": [
|
|
"config.ru",
|
|
"Gemfile",
|
|
"app/controllers",
|
|
"app/models",
|
|
"app/views",
|
|
],
|
|
"laravel": [
|
|
"artisan",
|
|
"composer.json",
|
|
"bootstrap/app.php",
|
|
"config/app.php",
|
|
],
|
|
"spring": [
|
|
"pom.xml",
|
|
"build.gradle",
|
|
"src/main/java",
|
|
"src/main/resources",
|
|
],
|
|
"vscode": [".vscode", ".vscode/settings.json"],
|
|
"jetbrains": [
|
|
".idea",
|
|
"*.iml",
|
|
"*.ipr",
|
|
"*.iws",
|
|
".idea_modules",
|
|
],
|
|
"visualstudiocode": [
|
|
".vscode",
|
|
".vscode/extensions.json",
|
|
".vscode/settings.json",
|
|
],
|
|
"linux": [".linux", "linux"],
|
|
"macos": [".DS_Store", "macos"],
|
|
"windows": ["Thumbs.db", "desktop.ini", "windows"],
|
|
"docker": [
|
|
"Dockerfile",
|
|
"docker-compose.yml",
|
|
"docker-compose.yaml",
|
|
".docker",
|
|
],
|
|
"gradle": [
|
|
"build.gradle",
|
|
"build.gradle.kts",
|
|
"gradle.properties",
|
|
"gradlew",
|
|
"gradlew.bat",
|
|
],
|
|
"maven": ["pom.xml", ".mvn", "mvnw", "mvnw.cmd"],
|
|
"jupyter": [
|
|
"*.ipynb",
|
|
".ipynb_checkpoints",
|
|
"jupyter_notebook_config.py",
|
|
],
|
|
"terraform": ["*.tf", "*.tfvars", "terraform.tfstate", ".terraform"],
|
|
}
|
|
|
|
FRAMEWORK_INDICATORS: Dict[str, List[str]] = {
|
|
"django": ["Django", "django"],
|
|
"flask": ["Flask", "flask"],
|
|
"react": ["react", "react-dom", "create-react-app"],
|
|
"vue": ["vue", "@vue"],
|
|
"angular": ["@angular/core", "angular"],
|
|
"rails": ["Rails", "rails"],
|
|
"laravel": ["Laravel", "laravel/framework"],
|
|
"spring": ["org.springframework", "spring"],
|
|
}
|
|
|
|
def __init__(self, path: Path) -> None:
|
|
"""Initialize detector with path to scan."""
|
|
self.path = path
|
|
self.detected_types: Set[str] = set()
|
|
|
|
def _file_exists_in_tree(
|
|
self, filename: str, path: Optional[Path] = None
|
|
) -> bool:
|
|
"""Check if file exists in directory tree."""
|
|
search_path = path or self.path
|
|
if search_path.is_file():
|
|
return search_path.name == filename
|
|
for root, dirs, files in os.walk(search_path):
|
|
if filename in files:
|
|
return True
|
|
for d in dirs[:]:
|
|
if d == "__pycache__" or d == "node_modules":
|
|
dirs.remove(d)
|
|
return False
|
|
|
|
def _file_exists_in_root(self, filename: str) -> bool:
|
|
"""Check if file exists in root directory."""
|
|
return (self.path / filename).exists()
|
|
|
|
def _scan_package_json(self) -> Optional[Set[str]]:
|
|
"""Scan package.json for framework indicators."""
|
|
package_json = self.path / "package.json"
|
|
if package_json.exists():
|
|
try:
|
|
import json
|
|
|
|
with open(package_json) as f:
|
|
data = json.load(f)
|
|
deps = set()
|
|
if "dependencies" in data:
|
|
deps.update(data["dependencies"].keys())
|
|
if "devDependencies" in data:
|
|
deps.update(data["devDependencies"].keys())
|
|
|
|
for framework, indicators in self.FRAMEWORK_INDICATORS.items():
|
|
if framework in ["react", "vue", "angular"]:
|
|
for indicator in indicators:
|
|
if indicator in deps:
|
|
self.detected_types.add(framework)
|
|
except (json.JSONDecodeError, OSError):
|
|
pass
|
|
return None
|
|
|
|
def _scan_requirements_txt(self) -> None:
|
|
"""Scan requirements.txt for framework indicators."""
|
|
req_file = self.path / "requirements.txt"
|
|
if req_file.exists():
|
|
try:
|
|
with open(req_file) as f:
|
|
content = f.read().lower()
|
|
for framework, indicators in self.FRAMEWORK_INDICATORS.items():
|
|
if framework in ["django", "flask"]:
|
|
for indicator in indicators:
|
|
if indicator.lower() in content:
|
|
self.detected_types.add(framework)
|
|
break
|
|
except OSError:
|
|
pass
|
|
|
|
def _scan_build_files(self) -> None:
|
|
"""Scan build files for framework indicators."""
|
|
for root, dirs, files in os.walk(self.path):
|
|
dirs[:] = [
|
|
d
|
|
for d in dirs
|
|
if d not in ["__pycache__", "node_modules", ".git"]
|
|
]
|
|
for filename in files:
|
|
if filename in ["pom.xml", "build.gradle"]:
|
|
filepath = Path(root) / filename
|
|
try:
|
|
with open(filepath) as f:
|
|
content = f.read()
|
|
for framework, indicators in self.FRAMEWORK_INDICATORS.items():
|
|
if framework == "spring":
|
|
for indicator in indicators:
|
|
if indicator in content:
|
|
self.detected_types.add(framework)
|
|
break
|
|
except OSError:
|
|
pass
|
|
|
|
def detect(self) -> List[str]:
|
|
"""Detect project types from directory structure."""
|
|
self.detected_types = set()
|
|
|
|
for project_type, patterns in self.PATTERNS.items():
|
|
for pattern in patterns:
|
|
if "*" in pattern:
|
|
import fnmatch
|
|
|
|
for root, dirs, files in os.walk(self.path):
|
|
dirs[:] = [
|
|
d
|
|
for d in dirs
|
|
if d
|
|
not in [
|
|
"__pycache__",
|
|
"node_modules",
|
|
".git",
|
|
".idea",
|
|
".vscode",
|
|
]
|
|
]
|
|
for filename in files:
|
|
if fnmatch.fnmatch(filename, pattern):
|
|
if project_type in [
|
|
"dotnet",
|
|
"jetbrains",
|
|
]:
|
|
self.detected_types.add(project_type)
|
|
break
|
|
elif "/" not in pattern:
|
|
if self._file_exists_in_root(pattern):
|
|
if project_type in [
|
|
"python",
|
|
"javascript",
|
|
"typescript",
|
|
"java",
|
|
"go",
|
|
"rust",
|
|
"dotnet",
|
|
"php",
|
|
"ruby",
|
|
"vscode",
|
|
"jetbrains",
|
|
"visualstudiocode",
|
|
"linux",
|
|
"macos",
|
|
"windows",
|
|
"docker",
|
|
"gradle",
|
|
"maven",
|
|
"jupyter",
|
|
"terraform",
|
|
]:
|
|
self.detected_types.add(project_type)
|
|
break
|
|
|
|
self._scan_package_json()
|
|
self._scan_requirements_txt()
|
|
self._scan_build_files()
|
|
|
|
if "javascript" in self.detected_types and "typescript" in self.detected_types:
|
|
self.detected_types.discard("javascript")
|
|
|
|
return sorted(self.detected_types)
|
|
|
|
def get_language(self) -> Optional[str]:
|
|
"""Get primary programming language."""
|
|
languages = [
|
|
"python",
|
|
"javascript",
|
|
"typescript",
|
|
"java",
|
|
"go",
|
|
"rust",
|
|
"dotnet",
|
|
"php",
|
|
"ruby",
|
|
]
|
|
for lang in languages:
|
|
if lang in self.detected_types:
|
|
return lang
|
|
return None
|
|
|
|
def get_framework(self) -> Optional[str]:
|
|
"""Get framework if detected."""
|
|
frameworks = [
|
|
"django",
|
|
"flask",
|
|
"react",
|
|
"vue",
|
|
"angular",
|
|
"rails",
|
|
"laravel",
|
|
"spring",
|
|
]
|
|
for fw in frameworks:
|
|
if fw in self.detected_types:
|
|
return fw
|
|
return None
|
|
|
|
def get_ide(self) -> Optional[str]:
|
|
"""Get IDE if detected."""
|
|
ides = ["vscode", "jetbrains", "visualstudiocode"]
|
|
for ide in ides:
|
|
if ide in self.detected_types:
|
|
return ide
|
|
return None
|