sourcecode 0.17.0__tar.gz → 0.19.0__tar.gz

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.
Files changed (110) hide show
  1. {sourcecode-0.17.0 → sourcecode-0.19.0}/PKG-INFO +1 -1
  2. {sourcecode-0.17.0 → sourcecode-0.19.0}/docs/schema.md +2 -2
  3. {sourcecode-0.17.0 → sourcecode-0.19.0}/pyproject.toml +1 -1
  4. {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/__init__.py +1 -1
  5. {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/cli.py +8 -0
  6. {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/dependency_analyzer.py +4 -0
  7. sourcecode-0.19.0/src/sourcecode/detectors/java.py +184 -0
  8. {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/env_analyzer.py +31 -0
  9. {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/schema.py +1 -0
  10. {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/serializer.py +1 -0
  11. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_dependency_analyzer_node_python.py +2 -2
  12. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_dependency_analyzer_polyglot.py +76 -0
  13. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_integration_dependencies.py +1 -1
  14. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_integration_lqn.py +8 -6
  15. sourcecode-0.17.0/src/sourcecode/detectors/java.py +0 -87
  16. {sourcecode-0.17.0 → sourcecode-0.19.0}/.gitignore +0 -0
  17. {sourcecode-0.17.0 → sourcecode-0.19.0}/.ruff.toml +0 -0
  18. {sourcecode-0.17.0 → sourcecode-0.19.0}/README.md +0 -0
  19. {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/architecture_analyzer.py +0 -0
  20. {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/architecture_summary.py +0 -0
  21. {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/classifier.py +0 -0
  22. {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/code_notes_analyzer.py +0 -0
  23. {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/coverage_parser.py +0 -0
  24. {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/detectors/__init__.py +0 -0
  25. {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/detectors/base.py +0 -0
  26. {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/detectors/dart.py +0 -0
  27. {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/detectors/dotnet.py +0 -0
  28. {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/detectors/elixir.py +0 -0
  29. {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/detectors/go.py +0 -0
  30. {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/detectors/heuristic.py +0 -0
  31. {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
  32. {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/detectors/nodejs.py +0 -0
  33. {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/detectors/parsers.py +0 -0
  34. {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/detectors/php.py +0 -0
  35. {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/detectors/project.py +0 -0
  36. {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/detectors/python.py +0 -0
  37. {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/detectors/ruby.py +0 -0
  38. {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/detectors/rust.py +0 -0
  39. {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/detectors/systems.py +0 -0
  40. {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/detectors/terraform.py +0 -0
  41. {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/detectors/tooling.py +0 -0
  42. {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/doc_analyzer.py +0 -0
  43. {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/git_analyzer.py +0 -0
  44. {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/graph_analyzer.py +0 -0
  45. {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/metrics_analyzer.py +0 -0
  46. {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/prepare_context.py +0 -0
  47. {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/redactor.py +0 -0
  48. {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/scanner.py +0 -0
  49. {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/semantic_analyzer.py +0 -0
  50. {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/summarizer.py +0 -0
  51. {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/tree_utils.py +0 -0
  52. {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/workspace.py +0 -0
  53. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/__init__.py +0 -0
  54. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/conftest.py +0 -0
  55. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/fixtures/coverage.xml +0 -0
  56. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/fixtures/fastapi_app/pyproject.toml +0 -0
  57. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/fixtures/fastapi_app/src/main.py +0 -0
  58. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/fixtures/go_service/cmd/api/main.go +0 -0
  59. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/fixtures/go_service/go.mod +0 -0
  60. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/fixtures/jacoco.xml +0 -0
  61. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/fixtures/lcov.info +0 -0
  62. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/fixtures/nextjs_app/app/page.tsx +0 -0
  63. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/fixtures/nextjs_app/package.json +0 -0
  64. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/fixtures/nextjs_app/pnpm-lock.yaml +0 -0
  65. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/fixtures/pnpm_monorepo/apps/web/app/page.tsx +0 -0
  66. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/fixtures/pnpm_monorepo/apps/web/package.json +0 -0
  67. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/fixtures/pnpm_monorepo/packages/api/main.py +0 -0
  68. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/fixtures/pnpm_monorepo/packages/api/pyproject.toml +0 -0
  69. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/fixtures/pnpm_monorepo/pnpm-workspace.yaml +0 -0
  70. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_architecture_analyzer.py +0 -0
  71. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_architecture_summary.py +0 -0
  72. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_classifier.py +0 -0
  73. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_cli.py +0 -0
  74. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_code_notes_analyzer.py +0 -0
  75. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_coverage_parser.py +0 -0
  76. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_cross_consistency.py +0 -0
  77. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_dependency_schema.py +0 -0
  78. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_detector_go_rust_java.py +0 -0
  79. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_detector_nodejs.py +0 -0
  80. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_detector_php_ruby_dart.py +0 -0
  81. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_detector_python.py +0 -0
  82. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_detector_universal_managed.py +0 -0
  83. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_detector_universal_systems.py +0 -0
  84. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_detectors_base.py +0 -0
  85. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_doc_analyzer_jsdom.py +0 -0
  86. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_doc_analyzer_python.py +0 -0
  87. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_graph_analyzer_polyglot.py +0 -0
  88. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_graph_analyzer_python_node.py +0 -0
  89. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_graph_schema.py +0 -0
  90. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_integration.py +0 -0
  91. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_integration_detection.py +0 -0
  92. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_integration_docs.py +0 -0
  93. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_integration_graph_modules.py +0 -0
  94. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_integration_metrics.py +0 -0
  95. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_integration_multistack.py +0 -0
  96. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_integration_semantics.py +0 -0
  97. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_integration_universal.py +0 -0
  98. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_metrics_analyzer.py +0 -0
  99. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_packaging.py +0 -0
  100. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_real_projects.py +0 -0
  101. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_redactor.py +0 -0
  102. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_scanner.py +0 -0
  103. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_schema.py +0 -0
  104. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_schema_normalization.py +0 -0
  105. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_semantic_analyzer_node.py +0 -0
  106. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_semantic_analyzer_python.py +0 -0
  107. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_semantic_import_resolution.py +0 -0
  108. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_semantic_schema.py +0 -0
  109. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_summarizer.py +0 -0
  110. {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_workspace_analyzer.py +0 -0
@@ -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
@@ -81,7 +81,7 @@ Campos:
81
81
  {
82
82
  "schema_version": "1.0",
83
83
  "generated_at": "2026-04-07T19:41:05.686277+00:00",
84
- "sourcecode_version": "0.17.0",
84
+ "sourcecode_version": "0.19.0",
85
85
  "analyzed_path": "/abs/path/to/project"
86
86
  }
87
87
  ```
@@ -645,7 +645,7 @@ Ejemplo de salida para un monorepo con web Node.js y API Python con `--dependenc
645
645
  "metadata": {
646
646
  "schema_version": "1.0",
647
647
  "generated_at": "2026-04-07T19:41:05.686277+00:00",
648
- "sourcecode_version": "0.17.0",
648
+ "sourcecode_version": "0.19.0",
649
649
  "analyzed_path": "/abs/path/to/project"
650
650
  },
651
651
  "file_tree": {
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "sourcecode"
7
- version = "0.17.0"
7
+ version = "0.19.0"
8
8
  description = "Genera un mapa de contexto estructurado de proyectos de software para agentes IA"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -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"
@@ -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]:
@@ -0,0 +1,184 @@
1
+ from __future__ import annotations
2
+
3
+ import re
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
+ _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
+
25
+
26
+ class JavaDetector(AbstractDetector):
27
+ name = "java"
28
+ priority = 60
29
+
30
+ def can_detect(self, context: DetectionContext) -> bool:
31
+ return any(manifest in context.manifests for manifest in ("pom.xml", "build.gradle"))
32
+
33
+ def detect(self, context: DetectionContext) -> tuple[list[StackDetection], list[EntryPoint]]:
34
+ frameworks: list[FrameworkDetection] = []
35
+ manifests: list[str] = []
36
+
37
+ if "pom.xml" in context.manifests:
38
+ manifests.append("pom.xml")
39
+ frameworks.extend(self._frameworks_from_pom(context.root / "pom.xml"))
40
+ if "build.gradle" in context.manifests:
41
+ manifests.append("build.gradle")
42
+ frameworks.extend(self._frameworks_from_gradle(context.root / "build.gradle"))
43
+
44
+ entry_points = self._collect_entry_points(context)
45
+ stack = StackDetection(
46
+ stack="java",
47
+ detection_method="manifest",
48
+ confidence="high",
49
+ frameworks=self._dedupe_frameworks(frameworks),
50
+ manifests=manifests,
51
+ )
52
+ return [stack], entry_points
53
+
54
+ def _frameworks_from_pom(self, path: Path) -> list[FrameworkDetection]:
55
+ try:
56
+ tree = ElementTree.parse(path)
57
+ except (OSError, ElementTree.ParseError):
58
+ return []
59
+ text = ElementTree.tostring(tree.getroot(), encoding="unicode").lower()
60
+ frameworks: list[FrameworkDetection] = []
61
+ if "spring-boot" in text:
62
+ frameworks.append(FrameworkDetection(name="Spring Boot", source="pom.xml"))
63
+ if "quarkus" in text:
64
+ frameworks.append(FrameworkDetection(name="Quarkus", source="pom.xml"))
65
+ return frameworks
66
+
67
+ def _frameworks_from_gradle(self, path: Path) -> list[FrameworkDetection]:
68
+ content = "\n".join(read_text_lines(path)).lower()
69
+ frameworks: list[FrameworkDetection] = []
70
+ if "com.android.application" in content or "com.android.library" in content:
71
+ frameworks.append(FrameworkDetection(name="Android", source="build.gradle"))
72
+ if "spring-boot" in content:
73
+ frameworks.append(FrameworkDetection(name="Spring Boot", source="build.gradle"))
74
+ if "quarkus" in content:
75
+ frameworks.append(FrameworkDetection(name="Quarkus", source="build.gradle"))
76
+ return frameworks
77
+
78
+ def _collect_entry_points(self, context: DetectionContext) -> list[EntryPoint]:
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"))
86
+ ]
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
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
176
+
177
+ def _dedupe_frameworks(self, frameworks: list[FrameworkDetection]) -> list[FrameworkDetection]:
178
+ seen: set[str] = set()
179
+ result: list[FrameworkDetection] = []
180
+ for framework in frameworks:
181
+ if framework.name not in seen:
182
+ seen.add(framework.name)
183
+ result.append(framework)
184
+ return result
@@ -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:
@@ -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
@@ -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:
@@ -99,13 +99,13 @@ version = "0.38.6"
99
99
 
100
100
 
101
101
  def test_python_requirements_without_lockfile_keeps_declared_versions(tmp_path: Path) -> None:
102
- (tmp_path / "requirements.txt").write_text("typer==0.17.0\nrich>=13.0\n")
102
+ (tmp_path / "requirements.txt").write_text("typer==0.18.0\nrich>=13.0\n")
103
103
 
104
104
  records, summary = DependencyAnalyzer().analyze(tmp_path)
105
105
 
106
106
  assert summary.direct_count == 2
107
107
  typer_dep = next(record for record in records if record.name == "typer")
108
108
  rich_dep = next(record for record in records if record.name == "rich")
109
- assert typer_dep.declared_version == "==0.17.0"
109
+ assert typer_dep.declared_version == "==0.19.0"
110
110
  assert typer_dep.resolved_version is None
111
111
  assert rich_dep.declared_version == ">=13.0"
@@ -139,3 +139,79 @@ require github.com/gin-gonic/gin v1.10.0
139
139
  assert json_dep.resolved_version == "13.0.3"
140
140
  assert transitive_dotnet.scope == "transitive"
141
141
  assert "dotnet" in dotnet_summary.ecosystems
142
+
143
+
144
+ def test_java_pom_dependencies_listed_in_summary(tmp_path: Path) -> None:
145
+ (tmp_path / "pom.xml").write_text(
146
+ """<?xml version="1.0" encoding="UTF-8"?>
147
+ <project xmlns="http://maven.apache.org/POM/4.0.0">
148
+ <modelVersion>4.0.0</modelVersion>
149
+ <groupId>com.example</groupId>
150
+ <artifactId>demo</artifactId>
151
+ <version>1.0.0</version>
152
+ <dependencies>
153
+ <dependency>
154
+ <groupId>org.springframework.boot</groupId>
155
+ <artifactId>spring-boot-starter-web</artifactId>
156
+ <version>3.2.0</version>
157
+ </dependency>
158
+ <dependency>
159
+ <groupId>org.springframework.boot</groupId>
160
+ <artifactId>spring-boot-starter-test</artifactId>
161
+ <version>3.2.0</version>
162
+ <scope>test</scope>
163
+ </dependency>
164
+ <dependency>
165
+ <groupId>com.fasterxml.jackson.core</groupId>
166
+ <artifactId>jackson-databind</artifactId>
167
+ <version>2.15.0</version>
168
+ </dependency>
169
+ </dependencies>
170
+ </project>"""
171
+ )
172
+
173
+ records, summary = DependencyAnalyzer().analyze(tmp_path)
174
+
175
+ assert summary.total_count == 3
176
+ assert len(summary.dependencies) == 3
177
+ assert summary.dependencies == records
178
+
179
+ names = {r.name for r in summary.dependencies}
180
+ assert "org.springframework.boot:spring-boot-starter-web" in names
181
+ assert "org.springframework.boot:spring-boot-starter-test" in names
182
+ assert "com.fasterxml.jackson.core:jackson-databind" in names
183
+
184
+ web = next(r for r in summary.dependencies if "starter-web" in r.name)
185
+ assert web.declared_version == "3.2.0"
186
+ assert web.scope == "direct"
187
+ assert web.ecosystem == "java"
188
+
189
+ test_dep = next(r for r in summary.dependencies if "starter-test" in r.name)
190
+ assert test_dep.scope == "dev"
191
+
192
+
193
+ def test_java_dependency_summary_excluded_from_compact_view(tmp_path: Path) -> None:
194
+ import json
195
+ from typer.testing import CliRunner
196
+ from sourcecode.cli import app
197
+
198
+ (tmp_path / "pom.xml").write_text(
199
+ """<?xml version="1.0" encoding="UTF-8"?>
200
+ <project xmlns="http://maven.apache.org/POM/4.0.0">
201
+ <modelVersion>4.0.0</modelVersion>
202
+ <groupId>com.example</groupId><artifactId>demo</artifactId><version>1.0.0</version>
203
+ <dependencies>
204
+ <dependency>
205
+ <groupId>org.springframework.boot</groupId>
206
+ <artifactId>spring-boot-starter-web</artifactId>
207
+ <version>3.2.0</version>
208
+ </dependency>
209
+ </dependencies>
210
+ </project>"""
211
+ )
212
+
213
+ runner = CliRunner()
214
+ result = runner.invoke(app, ["--compact", str(tmp_path)])
215
+ assert result.exit_code == 0, result.output
216
+ data = json.loads(result.output)
217
+ assert "dependencies" not in data["dependency_summary"]
@@ -34,7 +34,7 @@ def test_cli_dependencies_flag_enables_dependency_block(tmp_path: Path) -> None:
34
34
 
35
35
 
36
36
  def test_cli_without_dependencies_flag_keeps_block_disabled(tmp_path: Path) -> None:
37
- (tmp_path / "requirements.txt").write_text("typer==0.17.0\n")
37
+ (tmp_path / "requirements.txt").write_text("typer==0.19.0\n")
38
38
 
39
39
  result = runner.invoke(app, [str(tmp_path)])
40
40
 
@@ -184,18 +184,20 @@ def test_lqn06_compact_with_dependencies() -> None:
184
184
 
185
185
 
186
186
  # ---------------------------------------------------------------------------
187
- # LQN-06c: compact without --dependencies -> dependency_summary is None
187
+ # LQN-06c: compact auto-enables dependency analysis -> dependency_summary populated
188
188
  # ---------------------------------------------------------------------------
189
189
 
190
190
 
191
- def test_lqn06_compact_no_dep_summary_without_flag() -> None:
192
- """LQN-06c: sourcecode . --compact (no --dependencies) -> dependency_summary is None."""
191
+ def test_lqn06_compact_auto_populates_dep_summary() -> None:
192
+ """LQN-06c: sourcecode . --compact auto-enables dependency analysis so dependency_summary is populated."""
193
193
  compact = _invoke_json("--compact")
194
194
 
195
195
  dep_sum = compact.get("dependency_summary")
196
- assert dep_sum is None, (
197
- f"LQN-06c: compact without --dependencies should have dependency_summary=None, "
198
- f"got {dep_sum!r}"
196
+ assert dep_sum is not None, (
197
+ "LQN-06c: --compact must auto-enable dependency analysis; dependency_summary should be populated"
198
+ )
199
+ assert dep_sum["requested"] is True, (
200
+ f"LQN-06c: dependency_summary.requested must be True when --compact is used, got {dep_sum['requested']}"
199
201
  )
200
202
 
201
203
 
@@ -1,87 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from pathlib import Path
4
- from xml.etree import ElementTree
5
-
6
- from sourcecode.detectors.base import (
7
- AbstractDetector,
8
- DetectionContext,
9
- EntryPoint,
10
- StackDetection,
11
- )
12
- from sourcecode.detectors.parsers import read_text_lines, unique_strings
13
- from sourcecode.schema import FrameworkDetection
14
- from sourcecode.tree_utils import flatten_file_tree
15
-
16
-
17
- class JavaDetector(AbstractDetector):
18
- name = "java"
19
- priority = 60
20
-
21
- def can_detect(self, context: DetectionContext) -> bool:
22
- return any(manifest in context.manifests for manifest in ("pom.xml", "build.gradle"))
23
-
24
- def detect(self, context: DetectionContext) -> tuple[list[StackDetection], list[EntryPoint]]:
25
- frameworks: list[FrameworkDetection] = []
26
- manifests: list[str] = []
27
-
28
- if "pom.xml" in context.manifests:
29
- manifests.append("pom.xml")
30
- frameworks.extend(self._frameworks_from_pom(context.root / "pom.xml"))
31
- if "build.gradle" in context.manifests:
32
- manifests.append("build.gradle")
33
- frameworks.extend(self._frameworks_from_gradle(context.root / "build.gradle"))
34
-
35
- entry_points = self._collect_entry_points(context)
36
- stack = StackDetection(
37
- stack="java",
38
- detection_method="manifest",
39
- confidence="high",
40
- frameworks=self._dedupe_frameworks(frameworks),
41
- manifests=manifests,
42
- )
43
- return [stack], entry_points
44
-
45
- def _frameworks_from_pom(self, path: Path) -> list[FrameworkDetection]:
46
- try:
47
- tree = ElementTree.parse(path)
48
- except (OSError, ElementTree.ParseError):
49
- return []
50
- text = ElementTree.tostring(tree.getroot(), encoding="unicode").lower()
51
- frameworks: list[FrameworkDetection] = []
52
- if "spring-boot" in text:
53
- frameworks.append(FrameworkDetection(name="Spring Boot", source="pom.xml"))
54
- if "quarkus" in text:
55
- frameworks.append(FrameworkDetection(name="Quarkus", source="pom.xml"))
56
- return frameworks
57
-
58
- def _frameworks_from_gradle(self, path: Path) -> list[FrameworkDetection]:
59
- content = "\n".join(read_text_lines(path)).lower()
60
- frameworks: list[FrameworkDetection] = []
61
- if "com.android.application" in content or "com.android.library" in content:
62
- frameworks.append(FrameworkDetection(name="Android", source="build.gradle"))
63
- if "spring-boot" in content:
64
- frameworks.append(FrameworkDetection(name="Spring Boot", source="build.gradle"))
65
- if "quarkus" in content:
66
- frameworks.append(FrameworkDetection(name="Quarkus", source="build.gradle"))
67
- return frameworks
68
-
69
- 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"))
74
- ]
75
- return [
76
- EntryPoint(path=path, stack="java", kind="application", source="manifest")
77
- for path in unique_strings(candidates)
78
- ]
79
-
80
- def _dedupe_frameworks(self, frameworks: list[FrameworkDetection]) -> list[FrameworkDetection]:
81
- seen: set[str] = set()
82
- result: list[FrameworkDetection] = []
83
- for framework in frameworks:
84
- if framework.name not in seen:
85
- seen.add(framework.name)
86
- result.append(framework)
87
- return result
File without changes
File without changes
File without changes