This commit is contained in:
331
src/detector.py
Normal file
331
src/detector.py
Normal file
@@ -0,0 +1,331 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user