sourcecode 0.17.0__py3-none-any.whl → 0.19.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 CHANGED
@@ -1,3 +1,3 @@
1
1
  """sourcecode — Genera mapas de contexto estructurado para agentes IA."""
2
2
 
3
- __version__ = "0.17.0"
3
+ __version__ = "0.19.0"
sourcecode/cli.py CHANGED
@@ -260,6 +260,14 @@ def main(
260
260
  file_tree = filter_sensitive_files(raw_tree)
261
261
  detector = ProjectDetector(build_default_detectors())
262
262
  workspace_analysis = WorkspaceAnalyzer().analyze(target, manifests)
263
+
264
+ # --compact implicitly enables lightweight analysis passes so that
265
+ # dependency_summary, env_summary and code_notes_summary are never null.
266
+ if compact:
267
+ dependencies = True
268
+ env_map = True
269
+ code_notes = True
270
+
263
271
  dependency_analyzer = DependencyAnalyzer() if dependencies else None
264
272
  graph_analyzer = GraphAnalyzer() if graph_modules else None
265
273
  parsed_graph_edges = (
@@ -47,6 +47,7 @@ class DependencyAnalyzer:
47
47
  ecosystems: set[str] = set()
48
48
  sources: set[str] = set()
49
49
  limitations: list[str] = []
50
+ all_dependencies: list[DependencyRecord] = []
50
51
  for summary in summaries:
51
52
  result.total_count += summary.total_count
52
53
  result.direct_count += summary.direct_count
@@ -56,9 +57,11 @@ class DependencyAnalyzer:
56
57
  for limitation in summary.limitations:
57
58
  if limitation not in limitations:
58
59
  limitations.append(limitation)
60
+ all_dependencies.extend(summary.dependencies)
59
61
  result.ecosystems = sorted(ecosystems)
60
62
  result.sources = sorted(sources)
61
63
  result.limitations = limitations
64
+ result.dependencies = all_dependencies
62
65
  return result
63
66
 
64
67
  def _build_summary(
@@ -80,6 +83,7 @@ class DependencyAnalyzer:
80
83
  ecosystems=ecosystems,
81
84
  sources=sources,
82
85
  limitations=unique_limitations,
86
+ dependencies=list(records),
83
87
  )
84
88
 
85
89
  def _dedupe(self, records: Iterable[DependencyRecord]) -> list[DependencyRecord]:
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import re
3
4
  from pathlib import Path
4
5
  from xml.etree import ElementTree
5
6
 
@@ -13,6 +14,14 @@ from sourcecode.detectors.parsers import read_text_lines, unique_strings
13
14
  from sourcecode.schema import FrameworkDetection
14
15
  from sourcecode.tree_utils import flatten_file_tree
15
16
 
17
+ _MAX_FILE_SIZE = 256 * 1024 # 256 KB
18
+ _MAX_JAVA_ENTRY_SCAN = 200
19
+ _MAX_ANNOTATION_ENTRY_POINTS = 20
20
+
21
+ _REST_CONTROLLER_RE = re.compile(r'@(?:Rest)?Controller\b')
22
+ _WEB_FILTER_RE = re.compile(r'@WebFilter\b')
23
+ _FILTER_BEAN_RE = re.compile(r'FilterRegistrationBean\b')
24
+
16
25
 
17
26
  class JavaDetector(AbstractDetector):
18
27
  name = "java"
@@ -67,15 +76,103 @@ class JavaDetector(AbstractDetector):
67
76
  return frameworks
68
77
 
69
78
  def _collect_entry_points(self, context: DetectionContext) -> list[EntryPoint]:
70
- candidates = [
71
- path
72
- for path in flatten_file_tree(context.file_tree)
73
- if path.endswith(("Application.java", "Main.java"))
79
+ all_paths = flatten_file_tree(context.file_tree)
80
+ all_java = [p for p in all_paths if p.endswith(".java")]
81
+
82
+ # 1. @SpringBootApplication entry: Application.java / Main.java by name
83
+ app_candidates = [
84
+ p for p in all_java
85
+ if p.endswith(("Application.java", "Main.java"))
74
86
  ]
75
- return [
76
- EntryPoint(path=path, stack="java", kind="application", source="manifest")
77
- for path in unique_strings(candidates)
87
+ entry_points: list[EntryPoint] = [
88
+ EntryPoint(path=p, stack="java", kind="application", source="manifest")
89
+ for p in unique_strings(app_candidates)
90
+ ]
91
+
92
+ # 2. Annotation-based scan: @RestController, @WebFilter, FilterRegistrationBean
93
+ scan_candidates = [
94
+ p for p in all_java
95
+ if "/test/" not in p and "/tests/" not in p
96
+ ][:_MAX_JAVA_ENTRY_SCAN]
97
+
98
+ annotation_eps: list[EntryPoint] = []
99
+ for rel_path in scan_candidates:
100
+ if len(annotation_eps) >= _MAX_ANNOTATION_ENTRY_POINTS:
101
+ break
102
+ abs_path = context.root / rel_path
103
+ annotation_eps.extend(self._scan_java_file_for_entry_points(abs_path, rel_path))
104
+ entry_points.extend(annotation_eps)
105
+
106
+ # 3. web.xml servlet/filter declarations
107
+ web_xml_paths = [
108
+ p for p in all_paths
109
+ if p.endswith("web.xml") and "WEB-INF" in p
78
110
  ]
111
+ for rel_path in web_xml_paths[:1]:
112
+ abs_path = context.root / rel_path
113
+ entry_points.extend(self._parse_web_xml(abs_path, rel_path))
114
+
115
+ # Deduplicate by (path, kind)
116
+ seen: set[tuple[str, str]] = set()
117
+ unique_eps: list[EntryPoint] = []
118
+ for ep in entry_points:
119
+ key = (ep.path, ep.kind)
120
+ if key not in seen:
121
+ seen.add(key)
122
+ unique_eps.append(ep)
123
+ return unique_eps
124
+
125
+ def _scan_java_file_for_entry_points(self, abs_path: Path, rel_path: str) -> list[EntryPoint]:
126
+ try:
127
+ if abs_path.stat().st_size > _MAX_FILE_SIZE:
128
+ return []
129
+ content = abs_path.read_text(encoding="utf-8", errors="replace")
130
+ except OSError:
131
+ return []
132
+
133
+ # Quick pre-filter before running regexes
134
+ if "Controller" not in content and "Filter" not in content:
135
+ return []
136
+
137
+ if _REST_CONTROLLER_RE.search(content):
138
+ return [EntryPoint(
139
+ path=rel_path, stack="java", kind="http_handler",
140
+ source="annotation", confidence="high",
141
+ )]
142
+ if _WEB_FILTER_RE.search(content):
143
+ return [EntryPoint(
144
+ path=rel_path, stack="java", kind="filter",
145
+ source="annotation", confidence="high",
146
+ )]
147
+ if _FILTER_BEAN_RE.search(content):
148
+ return [EntryPoint(
149
+ path=rel_path, stack="java", kind="filter",
150
+ source="annotation", confidence="medium",
151
+ )]
152
+ return []
153
+
154
+ def _parse_web_xml(self, abs_path: Path, rel_path: str) -> list[EntryPoint]:
155
+ try:
156
+ tree = ElementTree.parse(abs_path)
157
+ except (OSError, ElementTree.ParseError):
158
+ return []
159
+
160
+ root = tree.getroot()
161
+ ns_match = re.match(r"\{[^}]+\}", root.tag)
162
+ ns = ns_match.group(0) if ns_match else ""
163
+
164
+ results: list[EntryPoint] = []
165
+ if root.findall(f".//{ns}servlet-class"):
166
+ results.append(EntryPoint(
167
+ path=rel_path, stack="java", kind="servlet",
168
+ source="xml_config", confidence="high",
169
+ ))
170
+ if root.findall(f".//{ns}filter-class"):
171
+ results.append(EntryPoint(
172
+ path=rel_path, stack="java", kind="filter",
173
+ source="xml_config", confidence="high",
174
+ ))
175
+ return results
79
176
 
80
177
  def _dedupe_frameworks(self, frameworks: list[FrameworkDetection]) -> list[FrameworkDetection]:
81
178
  seen: set[str] = set()
@@ -25,6 +25,12 @@ _ENV_EXAMPLE_NAMES = {
25
25
  "example.env", "sample.env",
26
26
  }
27
27
 
28
+ # Spring Boot application.properties / application.yml and their profile variants
29
+ _SPRING_CONF_BASE = {"application.properties", "application.yml", "application.yaml"}
30
+ _SPRING_CONF_PROFILE_RE = re.compile(r'^application-[a-z0-9_-]+\.(properties|ya?ml)$', re.IGNORECASE)
31
+ # Matches ${ENV_VAR} or ${ENV_VAR:default} where ENV_VAR is UPPER_SNAKE_CASE
32
+ _SPRING_ENV_REF_RE = re.compile(r'\$\{([A-Z][A-Z0-9_]*)(?::[^}]*)?\}')
33
+
28
34
  # (pattern_id, compiled_regex)
29
35
  # Grupos de captura: group(1)=key, group(2)=default si existe
30
36
  _PATTERNS: list[tuple[str, re.Pattern]] = [
@@ -55,6 +61,9 @@ _PATTERNS: list[tuple[str, re.Pattern]] = [
55
61
  ("java_getenv", re.compile(
56
62
  r"""System\.getenv\(\s*["']([A-Z][A-Z0-9_]*)["']\s*\)"""
57
63
  )),
64
+ ("java_spring_value", re.compile(
65
+ r"""@Value\(\s*["']\$\{([A-Z][A-Z0-9_]*)(?::[^}]*)?\}["']\s*\)"""
66
+ )),
58
67
  ("php_getenv", re.compile(
59
68
  r"""getenv\(\s*["']([A-Z][A-Z0-9_]*)["']\s*\)"""
60
69
  )),
@@ -188,6 +197,23 @@ def _parse_env_example(
188
197
  return results
189
198
 
190
199
 
200
+ def _parse_spring_config(
201
+ path: Path,
202
+ rel_path: str,
203
+ findings: dict,
204
+ ) -> None:
205
+ """Parse application.properties / application.yml looking for ${ENV_VAR} refs."""
206
+ try:
207
+ content = path.read_text(encoding="utf-8", errors="replace")
208
+ except OSError:
209
+ return
210
+
211
+ for m in _SPRING_ENV_REF_RE.finditer(content):
212
+ key = m.group(1)
213
+ line_num = content.count("\n", 0, m.start()) + 1
214
+ findings[key].append((f"{rel_path}:{line_num}", None))
215
+
216
+
191
217
  class EnvAnalyzer:
192
218
  """Extrae el mapa de variables de entorno del proyecto."""
193
219
 
@@ -298,11 +324,16 @@ class EnvAnalyzer:
298
324
  self._walk(root, entry, findings, example_entries, example_files_found, limitations)
299
325
  elif entry.is_file():
300
326
  rel = entry.relative_to(root).as_posix()
327
+ name_lower = name.lower()
301
328
  # .env.example and similar
302
329
  if name in _ENV_EXAMPLE_NAMES:
303
330
  example_files_found.append(rel)
304
331
  example_entries.extend(_parse_env_example(entry, rel))
305
332
  continue
333
+ # Spring Boot application.properties / application.yml (incl. profiles)
334
+ if name_lower in _SPRING_CONF_BASE or _SPRING_CONF_PROFILE_RE.match(name_lower):
335
+ _parse_spring_config(entry, rel, findings)
336
+ continue
306
337
  # Source code files
307
338
  suffix = entry.suffix.lower()
308
339
  if suffix in _CODE_EXTENSIONS:
sourcecode/schema.py CHANGED
@@ -96,6 +96,7 @@ class DependencySummary:
96
96
  ecosystems: list[str] = field(default_factory=list)
97
97
  sources: list[str] = field(default_factory=list)
98
98
  limitations: list[str] = field(default_factory=list)
99
+ dependencies: list["DependencyRecord"] = field(default_factory=list)
99
100
 
100
101
 
101
102
  @dataclass
sourcecode/serializer.py CHANGED
@@ -74,6 +74,7 @@ def compact_view(sm: SourceMap) -> dict[str, Any]:
74
74
  dep_summary_dict: Any = None
75
75
  if sm.dependency_summary is not None and sm.dependency_summary.requested:
76
76
  dep_summary_dict = asdict(sm.dependency_summary)
77
+ dep_summary_dict.pop("dependencies", None)
77
78
 
78
79
  env_summary_dict: Any = None
79
80
  if sm.env_summary is not None and sm.env_summary.requested:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sourcecode
3
- Version: 0.17.0
3
+ Version: 0.19.0
4
4
  Summary: Genera un mapa de contexto estructurado de proyectos de software para agentes IA
5
5
  License: MIT
6
6
  Requires-Python: >=3.9
@@ -1,22 +1,22 @@
1
- sourcecode/__init__.py,sha256=kGV-7biOu1jEQzo70FiCy-DkYFbItVMEkqxy37z-zJ8,100
1
+ sourcecode/__init__.py,sha256=K3oF2tC36Se4IiYU6MvNhrg4EoK_jaLeik_nUWPID4Y,100
2
2
  sourcecode/architecture_analyzer.py,sha256=zxgqeqn2FhIUDn06X-whjGKiXbrHFkRlzZcr2zKd0Gw,16874
3
3
  sourcecode/architecture_summary.py,sha256=r9IciS9z-7h8xpiME7DKBLhLV36xzXtSzbZ1cRDWklo,11709
4
4
  sourcecode/classifier.py,sha256=fcp2wIq198wq3Rsh_bDmZIK_W5UI6FEL5oqm1RrQfpA,6846
5
- sourcecode/cli.py,sha256=_ihMuJjjwTr50M77kMafOVX0kY9hqrdvl8NUQ_h0cbc,25178
5
+ sourcecode/cli.py,sha256=pB0NmyaH-gCrHCFlNxYSMfuArM6nitv6PyrMLkYKKRs,25429
6
6
  sourcecode/code_notes_analyzer.py,sha256=rRd8bFYV0krjlxxQV0wenwE9K7pVpUQSR7KvSvUQKw4,9226
7
7
  sourcecode/coverage_parser.py,sha256=q0LeZJaX1bnntLu-ImksdBsMlpsVmk_iUfSaB4eaJGo,19702
8
- sourcecode/dependency_analyzer.py,sha256=9BEAyLoBAS2R0twYkprJj_e0c83pzz9QPMRWKfinNn4,40948
8
+ sourcecode/dependency_analyzer.py,sha256=KITti0XLHK3RvZGqAUseWnlFLXGFyv5FqvSK5R6PB1g,41147
9
9
  sourcecode/doc_analyzer.py,sha256=Ec3orx6vBKsh5cNM3-F4y2Got2KuKx8w3dErwtdtM-A,19891
10
- sourcecode/env_analyzer.py,sha256=_UTkAzHNxDgJVpSMviWcn9e2V5o-SAV_oF6xsLHDxfI,12206
10
+ sourcecode/env_analyzer.py,sha256=JZxBOuIxnM0xw0IaJFL6LiPJBErL848L3XoTOHGBqZI,13554
11
11
  sourcecode/git_analyzer.py,sha256=S9PGt8RDasBQYQUsZh9-H_KWVlPvzwR4jgM7TKN9ZCk,7643
12
12
  sourcecode/graph_analyzer.py,sha256=cxl7Xb88aGxIVOCsatZJAE1CtgXGzIbJfqytV-9kkFs,45172
13
13
  sourcecode/metrics_analyzer.py,sha256=4uh11v-Q0gdrN87BOxuFWUym3N3AOkOuy21K5N8peB8,20126
14
14
  sourcecode/prepare_context.py,sha256=X43dEPe2cb7uMwlpQwVJMe0rQIZ0juaJ7H3F37sPbNc,5371
15
15
  sourcecode/redactor.py,sha256=xuGcadGEHaPw4qZXlMDvzMCsr4VOkdp3oBQptHyJk8c,2884
16
16
  sourcecode/scanner.py,sha256=Ga4jDRAOhkcwlRmlE2ko6cE39qKy0aI4iE1lGBWmAlk,6410
17
- sourcecode/schema.py,sha256=93IrVRLAbzksVfFzr6_FhFSPpKJVwgVoTBcov-jTVo8,15669
17
+ sourcecode/schema.py,sha256=dnNdnQxJtBv0PRVWi6-xnpr16dj6IsUGbso9PeeEolA,15743
18
18
  sourcecode/semantic_analyzer.py,sha256=SRQSGJzEi4ZsGvb7U3qvnRiKHabz4hvIzCqiG92x9K8,79349
19
- sourcecode/serializer.py,sha256=IMU4lmr0FTDSTocws47abpJL01HH1cBK2UMGSUv00yo,13404
19
+ sourcecode/serializer.py,sha256=edOfD25lUKkZ0gIjFcjqWmY4yHSnOKvudzNisvW_RYg,13456
20
20
  sourcecode/summarizer.py,sha256=2Yz5xJ1bhtJLlrUpKcd74wijypRRo9GJmGT8u971EW0,11572
21
21
  sourcecode/tree_utils.py,sha256=xla45Whq24j6U9kEgTztJXkMpmOEDieAulKxP3nl89s,935
22
22
  sourcecode/workspace.py,sha256=fQlVoNx8S-fSHpKoJ0JBvEHCFkxszH0KZVJed1i3TRk,6845
@@ -27,7 +27,7 @@ sourcecode/detectors/dotnet.py,sha256=UC-74VW850r3admcl36R-rl3M0-nBFXw8ph5e9RSas
27
27
  sourcecode/detectors/elixir.py,sha256=jCpvt5Yi6jvplc80ovRtWh17q-11ZGo9qX7o8b57TJE,1713
28
28
  sourcecode/detectors/go.py,sha256=KehAyvC3H_s8zwwqFM0bAbztQbbUBDjpNbGvM2kQlk8,1836
29
29
  sourcecode/detectors/heuristic.py,sha256=GvniuNzeCOzmKyY5Sd88_bmBnVrJE5O9cbs4NoEPBKY,1891
30
- sourcecode/detectors/java.py,sha256=xKM8n9UHZ4Dpc9tRL42wUBEIFmbFn8NRMhUVm8CTrjU,3513
30
+ sourcecode/detectors/java.py,sha256=qFVpMy5mu490ENhutO1LV2Gidms4ZZUfNX5Qv5Qxh2I,7188
31
31
  sourcecode/detectors/jvm_ext.py,sha256=EgHJ5W8EE-ZTN9V607mVzohyKgZE8Mc2jCi-DF8RAZU,2616
32
32
  sourcecode/detectors/nodejs.py,sha256=cCK50aY7RERQUQ7FK2UmqVTVFAW3VksDxN6rYADtsC0,4498
33
33
  sourcecode/detectors/parsers.py,sha256=ugPg8yNUf0Ai1gA7Fnn6wAkYGFjTxRodSP3IeViYJJ4,2290
@@ -39,7 +39,7 @@ sourcecode/detectors/rust.py,sha256=V23NO6Y8NsrhrUlGC2feUEcNK63hf3gc1y3jI4tes1Q,
39
39
  sourcecode/detectors/systems.py,sha256=nYaKbGDFu0EOXFcd_1doWFT3tTUdkbxc2DjHUF5TcqQ,1627
40
40
  sourcecode/detectors/terraform.py,sha256=cxORPR_zVLOJpHlh4e9JnFpkQsn_UnqMMom5yG65hZ4,1693
41
41
  sourcecode/detectors/tooling.py,sha256=hIvop80No22pqyGVJ32NKliSdjkHRePQkIRroqG01bY,1875
42
- sourcecode-0.17.0.dist-info/METADATA,sha256=sbwLpqrcDu_SkhDmPOqsq5YIT4wPn8V00GF1DRRohoI,25900
43
- sourcecode-0.17.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
44
- sourcecode-0.17.0.dist-info/entry_points.txt,sha256=voDtdlGU2-jR3wCZpQoOls_0eUKaDVCKIfajGHFDATs,50
45
- sourcecode-0.17.0.dist-info/RECORD,,
42
+ sourcecode-0.19.0.dist-info/METADATA,sha256=cv1q6sPPTkTruEdpHr8JioUXmXuSec5dPAhAw9NXoW8,25900
43
+ sourcecode-0.19.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
44
+ sourcecode-0.19.0.dist-info/entry_points.txt,sha256=voDtdlGU2-jR3wCZpQoOls_0eUKaDVCKIfajGHFDATs,50
45
+ sourcecode-0.19.0.dist-info/RECORD,,