sourcecode 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,88 @@
1
+ """Detector de proyectos Java."""
2
+ from __future__ import annotations
3
+
4
+ from pathlib import Path
5
+ from xml.etree import ElementTree
6
+
7
+ from sourcecode.detectors.base import (
8
+ AbstractDetector,
9
+ DetectionContext,
10
+ EntryPoint,
11
+ StackDetection,
12
+ )
13
+ from sourcecode.detectors.parsers import read_text_lines, unique_strings
14
+ from sourcecode.schema import FrameworkDetection
15
+ from sourcecode.tree_utils import flatten_file_tree
16
+
17
+
18
+ class JavaDetector(AbstractDetector):
19
+ name = "java"
20
+ priority = 60
21
+
22
+ def can_detect(self, context: DetectionContext) -> bool:
23
+ return any(manifest in context.manifests for manifest in ("pom.xml", "build.gradle"))
24
+
25
+ def detect(self, context: DetectionContext) -> tuple[list[StackDetection], list[EntryPoint]]:
26
+ frameworks: list[FrameworkDetection] = []
27
+ manifests: list[str] = []
28
+
29
+ if "pom.xml" in context.manifests:
30
+ manifests.append("pom.xml")
31
+ frameworks.extend(self._frameworks_from_pom(context.root / "pom.xml"))
32
+ if "build.gradle" in context.manifests:
33
+ manifests.append("build.gradle")
34
+ frameworks.extend(self._frameworks_from_gradle(context.root / "build.gradle"))
35
+
36
+ entry_points = self._collect_entry_points(context)
37
+ stack = StackDetection(
38
+ stack="java",
39
+ detection_method="manifest",
40
+ confidence="high",
41
+ frameworks=self._dedupe_frameworks(frameworks),
42
+ manifests=manifests,
43
+ )
44
+ return [stack], entry_points
45
+
46
+ def _frameworks_from_pom(self, path: Path) -> list[FrameworkDetection]:
47
+ try:
48
+ tree = ElementTree.parse(path)
49
+ except (OSError, ElementTree.ParseError):
50
+ return []
51
+ text = ElementTree.tostring(tree.getroot(), encoding="unicode").lower()
52
+ frameworks: list[FrameworkDetection] = []
53
+ if "spring-boot" in text:
54
+ frameworks.append(FrameworkDetection(name="Spring Boot", source="pom.xml"))
55
+ if "quarkus" in text:
56
+ frameworks.append(FrameworkDetection(name="Quarkus", source="pom.xml"))
57
+ return frameworks
58
+
59
+ def _frameworks_from_gradle(self, path: Path) -> list[FrameworkDetection]:
60
+ content = "\n".join(read_text_lines(path)).lower()
61
+ frameworks: list[FrameworkDetection] = []
62
+ if "com.android.application" in content or "com.android.library" in content:
63
+ frameworks.append(FrameworkDetection(name="Android", source="build.gradle"))
64
+ if "spring-boot" in content:
65
+ frameworks.append(FrameworkDetection(name="Spring Boot", source="build.gradle"))
66
+ if "quarkus" in content:
67
+ frameworks.append(FrameworkDetection(name="Quarkus", source="build.gradle"))
68
+ return frameworks
69
+
70
+ def _collect_entry_points(self, context: DetectionContext) -> list[EntryPoint]:
71
+ candidates = [
72
+ path
73
+ for path in flatten_file_tree(context.file_tree)
74
+ if path.endswith(("Application.java", "Main.java"))
75
+ ]
76
+ return [
77
+ EntryPoint(path=path, stack="java", kind="application", source="manifest")
78
+ for path in unique_strings(candidates)
79
+ ]
80
+
81
+ def _dedupe_frameworks(self, frameworks: list[FrameworkDetection]) -> list[FrameworkDetection]:
82
+ seen: set[str] = set()
83
+ result: list[FrameworkDetection] = []
84
+ for framework in frameworks:
85
+ if framework.name not in seen:
86
+ seen.add(framework.name)
87
+ result.append(framework)
88
+ return result
@@ -0,0 +1,112 @@
1
+ """Detector de proyectos Node.js."""
2
+ from __future__ import annotations
3
+
4
+ from typing import Any
5
+
6
+ from sourcecode.detectors.base import (
7
+ AbstractDetector,
8
+ DetectionContext,
9
+ EntryPoint,
10
+ StackDetection,
11
+ )
12
+ from sourcecode.detectors.parsers import load_json_file, unique_strings
13
+ from sourcecode.schema import FrameworkDetection
14
+ from sourcecode.tree_utils import path_exists_in_tree
15
+
16
+ _FRAMEWORK_MAP = {
17
+ "next": "Next.js",
18
+ "express": "Express",
19
+ "react": "React",
20
+ "vite": "Vite",
21
+ "vue": "Vue",
22
+ "svelte": "Svelte",
23
+ "@nestjs/core": "NestJS",
24
+ }
25
+
26
+
27
+ class NodejsDetector(AbstractDetector):
28
+ name = "nodejs"
29
+ priority = 20
30
+
31
+ def can_detect(self, context: DetectionContext) -> bool:
32
+ return "package.json" in context.manifests
33
+
34
+ def detect(self, context: DetectionContext) -> tuple[list[StackDetection], list[EntryPoint]]:
35
+ package_json = load_json_file(context.root / "package.json")
36
+ if package_json is None:
37
+ return [], []
38
+
39
+ dependency_names = self._collect_dependency_names(package_json)
40
+ frameworks = [
41
+ FrameworkDetection(name=label, source="package.json")
42
+ for package_name, label in _FRAMEWORK_MAP.items()
43
+ if package_name in dependency_names
44
+ ]
45
+ package_manager = self._detect_package_manager(context)
46
+ entry_points = self._collect_entry_points(context, package_json)
47
+
48
+ stack = StackDetection(
49
+ stack="nodejs",
50
+ detection_method="manifest",
51
+ confidence="high",
52
+ frameworks=frameworks,
53
+ package_manager=package_manager,
54
+ manifests=["package.json"],
55
+ )
56
+ return [stack], entry_points
57
+
58
+ def _collect_dependency_names(self, package_json: dict[str, Any]) -> set[str]:
59
+ names: set[str] = set()
60
+ for field in ("dependencies", "devDependencies", "peerDependencies", "optionalDependencies"):
61
+ raw = package_json.get(field, {})
62
+ if isinstance(raw, dict):
63
+ names.update(str(name) for name in raw)
64
+ return names
65
+
66
+ def _detect_package_manager(self, context: DetectionContext) -> str | None:
67
+ for filename, package_manager in (
68
+ ("pnpm-lock.yaml", "pnpm"),
69
+ ("package-lock.json", "npm"),
70
+ ("yarn.lock", "yarn"),
71
+ ):
72
+ if path_exists_in_tree(context.file_tree, filename):
73
+ return package_manager
74
+ return None
75
+
76
+ def _collect_entry_points(
77
+ self, context: DetectionContext, package_json: dict[str, Any]
78
+ ) -> list[EntryPoint]:
79
+ candidate_paths: list[str] = []
80
+ main = package_json.get("main")
81
+ if isinstance(main, str) and main.strip():
82
+ candidate_paths.append(main.strip())
83
+
84
+ bin_field = package_json.get("bin")
85
+ if isinstance(bin_field, str) and bin_field.strip():
86
+ candidate_paths.append(bin_field.strip())
87
+ elif isinstance(bin_field, dict):
88
+ candidate_paths.extend(
89
+ str(value).strip() for value in bin_field.values() if isinstance(value, str) and value.strip()
90
+ )
91
+
92
+ candidate_paths.extend(
93
+ [
94
+ "server.js",
95
+ "src/index.js",
96
+ "src/index.ts",
97
+ "src/main.js",
98
+ "src/main.ts",
99
+ "src/main.tsx",
100
+ "app/page.tsx",
101
+ "pages/index.js",
102
+ ]
103
+ )
104
+
105
+ entry_points: list[EntryPoint] = []
106
+ for path in unique_strings(candidate_paths):
107
+ if path_exists_in_tree(context.file_tree, path):
108
+ kind = "web" if path.startswith(("app/", "pages/")) else "server"
109
+ entry_points.append(
110
+ EntryPoint(path=path, stack="nodejs", kind=kind, source="package.json")
111
+ )
112
+ return entry_points
@@ -0,0 +1,73 @@
1
+ """Helpers ligeros de parseo para manifests."""
2
+ from __future__ import annotations
3
+
4
+ import json
5
+ from collections.abc import Iterable
6
+ from pathlib import Path
7
+ from typing import Any, Optional
8
+
9
+
10
+ def load_json_file(path: Path) -> Optional[dict[str, Any]]:
11
+ """Carga un fichero JSON sin lanzar si el contenido es invalido."""
12
+ try:
13
+ data = json.loads(path.read_text(encoding="utf-8"))
14
+ except (FileNotFoundError, OSError, UnicodeDecodeError, json.JSONDecodeError):
15
+ return None
16
+ return data if isinstance(data, dict) else None
17
+
18
+
19
+ def load_toml_file(path: Path) -> Optional[dict[str, Any]]:
20
+ """Carga un fichero TOML con fallback portable para Python 3.9+."""
21
+ try:
22
+ content = path.read_text(encoding="utf-8")
23
+ except (FileNotFoundError, OSError, UnicodeDecodeError):
24
+ return None
25
+
26
+ try:
27
+ import tomllib
28
+ except ModuleNotFoundError: # pragma: no cover - solo en 3.9/3.10
29
+ import tomli as tomllib
30
+
31
+ try:
32
+ data = tomllib.loads(content)
33
+ except Exception:
34
+ return None
35
+ return data if isinstance(data, dict) else None
36
+
37
+
38
+ def read_text_lines(path: Path) -> list[str]:
39
+ """Lee un fichero de texto y retorna lineas; si falla, retorna lista vacia."""
40
+ try:
41
+ return path.read_text(encoding="utf-8", errors="replace").splitlines()
42
+ except OSError:
43
+ return []
44
+
45
+
46
+ def find_declared_dependencies(raw: Any) -> list[str]:
47
+ """Normaliza dependencias declaradas desde listas o mapas simples."""
48
+ if isinstance(raw, dict):
49
+ return [str(key).strip() for key in raw if str(key).strip()]
50
+ if isinstance(raw, (list, tuple, set)):
51
+ result: list[str] = []
52
+ for item in raw:
53
+ if isinstance(item, str):
54
+ name = item.strip()
55
+ if name:
56
+ result.append(name)
57
+ elif isinstance(item, dict):
58
+ result.extend(find_declared_dependencies(item))
59
+ return result
60
+ if isinstance(raw, str):
61
+ return [raw.strip()] if raw.strip() else []
62
+ return []
63
+
64
+
65
+ def unique_strings(values: Iterable[str]) -> list[str]:
66
+ """Deduplica preservando orden."""
67
+ seen: set[str] = set()
68
+ ordered: list[str] = []
69
+ for value in values:
70
+ if value not in seen:
71
+ seen.add(value)
72
+ ordered.append(value)
73
+ return ordered
@@ -0,0 +1,58 @@
1
+ """Detector de proyectos PHP."""
2
+ from __future__ import annotations
3
+
4
+ from sourcecode.detectors.base import (
5
+ AbstractDetector,
6
+ DetectionContext,
7
+ EntryPoint,
8
+ StackDetection,
9
+ )
10
+ from sourcecode.detectors.parsers import load_json_file
11
+ from sourcecode.schema import FrameworkDetection
12
+ from sourcecode.tree_utils import path_exists_in_tree
13
+
14
+ _FRAMEWORK_MAP = {
15
+ "laravel/framework": "Laravel",
16
+ "symfony/framework-bundle": "Symfony",
17
+ }
18
+
19
+
20
+ class PhpDetector(AbstractDetector):
21
+ name = "php"
22
+ priority = 70
23
+
24
+ def can_detect(self, context: DetectionContext) -> bool:
25
+ return "composer.json" in context.manifests
26
+
27
+ def detect(self, context: DetectionContext) -> tuple[list[StackDetection], list[EntryPoint]]:
28
+ composer = load_json_file(context.root / "composer.json")
29
+ if composer is None:
30
+ return [], []
31
+
32
+ dependencies = {}
33
+ if isinstance(composer.get("require"), dict):
34
+ dependencies.update(composer["require"])
35
+ if isinstance(composer.get("require-dev"), dict):
36
+ dependencies.update(composer["require-dev"])
37
+
38
+ frameworks = [
39
+ FrameworkDetection(name=label, source="composer.json")
40
+ for package_name, label in _FRAMEWORK_MAP.items()
41
+ if package_name in dependencies
42
+ ]
43
+ entry_points: list[EntryPoint] = []
44
+ for path in ("artisan", "public/index.php"):
45
+ if path_exists_in_tree(context.file_tree, path):
46
+ entry_points.append(
47
+ EntryPoint(path=path, stack="php", kind="application", source="composer.json")
48
+ )
49
+
50
+ stack = StackDetection(
51
+ stack="php",
52
+ detection_method="manifest",
53
+ confidence="high",
54
+ frameworks=frameworks,
55
+ package_manager="composer",
56
+ manifests=["composer.json"],
57
+ )
58
+ return [stack], entry_points
@@ -0,0 +1,129 @@
1
+ """Orquestador de detectores para sourcecode."""
2
+ from __future__ import annotations
3
+
4
+ from collections.abc import Iterable, Sequence
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from sourcecode.classifier import TypeClassifier
9
+ from sourcecode.detectors.base import AbstractDetector, DetectionContext
10
+ from sourcecode.schema import EntryPoint, FrameworkDetection, StackDetection
11
+
12
+ _CONFIDENCE_RANK = {"low": 0, "medium": 1, "high": 2}
13
+
14
+
15
+ class ProjectDetector:
16
+ """Ejecuta detectores y fusiona resultados de forma determinista."""
17
+
18
+ def __init__(
19
+ self,
20
+ detectors: Sequence[AbstractDetector],
21
+ classifier: TypeClassifier | None = None,
22
+ ) -> None:
23
+ self.detectors = sorted(detectors, key=lambda detector: detector.priority)
24
+ self.classifier = classifier or TypeClassifier()
25
+
26
+ def detect(
27
+ self,
28
+ root: Path,
29
+ file_tree: dict[str, Any],
30
+ manifests: Sequence[str],
31
+ ) -> tuple[list[StackDetection], list[EntryPoint], str | None]:
32
+ manifest_names = [Path(manifest).name for manifest in manifests]
33
+ context = DetectionContext(root=root, file_tree=file_tree, manifests=manifest_names)
34
+ merged_stacks: dict[str, StackDetection] = {}
35
+ merged_entry_points: dict[str, EntryPoint] = {}
36
+
37
+ for detector in self.detectors:
38
+ if not detector.can_detect(context):
39
+ continue
40
+
41
+ stacks, entry_points = detector.detect(context)
42
+ for stack in stacks:
43
+ existing = merged_stacks.get(stack.stack)
44
+ if existing is None:
45
+ copied = self._copy_stack(stack)
46
+ if copied.root is None:
47
+ copied.root = "."
48
+ merged_stacks[stack.stack] = copied
49
+ continue
50
+ merged_stacks[stack.stack] = self._merge_stack(existing, stack)
51
+
52
+ detected_stack_names = set(merged_stacks.keys())
53
+ for entry_point in entry_points:
54
+ if entry_point.stack not in detected_stack_names:
55
+ continue
56
+ merged_entry_points.setdefault(entry_point.path, entry_point)
57
+
58
+ enriched_stacks, project_type = self.classify_results(
59
+ file_tree,
60
+ list(merged_stacks.values()),
61
+ list(merged_entry_points.values()),
62
+ )
63
+ return enriched_stacks, list(merged_entry_points.values()), project_type
64
+
65
+ def classify_results(
66
+ self,
67
+ file_tree: dict[str, Any],
68
+ stacks: Sequence[StackDetection],
69
+ entry_points: Sequence[EntryPoint],
70
+ *,
71
+ project_type_override: str | None = None,
72
+ ) -> tuple[list[StackDetection], str | None]:
73
+ enriched_stacks, project_type = self.classifier.enrich(file_tree, stacks, entry_points)
74
+ return enriched_stacks, project_type_override or project_type
75
+
76
+ def _copy_stack(self, stack: StackDetection) -> StackDetection:
77
+ return StackDetection(
78
+ stack=stack.stack,
79
+ detection_method=stack.detection_method,
80
+ confidence=stack.confidence,
81
+ frameworks=[
82
+ FrameworkDetection(name=framework.name, source=framework.source)
83
+ for framework in stack.frameworks
84
+ ],
85
+ package_manager=stack.package_manager,
86
+ manifests=list(stack.manifests),
87
+ primary=stack.primary,
88
+ root=stack.root,
89
+ workspace=stack.workspace,
90
+ signals=list(stack.signals),
91
+ )
92
+
93
+ def _merge_stack(self, current: StackDetection, incoming: StackDetection) -> StackDetection:
94
+ current.frameworks = self._merge_frameworks(current.frameworks, incoming.frameworks)
95
+ current.manifests = self._merge_manifests(current.manifests, incoming.manifests)
96
+ if incoming.package_manager and not current.package_manager:
97
+ current.package_manager = incoming.package_manager
98
+ if self._confidence_rank(incoming.confidence) > self._confidence_rank(current.confidence):
99
+ current.confidence = incoming.confidence
100
+ current.detection_method = incoming.detection_method
101
+ elif self._confidence_rank(incoming.confidence) == self._confidence_rank(current.confidence):
102
+ if current.detection_method == "heuristic" and incoming.detection_method != "heuristic":
103
+ current.detection_method = incoming.detection_method
104
+ return current
105
+
106
+ def _merge_frameworks(
107
+ self,
108
+ current: Iterable[FrameworkDetection],
109
+ incoming: Iterable[FrameworkDetection],
110
+ ) -> list[FrameworkDetection]:
111
+ merged: dict[str, FrameworkDetection] = {}
112
+ for framework in list(current) + list(incoming):
113
+ merged.setdefault(
114
+ framework.name,
115
+ FrameworkDetection(name=framework.name, source=framework.source),
116
+ )
117
+ return list(merged.values())
118
+
119
+ def _merge_manifests(self, current: Iterable[str], incoming: Iterable[str]) -> list[str]:
120
+ merged: list[str] = []
121
+ seen: set[str] = set()
122
+ for manifest in list(current) + list(incoming):
123
+ if manifest not in seen:
124
+ seen.add(manifest)
125
+ merged.append(manifest)
126
+ return merged
127
+
128
+ def _confidence_rank(self, confidence: str) -> int:
129
+ return _CONFIDENCE_RANK.get(confidence, -1)
@@ -0,0 +1,120 @@
1
+ """Detector de proyectos Python."""
2
+ from __future__ import annotations
3
+
4
+ import re
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ from sourcecode.detectors.base import (
9
+ AbstractDetector,
10
+ DetectionContext,
11
+ EntryPoint,
12
+ StackDetection,
13
+ )
14
+ from sourcecode.detectors.parsers import load_toml_file, read_text_lines, unique_strings
15
+ from sourcecode.schema import FrameworkDetection
16
+ from sourcecode.tree_utils import path_exists_in_tree
17
+
18
+ _FRAMEWORK_MAP = {
19
+ "fastapi": "FastAPI",
20
+ "django": "Django",
21
+ "flask": "Flask",
22
+ "typer": "Typer",
23
+ }
24
+
25
+
26
+ class PythonDetector(AbstractDetector):
27
+ name = "python"
28
+ priority = 30
29
+
30
+ def can_detect(self, context: DetectionContext) -> bool:
31
+ supported = {"pyproject.toml", "requirements.txt", "setup.py", "Pipfile", "uv.lock"}
32
+ return any(manifest in supported for manifest in context.manifests)
33
+
34
+ def detect(self, context: DetectionContext) -> tuple[list[StackDetection], list[EntryPoint]]:
35
+ dependencies = self._collect_dependencies(context)
36
+ frameworks = [
37
+ FrameworkDetection(name=label, source="manifest")
38
+ for package_name, label in _FRAMEWORK_MAP.items()
39
+ if package_name in dependencies
40
+ ]
41
+ entry_points = self._collect_entry_points(context)
42
+
43
+ manifests = [
44
+ manifest
45
+ for manifest in ("pyproject.toml", "requirements.txt", "setup.py", "Pipfile", "uv.lock")
46
+ if manifest in context.manifests
47
+ ]
48
+ stack = StackDetection(
49
+ stack="python",
50
+ detection_method="manifest",
51
+ confidence="high" if "pyproject.toml" in manifests else "medium",
52
+ frameworks=frameworks,
53
+ package_manager="pip",
54
+ manifests=manifests,
55
+ )
56
+ return [stack], entry_points
57
+
58
+ def _collect_dependencies(self, context: DetectionContext) -> set[str]:
59
+ deps: set[str] = set()
60
+ pyproject = load_toml_file(context.root / "pyproject.toml")
61
+ if pyproject:
62
+ project = pyproject.get("project", {})
63
+ if isinstance(project, dict):
64
+ deps.update(self._normalize_requirement(dep) for dep in project.get("dependencies", []))
65
+ optional = project.get("optional-dependencies", {})
66
+ if isinstance(optional, dict):
67
+ for group in optional.values():
68
+ if isinstance(group, list):
69
+ deps.update(self._normalize_requirement(dep) for dep in group)
70
+
71
+ deps.update(self._read_requirements(context.root / "requirements.txt"))
72
+ deps.update(self._read_requirements(context.root / "Pipfile"))
73
+ deps.update(self._read_setup_py(context.root / "setup.py"))
74
+ return {dep for dep in deps if dep}
75
+
76
+ def _normalize_requirement(self, requirement: Any) -> str:
77
+ if not isinstance(requirement, str):
78
+ return ""
79
+ normalized = requirement.strip().lower()
80
+ normalized = re.split(r"[<>=!~\[\s]", normalized, maxsplit=1)[0]
81
+ return normalized
82
+
83
+ def _read_requirements(self, path: Path) -> set[str]:
84
+ result: set[str] = set()
85
+ for line in read_text_lines(path):
86
+ stripped = line.strip()
87
+ if not stripped or stripped.startswith(("#", "[")):
88
+ continue
89
+ result.add(self._normalize_requirement(stripped))
90
+ return result
91
+
92
+ def _read_setup_py(self, path: Path) -> set[str]:
93
+ content = "\n".join(read_text_lines(path))
94
+ if not content:
95
+ return set()
96
+ matches = re.findall(r"['\"]([A-Za-z0-9_.-]+)(?:[<>=!~].*?)?['\"]", content)
97
+ return {match.lower() for match in matches}
98
+
99
+ def _collect_entry_points(self, context: DetectionContext) -> list[EntryPoint]:
100
+ candidates: list[str] = []
101
+ pyproject = load_toml_file(context.root / "pyproject.toml")
102
+ if pyproject:
103
+ project = pyproject.get("project", {})
104
+ if isinstance(project, dict):
105
+ scripts = project.get("scripts", {})
106
+ if isinstance(scripts, dict):
107
+ for value in scripts.values():
108
+ if isinstance(value, str) and ":" in value:
109
+ module, _callable = value.split(":", 1)
110
+ candidates.append(module.replace(".", "/") + ".py")
111
+
112
+ candidates.extend(["__main__.py", "main.py", "app.py", "manage.py", "src/main.py"])
113
+ entry_points: list[EntryPoint] = []
114
+ for path in unique_strings(candidates):
115
+ if path_exists_in_tree(context.file_tree, path):
116
+ kind = "cli" if path.endswith(("__main__.py", "main.py")) else "app"
117
+ entry_points.append(
118
+ EntryPoint(path=path, stack="python", kind=kind, source="manifest")
119
+ )
120
+ return entry_points
@@ -0,0 +1,44 @@
1
+ """Detector de proyectos Ruby."""
2
+ from __future__ import annotations
3
+
4
+ from sourcecode.detectors.base import (
5
+ AbstractDetector,
6
+ DetectionContext,
7
+ EntryPoint,
8
+ StackDetection,
9
+ )
10
+ from sourcecode.detectors.parsers import read_text_lines
11
+ from sourcecode.schema import FrameworkDetection
12
+ from sourcecode.tree_utils import path_exists_in_tree
13
+
14
+
15
+ class RubyDetector(AbstractDetector):
16
+ name = "ruby"
17
+ priority = 80
18
+
19
+ def can_detect(self, context: DetectionContext) -> bool:
20
+ return "Gemfile" in context.manifests
21
+
22
+ def detect(self, context: DetectionContext) -> tuple[list[StackDetection], list[EntryPoint]]:
23
+ content = "\n".join(read_text_lines(context.root / "Gemfile")).lower()
24
+ frameworks: list[FrameworkDetection] = []
25
+ if "gem 'rails'" in content or 'gem "rails"' in content:
26
+ frameworks.append(FrameworkDetection(name="Rails", source="Gemfile"))
27
+ if "gem 'sinatra'" in content or 'gem "sinatra"' in content:
28
+ frameworks.append(FrameworkDetection(name="Sinatra", source="Gemfile"))
29
+
30
+ entry_points: list[EntryPoint] = []
31
+ for path in ("bin/rails", "config.ru", "app.rb"):
32
+ if path_exists_in_tree(context.file_tree, path):
33
+ entry_points.append(
34
+ EntryPoint(path=path, stack="ruby", kind="application", source="Gemfile")
35
+ )
36
+
37
+ stack = StackDetection(
38
+ stack="ruby",
39
+ detection_method="manifest",
40
+ confidence="high",
41
+ frameworks=frameworks,
42
+ manifests=["Gemfile"],
43
+ )
44
+ return [stack], entry_points