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.
- {sourcecode-0.17.0 → sourcecode-0.19.0}/PKG-INFO +1 -1
- {sourcecode-0.17.0 → sourcecode-0.19.0}/docs/schema.md +2 -2
- {sourcecode-0.17.0 → sourcecode-0.19.0}/pyproject.toml +1 -1
- {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/__init__.py +1 -1
- {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/cli.py +8 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/dependency_analyzer.py +4 -0
- sourcecode-0.19.0/src/sourcecode/detectors/java.py +184 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/env_analyzer.py +31 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/schema.py +1 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/serializer.py +1 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_dependency_analyzer_node_python.py +2 -2
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_dependency_analyzer_polyglot.py +76 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_integration_dependencies.py +1 -1
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_integration_lqn.py +8 -6
- sourcecode-0.17.0/src/sourcecode/detectors/java.py +0 -87
- {sourcecode-0.17.0 → sourcecode-0.19.0}/.gitignore +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/.ruff.toml +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/README.md +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/architecture_analyzer.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/architecture_summary.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/classifier.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/code_notes_analyzer.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/coverage_parser.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/detectors/__init__.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/detectors/base.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/detectors/dart.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/detectors/dotnet.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/detectors/elixir.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/detectors/go.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/detectors/heuristic.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/detectors/jvm_ext.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/detectors/nodejs.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/detectors/parsers.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/detectors/php.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/detectors/project.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/detectors/python.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/detectors/ruby.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/detectors/rust.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/detectors/systems.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/detectors/terraform.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/detectors/tooling.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/doc_analyzer.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/git_analyzer.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/graph_analyzer.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/metrics_analyzer.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/prepare_context.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/redactor.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/scanner.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/semantic_analyzer.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/summarizer.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/tree_utils.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/src/sourcecode/workspace.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/__init__.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/conftest.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/fixtures/coverage.xml +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/fixtures/fastapi_app/pyproject.toml +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/fixtures/fastapi_app/src/main.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/fixtures/go_service/cmd/api/main.go +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/fixtures/go_service/go.mod +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/fixtures/jacoco.xml +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/fixtures/lcov.info +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/fixtures/nextjs_app/app/page.tsx +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/fixtures/nextjs_app/package.json +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/fixtures/nextjs_app/pnpm-lock.yaml +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/fixtures/pnpm_monorepo/apps/web/app/page.tsx +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/fixtures/pnpm_monorepo/apps/web/package.json +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/fixtures/pnpm_monorepo/packages/api/main.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/fixtures/pnpm_monorepo/packages/api/pyproject.toml +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/fixtures/pnpm_monorepo/pnpm-workspace.yaml +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_architecture_analyzer.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_architecture_summary.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_classifier.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_cli.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_code_notes_analyzer.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_coverage_parser.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_cross_consistency.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_dependency_schema.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_detector_go_rust_java.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_detector_nodejs.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_detector_php_ruby_dart.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_detector_python.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_detector_universal_managed.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_detector_universal_systems.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_detectors_base.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_doc_analyzer_jsdom.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_doc_analyzer_python.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_graph_analyzer_polyglot.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_graph_analyzer_python_node.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_graph_schema.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_integration.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_integration_detection.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_integration_docs.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_integration_graph_modules.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_integration_metrics.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_integration_multistack.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_integration_semantics.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_integration_universal.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_metrics_analyzer.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_packaging.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_real_projects.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_redactor.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_scanner.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_schema.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_schema_normalization.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_semantic_analyzer_node.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_semantic_analyzer_python.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_semantic_import_resolution.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_semantic_schema.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_summarizer.py +0 -0
- {sourcecode-0.17.0 → sourcecode-0.19.0}/tests/test_workspace_analyzer.py +0 -0
|
@@ -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.
|
|
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.
|
|
648
|
+
"sourcecode_version": "0.19.0",
|
|
649
649
|
"analyzed_path": "/abs/path/to/project"
|
|
650
650
|
},
|
|
651
651
|
"file_tree": {
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
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
|
|
187
|
+
# LQN-06c: compact auto-enables dependency analysis -> dependency_summary populated
|
|
188
188
|
# ---------------------------------------------------------------------------
|
|
189
189
|
|
|
190
190
|
|
|
191
|
-
def
|
|
192
|
-
"""LQN-06c: sourcecode . --compact
|
|
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
|
-
|
|
198
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{sourcecode-0.17.0 → sourcecode-0.19.0}/tests/fixtures/pnpm_monorepo/packages/api/pyproject.toml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|