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
sourcecode/__init__.py
ADDED
sourcecode/classifier.py
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
"""Clasificacion de tipo de proyecto y scoring de confianza."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from collections.abc import Iterable, Sequence
|
|
5
|
+
from dataclasses import replace
|
|
6
|
+
from typing import Any, Literal
|
|
7
|
+
|
|
8
|
+
from sourcecode.schema import EntryPoint, StackDetection
|
|
9
|
+
from sourcecode.tree_utils import flatten_file_tree
|
|
10
|
+
|
|
11
|
+
_API_FRAMEWORKS = {
|
|
12
|
+
"FastAPI",
|
|
13
|
+
"Django",
|
|
14
|
+
"Flask",
|
|
15
|
+
"Express",
|
|
16
|
+
"Gin",
|
|
17
|
+
"Echo",
|
|
18
|
+
"Axum",
|
|
19
|
+
"Actix Web",
|
|
20
|
+
"Rocket",
|
|
21
|
+
"Spring Boot",
|
|
22
|
+
"Quarkus",
|
|
23
|
+
"Laravel",
|
|
24
|
+
"Symfony",
|
|
25
|
+
}
|
|
26
|
+
_WEB_FRAMEWORKS = {"Next.js", "React", "Vue", "Svelte", "Vite", "Flutter"}
|
|
27
|
+
_CLI_FRAMEWORKS = {"Typer", "Cobra", "Clap"}
|
|
28
|
+
ConfidenceLevel = Literal["high", "medium", "low"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class TypeClassifier:
|
|
32
|
+
"""Clasifica project_type y enriquece stacks con confianza/primary."""
|
|
33
|
+
|
|
34
|
+
def enrich(
|
|
35
|
+
self,
|
|
36
|
+
file_tree: dict[str, Any],
|
|
37
|
+
stacks: Sequence[StackDetection],
|
|
38
|
+
entry_points: Sequence[EntryPoint],
|
|
39
|
+
) -> tuple[list[StackDetection], str | None]:
|
|
40
|
+
enriched = [self._enrich_stack(file_tree, stack, entry_points) for stack in stacks]
|
|
41
|
+
project_type = self._classify_project_type(file_tree, enriched, entry_points)
|
|
42
|
+
primary_stack = self._select_primary_stack(enriched, project_type)
|
|
43
|
+
|
|
44
|
+
final_stacks: list[StackDetection] = []
|
|
45
|
+
for stack in enriched:
|
|
46
|
+
final_stacks.append(replace(stack, primary=(stack.stack == primary_stack)))
|
|
47
|
+
return final_stacks, project_type
|
|
48
|
+
|
|
49
|
+
def _enrich_stack(
|
|
50
|
+
self,
|
|
51
|
+
file_tree: dict[str, Any],
|
|
52
|
+
stack: StackDetection,
|
|
53
|
+
entry_points: Sequence[EntryPoint],
|
|
54
|
+
) -> StackDetection:
|
|
55
|
+
signals = list(stack.signals)
|
|
56
|
+
score = 0
|
|
57
|
+
|
|
58
|
+
if stack.manifests:
|
|
59
|
+
score += 4
|
|
60
|
+
signals.extend(f"manifest:{manifest}" for manifest in stack.manifests)
|
|
61
|
+
if stack.frameworks:
|
|
62
|
+
score += 2
|
|
63
|
+
signals.extend(f"framework:{framework.name}" for framework in stack.frameworks)
|
|
64
|
+
if stack.package_manager:
|
|
65
|
+
score += 2
|
|
66
|
+
signals.append(f"package_manager:{stack.package_manager}")
|
|
67
|
+
|
|
68
|
+
matching_entry_points = [entry for entry in entry_points if entry.stack == stack.stack]
|
|
69
|
+
if matching_entry_points:
|
|
70
|
+
score += 2
|
|
71
|
+
signals.extend(f"entry:{entry.path}" for entry in matching_entry_points)
|
|
72
|
+
|
|
73
|
+
extension_hits = self._count_extension_hits(file_tree, stack.stack)
|
|
74
|
+
if extension_hits >= 2:
|
|
75
|
+
score += 1
|
|
76
|
+
signals.append(f"extensions:{extension_hits}")
|
|
77
|
+
|
|
78
|
+
if stack.detection_method == "heuristic":
|
|
79
|
+
score -= 2
|
|
80
|
+
signals.append("method:heuristic")
|
|
81
|
+
|
|
82
|
+
confidence = self._score_to_confidence(score)
|
|
83
|
+
return replace(stack, confidence=confidence, signals=self._unique(signals))
|
|
84
|
+
|
|
85
|
+
def _classify_project_type(
|
|
86
|
+
self,
|
|
87
|
+
file_tree: dict[str, Any],
|
|
88
|
+
stacks: Sequence[StackDetection],
|
|
89
|
+
entry_points: Sequence[EntryPoint],
|
|
90
|
+
) -> str | None:
|
|
91
|
+
flat_paths = set(flatten_file_tree(file_tree))
|
|
92
|
+
stack_names = {stack.stack for stack in stacks}
|
|
93
|
+
framework_names = {framework.name for stack in stacks for framework in stack.frameworks}
|
|
94
|
+
|
|
95
|
+
if len(stack_names) >= 2 and self._is_fullstack(stacks):
|
|
96
|
+
return "fullstack"
|
|
97
|
+
|
|
98
|
+
if "src/lib.rs" in flat_paths and not any(path.endswith("main.rs") for path in flat_paths):
|
|
99
|
+
return "library"
|
|
100
|
+
|
|
101
|
+
if framework_names & _WEB_FRAMEWORKS or any(
|
|
102
|
+
path.startswith(("app/", "pages/", "components/")) for path in flat_paths
|
|
103
|
+
):
|
|
104
|
+
return "webapp"
|
|
105
|
+
|
|
106
|
+
if framework_names & _API_FRAMEWORKS:
|
|
107
|
+
return "api"
|
|
108
|
+
|
|
109
|
+
if framework_names & _CLI_FRAMEWORKS or any(
|
|
110
|
+
entry.kind == "cli" for entry in entry_points
|
|
111
|
+
) or any(path.startswith("bin/") for path in flat_paths):
|
|
112
|
+
return "cli"
|
|
113
|
+
|
|
114
|
+
if stack_names:
|
|
115
|
+
single = next(iter(stack_names))
|
|
116
|
+
if single in {"python", "go", "java", "php", "ruby"} and framework_names & _API_FRAMEWORKS:
|
|
117
|
+
return "api"
|
|
118
|
+
|
|
119
|
+
return "unknown" if stacks else None
|
|
120
|
+
|
|
121
|
+
def _is_fullstack(self, stacks: Sequence[StackDetection]) -> bool:
|
|
122
|
+
has_web = False
|
|
123
|
+
has_api = False
|
|
124
|
+
for stack in stacks:
|
|
125
|
+
frameworks = {framework.name for framework in stack.frameworks}
|
|
126
|
+
if frameworks & _WEB_FRAMEWORKS or stack.stack == "nodejs":
|
|
127
|
+
has_web = True
|
|
128
|
+
if frameworks & _API_FRAMEWORKS or stack.stack in {"python", "go", "java", "php", "ruby"}:
|
|
129
|
+
has_api = True
|
|
130
|
+
return has_web and has_api
|
|
131
|
+
|
|
132
|
+
def _select_primary_stack(
|
|
133
|
+
self, stacks: Sequence[StackDetection], project_type: str | None
|
|
134
|
+
) -> str | None:
|
|
135
|
+
if not stacks:
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
def sort_key(stack: StackDetection) -> tuple[int, int, int]:
|
|
139
|
+
score = {"low": 0, "medium": 1, "high": 2}.get(stack.confidence, 0)
|
|
140
|
+
manifest_weight = 1 if stack.manifests else 0
|
|
141
|
+
priority = 0
|
|
142
|
+
if project_type in {"webapp", "fullstack"} and stack.stack == "nodejs" or project_type == "api" and stack.stack in {"python", "go", "java", "php", "ruby"} or project_type == "cli" and any(
|
|
143
|
+
framework.name in _CLI_FRAMEWORKS for framework in stack.frameworks
|
|
144
|
+
):
|
|
145
|
+
priority = 3
|
|
146
|
+
return (priority, score, manifest_weight)
|
|
147
|
+
|
|
148
|
+
return max(stacks, key=sort_key).stack
|
|
149
|
+
|
|
150
|
+
def _count_extension_hits(self, file_tree: dict[str, Any], stack: str) -> int:
|
|
151
|
+
extensions = {
|
|
152
|
+
"python": (".py",),
|
|
153
|
+
"nodejs": (".js", ".ts", ".tsx"),
|
|
154
|
+
"go": (".go",),
|
|
155
|
+
"rust": (".rs",),
|
|
156
|
+
"java": (".java",),
|
|
157
|
+
"php": (".php",),
|
|
158
|
+
"ruby": (".rb",),
|
|
159
|
+
"dart": (".dart",),
|
|
160
|
+
}.get(stack, ())
|
|
161
|
+
return sum(1 for path in flatten_file_tree(file_tree) if path.endswith(extensions))
|
|
162
|
+
|
|
163
|
+
def _score_to_confidence(self, score: int) -> ConfidenceLevel:
|
|
164
|
+
if score >= 6:
|
|
165
|
+
return "high"
|
|
166
|
+
if score >= 3:
|
|
167
|
+
return "medium"
|
|
168
|
+
return "low"
|
|
169
|
+
|
|
170
|
+
def _unique(self, values: Iterable[str]) -> list[str]:
|
|
171
|
+
seen: set[str] = set()
|
|
172
|
+
ordered: list[str] = []
|
|
173
|
+
for value in values:
|
|
174
|
+
if value not in seen:
|
|
175
|
+
seen.add(value)
|
|
176
|
+
ordered.append(value)
|
|
177
|
+
return ordered
|
sourcecode/cli.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""CLI de sourcecode — interfaz de linea de comandos principal."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any, Optional
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
from sourcecode import __version__
|
|
11
|
+
|
|
12
|
+
app = typer.Typer(
|
|
13
|
+
name="sourcecode",
|
|
14
|
+
help="Genera un mapa de contexto estructurado del proyecto para agentes IA.",
|
|
15
|
+
add_completion=False,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
FORMAT_CHOICES = ["json", "yaml"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def version_callback(value: bool) -> None:
|
|
22
|
+
if value:
|
|
23
|
+
typer.echo(f"sourcecode {__version__}")
|
|
24
|
+
raise typer.Exit()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@app.callback(invoke_without_command=True)
|
|
28
|
+
def main(
|
|
29
|
+
ctx: typer.Context,
|
|
30
|
+
path: Path = typer.Argument(Path("."), help="Directorio a analizar (default: directorio actual)"),
|
|
31
|
+
format: str = typer.Option(
|
|
32
|
+
"json",
|
|
33
|
+
"--format",
|
|
34
|
+
"-f",
|
|
35
|
+
help="Formato de salida: json|yaml",
|
|
36
|
+
show_default=True,
|
|
37
|
+
),
|
|
38
|
+
output: Optional[Path] = typer.Option(
|
|
39
|
+
None,
|
|
40
|
+
"--output",
|
|
41
|
+
"-o",
|
|
42
|
+
help="Fichero de salida (default: stdout)",
|
|
43
|
+
),
|
|
44
|
+
compact: bool = typer.Option(
|
|
45
|
+
False,
|
|
46
|
+
"--compact",
|
|
47
|
+
help="Output reducido (~500 tokens): tipo de proyecto, stacks, entradas y arbol nivel 1",
|
|
48
|
+
),
|
|
49
|
+
no_redact: bool = typer.Option(
|
|
50
|
+
False,
|
|
51
|
+
"--no-redact",
|
|
52
|
+
help="Desactivar redaccion de secretos (activa por defecto)",
|
|
53
|
+
),
|
|
54
|
+
version: Optional[bool] = typer.Option(
|
|
55
|
+
None,
|
|
56
|
+
"--version",
|
|
57
|
+
"-v",
|
|
58
|
+
callback=version_callback,
|
|
59
|
+
is_eager=True,
|
|
60
|
+
help="Mostrar version y salir",
|
|
61
|
+
),
|
|
62
|
+
depth: int = typer.Option(
|
|
63
|
+
4,
|
|
64
|
+
"--depth",
|
|
65
|
+
help="Profundidad maxima del arbol de ficheros (default: 4)",
|
|
66
|
+
min=1,
|
|
67
|
+
max=20,
|
|
68
|
+
),
|
|
69
|
+
) -> None:
|
|
70
|
+
"""Genera un mapa de contexto estructurado del proyecto en formato JSON o YAML."""
|
|
71
|
+
# Validar formato
|
|
72
|
+
if format not in FORMAT_CHOICES:
|
|
73
|
+
typer.echo(
|
|
74
|
+
f"Error: valor invalido '{format}' para --format. Opciones: {', '.join(FORMAT_CHOICES)}",
|
|
75
|
+
err=True,
|
|
76
|
+
)
|
|
77
|
+
raise typer.Exit(code=1)
|
|
78
|
+
|
|
79
|
+
# Resolver y validar path
|
|
80
|
+
target = path.resolve()
|
|
81
|
+
if not target.exists():
|
|
82
|
+
typer.echo(f"Error: el directorio '{target}' no existe.", err=True)
|
|
83
|
+
raise typer.Exit(code=1)
|
|
84
|
+
if not target.is_dir():
|
|
85
|
+
typer.echo(f"Error: '{target}' no es un directorio.", err=True)
|
|
86
|
+
raise typer.Exit(code=1)
|
|
87
|
+
|
|
88
|
+
# --- Importar modulos de logica ---
|
|
89
|
+
from dataclasses import asdict, replace
|
|
90
|
+
|
|
91
|
+
from sourcecode.detectors import ProjectDetector, build_default_detectors
|
|
92
|
+
from sourcecode.redactor import SecretRedactor, redact_dict
|
|
93
|
+
from sourcecode.scanner import FileScanner
|
|
94
|
+
from sourcecode.schema import AnalysisMetadata, EntryPoint, SourceMap, StackDetection
|
|
95
|
+
from sourcecode.serializer import compact_view, write_output
|
|
96
|
+
from sourcecode.workspace import WorkspaceAnalyzer
|
|
97
|
+
|
|
98
|
+
# 1. Escanear el directorio (SCAN-01 a SCAN-05)
|
|
99
|
+
redactor = SecretRedactor(enabled=not no_redact)
|
|
100
|
+
scanner = FileScanner(target, max_depth=depth)
|
|
101
|
+
raw_tree = scanner.scan_tree()
|
|
102
|
+
|
|
103
|
+
# 2. Filtrar del arbol las entradas de .env y *.secret (SEC-02, todos los niveles)
|
|
104
|
+
def filter_sensitive_files(tree: dict[str, Any]) -> dict[str, Any]:
|
|
105
|
+
filtered: dict[str, Any] = {}
|
|
106
|
+
for name, value in tree.items():
|
|
107
|
+
if redactor.should_exclude_file(name):
|
|
108
|
+
continue # excluir .env, *.secret del arbol
|
|
109
|
+
if isinstance(value, dict):
|
|
110
|
+
filtered[name] = filter_sensitive_files(value)
|
|
111
|
+
else:
|
|
112
|
+
filtered[name] = value
|
|
113
|
+
return filtered
|
|
114
|
+
|
|
115
|
+
file_tree = filter_sensitive_files(raw_tree)
|
|
116
|
+
manifests = scanner.find_manifests()
|
|
117
|
+
detector = ProjectDetector(build_default_detectors())
|
|
118
|
+
workspace_analysis = WorkspaceAnalyzer().analyze(target, manifests)
|
|
119
|
+
|
|
120
|
+
root_manifests = [
|
|
121
|
+
manifest
|
|
122
|
+
for manifest in manifests
|
|
123
|
+
if Path(manifest).resolve().parent == target
|
|
124
|
+
]
|
|
125
|
+
detection_manifests = root_manifests if workspace_analysis.workspaces else manifests
|
|
126
|
+
if workspace_analysis.is_monorepo and not root_manifests:
|
|
127
|
+
stacks: list[StackDetection] = []
|
|
128
|
+
entry_points: list[EntryPoint] = []
|
|
129
|
+
else:
|
|
130
|
+
stacks, entry_points, _project_type = detector.detect(target, file_tree, detection_manifests)
|
|
131
|
+
|
|
132
|
+
for workspace in workspace_analysis.workspaces:
|
|
133
|
+
workspace_root = target / workspace.path
|
|
134
|
+
if not workspace_root.exists() or not workspace_root.is_dir():
|
|
135
|
+
continue
|
|
136
|
+
workspace_scanner = FileScanner(workspace_root, max_depth=depth)
|
|
137
|
+
workspace_tree = filter_sensitive_files(workspace_scanner.scan_tree())
|
|
138
|
+
workspace_manifests = workspace_scanner.find_manifests()
|
|
139
|
+
workspace_stacks, workspace_entry_points, _ = detector.detect(
|
|
140
|
+
workspace_root,
|
|
141
|
+
workspace_tree,
|
|
142
|
+
workspace_manifests,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
stacks.extend(
|
|
146
|
+
replace(stack, root=workspace.path, workspace=workspace.path, primary=False)
|
|
147
|
+
for stack in workspace_stacks
|
|
148
|
+
)
|
|
149
|
+
entry_points.extend(
|
|
150
|
+
replace(
|
|
151
|
+
entry_point,
|
|
152
|
+
path=f"{workspace.path}/{entry_point.path}",
|
|
153
|
+
)
|
|
154
|
+
for entry_point in workspace_entry_points
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
stacks, project_type = detector.classify_results(
|
|
158
|
+
file_tree,
|
|
159
|
+
stacks,
|
|
160
|
+
entry_points,
|
|
161
|
+
project_type_override="monorepo" if workspace_analysis.is_monorepo else None,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
# 3. Construir el schema
|
|
165
|
+
metadata = AnalysisMetadata(analyzed_path=str(target))
|
|
166
|
+
sm = SourceMap(
|
|
167
|
+
metadata=metadata,
|
|
168
|
+
file_tree=file_tree,
|
|
169
|
+
stacks=stacks,
|
|
170
|
+
project_type=project_type,
|
|
171
|
+
entry_points=entry_points,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
# 4. Serializar (con o sin modo compact)
|
|
175
|
+
if compact:
|
|
176
|
+
data = compact_view(sm)
|
|
177
|
+
# Aplicar redaccion sobre el dict del compact view
|
|
178
|
+
if not no_redact:
|
|
179
|
+
data = redact_dict(data)
|
|
180
|
+
content = json.dumps(data, indent=2, ensure_ascii=False)
|
|
181
|
+
else:
|
|
182
|
+
# Redactar sobre el dict serializado (SEC-01, SEC-03)
|
|
183
|
+
raw_dict = asdict(sm)
|
|
184
|
+
if not no_redact:
|
|
185
|
+
raw_dict = redact_dict(raw_dict)
|
|
186
|
+
|
|
187
|
+
if format == "yaml":
|
|
188
|
+
# Para YAML, serializar el dict directamente con ruamel.yaml
|
|
189
|
+
from io import StringIO
|
|
190
|
+
|
|
191
|
+
from ruamel.yaml import YAML
|
|
192
|
+
|
|
193
|
+
yaml = YAML()
|
|
194
|
+
yaml.default_flow_style = False
|
|
195
|
+
yaml.representer.add_representer(
|
|
196
|
+
type(None),
|
|
197
|
+
lambda dumper, data_val: dumper.represent_scalar(
|
|
198
|
+
"tag:yaml.org,2002:null", "null"
|
|
199
|
+
),
|
|
200
|
+
)
|
|
201
|
+
stream = StringIO()
|
|
202
|
+
yaml.dump(raw_dict, stream)
|
|
203
|
+
content = stream.getvalue()
|
|
204
|
+
else:
|
|
205
|
+
content = json.dumps(raw_dict, indent=2, ensure_ascii=False)
|
|
206
|
+
|
|
207
|
+
# 5. Escribir output (CLI-04)
|
|
208
|
+
write_output(content, output=output)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""Infraestructura de deteccion para sourcecode."""
|
|
2
|
+
|
|
3
|
+
from sourcecode.detectors.base import (
|
|
4
|
+
AbstractDetector,
|
|
5
|
+
DetectionContext,
|
|
6
|
+
EntryPoint,
|
|
7
|
+
FrameworkDetection,
|
|
8
|
+
StackDetection,
|
|
9
|
+
)
|
|
10
|
+
from sourcecode.detectors.dart import DartDetector
|
|
11
|
+
from sourcecode.detectors.go import GoDetector
|
|
12
|
+
from sourcecode.detectors.heuristic import HeuristicDetector
|
|
13
|
+
from sourcecode.detectors.java import JavaDetector
|
|
14
|
+
from sourcecode.detectors.nodejs import NodejsDetector
|
|
15
|
+
from sourcecode.detectors.php import PhpDetector
|
|
16
|
+
from sourcecode.detectors.project import ProjectDetector
|
|
17
|
+
from sourcecode.detectors.python import PythonDetector
|
|
18
|
+
from sourcecode.detectors.ruby import RubyDetector
|
|
19
|
+
from sourcecode.detectors.rust import RustDetector
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def build_default_detectors() -> list[AbstractDetector]:
|
|
23
|
+
"""Registro por defecto de detectores para la CLI."""
|
|
24
|
+
return [
|
|
25
|
+
NodejsDetector(),
|
|
26
|
+
PythonDetector(),
|
|
27
|
+
GoDetector(),
|
|
28
|
+
RustDetector(),
|
|
29
|
+
JavaDetector(),
|
|
30
|
+
PhpDetector(),
|
|
31
|
+
RubyDetector(),
|
|
32
|
+
DartDetector(),
|
|
33
|
+
HeuristicDetector(),
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"AbstractDetector",
|
|
39
|
+
"build_default_detectors",
|
|
40
|
+
"DartDetector",
|
|
41
|
+
"DetectionContext",
|
|
42
|
+
"EntryPoint",
|
|
43
|
+
"FrameworkDetection",
|
|
44
|
+
"GoDetector",
|
|
45
|
+
"HeuristicDetector",
|
|
46
|
+
"JavaDetector",
|
|
47
|
+
"NodejsDetector",
|
|
48
|
+
"PhpDetector",
|
|
49
|
+
"PythonDetector",
|
|
50
|
+
"ProjectDetector",
|
|
51
|
+
"RubyDetector",
|
|
52
|
+
"RustDetector",
|
|
53
|
+
"StackDetection",
|
|
54
|
+
]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Contratos base de deteccion para sourcecode."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from abc import ABC, abstractmethod
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from sourcecode.schema import EntryPoint, FrameworkDetection, StackDetection
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class DetectionContext:
|
|
14
|
+
"""Contexto comun que reciben todos los detectores."""
|
|
15
|
+
|
|
16
|
+
root: Path
|
|
17
|
+
file_tree: dict[str, Any]
|
|
18
|
+
manifests: list[str] = field(default_factory=list)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class AbstractDetector(ABC):
|
|
22
|
+
"""Contrato base para detectores de stack."""
|
|
23
|
+
|
|
24
|
+
name: str = "abstract"
|
|
25
|
+
priority: int = 100
|
|
26
|
+
|
|
27
|
+
@abstractmethod
|
|
28
|
+
def can_detect(self, context: DetectionContext) -> bool:
|
|
29
|
+
"""Indica si este detector aplica al proyecto dado."""
|
|
30
|
+
|
|
31
|
+
@abstractmethod
|
|
32
|
+
def detect(self, context: DetectionContext) -> tuple[list[StackDetection], list[EntryPoint]]:
|
|
33
|
+
"""Produce stacks y entry points detectados."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"AbstractDetector",
|
|
38
|
+
"DetectionContext",
|
|
39
|
+
"EntryPoint",
|
|
40
|
+
"FrameworkDetection",
|
|
41
|
+
"StackDetection",
|
|
42
|
+
]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Detector de proyectos Dart y Flutter."""
|
|
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 DartDetector(AbstractDetector):
|
|
16
|
+
name = "dart"
|
|
17
|
+
priority = 90
|
|
18
|
+
|
|
19
|
+
def can_detect(self, context: DetectionContext) -> bool:
|
|
20
|
+
return "pubspec.yaml" in context.manifests
|
|
21
|
+
|
|
22
|
+
def detect(self, context: DetectionContext) -> tuple[list[StackDetection], list[EntryPoint]]:
|
|
23
|
+
content = "\n".join(read_text_lines(context.root / "pubspec.yaml")).lower()
|
|
24
|
+
frameworks: list[FrameworkDetection] = []
|
|
25
|
+
if "flutter:" in content:
|
|
26
|
+
frameworks.append(FrameworkDetection(name="Flutter", source="pubspec.yaml"))
|
|
27
|
+
|
|
28
|
+
entry_points: list[EntryPoint] = []
|
|
29
|
+
for path in ("lib/main.dart", "bin/main.dart"):
|
|
30
|
+
if path_exists_in_tree(context.file_tree, path):
|
|
31
|
+
entry_points.append(
|
|
32
|
+
EntryPoint(path=path, stack="dart", kind="application", source="pubspec.yaml")
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
stack = StackDetection(
|
|
36
|
+
stack="dart",
|
|
37
|
+
detection_method="manifest",
|
|
38
|
+
confidence="high",
|
|
39
|
+
frameworks=frameworks,
|
|
40
|
+
manifests=["pubspec.yaml"],
|
|
41
|
+
)
|
|
42
|
+
return [stack], entry_points
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Detector de proyectos Go."""
|
|
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, unique_strings
|
|
11
|
+
from sourcecode.schema import FrameworkDetection
|
|
12
|
+
from sourcecode.tree_utils import flatten_file_tree
|
|
13
|
+
|
|
14
|
+
_FRAMEWORK_MAP = {
|
|
15
|
+
"github.com/gin-gonic/gin": "Gin",
|
|
16
|
+
"github.com/labstack/echo": "Echo",
|
|
17
|
+
"github.com/spf13/cobra": "Cobra",
|
|
18
|
+
"github.com/gofiber/fiber": "Fiber",
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class GoDetector(AbstractDetector):
|
|
23
|
+
name = "go"
|
|
24
|
+
priority = 40
|
|
25
|
+
|
|
26
|
+
def can_detect(self, context: DetectionContext) -> bool:
|
|
27
|
+
return "go.mod" in context.manifests
|
|
28
|
+
|
|
29
|
+
def detect(self, context: DetectionContext) -> tuple[list[StackDetection], list[EntryPoint]]:
|
|
30
|
+
lines = read_text_lines(context.root / "go.mod")
|
|
31
|
+
content = "\n".join(lines)
|
|
32
|
+
frameworks = [
|
|
33
|
+
FrameworkDetection(name=label, source="go.mod")
|
|
34
|
+
for dependency, label in _FRAMEWORK_MAP.items()
|
|
35
|
+
if dependency in content
|
|
36
|
+
]
|
|
37
|
+
entry_candidates = [
|
|
38
|
+
path for path in flatten_file_tree(context.file_tree) if path.endswith("main.go")
|
|
39
|
+
]
|
|
40
|
+
preferred = [path for path in entry_candidates if path.startswith("cmd/")] or entry_candidates
|
|
41
|
+
entry_points = [
|
|
42
|
+
EntryPoint(path=path, stack="go", kind="binary", source="go.mod")
|
|
43
|
+
for path in unique_strings(preferred)
|
|
44
|
+
]
|
|
45
|
+
stack = StackDetection(
|
|
46
|
+
stack="go",
|
|
47
|
+
detection_method="manifest",
|
|
48
|
+
confidence="high",
|
|
49
|
+
frameworks=frameworks,
|
|
50
|
+
manifests=["go.mod"],
|
|
51
|
+
)
|
|
52
|
+
return [stack], entry_points
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Fallback heuristico por extension de fichero."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from collections import Counter
|
|
5
|
+
|
|
6
|
+
from sourcecode.detectors.base import (
|
|
7
|
+
AbstractDetector,
|
|
8
|
+
DetectionContext,
|
|
9
|
+
EntryPoint,
|
|
10
|
+
StackDetection,
|
|
11
|
+
)
|
|
12
|
+
from sourcecode.tree_utils import flatten_file_tree
|
|
13
|
+
|
|
14
|
+
_EXTENSION_MAP = {
|
|
15
|
+
".py": "python",
|
|
16
|
+
".js": "nodejs",
|
|
17
|
+
".ts": "nodejs",
|
|
18
|
+
".tsx": "nodejs",
|
|
19
|
+
".go": "go",
|
|
20
|
+
".rs": "rust",
|
|
21
|
+
".java": "java",
|
|
22
|
+
".php": "php",
|
|
23
|
+
".rb": "ruby",
|
|
24
|
+
".dart": "dart",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
_ENTRYPOINT_NAMES = {
|
|
28
|
+
"main.py": ("python", "script"),
|
|
29
|
+
"app.py": ("python", "app"),
|
|
30
|
+
"index.js": ("nodejs", "server"),
|
|
31
|
+
"main.go": ("go", "binary"),
|
|
32
|
+
"main.rs": ("rust", "binary"),
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class HeuristicDetector(AbstractDetector):
|
|
37
|
+
name = "heuristic"
|
|
38
|
+
priority = 999
|
|
39
|
+
|
|
40
|
+
def can_detect(self, context: DetectionContext) -> bool:
|
|
41
|
+
return True
|
|
42
|
+
|
|
43
|
+
def detect(self, context: DetectionContext) -> tuple[list[StackDetection], list[EntryPoint]]:
|
|
44
|
+
paths = flatten_file_tree(context.file_tree)
|
|
45
|
+
counts: Counter[str] = Counter()
|
|
46
|
+
for path in paths:
|
|
47
|
+
for extension, stack in _EXTENSION_MAP.items():
|
|
48
|
+
if path.endswith(extension):
|
|
49
|
+
counts[stack] += 1
|
|
50
|
+
break
|
|
51
|
+
|
|
52
|
+
stacks = [
|
|
53
|
+
StackDetection(
|
|
54
|
+
stack=stack,
|
|
55
|
+
detection_method="heuristic",
|
|
56
|
+
confidence="low",
|
|
57
|
+
manifests=[],
|
|
58
|
+
)
|
|
59
|
+
for stack, _count in counts.most_common()
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
entry_points: list[EntryPoint] = []
|
|
63
|
+
for path in paths:
|
|
64
|
+
filename = path.rsplit("/", 1)[-1]
|
|
65
|
+
if filename in _ENTRYPOINT_NAMES:
|
|
66
|
+
stack, kind = _ENTRYPOINT_NAMES[filename]
|
|
67
|
+
entry_points.append(
|
|
68
|
+
EntryPoint(path=path, stack=stack, kind=kind, source="heuristic")
|
|
69
|
+
)
|
|
70
|
+
return stacks, entry_points
|