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.
- sourcecode/__init__.py +3 -0
- sourcecode/classifier.py +177 -0
- sourcecode/cli.py +208 -0
- sourcecode/detectors/__init__.py +54 -0
- sourcecode/detectors/base.py +42 -0
- sourcecode/detectors/dart.py +42 -0
- sourcecode/detectors/go.py +52 -0
- sourcecode/detectors/heuristic.py +70 -0
- sourcecode/detectors/java.py +88 -0
- sourcecode/detectors/nodejs.py +112 -0
- sourcecode/detectors/parsers.py +73 -0
- sourcecode/detectors/php.py +58 -0
- sourcecode/detectors/project.py +129 -0
- sourcecode/detectors/python.py +120 -0
- sourcecode/detectors/ruby.py +44 -0
- sourcecode/detectors/rust.py +69 -0
- sourcecode/redactor.py +81 -0
- sourcecode/scanner.py +180 -0
- sourcecode/schema.py +87 -0
- sourcecode/serializer.py +90 -0
- sourcecode/tree_utils.py +28 -0
- sourcecode/workspace.py +166 -0
- sourcecode-0.1.0.dist-info/METADATA +172 -0
- sourcecode-0.1.0.dist-info/RECORD +26 -0
- sourcecode-0.1.0.dist-info/WHEEL +4 -0
- sourcecode-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -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
|