Source code for ralph.language_detector.signatures

"""Signature file heuristics for framework and package manager detection."""

from __future__ import annotations

from typing import TYPE_CHECKING

from .scanner import collect_signature_files

if TYPE_CHECKING:
    from collections.abc import Iterable

    from ralph.workspace.protocol import Workspace


[docs] class DetectionResults: """Accumulator for detected frameworks, test frameworks, and package managers.""" def __init__(self) -> None: self.frameworks: list[str] = [] self.test_frameworks: list[str] = [] self.package_managers: list[str] = [] def with_framework(self, value: str) -> DetectionResults: if value not in self.frameworks: self.frameworks.append(value) return self def with_test_framework(self, value: str) -> DetectionResults: if value not in self.test_frameworks: self.test_frameworks.append(value) return self def with_package_manager(self, value: str) -> DetectionResults: if value not in self.package_managers: self.package_managers.append(value) return self def finish(self) -> tuple[list[str], str | None, str | None]: return ( self.frameworks, _combine_unique(self.test_frameworks), _combine_unique(self.package_managers), )
def _combine_unique(items: list[str]) -> str | None: if not items: return None if len(items) == 1: return items[0] return " + ".join(items) def _read_signature_contents( workspace: Workspace, signatures: dict[str, list[str]], ) -> dict[str, str]: contents: dict[str, str] = {} for paths in signatures.values(): for path in paths: try: contents[path] = workspace.read(path).lower() except FileNotFoundError: continue return contents
[docs] def detect_signature_files( workspace: Workspace, root: str = "" ) -> tuple[list[str], str | None, str | None]: """Scan workspace signature files to detect frameworks, test frameworks, and package manager.""" signatures = collect_signature_files(workspace, root) contents = _read_signature_contents(workspace, signatures) results = DetectionResults() results = _detect_rust(signatures, contents, results) results = _detect_python(signatures, contents, results) results = _detect_javascript(signatures, contents, results) results = _detect_go(signatures, contents, results) results = _detect_ruby(signatures, contents, results) results = _detect_java(signatures, contents, results) results = _detect_php(signatures, contents, results) return results.finish()
def _detect_rust( signatures: dict[str, list[str]], contents: dict[str, str], results: DetectionResults ) -> DetectionResults: files = signatures.get("cargo.toml") if not files: return results results = results.with_package_manager("Cargo") for path in files: content = contents.get(path, "") if "[dev-dependencies]" in content or "[[test]]" in content: results = results.with_test_framework("cargo test") for pattern, name in ( ("actix", "Actix"), ("axum", "Axum"), ("rocket", "Rocket"), ("tokio", "Tokio"), ("warp", "Warp"), ("tauri", "Tauri"), ("leptos", "Leptos"), ("yew", "Yew"), ): if pattern in content: results = results.with_framework(name) return results def _detect_python( signatures: dict[str, list[str]], contents: dict[str, str], results: DetectionResults ) -> DetectionResults: paths = signatures.get("pyproject.toml") if paths is not None: results = results.with_package_manager("Poetry/pip") else: paths = signatures.get("requirements.txt") if paths is not None: results = results.with_package_manager("pip") elif "setup.py" in signatures: return results.with_package_manager("setuptools") elif "pipfile" in signatures: return results.with_package_manager("Pipenv") else: return results for path in paths: content = contents.get(path) if content is None: continue if "pytest" in content: results = results.with_test_framework("pytest") for pattern, name in ( ("django", "Django"), ("fastapi", "FastAPI"), ("flask", "Flask"), ): if pattern in content: results = results.with_framework(name) return results def _detect_javascript( signatures: dict[str, list[str]], contents: dict[str, str], results: DetectionResults ) -> DetectionResults: paths = signatures.get("package.json") if paths is None: return results if "pnpm-lock.yaml" in signatures: results = results.with_package_manager("pnpm") elif "yarn.lock" in signatures: results = results.with_package_manager("Yarn") elif "bun.lockb" in signatures or "bun.lock" in signatures: results = results.with_package_manager("Bun") else: results = results.with_package_manager("npm") for path in paths: content = contents.get(path) if content is None: continue for pattern, name in ( ('"jest"', "Jest"), ('"vitest"', "Vitest"), ('"mocha"', "Mocha"), ('"cypress"', "Cypress"), ('"playwright"', "Playwright"), ): if pattern in content: results = results.with_test_framework(name) for pattern, name in ( ('"react"', "React"), ('"vue"', "Vue"), ('"angular"', "Angular"), ('"svelte"', "Svelte"), ('"next"', "Next.js"), ('"nuxt"', "Nuxt"), ('"express"', "Express"), ('"fastify"', "Fastify"), ('"nestjs"', "NestJS"), ('"gatsby"', "Gatsby"), ): if pattern in content: results = results.with_framework(name) return results def _detect_go( signatures: dict[str, list[str]], contents: dict[str, str], results: DetectionResults ) -> DetectionResults: paths = signatures.get("go.mod") if paths is None: return results results = results.with_package_manager("Go Modules").with_test_framework("go test") for path in paths: content = contents.get(path) if content is None: continue for pattern, name in ( ("gin-gonic/gin", "Gin"), ("labstack/echo", "Echo"), ("gofiber/fiber", "Fiber"), ("gorilla/mux", "Gorilla"), ("go-chi/chi", "Chi"), ): if pattern in content: results = results.with_framework(name) return results def _detect_ruby( signatures: dict[str, list[str]], contents: dict[str, str], results: DetectionResults ) -> DetectionResults: paths = signatures.get("gemfile") if paths is None: return results results = results.with_package_manager("Bundler") for path in paths: content = contents.get(path) if content is None: continue if "rspec" in content: results = results.with_test_framework("RSpec") elif "minitest" in content: results = results.with_test_framework("Minitest") if "rails" in content: results = results.with_framework("Rails") elif "sinatra" in content: results = results.with_framework("Sinatra") return results def _detect_java( signatures: dict[str, list[str]], contents: dict[str, str], results: DetectionResults ) -> DetectionResults: pom_paths = signatures.get("pom.xml") if pom_paths is not None: results = results.with_package_manager("Maven") results = _detect_java_frameworks(contents, pom_paths, results) gradle_paths = [*signatures.get("build.gradle", []), *signatures.get("build.gradle.kts", [])] if gradle_paths: results = results.with_package_manager("Gradle") results = _detect_java_frameworks(contents, gradle_paths, results) return results def _detect_java_frameworks( contents: dict[str, str], paths: Iterable[str], results: DetectionResults ) -> DetectionResults: for path in paths: content = contents.get(path) if content is None: continue if "junit" in content: results = results.with_test_framework("JUnit") if "spring" in content: results = results.with_framework("Spring") return results def _detect_php( signatures: dict[str, list[str]], contents: dict[str, str], results: DetectionResults ) -> DetectionResults: paths = signatures.get("composer.json") if paths is None: return results results = results.with_package_manager("Composer") for path in paths: content = contents.get(path) if content is None: continue if "phpunit" in content: results = results.with_test_framework("PHPUnit") for pattern, name in (("laravel", "Laravel"), ("symfony", "Symfony")): if pattern in content: results = results.with_framework(name) return results